Initial Commit

This commit is contained in:
Braydon 2023-05-31 01:54:34 -04:00
parent 92c43041f0
commit 9d3a4cd1d0
14 changed files with 514 additions and 33 deletions

1
.gitignore vendored

@ -14,6 +14,7 @@ replay_pid*
.idea
cmake-build-*/
*.iws
work/
out/
build/
.idea_modules/

85
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>me.braydon</groupId>
<artifactId>LicenseServer</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.0.0</version>
<description>A simple open-source licensing server for your products.</description>
<properties>
<java.version>20</java.version>
@ -15,36 +22,48 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>
<!-- BCrypt -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
<scope>compile</scope>
</dependency>
<!-- MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<scope>compile</scope>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>compile</scope>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -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
}
}

@ -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);
}
}

@ -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()));
}
}
}

@ -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;
}
}

@ -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");
}
}

@ -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");
}
}

@ -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");
}
}

@ -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.
* <p>
* These IPs are encrypted using AES-256.
* </p>
*/
private Set<String> ips;
/**
* The hardware IDs that were used on this license.
*/
private Set<String> 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
}
}

@ -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<License, String> {
/**
* 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<License> getLicense(@NonNull String key, @NonNull String product);
}

@ -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<License> 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);
}
}

@ -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"

@ -0,0 +1,8 @@
_ _ ___
| | (_)__ ___ _ _ ___ ___ / __| ___ _ ___ _____ _ _
| |__| / _/ -_) ' \(_-</ -_) \__ \/ -_) '_\ V / -_) '_|
|____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_|
| API Version - v${application.version}
| Spring Version - ${spring-boot.formatted-version}
_______________________________________