57 Commits
1.0.0 ... 1.0.3

Author SHA1 Message Date
803b763198 Update README.md 2023-06-08 03:05:10 -04:00
fec32230fe Add license actions 2023-06-02 05:50:27 -04:00
68591c90c9 Add a command to view your owned license 2023-06-02 05:10:41 -04:00
5133ab688f Version bump 2023-06-02 05:07:13 -04:00
36673af4d0 Only inform license owners of new IPs and HWIDs if the license was actually used 2023-06-02 00:49:41 -04:00
3038e14b81 Update README 2023-06-02 00:44:10 -04:00
2920a42d76 Update README 2023-06-02 00:42:51 -04:00
c350138caa Simple HWID validation 2023-06-02 00:39:19 -04:00
cf932e2d90 oops, forgot this 2023-06-02 00:35:55 -04:00
6b6958640f Cleanup 2023-06-02 00:32:42 -04:00
e3b6507a6c Document DiscordService#sendOwnerLog 2023-06-02 00:32:27 -04:00
1b482b93e2 Update example 2023-06-02 00:29:45 -04:00
767646feae remove unused config option 2023-06-02 00:26:52 -04:00
c28694c878 Change to a better expiration system 2023-06-02 00:25:50 -04:00
18ce6548f6 Added logging for license owners 2023-06-01 23:43:26 -04:00
b41505a2b6 Version bump 2023-06-01 22:37:09 -04:00
091bb8ac4e Update Docker deployment examples in the README 2023-06-01 01:32:01 -04:00
d6f8e2cbcf Add Discord preview to the README 2023-06-01 01:29:21 -04:00
9c8fec5fd6 Include responses in API reference 2023-06-01 01:23:12 -04:00
2c08a08003 fix syntax? 2023-06-01 01:21:15 -04:00
1bcdae67f2 Merge branch 'master' of https://github.com/Rainnny7/LicenseServer
# Conflicts:
#	README.md
2023-06-01 01:20:27 -04:00
2a5351484a Moved docker-compose.yml to the README file 2023-06-01 01:17:53 -04:00
982ff08f11 Update README.md 2023-06-01 01:17:13 -04:00
05cc42b240 Update README.md 2023-06-01 00:55:53 -04:00
57b0e73768 re-write with okhttp 2023-06-01 00:46:36 -04:00
45161953db doc updates 2023-06-01 00:36:13 -04:00
8326e3b92c oops 2023-06-01 00:33:37 -04:00
ea4648ac92 Fix .gitignore 2023-06-01 00:33:22 -04:00
1fe351e209 Add Example 2023-06-01 00:32:04 -04:00
49ec70ab68 log changes 2023-06-01 00:18:30 -04:00
b8f10703b0 GET -> POST 2023-06-01 00:18:22 -04:00
858ead987c Update Lombok version 2023-05-31 23:54:05 -04:00
1591ef77d6 Include owner snowflake & name in LicenseDTO 2023-05-31 23:12:39 -04:00
cce6197e7a minor changes 2023-05-31 23:11:03 -04:00
01a356a09d Add tags to license usage logs 2023-05-31 23:09:48 -04:00
98b67aba4f Use real ip 2023-05-31 22:58:39 -04:00
2685bed581 Add optional owner snowflake & name to licenses 2023-05-31 22:54:17 -04:00
7276615298 Update log topics 2023-05-31 22:36:35 -04:00
baa5d39dd4 Document Dockerfile 2023-05-31 22:32:42 -04:00
4cd9caeacb Add Discord support 2023-05-31 22:30:27 -04:00
feaf965859 Added a DTO for licenses 2023-05-31 19:19:41 -04:00
dcb5e222e7 Cleanup 2023-05-31 19:10:56 -04:00
b60d965dec Added license durations 2023-05-31 19:09:05 -04:00
c1e1a9e462 Version bump 2023-05-31 19:04:29 -04:00
63bad8b0ce Update Docker compose 2023-05-31 19:02:28 -04:00
7b7e8e5d8f . 2023-05-31 18:49:09 -04:00
2035d168ea . 2023-05-31 18:13:29 -04:00
bc7128f75a . 2023-05-31 18:10:35 -04:00
23d73f87d7 bruh 2023-05-31 18:03:51 -04:00
e6cc1f6975 sigh 2023-05-31 18:02:10 -04:00
68fcd99f63 oops 2023-05-31 17:30:52 -04:00
562d5e8eba oops 2023-05-31 17:19:11 -04:00
c8f8b6efd6 xx? 2023-05-31 17:17:32 -04:00
f16557f64c Merge remote-tracking branch 'origin/master' 2023-05-31 16:57:39 -04:00
be419ef5b9 yes? 2023-05-31 16:45:19 -04:00
7cb8137130 Add 'docker-compose.yml' 2023-05-31 13:08:16 -07:00
dc46c44535 Update Dockerfile 2023-05-31 03:04:47 -04:00
19 changed files with 1293 additions and 54 deletions

1
.gitignore vendored
View File

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

View File

