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}