24 Commits
1.0.1 ... 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
11 changed files with 426 additions and 74 deletions

View File

@ -16,6 +16,9 @@ import oshi.hardware.HardwareAbstractionLayer;
import oshi.software.os.OperatingSystem; import oshi.software.os.OperatingSystem;
import java.io.IOException; import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -85,14 +88,21 @@ 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 duration = json.get("duration");
// 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 the license response
return new LicenseResponse(200, null, return new LicenseResponse(200, null,
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(),
duration.isJsonNull() ? -1 : duration.getAsLong() expires.isJsonNull() ? null : expiresDate
); );
} else { } else {
ResponseBody errorBody = response.body(); // Get the error body ResponseBody errorBody = response.body(); // Get the error body
@ -177,12 +187,9 @@ public final class LicenseExample {
private String ownerName; private String ownerName;
/** /**
* The duration of the license, present if valid. * The optional expiration {@link Date} of the license.
* <p>
* If -1, the license will be permanent.
* </p>
*/ */
private long duration; private Date expires;
public LicenseResponse(long status, @NonNull String error) { public LicenseResponse(long status, @NonNull String error) {
this.status = status; this.status = status;
@ -204,7 +211,7 @@ public final class LicenseExample {
* @return true if permanent, otherwise false * @return true if permanent, otherwise false
*/ */
public boolean isPermanent() { public boolean isPermanent() {
return duration == -1; return expires == null;
} }
} }
} }

View File

@ -1,7 +1,5 @@
package me.braydon.example; package me.braydon.example;
import java.util.concurrent.TimeUnit;
/** /**
* @author Braydon * @author Braydon
*/ */
@ -22,9 +20,8 @@ public final class Main {
} }
if (response.isPermanent()) { // License is permanent if (response.isPermanent()) { // License is permanent
System.out.println("Your license is permanent"); System.out.println("Your license is permanent");
} else { // License has a duration } else { // License has an expiration date
long durationSeconds = TimeUnit.SECONDS.toMillis(response.getDuration()); // The duration in seconds System.out.printf("Your license will expire at: %s%n", response.getExpires().toInstant());
System.out.println("Your license will expire in " + durationSeconds + " seconds");
} }
} }
} }

View File