@ -1,11 +1,29 @@
# Stage 1: Building
FROM maven:3.8.5-openjdk-17-slim AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn package
FROM openjdk:11-ea-17-jre-slim
# Set the work dir inside the container
WORKDIR /app
# Copy the POM file to the work dir
COPY pom.xml .
# Copy src to the work dir
COPY src ./src
# Run Maven to clean and package the app
RUN mvn clean package -T12
# Stage 2: Running
FROM openjdk:17.0.1-jdk-slim
# Set the work dir inside the container
WORKDIR /usr/local/app
# Copy the compiled JAR file from the builder stage to the work dir
COPY --from=builder /app/target/LicenseServer.jar .
# Expose the port
EXPOSE 7500
# Set the command to run the app when the container starts
CMD ["java", "-jar", "LicenseServer.jar"]

79
Example/pom.xml Normal file
View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<groupId>me.braydon</groupId>
<artifactId>Example</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<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>
</build>
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>compile</scope>
</dependency>
<!-- Oshi -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.2</version>
<scope>compile</scope>
</dependency>
<!-- OkHttp -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,217 @@
package me.braydon.example;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import okhttp3.*;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
import oshi.hardware.ComputerSystem;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.software.os.OperatingSystem;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* An example of how to interact
* with the license server. This
* can be conveniently used in
* any project by simply copying
* the class into your project.
*
* @author Braydon
* @see <a href="https://git.rainnny.club/Rainnny/LicenseServer">License Server</a>
*/
public final class LicenseExample {
/**
* The endpoint to check licenses at.
*/
private static final String CHECK_ENDPOINT = "http://localhost:7500/check";
/**
* The {@link Gson} instance to use.
*/
private static final Gson GSON = new GsonBuilder()
.serializeNulls()
.create();
/**
* Check the license with the given
* key for the given product.
*
* @param key the key to check
* @param product the product the key belongs to
* @return the license response
* @see LicenseResponse for response
*/
@NonNull
public static LicenseResponse check(@NonNull String key, @NonNull String product) {
String hardwareId = getHardwareId(); // Get the hardware id of the machine
// Build the json body
Map<String, Object> body = new HashMap<>();
body.put("key", key);
body.put("product", product);
body.put("hwid", hardwareId);
String bodyJson = GSON.toJson(body); // The json body
OkHttpClient client = new OkHttpClient(); // Create a new http client
MediaType mediaType = MediaType.parse("application/json"); // Ensure the media type is json
RequestBody requestBody = RequestBody.create(bodyJson, mediaType); // Build the request body
Request request = new Request.Builder()
.url(CHECK_ENDPOINT)
.post(requestBody)
.build(); // Build the POST request
Response response = null; // The response of the request
int responseCode = -1; // The response code of the request
try { // Attempt to execute the request
response = client.newCall(request).execute();
responseCode = response.code();
// If the response is successful, we can parse the response
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
assert responseBody != null; // We don't want the response body being null
JsonObject json = GSON.fromJson(responseBody.string(), JsonObject.class); // Parse the json
JsonElement description = json.get("description");
JsonElement ownerSnowflake = json.get("ownerSnowflake");
JsonElement ownerName = json.get("ownerName");
// Parsing the expiration date if we have one
JsonElement expires = json.get("expires");
Date expiresDate = null;
if (!expires.isJsonNull()) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(expires.getAsString(), DateTimeFormatter.ISO_OFFSET_DATE_TIME);
expiresDate = Date.from(offsetDateTime.toInstant());
}
// Return the license response
return new LicenseResponse(200, null,
description.isJsonNull() ? null : description.getAsString(),
ownerSnowflake.isJsonNull() ? -1 : ownerSnowflake.getAsLong(),
ownerName.isJsonNull() ? null : ownerName.getAsString(),
expires.isJsonNull() ? null : expiresDate
);
} else {
ResponseBody errorBody = response.body(); // Get the error body
if (errorBody != null) { // If we have an error body, we can parse it
String errorResponse = errorBody.string();
JsonObject jsonError = GSON.fromJson(errorResponse, JsonObject.class);
JsonElement errorMessage = jsonError.get("error");
if (!errorMessage.isJsonNull()) { // We have an error message, return it
return new LicenseResponse(responseCode, errorMessage.getAsString());
}
}
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
// Close the response if it's open
if (response != null) {
response.close();
}
}
// Return an unknown error
return new LicenseResponse(responseCode, "An unknown error occurred");
}
/**
* Get the unique hardware
* identifier of this machine.
*
* @return the hardware id
*/
@NonNull
private static String getHardwareId() {
SystemInfo systemInfo = new SystemInfo();
OperatingSystem operatingSystem = systemInfo.getOperatingSystem();
HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware();
CentralProcessor centralProcessor = hardwareAbstractionLayer.getProcessor();
ComputerSystem computerSystem = hardwareAbstractionLayer.getComputerSystem();
// Retrieve necessary hardware information
String vendor = operatingSystem.getManufacturer();
String processorSerialNumber = computerSystem.getSerialNumber();
String uuid = computerSystem.getHardwareUUID();
String processorIdentifier = centralProcessor.getProcessorIdentifier().getIdentifier();
int processors = centralProcessor.getLogicalProcessorCount();
// Generate a unique hardware id using the retrieved information
return String.format("%08x", vendor.hashCode()) + "-"
+ String.format("%08x", processorSerialNumber.hashCode()) + "-"
+ String.format("%08x", uuid.hashCode()) + "-"
+ String.format("%08x", processorIdentifier.hashCode()) + "-" + processors;
}
@AllArgsConstructor
@Getter
@ToString
public static class LicenseResponse {
/**
* The status code of the response.
*/
private final long status;
/**
* The error in the response, null if none.
*/
private String error;
/**
* The description of the license, present if valid.
*/
private String description;
/**
* The Discord snowflake of the license owner, present
* if valid and there is an owner.
*/
private long ownerSnowflake;
/**
* The Discord name of the license owner, present
* if valid and there is an owner.
*/
private String ownerName;
/**
* The optional expiration {@link Date} of the license.
*/
private Date expires;
public LicenseResponse(long status, @NonNull String error) {
this.status = status;
this.error = error;
}
/**
* Check if the license is valid.
*
* @return true if valid, otherwise false
*/
public boolean isValid() {
return status == 200;
}
/**
* Check if the license is permanent.
*
* @return true if permanent, otherwise false
*/
public boolean isPermanent() {
return expires == null;
}
}
}

