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}