001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 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 org.apache.http.client.config.RequestConfig; 037import org.apache.http.client.methods.HttpGet; 038import org.apache.http.impl.client.HttpClientBuilder; 039import org.apache.http.util.EntityUtils; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043@ApplicationScoped 044public class LicenseCheckLogic 045 extends BaseLogic { 046 047 private static final long DEMO_LICENSE_DURATION = 24 * 60 * 60 * 1000; // 24 hours 048 private static final long LICENSE_RENEWAL_PERIOD = 72 * 60 * 60 * 1000; // 72 hours 049 private static final long RETRY_DELAY = 15 * 60 * 1000; // 15 minutes 050 051 final private Logger log = LoggerFactory.getLogger(this.getClass()); 052 final private AtomicBoolean executionPermitted = new AtomicBoolean(true); 053 final private AtomicLong licenseValidUntilTime; 054 private Long lastLicenseAttempt; 055 056 protected LicenseCheckLogic() { 057 var initializedTime = System.currentTimeMillis(); 058 059 licenseValidUntilTime = new AtomicLong(initializedTime + DEMO_LICENSE_DURATION); 060 log.info("Demo license installed for this instance."); 061 } 062 063 public List<String> getFoundServerNames() { 064 var foundServerNames = new ArrayList<String>(); 065 066 try { 067 NetworkInterface.networkInterfaces() 068 .forEach(ni -> ni.inetAddresses() 069 .forEach(ia -> { 070 if(!ia.isLoopbackAddress()) { 071 foundServerNames.add(ia.getCanonicalHostName()); 072 } 073 })); 074 } catch(SocketException se) { 075 // Leave serverNames empty. 076 log.error("Exception determining server names: ", se); 077 } 078 079 return foundServerNames; 080 } 081 082 public boolean attemptRetrieval() { 083 var foundServerNames = getFoundServerNames(); 084 var licenseUpdated = false; 085 086 try(var client = HttpClientBuilder 087 .create() 088 .setUserAgent("Echo Three/1.0") 089 .setDefaultRequestConfig(RequestConfig.custom() 090 .setSocketTimeout(5000) 091 .setConnectTimeout(5000) 092 .setConnectionRequestTimeout(5000) 093 .build()) 094 .build()) { 095 for(var foundServerName : foundServerNames) { 096 var httpGet = new HttpGet("https://www.echothree.com/licenses/v1/" + URLEncoder.encode(foundServerName, StandardCharsets.UTF_8) + ".xml"); 097 098 log.info("Requesting license for: " + foundServerName); 099 100 try { 101 try(var closeableHttpResponse = client.execute(httpGet)) { 102 var statusCode = closeableHttpResponse.getStatusLine().getStatusCode(); 103 104 if(statusCode == 200) { 105 var entity = closeableHttpResponse.getEntity(); 106 107 if(entity != null) { 108 var text = CharStreams.toString(new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8)); 109 var properties = new Properties(); 110 111 properties.loadFromXML(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); 112 113 var retrievedServerName = properties.getProperty("serverName"); 114 115 if(foundServerName.equals(retrievedServerName)) { 116 var retrievedLicenseValidUntilTime = properties.getProperty("licenseValidUntilTime"); 117 118 licenseValidUntilTime.set(ZonedDateTime.parse(retrievedLicenseValidUntilTime).toInstant().toEpochMilli()); 119 120 log.info("License valid until: " + retrievedLicenseValidUntilTime); 121 122 licenseUpdated = true; 123 } else { 124 log.error("The detected server name is not equal to the retrieved server name."); 125 } 126 } 127 128 EntityUtils.consume(entity); 129 130 if(licenseUpdated) { 131 break; 132 } 133 } else { 134 log.info("Request failed for " + foundServerName + ": " + statusCode); 135 } 136 } 137 } catch(IOException ioe) { 138 log.error("Request failed: IOException.", ioe); 139 } 140 } 141 } catch(IOException ioe) { 142 log.error("HttpClientBuilder failed: IOException.", ioe); 143 } 144 145 return licenseUpdated; 146 } 147 148 public void updateLicense(final Session session) { 149 // If an attempt was made, and it failed, and RETRY_DELAY has passed, then 150 // clear the last attempt time and allow another to happen. 151 if(lastLicenseAttempt != null) { 152 if(session.getStartTime() - lastLicenseAttempt > RETRY_DELAY) { 153 log.info("Clearing last license retrieval attempt."); 154 lastLicenseAttempt = null; 155 } 156 } 157 158 // If we're within LICENSE_RENEWAL_PERIOD of the licenseValidUntilTime, start 159 // our attempts to renew the license. 160 if(session.getStartTime() + LICENSE_RENEWAL_PERIOD > licenseValidUntilTime.get() && lastLicenseAttempt == null) { 161 log.info("Attempting license retrieval."); 162 if(attemptRetrieval()) { 163 // If the attempt was successful in retrieving something, clear the time we last attempted. 164 log.info("Retrieval succeeded."); 165 lastLicenseAttempt = null; 166 } else { 167 // Otherwise, if there was no last attempt time record it, save it. 168 lastLicenseAttempt = session.getStartTime(); 169 log.info("Retrieval failed."); 170 } 171 } 172 } 173 174 public boolean permitExecution(final Session session) { 175 var result = executionPermitted.get(); 176 177 // If we're past the licenseValidUntilTime, disable command execution. 178 if(session.getStartTime() > licenseValidUntilTime.get() && executionPermitted.get()) { 179 log.error("Disabling command execution, license no longer valid."); 180 executionPermitted.set(false); 181 result = false; 182 } 183 184 return result; 185 } 186 187}