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}