43 Commits

Author SHA1 Message Date
Renovate Bot
5315e239c9 chore(deps): update dependency @types/node to v20.17.10 2024-12-19 09:03:15 +00:00
f5372c153c Merge pull request 'fix(deps): update dependency net.dv8tion:jda to v5.2.1' (#2) from renovate/net.dv8tion-jda-5.x into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Failing after 9s
Reviewed-on: #2
2024-12-18 23:25:50 -08:00
509c60e632 Merge pull request 'fix(deps): update dependency org.springframework.boot:spring-boot-starter-parent to v3.4.0' (#3) from renovate/spring-boot into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Has been cancelled
Reviewed-on: #3
2024-12-18 23:25:44 -08:00
247b2a6d6f Merge pull request 'chore(deps): update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.13.0' (#5) from renovate/org.apache.maven.plugins-maven-compiler-plugin-3.x into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Has been cancelled
Reviewed-on: #5
2024-12-18 23:25:37 -08:00
c0e2489828 Merge pull request 'chore(deps): update dependency org.apache.maven.plugins:maven-shade-plugin to v3.6.0' (#6) from renovate/org.apache.maven.plugins-maven-shade-plugin-3.x into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Has been cancelled
Reviewed-on: #6
2024-12-18 23:25:20 -08:00
980eaf05ba Merge pull request 'fix(deps): update dependency com.github.oshi:oshi-core to v6.6.5' (#9) from renovate/com.github.oshi-oshi-core-6.x into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Has been cancelled
Reviewed-on: #9
2024-12-18 23:24:52 -08:00
ed823e9391 Merge pull request 'fix(deps): update dependency com.google.guava:guava to v32.1.3-jre' (#10) from renovate/guava-monorepo into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Has been cancelled
Reviewed-on: #10
2024-12-18 23:24:45 -08:00
b8cd5b299d Merge pull request 'fix(deps): update dependency com.squareup.okhttp3:okhttp to v4.12.0' (#11) from renovate/okhttp-monorepo into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Has been cancelled
Reviewed-on: #11
2024-12-18 23:24:35 -08:00
Renovate Bot
768d4487fb fix(deps): update dependency org.springframework.boot:spring-boot-starter-parent to v3.4.0 2024-12-12 07:08:27 +00:00
Renovate Bot
8325b98b8b fix(deps): update dependency net.dv8tion:jda to v5.2.1 2024-11-10 17:01:55 +00:00
Renovate Bot
256b7d3d34 fix(deps): update dependency com.squareup.okhttp3:okhttp to v4.12.0 2024-11-07 20:10:56 +00:00
Renovate Bot
460d454f08 fix(deps): update dependency com.github.oshi:oshi-core to v6.6.5 2024-11-07 11:10:56 +00:00
Renovate Bot
bd0a2e907d fix(deps): update dependency com.google.guava:guava to v32.1.3-jre 2024-11-06 23:04:10 +00:00
Renovate Bot
968602ac0d chore(deps): update dependency org.apache.maven.plugins:maven-shade-plugin to v3.6.0 2024-09-09 19:26:30 +00:00
Renovate Bot
409f0ce80a chore(deps): update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.13.0 2024-03-18 19:01:38 +00:00
11ba593439 Merge pull request 'chore: Configure Renovate' (#1) from renovate/configure into master
Some checks failed
Publish / docker (ubuntu-latest, 2.38.4) (push) Failing after 30s
Reviewed-on: #1
2024-03-17 19:24:04 -07:00
Renovate Bot
ce90040ab7 chore(deps): add renovate.json 2024-03-18 02:21:55 +00:00
50e64a00c5 Remove this for now 2023-12-02 20:40:20 -05:00
0eb8ef6824 Initial Frontend Base 2023-12-02 04:48:37 -05:00
56ca2f5fd5 Add badges to README.md 2023-12-02 04:15:07 -05:00
Braydon
7c6e3b470b Update Example-Java/README.md 2023-12-02 01:12:40 -08:00
16968427b3 oops x.x 2023-12-02 04:08:46 -05:00
8d97e0efa4 Example -> Example-Java 2023-12-02 04:08:12 -05:00
df004e59e2 Bump lombok ver 2023-12-02 03:59:06 -05:00
b6f43a6510 Add docker-compose file 2023-12-02 03:57:30 -05:00
e1a829664a oops, forgot this 2023-12-02 03:53:11 -05:00
c6f968851a Add plan & latest product ver to licenses 2023-12-02 03:52:57 -05:00
f32a92fe54 Change ownership in licenses 2023-12-02 03:44:58 -05:00
49b8fe39a5 Add cryptography 2023-12-02 03:26:57 -05:00
9215ac87b0 Clean this up 2023-12-02 00:59:46 -05:00
d7dc635ffb Fix last used 2023-12-02 00:30:05 -05:00
ab98d752c8 put back onto another thread 2023-12-02 00:26:18 -05:00
3e16f212da test 2023-12-02 00:22:37 -05:00
e391d3e48e test 2023-12-02 00:15:50 -05:00
67a88d3366 test 2023-12-02 00:11:44 -05:00
97aea0dafc bump JDA ver 2023-12-02 00:08:22 -05:00
be0825bf15 yes no? 2023-12-02 00:02:21 -05:00
ce6abbdf55 yes no? 2023-12-02 00:01:08 -05:00
f95296db06 Update workflow file 2023-12-01 23:51:29 -05:00
5c7c7e9f68 will this work on the main thread? 2023-12-01 23:46:48 -05:00
1eda57a7e3 temp create default key when no others exist 2023-06-14 20:10:14 -04:00
57b5c4e05d Improve bot service 2023-06-14 20:10:04 -04:00
2e1e2f5127 Bump app ver and move to new JDA ver 2023-06-14 20:06:00 -04:00
49 changed files with 4483 additions and 98 deletions

View File

@ -1,4 +1,4 @@
name: Publish Image name: Publish
on: on:
push: push:
@ -10,16 +10,18 @@ jobs:
matrix: matrix:
arch: [ "ubuntu-latest" ] arch: [ "ubuntu-latest" ]
git-version: [ "2.38.4" ] git-version: [ "2.38.4" ]
runs-on: ${{ matrix.arch }} runs-on: ${{ matrix.arch }}
container: git.rainnny.club/rainnny/gitea-runner:node-18 container: fascinated/docker-images:nodejs_20
steps: steps:
# Checkout the branch
- name: Checkout - name: Checkout
uses: https://github.com/actions/checkout@v3 uses: https://github.com/actions/checkout@v3
# Setup Docker BuildX
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v2 uses: https://github.com/docker/setup-buildx-action@v2
# Login to Docker
- name: Login to Repo - name: Login to Repo
uses: https://github.com/docker/login-action@v2 uses: https://github.com/docker/login-action@v2
with: with:
@ -27,6 +29,7 @@ jobs:
username: ${{ secrets.REPO_USERNAME }} username: ${{ secrets.REPO_USERNAME }}
password: ${{ secrets.REPO_TOKEN }} password: ${{ secrets.REPO_TOKEN }}
# Build & Push to Docker
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
git.properties git.properties
CLI

View File

@ -14,7 +14,7 @@ COPY src ./src
RUN mvn clean package -T12 RUN mvn clean package -T12
# Stage 2: Running # Stage 2: Running
FROM openjdk:17.0.1-jdk-slim FROM openjdk:17.0.2-jdk-slim
# Set the work dir inside the container # Set the work dir inside the container
WORKDIR /usr/local/app WORKDIR /usr/local/app

2
Example-Java/README.md Normal file
View File

@ -0,0 +1,2 @@
# Java Example
This is the example of how to interact with the license server from within Java.

View File

@ -20,7 +20,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <version>3.13.0</version>
<configuration> <configuration>
<source>${java.version}</source> <source>${java.version}</source>
<target>${java.version}</target> <target>${java.version}</target>
@ -30,7 +30,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version> <version>3.6.0</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@ -48,7 +48,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.28</version> <version>1.18.30</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
@ -64,7 +64,7 @@
<dependency> <dependency>
<groupId>com.github.oshi</groupId> <groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId> <artifactId>oshi-core</artifactId>
<version>6.4.2</version> <version>6.6.5</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
@ -72,7 +72,7 @@
<dependency> <dependency>
<groupId>com.squareup.okhttp3</groupId> <groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId> <artifactId>okhttp</artifactId>
<version>4.11.0</version> <version>4.12.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -1,13 +1,15 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.example; package me.braydon.example;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.AllArgsConstructor; import lombok.*;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import okhttp3.*; import okhttp3.*;
import oshi.SystemInfo; import oshi.SystemInfo;
import oshi.hardware.CentralProcessor; import oshi.hardware.CentralProcessor;
@ -15,9 +17,18 @@ import oshi.hardware.ComputerSystem;
import oshi.hardware.HardwareAbstractionLayer; import oshi.hardware.HardwareAbstractionLayer;
import oshi.software.os.OperatingSystem; import oshi.software.os.OperatingSystem;
import javax.crypto.Cipher;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -32,51 +43,93 @@ import java.util.Map;
* @author Braydon * @author Braydon
* @see <a href="https://git.rainnny.club/Rainnny/LicenseServer">License Server</a> * @see <a href="https://git.rainnny.club/Rainnny/LicenseServer">License Server</a>
*/ */
public final class LicenseExample { public final class LicenseClient {
private static final String ALGORITHM = "RSA"; // The crypto algorithm to use
/**
* The endpoint to use for downloading the {@link PublicKey}.
*/
private static final String PUBLIC_KEY_ENDPOINT = "/crypto/pub";
/** /**
* The endpoint to check licenses at. * The endpoint to check licenses at.
*/ */
private static final String CHECK_ENDPOINT = "http://localhost:7500/check"; private static final String CHECK_ENDPOINT = "/check";
/** /**
* The {@link Gson} instance to use. * The {@link Gson} instance to use.
*/ */
private static final Gson GSON = new GsonBuilder() private static final Gson GSON = new GsonBuilder().serializeNulls().create();
.serializeNulls()
.create(); /**
* The URL of the license server to make requests to.
*/
@NonNull private final String appUrl;
/**
* The product to use for client.
*/
@NonNull private final String product;
/**
* The {@link OkHttpClient} to use for requests.
*/
@NonNull private final OkHttpClient httpClient;
/**
* The {@link PublicKey} to use for encryption.
*/
@NonNull private final PublicKey publicKey;
public LicenseClient(@NonNull String appUrl, @NonNull String product, @NonNull File publicKeyFile) {
this.appUrl = appUrl;
this.product = product;
httpClient = new OkHttpClient(); // Create a new http client
publicKey = fetchPublicKey(publicKeyFile); // Fetch our public key
}
/**
* Read the public key from the given bytes.
*
* @param bytes the bytes of the public key
* @return the public key
* @see PrivateKey for public key
*/
@SneakyThrows
private static PublicKey readPublicKey(byte[] bytes) {
return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(bytes));
}
/** /**
* Check the license with the given * Check the license with the given
* key for the given product. * key for the given product.
* *
* @param key the key to check * @param key the key to check
* @param product the product the key belongs to
* @return the license response * @return the license response
* @see LicenseResponse for response * @see LicenseResponse for response
*/ */
@NonNull @NonNull
public static LicenseResponse check(@NonNull String key, @NonNull String product) { public LicenseResponse check(@NonNull String key) {
String hardwareId = getHardwareId(); // Get the hardware id of the machine String hardwareId = getHardwareId(); // Get the hardware id of the machine
// Build the json body // Build the json body
Map<String, Object> body = new HashMap<>(); Map<String, Object> body = new HashMap<>();
body.put("key", key); body.put("key", encrypt(key));
body.put("product", product); body.put("product", product);
body.put("hwid", hardwareId); body.put("hwid", encrypt(hardwareId));
String bodyJson = GSON.toJson(body); // The json body 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 MediaType mediaType = MediaType.parse("application/json"); // Ensure the media type is json
RequestBody requestBody = RequestBody.create(bodyJson, mediaType); // Build the request body RequestBody requestBody = RequestBody.create(mediaType, bodyJson); // Build the request body
Request request = new Request.Builder() Request request = new Request.Builder()
.url(CHECK_ENDPOINT) .url(appUrl + CHECK_ENDPOINT)
.post(requestBody) .post(requestBody)
.build(); // Build the POST request .build(); // Build the POST request
Response response = null; // The response of the request Response response = null; // The response of the request
int responseCode = -1; // The response code of the request int responseCode = -1; // The response code of the request
try { // Attempt to execute the request try { // Attempt to execute the request
response = client.newCall(request).execute(); response = httpClient.newCall(request).execute();
responseCode = response.code(); responseCode = response.code();
// If the response is successful, we can parse the response // If the response is successful, we can parse the response
@ -88,6 +141,8 @@ public final class LicenseExample {
JsonElement description = json.get("description"); JsonElement description = json.get("description");
JsonElement ownerSnowflake = json.get("ownerSnowflake"); JsonElement ownerSnowflake = json.get("ownerSnowflake");
JsonElement ownerName = json.get("ownerName"); JsonElement ownerName = json.get("ownerName");
JsonElement plan = json.get("plan");
JsonElement latestVersion = json.get("latestVersion");
// Parsing the expiration date if we have one // Parsing the expiration date if we have one
JsonElement expires = json.get("expires"); JsonElement expires = json.get("expires");
@ -102,6 +157,8 @@ public final class LicenseExample {
description.isJsonNull() ? null : description.getAsString(), description.isJsonNull() ? null : description.getAsString(),
ownerSnowflake.isJsonNull() ? -1 : ownerSnowflake.getAsLong(), ownerSnowflake.isJsonNull() ? -1 : ownerSnowflake.getAsLong(),
ownerName.isJsonNull() ? null : ownerName.getAsString(), ownerName.isJsonNull() ? null : ownerName.getAsString(),
plan.getAsString(),
latestVersion.getAsString(),
expires.isJsonNull() ? null : expiresDate expires.isJsonNull() ? null : expiresDate
); );
} else { } else {
@ -127,6 +184,43 @@ public final class LicenseExample {
return new LicenseResponse(responseCode, "An unknown error occurred"); return new LicenseResponse(responseCode, "An unknown error occurred");
} }
/**
* Fetch the public key.
* <p>
* If the public key is not already present, we
* fetch it from the server. Otherwise, the public
* key is loaded from the file.
* </p>
*
* @param publicKeyFile the public key file
* @return the public key
* @see PublicKey for public key
*/
@SneakyThrows
private PublicKey fetchPublicKey(@NonNull File publicKeyFile) {
byte[] publicKey;
if (publicKeyFile.exists()) { // Public key exists, use it
publicKey = Files.readAllBytes(publicKeyFile.toPath());
} else {
Request request = new Request.Builder()
.url(appUrl + PUBLIC_KEY_ENDPOINT)
.build(); // Build the GET request
@Cleanup Response response = httpClient.newCall(request).execute(); // Make the request
if (!response.isSuccessful()) { // Response wasn't successful
throw new IOException("Failed to download the public key, got response " + response.code());
}
ResponseBody body = response.body(); // Get the response body
assert body != null; // We need a response body
publicKey = body.bytes(); // Read our public key
// Write the response to the public key file
try (FileOutputStream outputStream = new FileOutputStream(publicKeyFile)) {
outputStream.write(publicKey);
}
}
return readPublicKey(publicKey);
}
/** /**
* Get the unique hardware * Get the unique hardware
* identifier of this machine. * identifier of this machine.
@ -134,7 +228,7 @@ public final class LicenseExample {
* @return the hardware id * @return the hardware id
*/ */
@NonNull @NonNull
private static String getHardwareId() { private String getHardwareId() {
SystemInfo systemInfo = new SystemInfo(); SystemInfo systemInfo = new SystemInfo();
OperatingSystem operatingSystem = systemInfo.getOperatingSystem(); OperatingSystem operatingSystem = systemInfo.getOperatingSystem();
HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware(); HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware();
@ -155,9 +249,25 @@ public final class LicenseExample {
+ String.format("%08x", processorIdentifier.hashCode()) + "-" + processors; + String.format("%08x", processorIdentifier.hashCode()) + "-" + processors;
} }
@AllArgsConstructor /**
@Getter * Encrypt the given input.
@ToString *
* @param input the encrypted input
* @return the encrypted result
*/
@SneakyThrows @NonNull
private String encrypt(@NonNull String input) {
Cipher cipher = Cipher.getInstance(ALGORITHM); // Create our cipher
cipher.init(Cipher.ENCRYPT_MODE, publicKey); // Set our mode and public key
return Base64.getEncoder().encodeToString(cipher.doFinal(input.getBytes())); // Return our encrypted result
}
/**
* The response of a license check.
*
* @see #check(String)
*/
@AllArgsConstructor @Getter @ToString
public static class LicenseResponse { public static class LicenseResponse {
/** /**
* The status code of the response. * The status code of the response.
@ -186,6 +296,16 @@ public final class LicenseExample {
*/ */
private String ownerName; private String ownerName;
/**
* The plan for this license.
*/
@NonNull private String plan;
/**
* The latest version of the product this license is for.
*/
@NonNull private String latestVersion;
/** /**
* The optional expiration {@link Date} of the license. * The optional expiration {@link Date} of the license.
*/ */

View File

@ -1,11 +1,19 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.example; package me.braydon.example;
import java.io.File;
/** /**
* @author Braydon * @author Braydon
*/ */
public final class Main { public final class Main {
public static void main(String[] args) { public static void main(String[] args) {
LicenseExample.LicenseResponse response = LicenseExample.check("XXXX-XXXX-XXXX-XXXX", "Example"); LicenseClient client = new LicenseClient("http://localhost:7500", "Example", new File("public.key")); // Create the client
LicenseClient.LicenseResponse response = client.check("XXXX-XXXX-XXXX-XXXX"); // Check our license
if (!response.isValid()) { // License isn't valid if (!response.isValid()) { // License isn't valid
System.err.println("Invalid license: " + response.getError()); System.err.println("Invalid license: " + response.getError());
return; return;
@ -13,7 +21,7 @@ public final class Main {
// License is valid // License is valid
System.out.println("License is valid!"); System.out.println("License is valid!");
if (response.getOwnerName() != null) { if (response.getOwnerName() != null) {
System.out.println("Welcome " + response.getOwnerName() + "!"); System.out.println("Welcome " + response.getOwnerName() + "! Your plan is " + response.getPlan() + " and the latest version is " + response.getLatestVersion());
} }
if (response.getDescription() != null) { if (response.getDescription() != null) {
System.out.println("Description: " + response.getDescription()); // License description System.out.println("Description: " + response.getDescription()); // License description

3
Frontend/.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

17
Frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
/node_modules
/.pnp
.pnp.js
/coverage
/.next/
/out/
/build
.DS_Store
*.pem
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
.vercel
*.tsbuildinfo
next-env.d.ts
.directory

36
Frontend/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

16
Frontend/components.json Normal file
View File

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/libs"
}
}

4
Frontend/next.config.js Normal file
View File

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig

42
Frontend/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "license-server",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@8.7.1",
"authors": [
{
"name": "Braydon",
"email": "braydonrainnny@gmail.com",
"url": "https://rainnny.club"
}
],
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"next": "14.0.3",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.3",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

3204
Frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
Frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import "../globals.css";
import { Inter as FontSans } from "next/font/google";
import { cn } from "../libs/libs";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable
)}
>
{children}
</body>
</html>
);
}

View File

@ -0,0 +1,26 @@
import { Button } from "../components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "../components/ui/context-menu";
const Home = (): JSX.Element => (
<div className="flex flex-col gap-5">
<h1>Hello World</h1>
<ContextMenu>
<ContextMenuTrigger>
<Button>Click Me</Button>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</div>
);
export default Home;

View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/libs"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/libs"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -0,0 +1,76 @@
const { fontFamily } = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./src/app/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
},
},
plugins: [require("tailwindcss-animate")],
};

29
Frontend/tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/app/*"],
"@/components/*": ["./src/app/components/*"],
"@/libs": ["./src/app/libs/libs.ts"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,5 +1,8 @@
# LicenseServer [![Discord](https://discord.com/api/guilds/827863713855176755/widget.png)](https://discord.gg/p9gzFE2bc6)
![Wakatime Hours](https://wakatime.rainnny.club/api/badge/Rainnny/interval:any/project:LicenseServer)
[![Download](https://img.shields.io/badge/Download-Releases-darkgreen.svg)](https://git.rainnny.club/Rainnny/LicenseServer/releases)
# LicenseServer
A simple open-source licensing server for your products. A simple open-source licensing server for your products.
## Discord Preview ## Discord Preview
@ -18,11 +21,11 @@ POST /check
#### Body #### Body
| Key | Type | Description | | Key | Type | Description |
|:----------|:---------|:-----------------------------------------------| |:----------|:---------|:----------------------------------------------------------------|
| `key` | `string` | **Required**. Your license key | | `key` | `string` | **Required**. Your base64 encrypted license key |
| `product` | `string` | **Required**. The product the license is for | | `product` | `string` | **Required**. The product the license is for |
| `hwid` | `string` | **Required**. The hardware id of the requester | | `hwid` | `string` | **Required**. The base64 encrypted hardware id of the requester |
#### Response #### Response

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: '3'
services:
app:
image: git.rainnny.club/rainnny/licenseserver:latest
volumes:
- ./data/application.yml:/usr/local/app/application.yml
ports:
- "7500:7500"

10
pom.xml
View File

@ -6,13 +6,13 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version> <version>3.4.0</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>me.braydon</groupId> <groupId>me.braydon</groupId>
<artifactId>LicenseServer</artifactId> <artifactId>LicenseServer</artifactId>
<version>1.0.3</version> <version>1.0.5</version>
<description>A simple open-source licensing server for your products.</description> <description>A simple open-source licensing server for your products.</description>
<properties> <properties>
@ -52,7 +52,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.28</version> <version>1.18.30</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
@ -60,7 +60,7 @@
<dependency> <dependency>
<groupId>net.dv8tion</groupId> <groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId> <artifactId>JDA</artifactId>
<version>5.0.0-beta.9</version> <version>5.2.1</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>club.minnced</groupId> <groupId>club.minnced</groupId>
@ -96,7 +96,7 @@
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
<version>32.0.0-jre</version> <version>32.1.3-jre</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>Rainnny/renovate-config"
]
}

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license; package me.braydon.license;
import com.google.gson.Gson; import com.google.gson.Gson;

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import javax.crypto.Cipher;
import java.io.File;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* @author Braydon
*/
@UtilityClass
public final class CryptographyUtils {
private static final String ALGORITHM = "RSA"; // The algorithm to use
/**
* Generate a new key pair.
*
* @return the key pair
* @see KeyPair for key pair
*/
@NonNull @SneakyThrows
public static KeyPair generateKeyPair() {
KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM); // Create a generator
generator.initialize(2048); // Set the key size
return generator.generateKeyPair(); // Return our generated key pair
}
/**
* Read the public key from the given file.
*
* @param keyFile the key file to read
* @return the public key
* @see PrivateKey for public key
*/
@SneakyThrows
public static PublicKey readPublicKey(@NonNull File keyFile) {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Files.readAllBytes(keyFile.toPath())); // Get the key spec
return KeyFactory.getInstance(ALGORITHM).generatePublic(keySpec); // Return the public key from the key spec
}
/**
* Read the private key from the given file.
*
* @param keyFile the key file to read
* @return the private key
* @see PrivateKey for private key
*/
@SneakyThrows
public static PrivateKey readPrivateKey(@NonNull File keyFile) {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Files.readAllBytes(keyFile.toPath())); // Get the key spec
return KeyFactory.getInstance(ALGORITHM).generatePrivate(keySpec); // Return the private key from the key spec
}
/**
* Decrypt the given input with
* the provided private key.
*
* @param input the encrypted input
* @param privateKey the private key
* @return the decrypted result
* @see PrivateKey for private key
*/
@SneakyThrows @NonNull
public static String decryptMessage(@NonNull String input, @NonNull PrivateKey privateKey) {
Cipher cipher = Cipher.getInstance(ALGORITHM); // Create the cipher
cipher.init(Cipher.DECRYPT_MODE, privateKey); // Set our mode and private key
return new String(cipher.doFinal(Base64.getDecoder().decode(input))); // Return our decrypted result
}
}

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common; package me.braydon.license.common;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common; package me.braydon.license.common;
import lombok.NonNull; import lombok.NonNull;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common; package me.braydon.license.common;
import lombok.NonNull; import lombok.NonNull;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common; package me.braydon.license.common;
import lombok.NonNull; import lombok.NonNull;

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.controller;
import lombok.NonNull;
import me.braydon.license.model.License;
import me.braydon.license.service.CryptographyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.security.PublicKey;
/**
* @author Braydon
*/
@RestController
@RequestMapping(value = "/crypto", produces = MediaType.APPLICATION_JSON_VALUE)
public final class CryptographyController {
/**
* The {@link CryptographyService} to use.
*/
@NonNull private final CryptographyService service;
@Autowired
public CryptographyController(@NonNull CryptographyService service) {
this.service = service;
}
/**
* Downloads the public key file.
*
* @return the response entity
* @see PublicKey for public key
* @see License for license
* @see ResponseEntity for response entity
*/
@GetMapping("/pub")
@ResponseBody
public ResponseEntity<Resource> publicKey() {
byte[] publicKey = service.getKeyPair().getPublic().getEncoded(); // Get the public key
String fileName = "public.key"; // The name of the file to download
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentLength(publicKey.length)
.body(new ByteArrayResource(publicKey));
}
}

View File

@ -1,14 +1,19 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.controller; package me.braydon.license.controller;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull; import lombok.NonNull;
import me.braydon.license.LicenseServer; import me.braydon.license.common.CryptographyUtils;
import me.braydon.license.common.IPUtils; import me.braydon.license.common.IPUtils;
import me.braydon.license.dto.LicenseCheckBodyDTO;
import me.braydon.license.dto.LicenseDTO; import me.braydon.license.dto.LicenseDTO;
import me.braydon.license.exception.APIException; import me.braydon.license.exception.APIException;
import me.braydon.license.model.License; import me.braydon.license.model.License;
import me.braydon.license.service.CryptographyService;
import me.braydon.license.service.LicenseService; import me.braydon.license.service.LicenseService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -16,6 +21,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.security.PrivateKey;
import java.util.Map; import java.util.Map;
/** /**
@ -24,14 +30,20 @@ import java.util.Map;
@RestController @RestController
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public final class LicenseController { public final class LicenseController {
/**
* The {@link CryptographyService} to use.
*/
@NonNull private final CryptographyService cryptographyService;
/** /**
* The {@link LicenseService} to use. * The {@link LicenseService} to use.
*/ */
@NonNull private final LicenseService service; @NonNull private final LicenseService licenseService;
@Autowired @Autowired
public LicenseController(@NonNull LicenseService service) { public LicenseController(@NonNull CryptographyService cryptographyService, @NonNull LicenseService licenseService) {
this.service = service; this.cryptographyService = cryptographyService;
this.licenseService = licenseService;
} }
/** /**
@ -40,53 +52,57 @@ public final class LicenseController {
* @param body the body of the request * @param body the body of the request
* @return the response entity * @return the response entity
* @see License for license * @see License for license
* @see LicenseCheckBodyDTO for body
* @see ResponseEntity for response entity * @see ResponseEntity for response entity
*/ */
@PostMapping("/check") @PostMapping("/check")
@ResponseBody @ResponseBody
public ResponseEntity<?> check(@NonNull HttpServletRequest request, @RequestBody @NonNull String body) { public ResponseEntity<?> check(@NonNull HttpServletRequest request, @RequestBody @NonNull LicenseCheckBodyDTO body) {
try { // Attempt to check the license try { // Attempt to check the license
String ip = IPUtils.getRealIp(request); // The IP of the requester if (!body.isValid()) {
JsonObject jsonObject = LicenseServer.GSON.fromJson(body, JsonObject.class);
JsonElement key = jsonObject.get("key"); // Get the key
JsonElement product = jsonObject.get("product"); // Get the product
JsonElement hwid = jsonObject.get("hwid"); // Get the hwid
// Ensure the body keys aren't null
if (key.isJsonNull() || product.isJsonNull() || hwid.isJsonNull()) {
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid request body"); throw new APIException(HttpStatus.BAD_REQUEST, "Invalid request body");
} }
// Ensure the IP is valid // Ensure the IP is valid
String ip = IPUtils.getRealIp(request); // The IP of the requester
if (IPUtils.getIpType(ip) == -1) { if (IPUtils.getIpType(ip) == -1) {
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid IP address"); throw new APIException(HttpStatus.BAD_REQUEST, "Invalid IP address");
} }
// Ensure the HWID is valid String key;
// TODO: improve :) String hwid;
String hwidString = hwid.getAsString(); try {
PrivateKey privateKey = cryptographyService.getKeyPair().getPrivate(); // Get our private key
key = CryptographyUtils.decryptMessage(body.getKey(), privateKey); // Decrypt our license key
hwid = CryptographyUtils.decryptMessage(body.getHwid(), privateKey); // Decrypt our hwid
} catch (IllegalArgumentException ex) {
throw new APIException(HttpStatus.BAD_REQUEST, "Signature Error");
}
// Validating that the UUID is in the correct format
boolean invalidHwid = true; boolean invalidHwid = true;
if (hwidString.contains("-")) { if (hwid.contains("-")) {
int segments = hwidString.substring(0, hwidString.lastIndexOf("-")).split("-").length; int segments = hwid.substring(0, hwid.lastIndexOf("-")).split("-").length;
if (segments == 4) { if (segments == 4) {
invalidHwid = false; invalidHwid = false;
} }
} }
if (invalidHwid) { if (invalidHwid) { // Invalid HWID
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid HWID"); throw new APIException(HttpStatus.BAD_REQUEST, "Invalid HWID");
} }
// Check the license // Check the license
License license = service.check( License license = licenseService.check(
key.getAsString(), key,
product.getAsString(), body.getProduct(),
ip, ip,
hwidString hwid
); );
// Return OK with the license DTO // Return OK with the license DTO
return ResponseEntity.ok(new LicenseDTO( return ResponseEntity.ok(new LicenseDTO(
license.getDescription(), license.getDescription(),
license.getOwnerSnowflake(), license.getOwnerSnowflake(),
license.getOwnerName(), license.getOwnerName(),
license.getPlan(),
license.getLatestVersion(),
license.getExpires() license.getExpires()
)); ));
} catch (APIException ex) { // Handle the exception } catch (APIException ex) { // Handle the exception

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import me.braydon.license.model.License;
/**
* A data transfer object that contains
* the body for checking a {@link License}.
*
* @author Braydon
*/
@AllArgsConstructor @Getter @ToString
public class LicenseCheckBodyDTO {
/**
* The license key to check.
*/
private String key;
/**
* The product of the license to check.
*/
private String product;
/**
* The hardware id of the user checking the license.
*/
private String hwid;
/**
* Are these params valid?
*
* @return whether the params are valid
*/
public boolean isValid() {
return key != null && product != null && hwid != null;
}
}

View File

@ -1,7 +1,13 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.dto; package me.braydon.license.dto;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull;
import lombok.ToString; import lombok.ToString;
import me.braydon.license.model.License; import me.braydon.license.model.License;
@ -35,6 +41,16 @@ public class LicenseDTO {
*/ */
private String ownerName; private String ownerName;
/**
* The plan for this license.
*/
@NonNull private String plan;
/**
* The latest version of the product this license is for.
*/
@NonNull private String latestVersion;
/** /**
* The optional expiration {@link Date} of this license. * The optional expiration {@link Date} of this license.
*/ */

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception; package me.braydon.license.exception;
import lombok.Getter; import lombok.Getter;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception; package me.braydon.license.exception;
import me.braydon.license.model.License; import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception; package me.braydon.license.exception;
import me.braydon.license.model.License; import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception; package me.braydon.license.exception;
import me.braydon.license.model.License; import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception; package me.braydon.license.exception;
import me.braydon.license.model.License; import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.model; package me.braydon.license.model;
import lombok.Getter; import lombok.Getter;
@ -9,6 +14,7 @@ import me.braydon.license.exception.LicenseHwidLimitExceededException;
import me.braydon.license.exception.LicenseIpLimitExceededException; import me.braydon.license.exception.LicenseIpLimitExceededException;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.util.Date; import java.util.Date;
import java.util.Set; import java.util.Set;
@ -44,6 +50,7 @@ public class License {
* If this is -1, the license is not owned by anyone. * If this is -1, the license is not owned by anyone.
* </p> * </p>
*/ */
@Field("owner.snowflake")
private long ownerSnowflake; private long ownerSnowflake;
/** /**
@ -52,8 +59,19 @@ public class License {
* If this is null, the license is not owned by anyone. * If this is null, the license is not owned by anyone.
* </p> * </p>
*/ */
@Field("owner.name")
private String ownerName; private String ownerName;
/**
* The plan for this license.
*/
@NonNull private String plan;
/**
* The latest version of the product this license is for.
*/
@NonNull private String latestVersion;
/** /**
* The amount of uses this license has. * The amount of uses this license has.
*/ */

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.repository; package me.braydon.license.repository;
import lombok.NonNull; import lombok.NonNull;

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.service;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.braydon.license.common.CryptographyUtils;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyPair;
import java.util.Base64;
/**
* @author Braydon
*/
@Service
@Slf4j(topic = "Cryptography")
@Getter
public final class CryptographyService {
/**
* Our {@link KeyPair}.
*/
@NonNull private final KeyPair keyPair;
@SneakyThrows
public CryptographyService() {
File publicKeyFile = new File("public.key"); // The private key
File privateKeyFile = new File("private.key"); // The private key
if (!publicKeyFile.exists() || !privateKeyFile.exists()) { // Missing private key, generate new key pair.
keyPair = CryptographyUtils.generateKeyPair(); // Generate new key pair
writeKey(keyPair.getPublic().getEncoded(), publicKeyFile); // Write our public key
writeKey(keyPair.getPrivate().getEncoded(), privateKeyFile); // Write our private key
log.info("New key pair has been generated");
log.info(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
return;
}
// Load our private key from the file
keyPair = new KeyPair(CryptographyUtils.readPublicKey(publicKeyFile), CryptographyUtils.readPrivateKey(privateKeyFile));
log.info("Loaded private key from file " + privateKeyFile.getPath());
}
/**
* Write the given contents to the provided file.
*
* @param contents the content bytes to write
* @param file the file to write to
*/
private void writeKey(byte[] contents, @NonNull File file) {
try (FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(contents);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.service; package me.braydon.license.service;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
@ -33,7 +38,6 @@ import net.dv8tion.jda.api.requests.GatewayIntent;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.awt.*; import java.awt.*;
@ -59,7 +63,7 @@ public final class DiscordService {
/** /**
* The version of this Springboot application. * The version of this Springboot application.
*/ */
@NonNull private final String applicationVersion; @NonNull private final String applicationVersion = "n/a";
/** /**
* The salt to use for hashing license keys. * The salt to use for hashing license keys.
@ -140,9 +144,9 @@ public final class DiscordService {
.build(); .build();
@Autowired @Autowired
public DiscordService(@NonNull LicenseRepository licenseRepository, @NonNull BuildProperties buildProperties) { public DiscordService(@NonNull LicenseRepository licenseRepository/*, @NonNull BuildProperties buildProperties*/) {
this.licenseRepository = licenseRepository; this.licenseRepository = licenseRepository;
this.applicationVersion = buildProperties.getVersion(); // this.applicationVersion = buildProperties.getVersion();
} }
@PostConstruct @SneakyThrows @PostConstruct @SneakyThrows
@ -153,28 +157,34 @@ public final class DiscordService {
return; return;
} }
// Initialize the bot // Initialize the bot
long before = System.currentTimeMillis(); new Thread(() -> {
log.info("Logging in..."); // Log that we're logging in long before = System.currentTimeMillis();
jda = JDABuilder.createDefault(token) log.info("Logging in..."); // Log that we're logging in
.enableIntents( jda = JDABuilder.createDefault(token)
GatewayIntent.GUILD_MEMBERS .enableIntents(
).setStatus(OnlineStatus.DO_NOT_DISTURB) GatewayIntent.GUILD_MEMBERS
.setActivity(Activity.watching("your licenses")) ).setStatus(OnlineStatus.DO_NOT_DISTURB)
.addEventListeners(new EventHandler()) .setActivity(Activity.watching("your licenses"))
.build(); .addEventListeners(new EventHandler())
jda.awaitReady(); // Await JDA to be ready .build();
try {
jda.awaitReady(); // Await JDA to be ready
// Log that we're logged in // Log that we're logged in
log.info("Logged into {} in {}ms", log.info("Logged into {} in {}ms",
jda.getSelfUser().getAsTag(), System.currentTimeMillis() - before jda.getSelfUser().getEffectiveName(), System.currentTimeMillis() - before
); );
// Registering slash commands // Registering slash commands
jda.updateCommands().addCommands( jda.updateCommands().addCommands(
Commands.slash("license", "Manage one of your licenses") Commands.slash("license", "Manage one of your licenses")
.addOption(OptionType.STRING, "key", "The license key", true) .addOption(OptionType.STRING, "key", "The license key", true)
.addOption(OptionType.STRING, "product", "The product the license is for", true) .addOption(OptionType.STRING, "product", "The product the license is for", true)
).queue(); ).queue();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}, "Discord Bot Thread").start();
} }
/** /**
@ -269,6 +279,10 @@ public final class DiscordService {
public class EventHandler extends ListenerAdapter { public class EventHandler extends ListenerAdapter {
@Override @Override
public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) { public void onSlashCommandInteraction(@NonNull SlashCommandInteractionEvent event) {
// Bot isn't ready, don't handle events
if (!isReady()) {
return;
}
User user = event.getUser(); // The command executor User user = event.getUser(); // The command executor
// Handle the license command // Handle the license command
@ -292,7 +306,7 @@ public final class DiscordService {
License license = optionalLicense.get(); // The found license License license = optionalLicense.get(); // The found license
String obfuscateKey = MiscUtils.obfuscateKey(key); // Obfuscate the key String obfuscateKey = MiscUtils.obfuscateKey(key); // Obfuscate the key
long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L; long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L;
long lastUsed = license.getLastUsed() == null ? -1L : license.getExpires().getTime() / 1000L; long lastUsed = license.getLastUsed() == null ? -1L : license.getLastUsed().getTime() / 1000L;
event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder() event.getHook().sendMessageEmbeds(buildEmbed(new EmbedBuilder()
.setColor(Color.BLUE) .setColor(Color.BLUE)
.setTitle("Your License") .setTitle("Your License")
@ -303,6 +317,7 @@ public final class DiscordService {
expires == -1L ? "Never" : "<t:" + expires + ":R>", expires == -1L ? "Never" : "<t:" + expires + ":R>",
true true
) )
.addField("Plan", license.getPlan(), true)
.addField("Uses", String.valueOf(license.getUses()), true) .addField("Uses", String.valueOf(license.getUses()), true)
.addField("Last Used", .addField("Last Used",
lastUsed == -1L ? "Never" : "<t:" + lastUsed + ":R>", lastUsed == -1L ? "Never" : "<t:" + lastUsed + ":R>",
@ -339,6 +354,10 @@ public final class DiscordService {
@Override @Override
public void onButtonInteraction(@NonNull ButtonInteractionEvent event) { public void onButtonInteraction(@NonNull ButtonInteractionEvent event) {
// Bot isn't ready, don't handle events
if (!isReady()) {
return;
}
User user = event.getUser(); // The user who clicked the button User user = event.getUser(); // The user who clicked the button
String componentId = event.getComponentId(); // The button id String componentId = event.getComponentId(); // The button id

View File

@ -1,8 +1,15 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.service; package me.braydon.license.service;
import jakarta.annotation.PostConstruct;
import lombok.NonNull; import lombok.NonNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.braydon.license.common.MiscUtils; import me.braydon.license.common.MiscUtils;
import me.braydon.license.common.RandomUtils;
import me.braydon.license.exception.*; import me.braydon.license.exception.*;
import me.braydon.license.model.License; import me.braydon.license.model.License;
import me.braydon.license.repository.LicenseRepository; import me.braydon.license.repository.LicenseRepository;
@ -53,6 +60,29 @@ public final class LicenseService {
this.discordService = discordService; this.discordService = discordService;
} }
/**
* Create a default license key
* when no other keys exist.
* TODO: Remove this in the future and replace with creation API route
*/
@PostConstruct
public void onInitialize() {
if (repository.count() == 0L) { // No license keys found, create default
String licenseKey = RandomUtils.generateLicenseKey(); // The license key
create(
licenseKey,
"Example",
"Example",
0L,
null,
1,
1,
null
);
log.info("Generated default license: {}", licenseKey);
}
}
/** /**
* Create a new license key. * Create a new license key.
* *
@ -76,6 +106,8 @@ public final class LicenseService {
license.setDescription(description); // Use the given description, if any license.setDescription(description); // Use the given description, if any
license.setOwnerSnowflake(ownerSnowflake); license.setOwnerSnowflake(ownerSnowflake);
license.setOwnerName(ownerName); license.setOwnerName(ownerName);
license.setPlan("Basic");
license.setLatestVersion("1.0");
license.setIps(new HashSet<>()); license.setIps(new HashSet<>());
license.setHwids(new HashSet<>()); license.setHwids(new HashSet<>());
license.setIpLimit(ipLimit); // Use the given IP limit license.setIpLimit(ipLimit); // Use the given IP limit