001// -------------------------------------------------------------------------------- 002// Copyright 2002-2025 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.license.server.logic; 018 019import com.echothree.util.server.control.BaseLogic; 020import com.echothree.util.server.persistence.Session; 021import com.google.common.io.CharStreams; 022import java.io.ByteArrayInputStream; 023import java.io.IOException; 024import java.io.InputStreamReader; 025import java.net.NetworkInterface; 026import java.net.SocketException; 027import java.net.URLEncoder; 028import java.nio.charset.StandardCharsets; 029import java.time.ZonedDateTime; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.Properties; 033import java.util.concurrent.atomic.AtomicBoolean; 034import java.util.concurrent.atomic.AtomicLong; 035import javax.enterprise.context.ApplicationScoped; 036import javax.enterprise.inject.spi.CDI; 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.apache.http.client.config.RequestConfig; 040import org.apache.http.client.methods.HttpGet; 041import org.apache.http.impl.client.HttpClientBuilder; 042import org.apache.http.util.EntityUtils; 043 044@ApplicationScoped 045public class LicenseCheckLogic 046 extends BaseLogic { 047 048 private static final long DEMO_LICENSE_DURATION = 24 * 60 * 60 * 1000; // 24 hours 049 private static final long LICENSE_RENEWAL_PERIOD = 72 * 60 * 60 * 1000; // 72 hours 050 private static final long RETRY_DELAY = 15 * 60 * 1000; // 15 minutes 051 052 final private Log log = LogFactory.getLog(this.getClass()); 053 final private AtomicBoolean executionPermitted = new AtomicBoolean(true); 054 final private AtomicLong licenseValidUntilTime; 055 private Long lastLicenseAttempt; 056 057 protected LicenseCheckLogic() { 058 var initializedTime = System.currentTimeMillis(); 059 060 licenseValidUntilTime = new AtomicLong(initializedTime + DEMO_LICENSE_DURATION); 061 log.info("Demo license installed for this instance."); 062 } 063 064 public static LicenseCheckLogic getInstance() { 065 return CDI.current().select(LicenseCheckLogic.class).get(); 066 } 067 068 public List<String> getFoundServerNames() { 069 var foundServerNames = new ArrayList<String>(); 070 071 try { 072 NetworkInterface.networkInterfaces() 073 .forEach(ni -> ni.inetAddresses() 074 .forEach(ia -> { 075 if(!ia.isLoopbackAddress()) { 076 foundServerNames.add(ia.getCanonicalHostName()); 077 } 078 })); 079 } catch(SocketException se) { 080 // Leave serverNames empty. 081 log.error("Exception determining server names: ", se); 082 } 083 084 return foundServerNames; 085 } 086 087 public boolean attemptRetrieval() { 088 var foundServerNames = getFoundServerNames(); 089 var licenseUpdated = false; 090 091 try(var client = HttpClientBuilder 092 .create() 093 .setUserAgent("Echo Three/1.0") 094 .setDefaultRequestConfig(RequestConfig.custom() 095 .setSocketTimeout(5000) 096 .setConnectTimeout(5000) 097 .setConnectionRequestTimeout(5000) 098 .build()) 099 .build()) { 100 for(var foundServerName : foundServerNames) { 101 var httpGet = new HttpGet("https://www.echothree.com/licenses/v1/" + URLEncoder.encode(foundServerName, StandardCharsets.UTF_8) + ".xml"); 102 103 log.info("Requesting license for: " + foundServerName); 104 105 try { 106 try(var closeableHttpResponse = client.execute(httpGet)) { 107 var statusCode = closeableHttpResponse.getStatusLine().getStatusCode(); 108 109 if(statusCode == 200) { 110 var entity = closeableHttpResponse.getEntity(); 111 112 if(entity != null) { 113 var text = CharStreams.toString(new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8)); 114 var properties = new Properties(); 115 116 properties.loadFromXML(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); 117 118 var retrievedServerName = properties.getProperty("serverName"); 119 120 if(foundServerName.equals(retrievedServerName)) { 121 var retrievedLicenseValidUntilTime = properties.getProperty("licenseValidUntilTime"); 122 123 licenseValidUntilTime.set(ZonedDateTime.parse(retrievedLicenseValidUntilTime).toInstant().toEpochMilli()); 124 125 log.info("License valid until: " + retrievedLicenseValidUntilTime); 126 127 licenseUpdated = true; 128 } else { 129 log.error("The detected server name is not equal to the retrieved server name."); 130 } 131 } 132 133 EntityUtils.consume(entity); 134 135 if(licenseUpdated) { 136 break; 137 } 138 } else { 139 log.info("Request failed for " + foundServerName + ": " + statusCode); 140 } 141 } 142 } catch(IOException ioe) { 143 log.error("Request failed: IOException.", ioe); 144 } 145 } 146 } catch(IOException ioe) { 147 log.error("HttpClientBuilder failed: IOException.", ioe); 148 } 149 150 return licenseUpdated; 151 } 152 153 public void updateLicense(final Session session) { 154 // If an attempt was made, and it failed, and RETRY_DELAY has passed, then 155 // clear the last attempt time and allow another to happen. 156 if(lastLicenseAttempt != null) { 157 if(session.START_TIME - lastLicenseAttempt > RETRY_DELAY) { 158 log.info("Clearing last license retrieval attempt."); 159 lastLicenseAttempt = null; 160 } 161 } 162 163 // If we're within LICENSE_RENEWAL_PERIOD of the licenseValidUntilTime, start 164 // our attempts to renew the license. 165 if(session.START_TIME + LICENSE_RENEWAL_PERIOD > licenseValidUntilTime.get() && lastLicenseAttempt == null) { 166 log.info("Attempting license retrieval."); 167 if(attemptRetrieval()) { 168 // If the attempt was successful in retrieving something, clear the time we last attempted. 169 log.info("Retrieval succeeded."); 170 lastLicenseAttempt = null; 171 } else { 172 // Otherwise, if there was no last attempt time record it, save it. 173 lastLicenseAttempt = session.START_TIME_LONG; 174 log.info("Retrieval failed."); 175 } 176 } 177 } 178 179 public boolean permitExecution(final Session session) { 180 var result = executionPermitted.get(); 181 182 // If we're past the licenseValidUntilTime, disable command execution. 183 if(session.START_TIME > licenseValidUntilTime.get() && executionPermitted.get()) { 184 log.error("Disabling command execution, license no longer valid."); 185 executionPermitted.set(false); 186 result = false; 187 } 188 189 return result; 190 } 191 192}