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