View File

@ -0,0 +1,27 @@
package me.braydon.example;
/**
* @author Braydon
*/
public final class Main {
public static void main(String[] args) {
LicenseExample.LicenseResponse response = LicenseExample.check("XXXX-XXXX-XXXX-XXXX", "Example");
if (!response.isValid()) { // License isn't valid
System.err.println("Invalid license: " + response.getError());
return;
}
// License is valid
System.out.println("License is valid!");
if (response.getOwnerName() != null) {
System.out.println("Welcome " + response.getOwnerName() + "!");
}
if (response.getDescription() != null) {
System.out.println("Description: " + response.getDescription()); // License description
}
if (response.isPermanent()) { // License is permanent
System.out.println("Your license is permanent");
} else { // License has an expiration date
System.out.printf("Your license will expire at: %s%n", response.getExpires().toInstant());
}
}
}

View File

@ -1,3 +1,67 @@
# LicenseServer
A simple open-source licensing server for your products.
A simple open-source licensing server for your products.
## Discord Preview
![License Global Log](https://cdn.rainnny.club/SagsCD0I.png)
![License Owner Log](https://cdn.rainnny.club/JZdFxTCy.png)
![License Owner Lookup](https://cdn.rainnny.club/EU0g1iLZ.png)
## API Reference
### Check License
```http
POST /check
```
#### Body
| Key | Type | Description |
|:----------|:---------|:-----------------------------------------------|
| `key` | `string` | **Required**. Your license key |
| `product` | `string` | **Required**. The product the license is for |
| `hwid` | `string` | **Required**. The hardware id of the requester |
#### Response
##### Error
```json
{
"error": "Error message"
}
```
##### Success
```json
{
"description": "Testing",
"ownerSnowflake": 504147739131641857,
"ownerName": "Braydon#2712",
"expires": "2023-06-02T06:00:47.270+00:00"
}
```
## Deployment
### Docker
```bash
docker run -d -p 7500:7500 -v "$(pwd)/data/application.yml:/usr/local/app/application.yml" git.rainnny.club/rainnny/licenseserver:latest
```
### Docker Compose
```yml
version: '3'
services:
app:
image: git.rainnny.club/rainnny/licenseserver:latest
volumes:
- ./data/application.yml:/usr/local/app/application.yml
ports:
- "7500:7500"
```

32
pom.xml
View File

@ -12,7 +12,7 @@
<groupId>me.braydon</groupId>
<artifactId>LicenseServer</artifactId>
<version>1.0.0</version>
<version>1.0.3</version>
<description>A simple open-source licensing server for your products.</description>
<properties>
@ -36,6 +36,13 @@
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
@ -45,10 +52,23 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!-- Discord JDA -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-beta.9</version>
<exclusions>
<exclusion>
<groupId>club.minnced</groupId>
<artifactId>opus-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- BCrypt -->
<dependency>
<groupId>org.mindrot</groupId>
@ -72,6 +92,14 @@
<scope>compile</scope>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.0.0-jre</version>
<scope>compile</scope>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -2,9 +2,11 @@ package me.braydon.license;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import jakarta.annotation.PostConstruct;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -17,7 +19,7 @@ import java.util.Objects;
* @author Braydon
*/
@SpringBootApplication
@Slf4j
@Slf4j(topic = "License Server")
public class LicenseServer {
public static final Gson GSON = new GsonBuilder()
.serializeNulls()
@ -36,4 +38,10 @@ public class LicenseServer {
log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
SpringApplication.run(LicenseServer.class, args); // Load the application
}
@PostConstruct
public void onInitialize() {
// Log a randomly generated salt
log.info("Generated a random salt: {} (This is only for you to copy and paste for config)", BCrypt.gensalt());
}
}

View File

@ -0,0 +1,86 @@
package me.braydon.license.common;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
/**
* @author Braydon
*/
@UtilityClass
public final class IPUtils {
/**
* The regex expression for validating IPv4 addresses.
*/
public static final String IPV4_REGEX = "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$";
/**
* The regex expression for validating IPv6 addresses.
*/
public static final String IPV6_REGEX = "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?$";
private static final String[] IP_HEADERS = new String[] {
"CF-Connecting-IP",
"X-Forwarded-For"
};
/**
* Get the real IP from the given request.
*
* @param request the request
* @return the real IP
*/
@NonNull
public static String getRealIp(@NonNull HttpServletRequest request) {
String ip = request.getRemoteAddr();
for (String headerName : IP_HEADERS) {
String header = request.getHeader(headerName);
if (header == null) {
continue;
}
if (!header.contains(",")) { // Handle single IP
ip = header;
break;
}
// Handle multiple IPs
String[] ips = header.split(",");
for (String ipHeader : ips) {
ip = ipHeader;
break;
}
}
return ip;
}
/**
* Get the IP type of the given input.
*
* @param input the input
* @return the IP type, -1 if invalid
*/
public static int getIpType(@NonNull String input) {
return isIpV4(input) ? 4 : isIpV6(input) ? 6 : -1;
}
/**
* Check if the given input is
* a valid IPv4 address.
*
* @param input the input
* @return true if IPv4, otherwise false
*/
public static boolean isIpV4(@NonNull String input) {
return input.matches(IPV4_REGEX);
}
/**
* Check if the given input is
* a valid IPv6 address.
*
* @param input the input
* @return true if IPv6, otherwise false
*/
public static boolean isIpV6(@NonNull String input) {
return input.matches(IPV6_REGEX);
}
}

View File

@ -0,0 +1,23 @@
package me.braydon.license.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
/**
* @author Braydon
*/
@UtilityClass
public final class MiscUtils {
/**
* Obfuscate the given key.
*
* @param rawKey the key to obfuscate
* @return the obfuscated key
*/
@NonNull
public static String obfuscateKey(@NonNull String rawKey) {
int length = 9; // The amount of chars to show
String key = rawKey.substring(0, length);
return key + "*".repeat(rawKey.length() - length);
}
}

View File

@ -0,0 +1,25 @@
package me.braydon.license.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author Braydon
*/
@UtilityClass
public final class TimeUtils {
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
/**
* Get the current date time.
*
* @return the current date time
*/
@NonNull
public static String dateTime() {
return DATE_TIME_FORMAT.format(new Date());
}
}

View File

@ -5,6 +5,8 @@ import com.google.gson.JsonObject;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import me.braydon.license.LicenseServer;
import me.braydon.license.common.IPUtils;
import me.braydon.license.dto.LicenseDTO;
import me.braydon.license.exception.APIException;
import me.braydon.license.model.License;
import me.braydon.license.service.LicenseService;
@ -40,11 +42,11 @@ public final class LicenseController {
* @see License for license
* @see ResponseEntity for response entity
*/
@GetMapping("/check")
@PostMapping("/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
String ip = IPUtils.getRealIp(request); // The IP of the requester
JsonObject jsonObject = LicenseServer.GSON.fromJson(body, JsonObject.class);
JsonElement key = jsonObject.get("key"); // Get the key
@ -55,14 +57,38 @@ public final class LicenseController {
if (key.isJsonNull() || product.isJsonNull() || hwid.isJsonNull()) {
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid request body");
}
// Ensure the IP is valid
if (IPUtils.getIpType(ip) == -1) {
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid IP address");
}
// Ensure the HWID is valid
// TODO: improve :)
String hwidString = hwid.getAsString();
boolean invalidHwid = true;
if (hwidString.contains("-")) {
int segments = hwidString.substring(0, hwidString.lastIndexOf("-")).split("-").length;
if (segments == 4) {
invalidHwid = false;
}
}
if (invalidHwid) {
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid HWID");
}
// Check the license
service.check(
License license = service.check(
key.getAsString(),
product.getAsString(),
ip,
hwid.getAsString()
hwidString
);
return ResponseEntity.ok().build(); // Return OK
// Return OK with the license DTO
return ResponseEntity.ok(new LicenseDTO(
license.getDescription(),
license.getOwnerSnowflake(),
license.getOwnerName(),
license.getExpires()
));
} catch (APIException ex) { // Handle the exception
return ResponseEntity.status(ex.getStatus())
.body(Map.of("error", ex.getLocalizedMessage()));

View File

@ -0,0 +1,42 @@
package me.braydon.license.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import me.braydon.license.model.License;
import java.util.Date;
/**
* A data transfer object for a {@link License}.
*
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public class LicenseDTO {
/**
* The optional description of this license.
*/
private String description;
/**
* The Discord snowflake of the owner of this license.
* <p>
* If this is -1, the license is not owned by anyone.
* </p>
*/
private long ownerSnowflake;
/**
* The Discord name of the owner of this license.
* <p>
* If this is null, the license is not owned by anyone.
* </p>
*/
private String ownerName;
/**
* The optional expiration {@link Date} of this license.
*/
private Date expires;
}

View File

@ -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 been used
* but is expired.
*
* @author Braydon
*/
public class LicenseExpiredException extends APIException {
public LicenseExpiredException() {
super(HttpStatus.BAD_REQUEST, "License has expired");
}
}

View File

@ -7,7 +7,6 @@ import lombok.ToString;
import me.braydon.license.exception.APIException;
import me.braydon.license.exception.LicenseHwidLimitExceededException;
import me.braydon.license.exception.LicenseIpLimitExceededException;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@ -39,6 +38,22 @@ public class License {
*/
private String description;
/**
* The Discord snowflake of the owner of this license.
* <p>
* If this is -1, the license is not owned by anyone.
* </p>
*/
private long ownerSnowflake;
/**
* The Discord name of the owner of this license.
* <p>
* If this is null, the license is not owned by anyone.
* </p>
*/
private String ownerName;
/**
* The amount of uses this license has.
*/
@ -64,6 +79,11 @@ public class License {
*/
private int hwidLimit;
/**
* The optional expiration {@link Date} of this license.
*/
private Date expires;
/**
* The {@link Date} this license was last used.
*/
@ -74,16 +94,53 @@ public class License {
*/
@NonNull private Date created;
/**
* Check if the Discord user
* with the given snowflake
* owns this license.
*
* @param snowflake the snowflake
* @return true if owns, otherwise false
*/
public boolean isOwner(long snowflake) {
return ownerSnowflake == snowflake;
}
/**
* Check if this license has expired.
* <p>
* If this license has no
* expiration, this will
* always return false.
* </p>
*
* @return true if expired, otherwise false
*/
public boolean hasExpired() {
// License is permanent, not expired
if (isPermanent()) {
return false;
}
// Check if the license has expired
return expires.before(new Date());
}
/**
* Check if this license has no expiration.
*
* @return true if permanent, otherwise false
*/
public boolean isPermanent() {
return expires == null;
}
/**
* Invoked when this license is used.
*
* @param ip the ip used
* @param ipSalt the IP salt to use
* @param hwid the hardware id used
* @param hashedIp the hashed ip used
* @param hwid the hardware id used
*/
public void use(@NonNull String ip, @NonNull String ipSalt, @NonNull String hwid) throws APIException {
String hashedIp = BCrypt.hashpw(ip, ipSalt); // Hash the IP
public void use(@NonNull String hashedIp, @NonNull String hwid) throws APIException {
// IP limit has been exceeded
if (!ips.contains(hashedIp) && ips.size() >= ipLimit) {
throw new LicenseIpLimitExceededException();

View File

@ -0,0 +1,384 @@
package me.braydon.license.service;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import jakarta.annotation.Nonnull;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.braydon.license.common.MiscUtils;
import me.braydon.license.common.TimeUtils;
import me.braydon.license.model.License;
import me.braydon.license.repository.LicenseRepository;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.requests.ErrorResponse;
import net.dv8tion.jda.api.requests.GatewayIntent;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.stereotype.Service;
import java.awt.*;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @author Braydon
*/
@Service
@Slf4j(topic = "Discord")
public final class DiscordService {
private static final String CLEAR_IPS_BUTTON_ID = "clearIps";
private static final String CLEAR_HWIDS_BUTTON_ID = "clearHwids";
/**
* The {@link LicenseRepository} to use.
*/
@Nonnull private final LicenseRepository licenseRepository;
/**
* The version of this Springboot application.
*/
@NonNull private final String applicationVersion;
/**
* The salt to use for hashing license keys.
*/
@Value("${salts.licenses}")
@NonNull private String licensesSalt;
/**
* The name of this Springboot application.
*/
@Value("${spring.application.name}")
@NonNull private String applicationName;
/**
* The token to the Discord bot.
*/
@Value("${discord.token}")
@NonNull private String token;
/**
* The channel ID to log to.
*/
@Value("${discord.logs.channel}")
private long logsChannel;
/**
* Should used licenses be logged?
*/
@Value("${discord.logs.uses}") @Getter
private boolean logUses;
/**
* Should we log if an expired license was used?
*/
@Value("${discord.logs.expired}") @Getter
private boolean logExpired;
/**
* Should IP limited licenses be logged when used?
*/
@Value("${discord.logs.expired}") @Getter
private boolean logIpLimitExceeded;
/**
* Should HWID limited licenses be logged when used?
*/
@Value("${discord.logs.expired}") @Getter
private boolean logHwidLimitExceeded;
/**
* Should new IPs be sent to the license owner?
*/
@Value("${discord.owner-logs.newIp}") @Getter
private boolean logNewIpsToOwner;
/**
* Should new HWIDs be sent to the license owner?
*/
@Value("${discord.owner-logs.newHwid}") @Getter
private boolean logNewHwidsToOwner;
/**
* The {@link JDA} instance of the bot.
*/
private JDA jda;
/**
* Cached licenses for messages.
* <p>
* When a license is looked up by it's owner, the
* response message is cached (key is the message snowflake)
* for 5 minutes. This is so we're able to get the message
* an action was performed on, as well as action timeouts.
* </p>
*/
private final Cache<Long, License> cachedLicenses = CacheBuilder.newBuilder()
.expireAfterWrite(5L, TimeUnit.MINUTES)
.build();
@Autowired
public DiscordService(@NonNull LicenseRepository licenseRepository, @NonNull BuildProperties buildProperties) {
this.licenseRepository = licenseRepository;
this.applicationVersion = buildProperties.getVersion();
}
@PostConstruct @SneakyThrows
public void onInitialize() {
// No token was provided
if (token.trim().isEmpty()) {
log.info("Not using Discord, no token provided");
return;
}
// Initialize the bot
long before = System.currentTimeMillis();
log.info("Logging in..."); // Log that we're logging in
jda = JDABuilder.createDefault(token)
.enableIntents(
GatewayIntent.GUILD_MEMBERS
).setStatus(OnlineStatus.DO_NOT_DISTURB)
.setActivity(Activity.watching("your licenses"))
.addEventListeners(new EventHandler())
.build();
jda.awaitReady(); // Await JDA to be ready
// Log that we're logged in
log.info("Logged into {} in {}ms",
jda.getSelfUser().getAsTag(), System.currentTimeMillis() - before
);
// Registering slash commands
jda.updateCommands().addCommands(
Commands.slash("license", "Manage one of your licenses")
.addOption(OptionType.STRING, "key", "The license key", true)
.addOption(OptionType.STRING, "product", "The product the license is for", true)
).queue();
}
/**
* Send a log to the logs channel
* with the given embed.
*
* @param embed the embed to send
* @see TextChannel for channel
* @see EmbedBuilder for embed
*/
public void sendLog(@NonNull EmbedBuilder embed) {
// JDA must be ready to send logs
if (!isReady()) {
return;
}
// Not enabled
if (logsChannel <= 0L) {
return;
}
TextChannel textChannel = jda.getTextChannelById(logsChannel); // Get the logs channel
if (textChannel == null) { // We must have a logs channel
throw new IllegalArgumentException("Log channel %s wasn't found".formatted(logsChannel));
}
// Send the log
textChannel.sendMessageEmbeds(buildEmbed(embed)).queue();
}
/**
* Send an embed to the owner
* of the given license.
*
* @param license the license
* @param embed the embed to send
* @see License for license
* @see EmbedBuilder for embed
*/
public void sendOwnerLog(@NonNull License license, @NonNull EmbedBuilder embed) {
// JDA must be ready to send logs
if (!isReady()) {
return;
}
// We need an owner for the license
if (license.getOwnerSnowflake() <= 0L) {
return;
}
// Lookup the owner of the license
jda.retrieveUserById(license.getOwnerSnowflake()).queue(owner -> {
if (owner == null) { // Couldn't locate the owner of the license
return;
}
owner.openPrivateChannel().queue(channel -> {
channel.sendMessageEmbeds(buildEmbed(embed)).queue(null, ex -> {
// Ignore the ex if the owner has priv msgs turned off, we don't care
if (((ErrorResponseException) ex).getErrorResponse() != ErrorResponse.CANNOT_SEND_TO_USER) {
ex.printStackTrace();
}
});
});
}, ex -> {
// Ignore the ex if the owner isn't found, we don't care
if (((ErrorResponseException) ex).getErrorResponse() != ErrorResponse.UNKNOWN_USER) {
ex.printStackTrace();
}
});
}
/**
* Check if the bot is ready.
*
* @return true if ready, otherwise false
*/
public boolean isReady() {
return jda != null && (jda.getStatus() == JDA.Status.CONNECTED);
}
/**
* Build the given embed.
*
* @param embedBuilder the embed builder
* @return the built embed
*/
@NonNull
private MessageEmbed buildEmbed(@NonNull EmbedBuilder embedBuilder) {
return embedBuilder.setFooter("%s v%s - %s".formatted(
applicationName, applicationVersion, TimeUtils.dateTime()
)).build();
}
/**
* The event handler for the bot.
*/
public class EventHandler extends ListenerAdapter {
@Override
public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) {
User user = event.getUser(); // The command executor
// Handle the license command
if (event.getName().equals("license")) {
String key = Objects.requireNonNull(event.getOption("key")).getAsString();
String product = Objects.requireNonNull(event.getOption("product")).getAsString();
event.deferReply(true).queue(); // Send thinking...
// License lookup
try {
Optional<License> optionalLicense = licenseRepository.getLicense(BCrypt.hashpw(key, licensesSalt), product);
if (optionalLicense.isEmpty() // License not found or owned by someone else
|| (!optionalLicense.get().isOwner(user.getIdLong()))) {
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("License not found")
.setDescription("Could not locate the license you were looking for")
)).queue(); // Send the error message
return;
}
License license = optionalLicense.get(); // The found license
String obfuscateKey = MiscUtils.obfuscateKey(key); // Obfuscate the key
long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L;
long lastUsed = license.getLastUsed() == null ? -1L : license.getExpires().getTime() / 1000L;
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.BLUE)
.setTitle("Your License")
.addField("License", "`" + obfuscateKey + "`", true)
.addField("Product", license.getProduct(), true)
.addField("Description", license.getDescription(), true)
.addField("Expiration",
expires == -1L ? "Never" : "<t:" + expires + ":R>",
true
)
.addField("Uses", String.valueOf(license.getUses()), true)
.addField("Last Used",
lastUsed == -1L ? "Never" : "<t:" + lastUsed + ":R>",
true
)
.addField("IPs",
license.getIps().size() + "/" + license.getIpLimit(),
true
)
.addField("HWIDs",
license.getHwids().size() + "/" + license.getHwidLimit(),
true
)
.addField("Created",
"<t:" + (license.getCreated().getTime() / 1000L) + ":R>",
true
)
)).addActionRow( // Buttons
Button.danger(CLEAR_IPS_BUTTON_ID, "Clear IPs")
.withEmoji(Emoji.fromUnicode("🗑️")),
Button.danger(CLEAR_HWIDS_BUTTON_ID, "Clear HWIDs")
.withEmoji(Emoji.fromUnicode("🗑️"))
).queue(message -> cachedLicenses.put(message.getIdLong(), license)); // Cache the license for the message
} catch (Exception ex) {
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("Lookup Failed")
.setDescription("More information has been logged")
)).queue(); // Send the error message
ex.printStackTrace();
}
}
}
@Override
public void onButtonInteraction(@NonNull ButtonInteractionEvent event) {
User user = event.getUser(); // The user who clicked the button
String componentId = event.getComponentId(); // The button id
// License Actions
boolean clearIps = componentId.equals(CLEAR_IPS_BUTTON_ID);
boolean clearHwids = componentId.equals(CLEAR_HWIDS_BUTTON_ID);
if (clearIps || clearHwids) {
event.deferReply(true).queue(); // Send thinking...
License license = cachedLicenses.getIfPresent(event.getMessageIdLong()); // Get the cached license
if (license == null || (!license.isOwner(user.getIdLong()))) { // License not found or owned by someone else
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("License Action Failed")
.setDescription("The license couldn't be found or the action timed out")
)).queue(); // Send the error message
return;
}
try {
// Clear IPs
if (clearIps) {
license.setIps(new HashSet<>());
}
// Clear HWIDs
if (clearHwids) {
license.setHwids(new HashSet<>());
}
licenseRepository.save(license); // Save the license
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.GREEN)
.setTitle("Cleared " + (clearIps ? "IP" : "HWID") + "s")
)).queue(); // Inform action success
} catch (Exception ex) {
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("License Action Failed")
.setDescription("More information has been logged")
)).queue(); // Send the error message
ex.printStackTrace();
}
}
}
}
}

