Initial Commit
This commit is contained in:
parent
92c43041f0
commit
9d3a4cd1d0
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,6 +14,7 @@ replay_pid*
|
||||
.idea
|
||||
cmake-build-*/
|
||||
*.iws
|
||||
work/
|
||||
out/
|
||||
build/
|
||||
.idea_modules/
|
||||
|
85
pom.xml
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>
|
39
src/main/java/me/braydon/license/LicenseServer.java
Normal file
39
src/main/java/me/braydon/license/LicenseServer.java
Normal file
@ -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
|
||||
}
|
||||
}
|
30
src/main/java/me/braydon/license/common/RandomUtils.java
Normal file
30
src/main/java/me/braydon/license/common/RandomUtils.java
Normal file
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
25
src/main/java/me/braydon/license/exception/APIException.java
Normal file
25
src/main/java/me/braydon/license/exception/APIException.java
Normal file
@ -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");
|
||||
}
|
||||
}
|
92
src/main/java/me/braydon/license/model/License.java
Normal file
92
src/main/java/me/braydon/license/model/License.java
Normal file
@ -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);
|
||||
}
|
102
src/main/java/me/braydon/license/service/LicenseService.java
Normal file
102
src/main/java/me/braydon/license/service/LicenseService.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
27
src/main/resources/application.yml
Normal file
27
src/main/resources/application.yml
Normal file
@ -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"
|
8
src/main/resources/banner.txt
Normal file
8
src/main/resources/banner.txt
Normal file
@ -0,0 +1,8 @@
|
||||
_ _ ___
|
||||
| | (_)__ ___ _ _ ___ ___ / __| ___ _ ___ _____ _ _
|
||||
| |__| / _/ -_) ' \(_-</ -_) \__ \/ -_) '_\ V / -_) '_|
|
||||
|____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_|
|
||||
|
||||
| API Version - v${application.version}
|
||||
| Spring Version - ${spring-boot.formatted-version}
|
||||
_______________________________________
|
Loading…
x
Reference in New Issue
Block a user