@ -1,3 +1,67 @@
# LicenseServer # 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"
```

View File

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

10
pom.xml
View File

@ -12,7 +12,7 @@
<groupId>me.braydon</groupId> <groupId>me.braydon</groupId>
<artifactId>LicenseServer</artifactId> <artifactId>LicenseServer</artifactId>
<version>1.0.1</version> <version>1.0.3</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>
@ -92,6 +92,14 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.0.0-jre</version>
<scope>compile</scope>
</dependency>
<!-- Spring --> <!-- Spring -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -61,20 +61,33 @@ public final class LicenseController {
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
// 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 // Check the license
License license = service.check( License license = service.check(
key.getAsString(), key.getAsString(),
product.getAsString(), product.getAsString(),
ip, ip,
hwid.getAsString() hwidString
); );
// 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.getDuration() license.getExpires()
)); ));
} catch (APIException ex) { // Handle the exception } catch (APIException ex) { // Handle the exception
return ResponseEntity.status(ex.getStatus()) return ResponseEntity.status(ex.getStatus())

View File

@ -5,6 +5,8 @@ import lombok.Getter;
import lombok.ToString; import lombok.ToString;
import me.braydon.license.model.License; import me.braydon.license.model.License;
import java.util.Date;
/** /**
* A data transfer object for a {@link License}. * A data transfer object for a {@link License}.
* *
@ -34,10 +36,7 @@ public class LicenseDTO {
private String ownerName; private String ownerName;
/** /**
* The duration that this licensee is valid for. * The optional expiration {@link Date} of this license.
* <p>
* If -1, the license will be permanent.
* </p>
*/ */
private long duration; private Date expires;
} }

View File

@ -80,12 +80,9 @@ public class License {
private int hwidLimit; private int hwidLimit;
/** /**
* The duration that this licensee is valid for. * The optional expiration {@link Date} of this license.
* <p>
* If -1, the license will be permanent.
* </p>
*/ */
private long duration; private Date expires;
/** /**
* The {@link Date} this license was last used. * The {@link Date} this license was last used.
@ -97,6 +94,18 @@ public class License {
*/ */
@NonNull private Date created; @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. * Check if this license has expired.
* <p> * <p>
@ -113,7 +122,7 @@ public class License {
return false; return false;
} }
// Check if the license has expired // Check if the license has expired
return System.currentTimeMillis() - created.getTime() >= duration; return expires.before(new Date());
} }
/** /**
@ -122,7 +131,7 @@ public class License {
* @return true if permanent, otherwise false * @return true if permanent, otherwise false
*/ */
public boolean isPermanent() { public boolean isPermanent() {
return duration == -1L; return expires == null;
} }
/** /**

View File

@ -1,38 +1,78 @@
package me.braydon.license.service; 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 jakarta.annotation.PostConstruct;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.braydon.license.common.MiscUtils;
import me.braydon.license.common.TimeUtils; 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.EmbedBuilder;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus; import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity; 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.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 net.dv8tion.jda.api.requests.GatewayIntent;
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.boot.info.BuildProperties;
import org.springframework.stereotype.Service; 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 * @author Braydon
*/ */
@Service @Service
@Slf4j(topic = "Discord") @Slf4j(topic = "Discord")
public final class DiscordService { 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. * The version of this Springboot application.
*/ */
@NonNull private final String applicationVersion; @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. * The name of this Springboot application.
*/ */
@Value("${spring.application.name}") @Value("${spring.application.name}")
@NonNull private String applicationName; @NonNull private String applicationName;
/** /**
* The token to the Discord bot. * The token to the Discord bot.
*/ */
@ -69,13 +109,39 @@ public final class DiscordService {
@Value("${discord.logs.expired}") @Getter @Value("${discord.logs.expired}") @Getter
private boolean logHwidLimitExceeded; 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. * The {@link JDA} instance of the bot.
*/ */
private JDA jda; 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 @Autowired
public DiscordService(@NonNull BuildProperties buildProperties) { public DiscordService(@NonNull LicenseRepository licenseRepository, @NonNull BuildProperties buildProperties) {
this.licenseRepository = licenseRepository;
this.applicationVersion = buildProperties.getVersion(); this.applicationVersion = buildProperties.getVersion();
} }
@ -94,6 +160,7 @@ public final class DiscordService {
GatewayIntent.GUILD_MEMBERS GatewayIntent.GUILD_MEMBERS
).setStatus(OnlineStatus.DO_NOT_DISTURB) ).setStatus(OnlineStatus.DO_NOT_DISTURB)
.setActivity(Activity.watching("your licenses")) .setActivity(Activity.watching("your licenses"))
.addEventListeners(new EventHandler())
.build(); .build();
jda.awaitReady(); // Await JDA to be ready jda.awaitReady(); // Await JDA to be ready
@ -101,6 +168,13 @@ public final class DiscordService {
log.info("Logged into {} in {}ms", log.info("Logged into {} in {}ms",
jda.getSelfUser().getAsTag(), System.currentTimeMillis() - before 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();
} }
/** /**
@ -125,9 +199,46 @@ public final class DiscordService {
throw new IllegalArgumentException("Log channel %s wasn't found".formatted(logsChannel)); throw new IllegalArgumentException("Log channel %s wasn't found".formatted(logsChannel));
} }
// Send the log // Send the log
textChannel.sendMessageEmbeds(embed.setFooter("%s v%s - %s".formatted( textChannel.sendMessageEmbeds(buildEmbed(embed)).queue();
applicationName, applicationVersion, TimeUtils.dateTime() }
)).build()).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();
}
});
} }
/** /**
@ -138,4 +249,136 @@ public final class DiscordService {
public boolean isReady() { public boolean isReady() {
return jda != null && (jda.getStatus() == JDA.Status.CONNECTED); 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

@ -63,12 +63,12 @@ public final class LicenseService {
* @param ownerName the optional owner name of the license * @param ownerName the optional owner name of the license
* @param ipLimit the IP limit of the license * @param ipLimit the IP limit of the license
* @param hwidLimit the HWID limit of the license * @param hwidLimit the HWID limit of the license
* @param duration the duration of the license, -1 for permanent * @param expires the optional expiration date of the license
* @return the created license * @return the created license
* @see License for license * @see License for license
*/ */
public License create(@NonNull String key, @NonNull String product, String description, long ownerSnowflake, public License create(@NonNull String key, @NonNull String product, String description, long ownerSnowflake,
String ownerName, int ipLimit, int hwidLimit, long duration) { String ownerName, int ipLimit, int hwidLimit, Date expires) {
// Create the new license // Create the new license
License license = new License(); License license = new License();
license.setKey(BCrypt.hashpw(key, licensesSalt)); // Hash the key license.setKey(BCrypt.hashpw(key, licensesSalt)); // Hash the key
@ -80,7 +80,7 @@ public final class LicenseService {
license.setHwids(new HashSet<>()); license.setHwids(new HashSet<>());
license.setIpLimit(ipLimit); // Use the given IP limit license.setIpLimit(ipLimit); // Use the given IP limit
license.setHwidLimit(hwidLimit); // Use the given HWID limit license.setHwidLimit(hwidLimit); // Use the given HWID limit
license.setDuration(duration); license.setExpires(expires);
license.setCreated(new Date()); license.setCreated(new Date());
repository.insert(license); // Insert the newly created license repository.insert(license); // Insert the newly created license
return license; return license;
@ -107,12 +107,13 @@ public final class LicenseService {
} }
License license = optionalLicense.get(); // The license found License license = optionalLicense.get(); // The license found
String hashedIp = BCrypt.hashpw(ip, ipsSalt); // Hash the IP 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 // Log the license being used, if enabled
if (discordService.isLogUses()) { if (discordService.isLogUses()) {
// god i hate sending discord embeds, it's so big and ugly :( // god i hate sending discord embeds, it's so big and ugly :(
boolean newIp = !license.getIps().contains(hashedIp); // Is the IP new?
boolean newHwid = !license.getHwids().contains(hwid); // Is the HWID new?
// Constructing tags // Constructing tags
StringBuilder tags = new StringBuilder(); StringBuilder tags = new StringBuilder();
@ -125,22 +126,15 @@ public final class LicenseService {
} }
tags.append("HWID"); tags.append("HWID");
} }
long expirationDate = (license.getCreated().getTime() + license.getDuration()) / 1000L; long expires = license.isPermanent() ? -1L : license.getExpires().getTime() / 1000L;
int ipCount = license.getIps().size();
int hwidCount = license.getHwids().size();
discordService.sendLog(new EmbedBuilder() discordService.sendLog(new EmbedBuilder()
.setColor(Color.BLUE) .setColor(Color.BLUE)
.setTitle("License Used" + (!tags.isEmpty() ? " (" + tags + ")" : "")) .setTitle("License Used" + (!tags.isEmpty() ? " (" + tags + ")" : ""))
.addField("License", .addField("License", "`" + obfuscateKey + "`", true)
"`" + MiscUtils.obfuscateKey(key) + "`", .addField("Product", license.getProduct(), true)
true .addField("Description", license.getDescription(), true)
)
.addField("Product",
license.getProduct(),
true
)
.addField("Description",
license.getDescription(),
true
)
.addField("Owner ID", .addField("Owner ID",
license.getOwnerSnowflake() <= 0L ? "N/A" : String.valueOf(license.getOwnerSnowflake()), license.getOwnerSnowflake() <= 0L ? "N/A" : String.valueOf(license.getOwnerSnowflake()),
true true
@ -149,24 +143,18 @@ public final class LicenseService {
license.getOwnerName() == null ? "N/A" : license.getOwnerName(), license.getOwnerName() == null ? "N/A" : license.getOwnerName(),
true true
) )
.addField("Expires", .addField("Expiration",
license.isPermanent() ? "Never" : "<t:" + expirationDate + ":R>", expires == -1L ? "Never" : "<t:" + expires + ":R>",
true true
) )
.addField("IP", .addField("IP", ip, true)
ip, .addField("HWID", "```" + hwid + "```", false)
true
)
.addField("HWID",
"```" + hwid + "```",
false
)
.addField("IPs", .addField("IPs",
license.getIps().size() + "/" + license.getIpLimit(), (newIp ? ipCount + 1 : ipCount) + "/" + license.getIpLimit(),
true true
) )
.addField("HWIDs", .addField("HWIDs",
license.getHwids().size() + "/" + license.getHwidLimit(), (newHwid ? hwidCount + 1 : hwidCount) + "/" + license.getHwidLimit(),
true true
) )
); );
@ -178,7 +166,7 @@ public final class LicenseService {
discordService.sendLog(new EmbedBuilder() discordService.sendLog(new EmbedBuilder()
.setColor(Color.RED) .setColor(Color.RED)
.setTitle("License Expired") .setTitle("License Expired")
.setDescription("License `%s` is expired".formatted(MiscUtils.obfuscateKey(key))) .setDescription("License `%s` is expired".formatted(obfuscateKey))
); );
} }
throw new LicenseExpiredException(); throw new LicenseExpiredException();
@ -186,6 +174,31 @@ public final class LicenseService {
try { try {
license.use(hashedIp, hwid); // Use the license license.use(hashedIp, hwid); // Use the license
repository.save(license); // Save the used 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); log.info("License key '{}' for product '{}' was used by {} (HWID: {})", key, product, ip, hwid);
return license; return license;
} catch (APIException ex) { } catch (APIException ex) {
@ -195,7 +208,7 @@ public final class LicenseService {
.setColor(Color.RED) .setColor(Color.RED)
.setTitle("License IP Limit Reached") .setTitle("License IP Limit Reached")
.setDescription("License `%s` has reached it's IP limit: **%s**".formatted( .setDescription("License `%s` has reached it's IP limit: **%s**".formatted(
MiscUtils.obfuscateKey(key), obfuscateKey,
license.getIpLimit() license.getIpLimit()
)) ))
); );
@ -204,7 +217,7 @@ public final class LicenseService {
.setColor(Color.RED) .setColor(Color.RED)
.setTitle("License HWID Limit Reached") .setTitle("License HWID Limit Reached")
.setDescription("License `%s` has reached it's HWID limit: **%s**".formatted( .setDescription("License `%s` has reached it's HWID limit: **%s**".formatted(
MiscUtils.obfuscateKey(key), obfuscateKey,
license.getHwidLimit() license.getHwidLimit()
)) ))
); );

View File

@ -12,6 +12,8 @@ salts:
# Discord Bot Configuration # Discord Bot Configuration
discord: discord:
token: "" token: ""
# Global Logs
logs: logs:
channel: 0 # The channel ID to log to, leave as 0 to disable channel: 0 # The channel ID to log to, leave as 0 to disable
uses: true # Should used licenses be logged? uses: true # Should used licenses be logged?
@ -19,6 +21,11 @@ discord:
ipLimitExceeded: true # Should IP limited licenses be logged when used? ipLimitExceeded: true # Should IP limited licenses be logged when used?
hwidLimitExceeded: true # Should HWID 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 # Log Configuration
logging: logging:
file: file: