diff --git a/.gitignore b/.gitignore
index 3ddf897..5d393b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ replay_pid*
.idea
cmake-build-*/
*.iws
+work/
out/
build/
.idea_modules/
diff --git a/pom.xml b/pom.xml
index ba4e6b5..1a1bb24 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,10 +3,17 @@
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.0
+
+ me.braydonLicenseServer
- 1.0-SNAPSHOT
+ 1.0.0
+ A simple open-source licensing server for your products.20
@@ -15,36 +22,48 @@
UTF-8
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.1
-
-
- ${java.version}
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.1.0
-
-
- package
-
- shade
-
-
-
-
-
-
-
- src/main/resources
- true
-
-
-
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.26
+ provided
+
+
+
+
+ org.mindrot
+ jbcrypt
+ 0.4
+ compile
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+ compile
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+ compile
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ compile
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
\ No newline at end of file
diff --git a/src/main/java/me/braydon/license/LicenseServer.java b/src/main/java/me/braydon/license/LicenseServer.java
new file mode 100644
index 0000000..b475ad6
--- /dev/null
+++ b/src/main/java/me/braydon/license/LicenseServer.java
@@ -0,0 +1,39 @@
+package me.braydon.license;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import lombok.NonNull;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Objects;
+
+/**
+ * @author Braydon
+ */
+@SpringBootApplication
+@Slf4j
+public class LicenseServer {
+ public static final Gson GSON = new GsonBuilder()
+ .serializeNulls()
+ .create();
+
+ @SneakyThrows
+ public static void main(@NonNull String[] args) {
+ File config = new File("application.yml");
+ if (!config.exists()) { // Saving the default config if it doesn't exist locally
+ Files.copy(Objects.requireNonNull(LicenseServer.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved
+ config.getAbsolutePath()
+ );
+ return;
+ }
+ log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
+ SpringApplication.run(LicenseServer.class, args); // Load the application
+ }
+}
diff --git a/src/main/java/me/braydon/license/common/RandomUtils.java b/src/main/java/me/braydon/license/common/RandomUtils.java
new file mode 100644
index 0000000..9fca53a
--- /dev/null
+++ b/src/main/java/me/braydon/license/common/RandomUtils.java
@@ -0,0 +1,30 @@
+package me.braydon.license.common;
+
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+
+/**
+ * @author Braydon
+ */
+@UtilityClass
+public final class RandomUtils {
+ /**
+ * The license key format to use.
+ */
+ private static final String LICENSE_KEY_FORMAT = "%04X-%04X-%04X-%04X";
+
+ /**
+ * Generate a random license key.
+ *
+ * @return the random license key
+ */
+ @NonNull
+ public static String generateLicenseKey() {
+ int segments = LICENSE_KEY_FORMAT.split("-").length; // The amount of segments
+ Object[] parts = new Object[segments];
+ for (int i = 0; i < segments; i++) { // Generate a random part for each segment
+ parts[i] = (int) (Math.random() * 0xFFFF);
+ }
+ return String.format(LICENSE_KEY_FORMAT, parts);
+ }
+}
diff --git a/src/main/java/me/braydon/license/controller/LicenseController.java b/src/main/java/me/braydon/license/controller/LicenseController.java
new file mode 100644
index 0000000..82b3fa9
--- /dev/null
+++ b/src/main/java/me/braydon/license/controller/LicenseController.java
@@ -0,0 +1,59 @@
+package me.braydon.license.controller;
+
+import com.google.gson.JsonObject;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.NonNull;
+import me.braydon.license.LicenseServer;
+import me.braydon.license.exception.APIException;
+import me.braydon.license.model.License;
+import me.braydon.license.service.LicenseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * @author Braydon
+ */
+@RestController
+@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
+public final class LicenseController {
+ /**
+ * The {@link LicenseService} to use.
+ */
+ @NonNull private final LicenseService service;
+
+ @Autowired
+ public LicenseController(@NonNull LicenseService service) {
+ this.service = service;
+ }
+
+ /**
+ * This route handle checking of licenses.
+ *
+ * @param body the body of the request
+ * @return the response entity
+ * @see License for license
+ * @see ResponseEntity for response entity
+ */
+ @GetMapping("/check")
+ @ResponseBody
+ public ResponseEntity> check(@NonNull HttpServletRequest request, @RequestBody @NonNull String body) {
+ try { // Attempt to check the license
+ String ip = request.getRemoteAddr(); // The IP of the requester
+
+ JsonObject jsonObject = LicenseServer.GSON.fromJson(body, JsonObject.class);
+ String key = jsonObject.get("key").getAsString(); // Get the key
+ String product = jsonObject.get("product").getAsString(); // Get the product
+ String hwid = jsonObject.get("hwid").getAsString(); // Get the hwid
+
+ service.check(key, product, ip, hwid); // Check the license
+ return ResponseEntity.ok().build(); // Return OK
+ } catch (APIException ex) { // Handle the exception
+ return ResponseEntity.status(ex.getStatus())
+ .body(Map.of("error", ex.getLocalizedMessage()));
+ }
+ }
+}
diff --git a/src/main/java/me/braydon/license/exception/APIException.java b/src/main/java/me/braydon/license/exception/APIException.java
new file mode 100644
index 0000000..d35fb30
--- /dev/null
+++ b/src/main/java/me/braydon/license/exception/APIException.java
@@ -0,0 +1,25 @@
+package me.braydon.license.exception;
+
+import lombok.Getter;
+import lombok.NonNull;
+import org.springframework.http.HttpStatus;
+
+/**
+ * Represents an API exception.
+ *
+ * @author Braydon
+ */
+@Getter
+public class APIException extends RuntimeException {
+ /**
+ * The status of this exception.
+ *
+ * @see HttpStatus for status
+ */
+ @NonNull private final HttpStatus status;
+
+ public APIException(@NonNull HttpStatus status, @NonNull String message) {
+ super(message);
+ this.status = status;
+ }
+}
diff --git a/src/main/java/me/braydon/license/exception/LicenseHwidLimitExceededException.java b/src/main/java/me/braydon/license/exception/LicenseHwidLimitExceededException.java
new file mode 100644
index 0000000..b22df78
--- /dev/null
+++ b/src/main/java/me/braydon/license/exception/LicenseHwidLimitExceededException.java
@@ -0,0 +1,17 @@
+package me.braydon.license.exception;
+
+import me.braydon.license.model.License;
+import org.springframework.http.HttpStatus;
+
+/**
+ * This exception is raised when
+ * a {@link License} has its HWID
+ * limit exceeded.
+ *
+ * @author Braydon
+ */
+public class LicenseHwidLimitExceededException extends APIException {
+ public LicenseHwidLimitExceededException() {
+ super(HttpStatus.BAD_REQUEST, "License key HWID limit has been exceeded");
+ }
+}
diff --git a/src/main/java/me/braydon/license/exception/LicenseIpLimitExceededException.java b/src/main/java/me/braydon/license/exception/LicenseIpLimitExceededException.java
new file mode 100644
index 0000000..0dddf54
--- /dev/null
+++ b/src/main/java/me/braydon/license/exception/LicenseIpLimitExceededException.java
@@ -0,0 +1,17 @@
+package me.braydon.license.exception;
+
+import me.braydon.license.model.License;
+import org.springframework.http.HttpStatus;
+
+/**
+ * This exception is raised when
+ * a {@link License} has its IP
+ * limit exceeded.
+ *
+ * @author Braydon
+ */
+public class LicenseIpLimitExceededException extends APIException {
+ public LicenseIpLimitExceededException() {
+ super(HttpStatus.BAD_REQUEST, "License key IP limit has been exceeded");
+ }
+}
diff --git a/src/main/java/me/braydon/license/exception/LicenseNotFoundException.java b/src/main/java/me/braydon/license/exception/LicenseNotFoundException.java
new file mode 100644
index 0000000..c25d77f
--- /dev/null
+++ b/src/main/java/me/braydon/license/exception/LicenseNotFoundException.java
@@ -0,0 +1,16 @@
+package me.braydon.license.exception;
+
+import me.braydon.license.model.License;
+import org.springframework.http.HttpStatus;
+
+/**
+ * This exception is raised when
+ * a {@link License} is not found.
+ *
+ * @author Braydon
+ */
+public class LicenseNotFoundException extends APIException {
+ public LicenseNotFoundException() {
+ super(HttpStatus.NOT_FOUND, "License not found");
+ }
+}
diff --git a/src/main/java/me/braydon/license/model/License.java b/src/main/java/me/braydon/license/model/License.java
new file mode 100644
index 0000000..73c3dd9
--- /dev/null
+++ b/src/main/java/me/braydon/license/model/License.java
@@ -0,0 +1,92 @@
+package me.braydon.license.model;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.ToString;
+import me.braydon.license.exception.APIException;
+import me.braydon.license.exception.LicenseHwidLimitExceededException;
+import me.braydon.license.exception.LicenseIpLimitExceededException;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * Represents a license key.
+ *
+ * @author Braydon
+ */
+@Document("keys")
+@Setter
+@Getter
+@ToString
+public class License {
+ /**
+ * The key of this license.
+ */
+ @Id @NonNull private String key;
+
+ /**
+ * The product this license is for.
+ */
+ @NonNull private String product;
+
+ /**
+ * The optional description of this license.
+ */
+ private String description;
+
+ /**
+ * The amount of uses this license has.
+ */
+ private int uses;
+
+ /**
+ * The IPs used on this license.
+ *
+ * These IPs are encrypted using AES-256.
+ *
+ */
+ private Set ips;
+
+ /**
+ * The hardware IDs that were used on this license.
+ */
+ private Set hwids;
+
+ /**
+ * The limit of IPs that can be used on this license.
+ */
+ private int ipLimit;
+
+ /**
+ * The limit of HWIDs that can be used on this license.
+ */
+ private int hwidLimit;
+
+ /**
+ * The date this license was created.
+ */
+ @NonNull private Date created;
+
+ /**
+ * Invoked when this license is used.
+ *
+ * @param ip the ip used
+ * @param hwid the hardware id used
+ */
+ public void use(@NonNull String ip, @NonNull String hwid) throws APIException {
+ if (!ips.contains(ip) && ips.size() >= ipLimit) { // IP limit has been exceeded
+ throw new LicenseIpLimitExceededException();
+ }
+ if (!hwids.contains(hwid) && hwids.size() >= hwidLimit) { // HWID limit has been exceeded
+ throw new LicenseHwidLimitExceededException();
+ }
+ // The license was used
+ uses++; // Increment uses
+ ips.add(ip); // Add the used IP
+ hwids.add(hwid); // Add the used HWID
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/license/repository/LicenseRepository.java b/src/main/java/me/braydon/license/repository/LicenseRepository.java
new file mode 100644
index 0000000..40890fc
--- /dev/null
+++ b/src/main/java/me/braydon/license/repository/LicenseRepository.java
@@ -0,0 +1,29 @@
+package me.braydon.license.repository;
+
+import lombok.NonNull;
+import me.braydon.license.model.License;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+/**
+ * The repository for {@link License}'s.
+ *
+ * @author Braydon
+ */
+@Repository
+public interface LicenseRepository extends MongoRepository {
+ /**
+ * Get the license that has the given
+ * key and is for the given product.
+ *
+ * @param key the key to get
+ * @param product the product the key is for
+ * @return the optional license
+ * @see License for license
+ */
+ @Query("{ key: ?0, product: ?1 }")
+ Optional getLicense(@NonNull String key, @NonNull String product);
+}
diff --git a/src/main/java/me/braydon/license/service/LicenseService.java b/src/main/java/me/braydon/license/service/LicenseService.java
new file mode 100644
index 0000000..2a41067
--- /dev/null
+++ b/src/main/java/me/braydon/license/service/LicenseService.java
@@ -0,0 +1,102 @@
+package me.braydon.license.service;
+
+import jakarta.annotation.PostConstruct;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import me.braydon.license.common.RandomUtils;
+import me.braydon.license.exception.APIException;
+import me.braydon.license.exception.LicenseNotFoundException;
+import me.braydon.license.model.License;
+import me.braydon.license.repository.LicenseRepository;
+import org.mindrot.jbcrypt.BCrypt;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Optional;
+
+/**
+ * The service for managing {@link License}'s.
+ *
+ * @author Braydon
+ */
+@Service
+@Slf4j
+public final class LicenseService {
+ /**
+ * The {@link LicenseRepository} to use.
+ */
+ @NonNull private final LicenseRepository repository;
+
+ /**
+ * The salt to use for hashing license keys.
+ */
+ @Value("${key-salt}")
+ @NonNull private String keySalt;
+
+ @Autowired
+ public LicenseService(@NonNull LicenseRepository repository) {
+ this.repository = repository;
+ }
+
+ @PostConstruct
+ public void onInitialize() {
+ String key = RandomUtils.generateLicenseKey();
+ log.info(create(key,
+ "CloudSpigot",
+ "Testing " + Math.random(), Integer.MAX_VALUE, Integer.MAX_VALUE).toString());
+ System.out.println("key = " + key);
+ }
+
+ /**
+ * Create a new license key.
+ *
+ * @param key the key of the license
+ * @param product the product the license is for
+ * @param description the optional description of the license
+ * @param ipLimit the IP limit of the license
+ * @param hwidLimit the HWID limit of the license
+ * @return the created license
+ * @see License for license
+ */
+ public License create(@NonNull String key, @NonNull String product,
+ String description, int ipLimit, int hwidLimit) {
+ // Create the new license
+ License license = new License();
+ license.setKey(BCrypt.hashpw(key, keySalt)); // Hash the key
+ license.setProduct(product); // Use the given product
+ license.setDescription(description); // Use the given description, if any
+ license.setIps(new HashSet<>());
+ license.setHwids(new HashSet<>());
+ license.setIpLimit(ipLimit); // Use the given IP limit
+ license.setHwidLimit(hwidLimit); // Use the given HWID limit
+ license.setCreated(new Date());
+ repository.insert(license); // Insert the newly created license
+ return license;
+ }
+
+ /**
+ * Check the given license.
+ *
+ * @param key the key to check
+ * @param product the product of the license
+ * @param ip the ip using the license
+ * @param hwid the hwid using the license
+ * @throws APIException if there was an error checking the license
+ * @see License for license
+ */
+ public void check(@NonNull String key, @NonNull String product,
+ @NonNull String ip, @NonNull String hwid) throws APIException {
+ Optional optionalLicense = repository.getLicense(BCrypt.hashpw(key, keySalt), product); // Get the license
+ if (optionalLicense.isEmpty()) { // License key not found
+ log.error("License key {} for product {} not found", key, product); // Log the error
+ throw new LicenseNotFoundException();
+ }
+ License license = optionalLicense.get(); // The license found
+ license.use(ip, hwid); // Use the license
+ repository.save(license); // Save the used license
+ log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..9a2912c
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,27 @@
+# Server Configuration
+server:
+ address: 0.0.0.0
+ port: 7500
+
+# The salt to use when hashing license keys.
+# This salt should be changed from the default.
+key-salt: "$2a$10$/nQyzQDMkCf97ZlJLLWa3O"
+
+# Log Configuration
+logging:
+ file:
+ path: "./logs"
+
+# Spring Configuration
+spring:
+ data:
+ # MongoDB - This is used to store persistent data
+ mongodb:
+ uri: "mongodb://licenseServer:p4$$w0rd@127.0.0.1:27017/licenseServer?authSource=admin"
+ auto-index-creation: true # Automatically create collection indexes
+
+ # Ignore
+ application:
+ name: "License Server"
+ banner:
+ location: "classpath:banner.txt"
\ No newline at end of file
diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt
new file mode 100644
index 0000000..9536b41
--- /dev/null
+++ b/src/main/resources/banner.txt
@@ -0,0 +1,8 @@
+ _ _ ___
+ | | (_)__ ___ _ _ ___ ___ / __| ___ _ ___ _____ _ _
+ | |__| / _/ -_) ' \(_- -_) \__ \/ -_) '_\ V / -_) '_|
+ |____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_|
+
+ | API Version - v${application.version}
+ | Spring Version - ${spring-boot.formatted-version}
+_______________________________________
\ No newline at end of file