diff --git a/pom.xml b/pom.xml index 72d27143e..e11f547e7 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,8 @@ 3.1.1-SNAPSHOT 3.0.2-SNAPSHOT + false + ${project.build.directory}/license @@ -319,6 +321,28 @@ + + org.codehaus.mojo + exec-maven-plugin + 3.6.2 + + + fetch-community-license + prepare-package + + java + + + ${license.fetch.skip} + io.mapsmessaging.license.tools.LicenseFetcher + compile + + ${license.fetch.dir} + + + + + org.apache.maven.plugins @@ -1194,6 +1218,18 @@ 5.5.7 test + + org.java-websocket + Java-WebSocket + 1.5.3 + + + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + diff --git a/src/main/assemble/scripts.xml b/src/main/assemble/scripts.xml index e9b0872a5..8e378429c 100644 --- a/src/main/assemble/scripts.xml +++ b/src/main/assemble/scripts.xml @@ -82,6 +82,13 @@ + + ${project.basedir}/target/license/ + conf + + *.lic + + ${project.basedir}/src/main/resources/ diff --git a/src/main/java/io/mapsmessaging/license/LicenseController.java b/src/main/java/io/mapsmessaging/license/LicenseController.java index cae1174ba..bc1088ff4 100644 --- a/src/main/java/io/mapsmessaging/license/LicenseController.java +++ b/src/main/java/io/mapsmessaging/license/LicenseController.java @@ -19,8 +19,6 @@ package io.mapsmessaging.license; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import global.namespace.fun.io.bios.BIOS; import global.namespace.truelicense.api.License; @@ -33,20 +31,18 @@ import io.mapsmessaging.logging.ServerLogMessages; import io.mapsmessaging.utilities.GsonFactory; -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.io.File; import java.time.Instant; import java.time.ZoneId; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; public class LicenseController { - private static final String LICENSE_SERVER_URL = "https://license.mapsmessaging.io/api/v1/license"; - - private static final String LICENSE_KEY="license_"; + private static final String LICENSE_KEY = "license_"; private final List licenses; private final Logger logger = LoggerFactory.getLogger(LicenseController.class); @@ -56,15 +52,23 @@ public LicenseController(String licensePath, String uniqueId, UUID serverUUID) { if (!licenseDir.exists() || !licenseDir.isDirectory()) { throw new IllegalArgumentException("Invalid license path: " + licensePath); } + installLicenses(licenseDir); licenses = loadInstalledLicenses(licenseDir); - if(licenses.isEmpty()) { - fetchLicenseFromServer(licenseDir, uniqueId, serverUUID); + + if (licenses.isEmpty()) { + boolean fetched = fetchLicenseFromServer(licenseDir, uniqueId, serverUUID); + if (!fetched) { + LicenseFileStore licenseFileStore = new LicenseFileStore(logger); + licenseFileStore.ensureFallbackLicensePresent(licenseDir); + } + installLicenses(licenseDir); licenses.addAll(loadInstalledLicenses(licenseDir)); } + Gson gson = GsonFactory.getInstance().getPrettyGson(); - for(FeatureDetails feature : licenses) { + for (FeatureDetails feature : licenses) { logger.log(ServerLogMessages.LICENSE_FEATURES_AVAILABLE, gson.toJson(feature.getFeature())); } } @@ -73,11 +77,6 @@ public FeatureManager getFeatureManager() { return new FeatureManager(licenses); } - /** - * Installs any licenses that have not yet been installed. - * - * @param licenseDir Directory containing license files. - */ private void installLicenses(File licenseDir) { File[] files = licenseDir.listFiles((dir, name) -> name.startsWith(LICENSE_KEY) && name.endsWith(".lic")); if (files == null) { @@ -94,18 +93,16 @@ private void installLicenses(File licenseDir) { } } - - private void processLicenseFile(File licenseFile, String edition, File installedFile) { + private void processLicenseFile(File licenseFile, String edition, File installedFile) { try { LicenseManager manager = getLicenseManager(edition); - if(manager != null) { + if (manager != null) { logger.log(ServerLogMessages.LICENSE_INSTALLING, edition); manager.install(manager.parameters().encryption().source(BIOS.file(licenseFile))); - if(!licenseFile.renameTo(installedFile)){ + if (!licenseFile.renameTo(installedFile)) { logger.log(ServerLogMessages.LICENSE_FILE_RENAME_FAILED, licenseFile.getAbsolutePath(), installedFile.getAbsolutePath()); } - } - else{ + } else { logger.log(ServerLogMessages.LICENSE_MANAGER_NOT_FOUND, edition); } } catch (IllegalArgumentException | LicenseManagementException e) { @@ -114,22 +111,19 @@ private void processLicenseFile(File licenseFile, String edition, File installe } private LicenseManager getLicenseManager(String edition) { - for(LicenseManager manager : LicenseManager.values()) { - if(edition.equalsIgnoreCase(manager.name())) { + for (LicenseManager manager : LicenseManager.values()) { + if (edition.equalsIgnoreCase(manager.name())) { return manager; } } return null; } - /** - * Scans installed licenses and loads them. - * - * @param licenseDir Directory containing installed license files. - */ - private List loadInstalledLicenses(File licenseDir) { + private List loadInstalledLicenses(File licenseDir) { File[] files = licenseDir.listFiles((dir, name) -> name.startsWith(LICENSE_KEY) && name.endsWith(".lic_installed")); - if (files == null) return new ArrayList<>(); + if (files == null) { + return new ArrayList<>(); + } List licenseList = new ArrayList<>(); @@ -137,17 +131,16 @@ private List loadInstalledLicenses(File licenseDir) { String edition = extractEdition(installedFile.getName()); try { LicenseManager manager = getLicenseManager(edition.toUpperCase()); - if(manager != null) { + if (manager != null) { logger.log(ServerLogMessages.LICENSE_LOADING, edition); - if(!processLicense( manager.load(), licenseList)){ + if (!processLicense(manager.load(), licenseList)) { logger.log(ServerLogMessages.LICENSE_UNINSTALLING, edition); - if(!installedFile.delete()){ + if (!installedFile.delete()) { logger.log(ServerLogMessages.LICENSE_FAILED_DELETE_FILE, installedFile.getAbsolutePath()); } manager.uninstall(); } - } - else{ + } else { logger.log(ServerLogMessages.LICENSE_MANAGER_NOT_FOUND, edition); } } catch (IllegalArgumentException | LicenseManagementException e) { @@ -157,127 +150,70 @@ private List loadInstalledLicenses(File licenseDir) { return licenseList; } - private boolean processLicense(License license,List licenseList) { + @SuppressWarnings("unchecked") + private boolean processLicense(License license, List licenseList) { long now = System.currentTimeMillis(); - if(license != null) { + if (license != null) { if (license.getNotBefore().getTime() < now && license.getNotAfter().getTime() > now) { Gson gson = GsonFactory.getInstance().getSimpleGson(); Map extraData = (Map) license.getExtra(); String json = gson.toJson(extraData); Features features = gson.fromJson(json, Features.class); + Date after = license.getNotAfter(); Date before = license.getNotBefore(); Date issued = license.getIssued(); String info = license.getInfo(); String who = license.getIssuer().getName(); + FeatureDetails featureDetails = new FeatureDetails(); featureDetails.setFeature(features); featureDetails.setExpiry(Instant.ofEpochMilli(after.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime()); featureDetails.setInfo(info); licenseList.add(featureDetails); - - logger.log(ServerLogMessages.LICENSE_LOADED, info, who, issued, after, before, gson.toJson(extraData)); + logger.log(ServerLogMessages.LICENSE_LOADED, info, who, issued, after, before, gson.toJson(extraData)); return true; } else { logger.log(ServerLogMessages.LICENSE_EXPIRED, license.getInfo(), license.getNotBefore(), license.getNotAfter()); - return (license.getNotAfter().getTime() > now); // Do NOT delete the license if it is still valid but can not yet be used + return (license.getNotAfter().getTime() > now); } } return false; } - private void fetchLicenseFromServer(File licenseDir, String uniqueId, UUID serverUUID) { + private boolean fetchLicenseFromServer(File licenseDir, String uniqueId, UUID serverUUID) { try { LicenseConfig licenseConfig = new LicenseConfig(); licenseConfig = (LicenseConfig) licenseConfig.load(null); String clientSecret = licenseConfig.getClientSecret(); String clientName = licenseConfig.getClientName(); - URI uri = URI.create(LICENSE_SERVER_URL); - URL url = uri.toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("POST"); - connection.setDoOutput(true); - connection.setRequestProperty("Content-Type", "application/json"); - -// Build JSON request body - String jsonBody = String.format( - "{\"clientName\":\"%s\",\"clientSecret\":\"%s\",\"uniqueServerId\":\"%s\",\"serverUUID\":\"%s\"}", - clientName, clientSecret, uniqueId, serverUUID.toString() - ); - logger.log(ServerLogMessages.LICENSE_CONTACTING_SERVER, clientName); - try (OutputStream os = connection.getOutputStream()) { - os.write(jsonBody.getBytes(StandardCharsets.UTF_8)); + + LicenseServerClient licenseServerClient = new LicenseServerClient(logger); + List serverLicenses = + licenseServerClient.fetchLicenses(clientName, clientSecret, uniqueId, serverUUID); + + if (serverLicenses.isEmpty()) { + return false; } - // Read response - int responseCode = connection.getResponseCode(); - if (responseCode == 200) { - try (InputStream is = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + LicenseFileStore licenseFileStore = new LicenseFileStore(logger); + boolean savedAny = false; - StringBuilder response = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - response.append(line); - } - // Parse response - List> mapList = parseLicenseResponse(response.toString()); - for (Map licenseData : mapList) { - String type = licenseData.get("type"); - Base64.Decoder decoder = Base64.getDecoder(); - byte[] license = decoder.decode (licenseData.get("license")); - if (type != null && license != null) { - saveLicenseFile(licenseDir, type, license); - } - } - } - } else { - logger.log(ServerLogMessages.LICENSE_ERROR_CONTACTING_SERVER, responseCode); + for (LicenseServerResponse response : serverLicenses) { + boolean saved = licenseFileStore.saveLicenseFile(licenseDir, response.getType(), response.getLicenseContent()); + savedAny = savedAny || saved; } - } catch (IOException e) { - logger.log(ServerLogMessages.LICENSE_FAILED_CONTACTING_SERVER, e); - } - } - /** - * Parses the JSON response from the license server. - * - * @param response JSON response string. - * @return A list of license details (type and license string). - */ - private List> parseLicenseResponse(String response) { - try { - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.readValue(response, new TypeReference>>() {}); + return savedAny; } catch (Exception e) { - return List.of(); - } - } - - /** - * Saves the retrieved license file to disk. - */ - private void saveLicenseFile(File licenseDir, String edition, byte[] licenseContent) { - File licenseFile = new File(licenseDir, LICENSE_KEY + edition + ".lic"); - try (FileOutputStream fos = new FileOutputStream(licenseFile)) { - fos.write(licenseContent); - logger.log(ServerLogMessages.LICENSE_SAVED_TO_FILE, licenseFile.getAbsolutePath()); - } catch (IOException e) { - logger.log(ServerLogMessages.LICENSE_FAILED_SAVED_TO_FILE, licenseFile.getAbsolutePath(), e); + logger.log(ServerLogMessages.LICENSE_FAILED_CONTACTING_SERVER, e); + return false; } } - /** - * Extracts the edition name from a license file name. - * Example: "license_enterprise.lic" -> "enterprise" - * - * @param filename License file name. - * @return Extracted edition. - */ private String extractEdition(String filename) { return filename.replace(LICENSE_KEY, "").replace(".lic", "").replace("_installed", ""); } - -} +} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/license/LicenseFileStore.java b/src/main/java/io/mapsmessaging/license/LicenseFileStore.java new file mode 100644 index 000000000..9f04e43a6 --- /dev/null +++ b/src/main/java/io/mapsmessaging/license/LicenseFileStore.java @@ -0,0 +1,156 @@ +/* + * + * Copyright [ 2020 - 2024 ] Matthew Buckton + * Copyright [ 2024 - 2026 ] MapsMessaging B.V. + * + * Licensed under the Apache License, Version 2.0 with the Commons Clause + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * https://commonsclause.com/ + * + * Unless required by applicable law or agreed to in writing, software + * distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mapsmessaging.license; + +import io.mapsmessaging.MapsEnvironment; +import io.mapsmessaging.logging.Logger; +import io.mapsmessaging.logging.ServerLogMessages; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class LicenseFileStore { + + private static final String LICENSE_KEY = "license_"; + private static final String FALLBACK_LICENSE_FILE_NAME = LICENSE_KEY+"community.lic"; + private static final String FALLBACK_EDITION = "community"; + + private final Logger logger; + + public LicenseFileStore(Logger logger) { + this.logger = logger; + } + + public boolean saveLicenseFile(File licenseDir, String edition, byte[] licenseContent) { + File licenseFile = new File(licenseDir, LICENSE_KEY + edition + ".lic"); + try (FileOutputStream fileOutputStream = new FileOutputStream(licenseFile)) { + fileOutputStream.write(licenseContent); + logger.log(ServerLogMessages.LICENSE_SAVED_TO_FILE, licenseFile.getAbsolutePath()); + return true; + } catch (IOException e) { + logger.log(ServerLogMessages.LICENSE_FAILED_SAVED_TO_FILE, licenseFile.getAbsolutePath(), e); + return false; + } + } + + /** + * Ensures a fallback bundled license exists in the expected "license_*.lic" form inside licenseDir. + * + * Source order: + * 1) Classpath: "/conf/community.lic" + * 2) Classpath: "/community.lic" + * 3) Filesystem: MAPS_HOME/conf/community.lic + * + * Target: + * - licenseDir/license_community.lic + */ + public void ensureFallbackLicensePresent(File licenseDir) { + if (licenseDir == null || !licenseDir.exists() || !licenseDir.isDirectory()) { + return; + } + + File[] existingLicenses = licenseDir.listFiles((dir, name) -> name.startsWith(LICENSE_KEY) && name.endsWith(".lic")); + if (existingLicenses != null && existingLicenses.length > 0) { + return; + } + + File preferred = new File(licenseDir, LICENSE_KEY + FALLBACK_EDITION + ".lic"); + if (preferred.exists()) { + return; + } + + if (copyFromClasspath(preferred)) { + return; + } + + copyFromMapsHome(preferred); + } + + private boolean copyFromClasspath(File destination) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = LicenseFileStore.class.getClassLoader(); + } + + InputStream inputStream = classLoader.getResourceAsStream( FALLBACK_LICENSE_FILE_NAME); + if (inputStream == null) { + inputStream = classLoader.getResourceAsStream(FALLBACK_LICENSE_FILE_NAME); + } + + if (inputStream == null) { + return false; + } + + try (InputStream sourceStream = inputStream) { + writeStreamToFile(sourceStream, destination); + logger.log(ServerLogMessages.LICENSE_SAVED_TO_FILE, destination.getAbsolutePath()); + return true; + } catch (IOException e) { + logger.log(ServerLogMessages.LICENSE_FAILED_SAVED_TO_FILE, destination.getAbsolutePath(), e); + return false; + } + } + + private void copyFromMapsHome(File destination) { + try { + String mapsHome = MapsEnvironment.getMapsHome(); + if (mapsHome == null || mapsHome.isEmpty()) { + return; + } + + Path sourcePath = Path.of(mapsHome, "conf", FALLBACK_LICENSE_FILE_NAME); + if (!Files.exists(sourcePath) || !Files.isRegularFile(sourcePath)) { + return; + } + + copyFile(sourcePath.toFile(), destination); + logger.log(ServerLogMessages.LICENSE_SAVED_TO_FILE, destination.getAbsolutePath()); + } catch (Exception e) { + logger.log(ServerLogMessages.LICENSE_FAILED_SAVED_TO_FILE, destination.getAbsolutePath(), e); + } + } + + private void copyFile(File source, File destination) throws IOException { + try (FileInputStream fileInputStream = new FileInputStream(source); + FileOutputStream fileOutputStream = new FileOutputStream(destination)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = fileInputStream.read(buffer)) != -1) { + fileOutputStream.write(buffer, 0, read); + } + } + } + + private void writeStreamToFile(InputStream sourceStream, File destination) throws IOException { + try (FileOutputStream fileOutputStream = new FileOutputStream(destination)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = sourceStream.read(buffer)) != -1) { + fileOutputStream.write(buffer, 0, read); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/license/LicenseServerClient.java b/src/main/java/io/mapsmessaging/license/LicenseServerClient.java new file mode 100644 index 000000000..c9dce1be5 --- /dev/null +++ b/src/main/java/io/mapsmessaging/license/LicenseServerClient.java @@ -0,0 +1,121 @@ +/* + * + * Copyright [ 2020 - 2024 ] Matthew Buckton + * Copyright [ 2024 - 2026 ] MapsMessaging B.V. + * + * Licensed under the Apache License, Version 2.0 with the Commons Clause + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * https://commonsclause.com/ + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mapsmessaging.license; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mapsmessaging.logging.Logger; +import io.mapsmessaging.logging.ServerLogMessages; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class LicenseServerClient { + + private static final String LICENSE_SERVER_URL = "https://license.mapsmessaging.io/api/v1/license"; + + private final Logger logger; + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + private final URI serverUrl; + + public LicenseServerClient(Logger logger) { + this(logger, URI.create(LICENSE_SERVER_URL)); + } + + public LicenseServerClient(Logger logger, URI serverUrl) { + this.logger = logger; + this.serverUrl = serverUrl; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(2)) + .build(); + this.objectMapper = new ObjectMapper(); + } + + public List fetchLicenses(String clientName, String clientSecret, String uniqueId, UUID serverUUID) { + try { + String jsonBody = String.format( + "{\"clientName\":\"%s\",\"clientSecret\":\"%s\",\"uniqueServerId\":\"%s\",\"serverUUID\":\"%s\"}", + clientName, clientSecret, uniqueId, serverUUID.toString() + ); + + HttpRequest request = HttpRequest.newBuilder() + .uri(serverUrl) + .timeout(Duration.ofSeconds(5)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8)) + .build(); + + logger.log(ServerLogMessages.LICENSE_CONTACTING_SERVER, clientName); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + int responseCode = response.statusCode(); + + if (responseCode != 200) { + logger.log(ServerLogMessages.LICENSE_ERROR_CONTACTING_SERVER, responseCode); + return List.of(); + } + + return parseResponse(response.body()); + } catch (Exception e) { + logger.log(ServerLogMessages.LICENSE_FAILED_CONTACTING_SERVER, e); + return List.of(); + } + } + + private List parseResponse(String responseBody) { + try { + List> mapList = + objectMapper.readValue(responseBody, new TypeReference>>() {}); + + if (mapList == null || mapList.isEmpty()) { + return List.of(); + } + + List results = new ArrayList<>(); + Base64.Decoder decoder = Base64.getDecoder(); + + for (Map licenseData : mapList) { + String type = licenseData.get("type"); + String encodedLicense = licenseData.get("license"); + + if (type == null || encodedLicense == null) { + continue; + } + + byte[] licenseContent = decoder.decode(encodedLicense); + results.add(new LicenseServerResponse(type, licenseContent)); + } + + return results; + } catch (Exception e) { + return List.of(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/license/LicenseServerResponse.java b/src/main/java/io/mapsmessaging/license/LicenseServerResponse.java new file mode 100644 index 000000000..cb60f4bad --- /dev/null +++ b/src/main/java/io/mapsmessaging/license/LicenseServerResponse.java @@ -0,0 +1,39 @@ +/* + * + * Copyright [ 2020 - 2024 ] Matthew Buckton + * Copyright [ 2024 - 2026 ] MapsMessaging B.V. + * + * Licensed under the Apache License, Version 2.0 with the Commons Clause + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * https://commonsclause.com/ + * + * Unless required by applicable law or agreed to in writing, software + * distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mapsmessaging.license; + +public class LicenseServerResponse { + + private final String type; + private final byte[] licenseContent; + + public LicenseServerResponse(String type, byte[] licenseContent) { + this.type = type; + this.licenseContent = licenseContent; + } + + public String getType() { + return type; + } + + public byte[] getLicenseContent() { + return licenseContent; + } +} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/license/tools/LicenseFetcher.java b/src/main/java/io/mapsmessaging/license/tools/LicenseFetcher.java new file mode 100644 index 000000000..ffb62f090 --- /dev/null +++ b/src/main/java/io/mapsmessaging/license/tools/LicenseFetcher.java @@ -0,0 +1,79 @@ +package io.mapsmessaging.license.tools; + +import io.mapsmessaging.license.LicenseFileStore; +import io.mapsmessaging.license.LicenseServerClient; +import io.mapsmessaging.license.LicenseServerResponse; +import io.mapsmessaging.logging.Logger; +import io.mapsmessaging.logging.LoggerFactory; + +import java.io.File; +import java.util.List; +import java.util.UUID; + +public class LicenseFetcher { + + public static void main(String[] args) throws Exception { + int exitCode = runMain(args); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + static int runMain(String[] args) throws Exception { + if (args == null || args.length != 1) { + System.err.println("Usage: LicenseFetcher "); + return 1; + } + + File licenseDir = new File(args[0]); + if (!licenseDir.exists() && !licenseDir.mkdirs()) { + System.err.println("Failed to create license directory: " + licenseDir.getAbsolutePath()); + return 2; + } + + Logger logger = LoggerFactory.getLogger(LicenseFetcher.class); + + String uniqueId = "build"; + UUID serverUUID = UUID.randomUUID(); + + LicenseServerClient client = new LicenseServerClient(logger); + LicenseFileStore store = new LicenseFileStore(logger); + + return run(licenseDir, client, store, uniqueId, serverUUID); + } + + static int run( + File licenseDir, + LicenseServerClient licenseServerClient, + LicenseFileStore licenseFileStore, + String uniqueId, + UUID serverUUID + ) { + if (licenseDir == null || licenseServerClient == null || licenseFileStore == null) { + return 3; + } + + List licenses = licenseServerClient.fetchLicenses("", "", uniqueId, serverUUID); + if (licenses == null || licenses.isEmpty()) { + return 10; + } + + boolean wroteAtLeastOne = false; + + for (LicenseServerResponse license : licenses) { + if (license == null || license.getType() == null || license.getLicenseContent() == null) { + continue; + } + + boolean saved = licenseFileStore.saveLicenseFile( + licenseDir, + license.getType(), + license.getLicenseContent() + ); + + wroteAtLeastOne = wroteAtLeastOne || saved; + } + + return wroteAtLeastOne ? 0 : 11; + } +} \ No newline at end of file diff --git a/src/test/java/io/mapsmessaging/license/LicenseServerClientTest.java b/src/test/java/io/mapsmessaging/license/LicenseServerClientTest.java new file mode 100644 index 000000000..3369178f8 --- /dev/null +++ b/src/test/java/io/mapsmessaging/license/LicenseServerClientTest.java @@ -0,0 +1,171 @@ +/* + * + * Copyright [ 2020 - 2024 ] Matthew Buckton + * Copyright [ 2024 - 2026 ] MapsMessaging B.V. + * + * Licensed under the Apache License, Version 2.0 with the Commons Clause + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * https://commonsclause.com/ + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.mapsmessaging.license; + +import io.mapsmessaging.logging.Logger; +import io.mapsmessaging.logging.LoggerFactory; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +class LicenseServerClientTest { + + private MockWebServer mockWebServer; + + @BeforeEach + void setup() throws Exception { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + } + + @AfterEach + void tearDown() throws Exception { + if (mockWebServer != null) { + mockWebServer.shutdown(); + } + } + + @Test + void fetchLicenses_success_returnsDecodedLicenses() throws Exception { + byte[] licenseBytes = "test-license-bytes".getBytes(StandardCharsets.UTF_8); + String base64License = Base64.getEncoder().encodeToString(licenseBytes); + + String responseJson = + "[" + + "{\"type\":\"community\",\"license\":\"" + base64License + "\"}" + + "]"; + + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(responseJson) + ); + + URI serverUri = mockWebServer.url("/api/v1/license").uri(); + + Logger logger = LoggerFactory.getLogger(LicenseServerClientTest.class); + LicenseServerClient licenseServerClient = new LicenseServerClient(logger, serverUri); + + String clientName = "build-client"; + String clientSecret = "build-secret"; + String uniqueId = "build"; + UUID serverUuid = UUID.fromString("11111111-2222-3333-4444-555555555555"); + + List responses = + licenseServerClient.fetchLicenses(clientName, clientSecret, uniqueId, serverUuid); + + Assertions.assertNotNull(responses); + Assertions.assertEquals(1, responses.size()); + + LicenseServerResponse first = responses.get(0); + Assertions.assertEquals("community", first.getType()); + Assertions.assertArrayEquals(licenseBytes, first.getLicenseContent()); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + Assertions.assertEquals("POST", recordedRequest.getMethod()); + Assertions.assertEquals("/api/v1/license", recordedRequest.getPath()); + Assertions.assertEquals("application/json", recordedRequest.getHeader("Content-Type")); + + String postedBody = recordedRequest.getBody().readUtf8(); + Assertions.assertTrue(postedBody.contains("\"clientName\":\"" + clientName + "\"")); + Assertions.assertTrue(postedBody.contains("\"clientSecret\":\"" + clientSecret + "\"")); + Assertions.assertTrue(postedBody.contains("\"uniqueServerId\":\"" + uniqueId + "\"")); + Assertions.assertTrue(postedBody.contains("\"serverUUID\":\"" + serverUuid + "\"")); + } + + @Test + void fetchLicenses_non200_returnsEmptyList() throws Exception { + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(500) + .addHeader("Content-Type", "application/json") + .setBody("{\"error\":\"nope\"}") + ); + + URI serverUri = mockWebServer.url("/api/v1/license").uri(); + + Logger logger = LoggerFactory.getLogger(LicenseServerClientTest.class); + LicenseServerClient licenseServerClient = new LicenseServerClient(logger, serverUri); + + List responses = + licenseServerClient.fetchLicenses("client", "secret", "build", UUID.randomUUID()); + + Assertions.assertNotNull(responses); + Assertions.assertTrue(responses.isEmpty()); + } + + @Test + void fetchLicenses_invalidJson_returnsEmptyList() throws Exception { + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody("this is not json") + ); + + URI serverUri = mockWebServer.url("/api/v1/license").uri(); + + Logger logger = LoggerFactory.getLogger(LicenseServerClientTest.class); + LicenseServerClient licenseServerClient = new LicenseServerClient(logger, serverUri); + + List responses = + licenseServerClient.fetchLicenses("client", "secret", "build", UUID.randomUUID()); + + Assertions.assertNotNull(responses); + Assertions.assertTrue(responses.isEmpty()); + } + + @Test + void fetchLicenses_invalidBase64_skipsEntryOrReturnsEmpty() throws Exception { + String responseJson = + "[" + + "{\"type\":\"community\",\"license\":\"NOT_BASE64!!!!\"}" + + "]"; + + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(responseJson) + ); + + URI serverUri = mockWebServer.url("/api/v1/license").uri(); + + Logger logger = LoggerFactory.getLogger(LicenseServerClientTest.class); + LicenseServerClient licenseServerClient = new LicenseServerClient(logger, serverUri); + + List responses = + licenseServerClient.fetchLicenses("client", "secret", "build", UUID.randomUUID()); + + Assertions.assertNotNull(responses); + Assertions.assertTrue(responses.isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/io/mapsmessaging/license/tools/LicenseFetcherTest.java b/src/test/java/io/mapsmessaging/license/tools/LicenseFetcherTest.java new file mode 100644 index 000000000..62f6d8961 --- /dev/null +++ b/src/test/java/io/mapsmessaging/license/tools/LicenseFetcherTest.java @@ -0,0 +1,83 @@ +package io.mapsmessaging.license.tools; + +import io.mapsmessaging.license.LicenseFileStore; +import io.mapsmessaging.license.LicenseServerClient; +import io.mapsmessaging.license.LicenseServerResponse; +import io.mapsmessaging.logging.Logger; +import io.mapsmessaging.logging.LoggerFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.UUID; + +class LicenseFetcherTest { + + @TempDir + File tempDir; + + @Test + void runMain_noArgs_returns1() throws Exception { + int code = LicenseFetcher.runMain(new String[0]); + Assertions.assertEquals(1, code); + } + + @Test + void runMain_createsDir_andReturns10WhenNoLicenses() throws Exception { + File outputDir = new File(tempDir, "license"); + + Logger logger = LoggerFactory.getLogger(LicenseFetcherTest.class); + + LicenseServerClient client = new FakeLicenseServerClient(logger, List.of()); + LicenseFileStore store = new LicenseFileStore(logger); + + int code = LicenseFetcher.run(outputDir, client, store, "build", UUID.fromString("11111111-2222-3333-4444-555555555555")); + + Assertions.assertEquals(10, code); + Assertions.assertFalse(outputDir.exists() && outputDir.listFiles() != null && outputDir.listFiles().length > 0); + } + + @Test + void run_writesLicenseFile_returns0() throws Exception { + File outputDir = new File(tempDir, "license"); + Assertions.assertTrue(outputDir.mkdirs()); + + byte[] licenseBytes = "community-license".getBytes(StandardCharsets.UTF_8); + + LicenseServerResponse response = new LicenseServerResponse("community", licenseBytes); + + Logger logger = LoggerFactory.getLogger(LicenseFetcherTest.class); + + LicenseServerClient client = new FakeLicenseServerClient(logger, List.of(response)); + LicenseFileStore store = new LicenseFileStore(logger); + + int code = LicenseFetcher.run(outputDir, client, store, "build", UUID.fromString("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")); + + Assertions.assertEquals(0, code); + + File expected = new File(outputDir, "license_community.lic"); + Assertions.assertTrue(expected.exists()); + + byte[] written = Files.readAllBytes(expected.toPath()); + Assertions.assertArrayEquals(licenseBytes, written); + } + + private static final class FakeLicenseServerClient extends LicenseServerClient { + + private final List responses; + + private FakeLicenseServerClient(Logger logger, List responses) { + super(logger); + this.responses = responses; + } + + @Override + public List fetchLicenses(String clientName, String clientSecret, String uniqueId, UUID serverUUID) { + return responses; + } + } +} \ No newline at end of file