View File

@ -1,17 +1,18 @@
package me.braydon.license.service;
import jakarta.annotation.PostConstruct;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import me.braydon.license.exception.APIException;
import me.braydon.license.exception.LicenseNotFoundException;
import me.braydon.license.common.MiscUtils;
import me.braydon.license.exception.*;
import me.braydon.license.model.License;
import me.braydon.license.repository.LicenseRepository;
import net.dv8tion.jda.api.EmbedBuilder;
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.awt.*;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
@ -22,13 +23,18 @@ import java.util.Optional;
* @author Braydon
*/
@Service
@Slf4j
@Slf4j(topic = "Licenses")
public final class LicenseService {
/**
* The {@link LicenseRepository} to use.
*/
@NonNull private final LicenseRepository repository;
/**
* The {@link DiscordService} to use for logging.
*/
@NonNull private final DiscordService discordService;
/**
* The salt to use for hashing license keys.
*/
@ -42,44 +48,39 @@ public final class LicenseService {
@NonNull private String ipsSalt;
@Autowired
public LicenseService(@NonNull LicenseRepository repository) {
public LicenseService(@NonNull LicenseRepository repository, @NonNull DiscordService discordService) {
this.repository = repository;
}
@PostConstruct
public void onInitialize() {
// TODO: remove this and make it either
// a test, or a route to gen a license
System.out.println("SALT - " + BCrypt.gensalt());
// String key = RandomUtils.generateLicenseKey();
// log.info(create(key,
// "CloudSpigot",
// "Testing " + Math.random(), Integer.MAX_VALUE, Integer.MAX_VALUE).toString());
// System.out.println("key = " + key);
this.discordService = discordService;
}
/**
* 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
* @param key the key of the license
* @param product the product the license is for
* @param description the optional description of the license
* @param ownerSnowflake the optional owner snowflake of the license
* @param ownerName the optional owner name of the license
* @param ipLimit the IP limit of the license
* @param hwidLimit the HWID limit of the license
* @param expires the optional expiration date 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) {
public License create(@NonNull String key, @NonNull String product, String description, long ownerSnowflake,
String ownerName, int ipLimit, int hwidLimit, Date expires) {
// Create the new license
License license = new License();
license.setKey(BCrypt.hashpw(key, licensesSalt)); // Hash the key
license.setProduct(product); // Use the given product
license.setDescription(description); // Use the given description, if any
license.setOwnerSnowflake(ownerSnowflake);
license.setOwnerName(ownerName);
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.setExpires(expires);
license.setCreated(new Date());
repository.insert(license); // Insert the newly created license
return license;
@ -92,19 +93,136 @@ public final class LicenseService {
* @param product the product of the license
* @param ip the ip using the license
* @param hwid the hwid using the license
* @return the checked 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 {
@NonNull
public License check(@NonNull String key, @NonNull String product, @NonNull String ip,
@NonNull String hwid) throws APIException {
Optional<License> optionalLicense = repository.getLicense(BCrypt.hashpw(key, licensesSalt), 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, ipsSalt, hwid); // Use the license
repository.save(license); // Save the used license
log.info("License key {} for product {} was used by {} ({})", key, product, ip, hwid);
String hashedIp = BCrypt.hashpw(ip, ipsSalt); // Hash the IP
String obfuscateKey = MiscUtils.obfuscateKey(key); // Obfuscate the key
boolean newIp = !license.getIps().contains(hashedIp); // Is the IP new?
boolean newHwid = !license.getHwids().contains(hwid); // Is the HWID new?
// Log the license being used, if enabled
if (discordService.isLogUses()) {
// god i hate sending discord embeds, it's so big and ugly :(
// Constructing tags
StringBuilder tags = new StringBuilder();
if (newIp) { // New IP
tags.append("New IP");
}
if (newHwid) { // New HWID
if (tags.length() > 0) {
tags.append(" & ");
}
tags.append("HWID");
}
long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L;
int ipCount = license.getIps().size();
int hwidCount = license.getHwids().size();
discordService.sendLog(new EmbedBuilder()
.setColor(Color.BLUE)
.setTitle("License Used" + (!tags.isEmpty() ? " (" + tags + ")" : ""))
.addField("License", "`" + obfuscateKey + "`", true)
.addField("Product", license.getProduct(), true)
.addField("Description", license.getDescription(), true)
.addField("Owner ID",
license.getOwnerSnowflake() <= 0L ? "N/A" : String.valueOf(license.getOwnerSnowflake()),
true
)
.addField("Owner Name",
license.getOwnerName() == null ? "N/A" : license.getOwnerName(),
true
)
.addField("Expiration",
expires == -1L ? "Never" : "<t:" + expires + ":R>",
true
)
.addField("IP", ip, true)
.addField("HWID", "```" + hwid + "```", false)
.addField("IPs",
(newIp ? ipCount + 1 : ipCount) + "/" + license.getIpLimit(),
true
)
.addField("HWIDs",
(newHwid ? hwidCount + 1 : hwidCount) + "/" + license.getHwidLimit(),
true
)
);
}
// The license has expired
if (license.hasExpired()) {
// Log the expired license
if (discordService.isLogExpired()) {
discordService.sendLog(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("License Expired")
.setDescription("License `%s` is expired".formatted(obfuscateKey))
);
}
throw new LicenseExpiredException();
}
try {
license.use(hashedIp, hwid); // Use the license
repository.save(license); // Save the used license
// Sending new IP log to the license owner
if (newIp && discordService.isLogNewIpsToOwner()) {
discordService.sendOwnerLog(license, new EmbedBuilder()
.setColor(0xF2781B)
.setTitle("New IP")
.setDescription("One of your licenses has been used on a new IP:")
.addField("License", "`" + obfuscateKey + "`", true)
.addField("Product", license.getProduct(), true)
.addField("IP", "```" + ip + "```", false)
);
}
// Sending new HWID log to the license owner
if (newHwid && discordService.isLogNewHwidsToOwner()) {
discordService.sendOwnerLog(license, new EmbedBuilder()
.setColor(0xF2781B)
.setTitle("New HWID")
.setDescription("One of your licenses has been used on a new HWID:")
.addField("License", "`" + obfuscateKey + "`", true)
.addField("Product", license.getProduct(), true)
.addField("HWID", "```" + hwid + "```", false)
);
}
// Logging the license use
log.info("License key '{}' for product '{}' was used by {} (HWID: {})", key, product, ip, hwid);
return license;
} catch (APIException ex) {
// Log that the license has reached it's IP limit
if (ex instanceof LicenseIpLimitExceededException && discordService.isLogIpLimitExceeded()) {
discordService.sendLog(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("License IP Limit Reached")
.setDescription("License `%s` has reached it's IP limit: **%s**".formatted(
obfuscateKey,
license.getIpLimit()
))
);
} else if (ex instanceof LicenseHwidLimitExceededException && discordService.isLogHwidLimitExceeded()) {
discordService.sendLog(new EmbedBuilder()
.setColor(Color.RED)
.setTitle("License HWID Limit Reached")
.setDescription("License `%s` has reached it's HWID limit: **%s**".formatted(
obfuscateKey,
license.getHwidLimit()
))
);
}
throw ex; // Rethrow to handle where this method was invoked
}
}
}

View File

@ -9,6 +9,23 @@ salts:
licenses: "$2a$10$/nQyzQDMkCf97ZlJLLWa3O"
ips: "$2a$10$Xus.AHTCas97Ofx0tFs85O"
# Discord Bot Configuration
discord:
token: ""
# Global Logs
logs:
channel: 0 # The channel ID to log to, leave as 0 to disable
uses: true # Should used licenses be logged?
expired: true # Should we log if an expired license was used?
ipLimitExceeded: true # Should IP limited licenses be logged when used?
hwidLimitExceeded: true # Should HWID limited licenses be logged when used?
# License Owner Logs
owner-logs:
newIp: true # Should new IPs be sent to the license owner?
newHwid: true # Should new HWIDs be sent to the license owner?
# Log Configuration
logging:
file:
@ -16,14 +33,16 @@ logging:
# Spring Configuration
spring:
application:
name: "License Server"
# Database Configuration
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
banner:
location: "classpath:banner.txt"

View File

@ -3,6 +3,6 @@
| |__| / _/ -_) ' \(_-</ -_) \__ \/ -_) '_\ V / -_) '_|
|____|_\__\___|_||_/__/\___| |___/\___|_| \_/\___|_|
| API Version - v${application.version}
| Application Version - v${application.version}
| Spring Version - ${spring-boot.formatted-version}
___________________________________________________________