Merge branch 'main' of git@github.com:Stirling-Tools/Stirling-PDF.git

into main
This commit is contained in:
Anthony Stirling 2024-10-01 13:43:08 +01:00
parent 20dc2f60cd
commit 5832147b30
41 changed files with 1129 additions and 202 deletions

View File

@ -127,6 +127,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
implementation 'com.posthog.java:posthog:1.1.1'
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"

View File

@ -17,9 +17,11 @@ public class EEAppConfig {
@Autowired ApplicationProperties applicationProperties;
@Bean(name = "RunningEE")
@Autowired
private LicenseKeyChecker licenseKeyChecker;
@Bean(name = "runningEE")
public boolean runningEnterpriseEdition() {
return applicationProperties.getEnterpriseEdition().getEnabled();
// TODO: check EE license key
return licenseKeyChecker.getEnterpriseEnabledResult();
}
}

View File

@ -14,58 +14,58 @@ import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class KeygenLicenseVerifier {
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
private static final String PRODUCT_ID = "f9bb2423-62c9-4d39-8def-4fdc5aca751e";
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
private static final String PUBLIC_KEY =
"-----BEGIN PUBLIC KEY-----\n"
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzJaf7jPx/bamT/ctmvrf\n"
+ "5HfzV9CrTx39Hv48NvRIjw9jBAlmcSndLbgcrTUWFrd7pJPPEhzmfJ9tLRg0a3Si\n"
+ "34Ed9gQ24mODj0Wpos5uwwxu1M5wzsKPjkLZDigB3d9L/79nyKvSUo+mx+dZmZnD\n"
+ "D19TMM93ZDxG+Bru5/rvvxaZzMHZAnqrTdoO55vFjpss5XJNt6kz4jxr+D6a3lFU\n"
+ "GGCx7bjeanHCNGRw84dLYbU8s5DGsx5JNX1xPGR1kODocvsHfHJvsxfdNtpH4vke\n"
+ "yOrtEUCp01Mh2kr3zM8R4Yjh4ae2qHiZne0FiVhiUaHmbf2dmcA9O1Kynz33634s\n"
+ "fwIDAQAB\n"
+ "-----END PUBLIC KEY-----";
private static final ObjectMapper objectMapper = new ObjectMapper();
public static boolean verifyLicense(String licenseKey) {
public boolean verifyLicense(String licenseKey) {
try {
String machineFingerprint = generateMachineFingerprint();
// First, try to validate the license
boolean isValid = validateLicense(licenseKey, machineFingerprint);
// If validation fails, try to activate the machine
if (!isValid) {
System.out.println(
"License validation failed. Attempting to activate the machine...");
isValid = activateMachine(licenseKey, machineFingerprint);
if (isValid) {
// If activation is successful, try to validate again
isValid = validateLicense(licenseKey, machineFingerprint);
JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint);
log.debug(validationResponse.asText());
if (validationResponse != null) {
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
String licenseId = validationResponse.path("data").path("id").asText();
if (!isValid) {
String code = validationResponse.path("meta").path("code").asText();
log.debug(code);
if ("NO_MACHINE".equals(code) || "NO_MACHINES".equals(code) || "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
log.info("License not activated for this machine. Attempting to activate...");
boolean activated = activateMachine(licenseKey, licenseId, machineFingerprint);
if (activated) {
// Revalidate after activation
validationResponse = validateLicense(licenseKey, machineFingerprint);
isValid = validationResponse != null && validationResponse.path("meta").path("valid").asBoolean();
}
}
}
return isValid;
}
return isValid;
return false;
} catch (Exception e) {
System.out.println("Error verifying license: " + e.getMessage());
log.error("Error verifying license: " + e.getMessage());
return false;
}
}
private static boolean validateLicense(String licenseKey, String machineFingerprint)
private static JsonNode validateLicense(String licenseKey, String machineFingerprint)
throws Exception {
HttpClient client = HttpClient.newHttpClient();
String requestBody =
String.format(
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\",\"product\":\"%s\"}}}",
licenseKey, machineFingerprint, PRODUCT_ID);
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
licenseKey, machineFingerprint );
HttpRequest request =
HttpRequest.newBuilder()
.uri(
@ -76,107 +76,81 @@ public class KeygenLicenseVerifier {
+ "/licenses/actions/validate-key"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header("Authorization", "license " + licenseKey)
//.header("Authorization", "License " + licenseKey)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug(" validateLicenseResponse body: " + response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) {
JsonNode jsonResponse = objectMapper.readTree(response.body());
JsonNode metaNode = jsonResponse.path("meta");
boolean isValid = metaNode.path("valid").asBoolean();
String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText();
System.out.println("License validity: " + isValid);
System.out.println("Validation detail: " + detail);
System.out.println("Validation code: " + code);
log.debug("License validity: " + isValid);
log.debug("Validation detail: " + detail);
log.debug("Validation code: " + code);
if (isValid) {
return verifySignature(metaNode);
}
} else {
System.out.println("Error validating license. Status code: " + response.statusCode());
System.out.println("Response body: " + response.body());
log.error("Error validating license. Status code: " + response.statusCode());
}
return false;
return jsonResponse;
}
private static boolean activateMachine(String licenseKey, String machineFingerprint)
private static boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint)
throws Exception {
HttpClient client = HttpClient.newHttpClient();
String requestBody =
String.format(
"{\"data\":{\"type\":\"machines\",\"attributes\":{\"fingerprint\":\"%s\"},\"relationships\":{\"license\":{\"data\":{\"type\":\"licenses\",\"id\":\"%s\"}}}}}",
machineFingerprint, licenseKey);
String licenseId = "8e072b67-3cea-454b-98bb-bb73bbc04bd4";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header("Authorization", "license " + licenseId)
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
String hostname;
try {
hostname = java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
hostname = "Unknown";
}
JSONObject body = new JSONObject()
.put("data", new JSONObject()
.put("type", "machines")
.put("attributes", new JSONObject()
.put("fingerprint", machineFingerprint)
.put("platform", System.getProperty("os.name")) // Added platform parameter
.put("name", hostname)) // Added name parameter
.put("relationships", new JSONObject()
.put("license", new JSONObject()
.put("data", new JSONObject()
.put("type", "licenses")
.put("id", licenseId)))));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header("Authorization", "License " + licenseKey) // Keep the license key authentication
.POST(HttpRequest.BodyPublishers.ofString(body.toString())) // Send the JSON body
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("activateMachine Response body: " + response.body());
if (response.statusCode() == 201) {
System.out.println("Machine activated successfully");
log.info("Machine activated successfully");
return true;
} else {
System.out.println("Error activating machine. Status code: " + response.statusCode());
System.out.println("Response body: " + response.body());
log.error("Error activating machine. Status code: " + response.statusCode());
return false;
}
}
private static boolean verifySignature(JsonNode metaNode) throws Exception {
String signature = metaNode.path("signature").asText();
String data = metaNode.path("data").asText();
PublicKey publicKey =
KeyFactory.getInstance("RSA")
.generatePublic(
new X509EncodedKeySpec(
Base64.getDecoder()
.decode(
PUBLIC_KEY
.replace(
"-----BEGIN PUBLIC KEY-----",
"")
.replace(
"-----END PUBLIC KEY-----",
"")
.replaceAll("\\s", ""))));
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(data.getBytes());
boolean isSignatureValid = sig.verify(Base64.getDecoder().decode(signature));
System.out.println("Signature validity: " + isSignatureValid);
return isSignatureValid;
}
private static String generateMachineFingerprint() {
// This is a simplified example. In a real-world scenario, you'd want to generate
// a more robust and unique fingerprint based on hardware characteristics.
return "example-fingerprint-" + System.currentTimeMillis();
return "example-fingerprint";
}
public static void test() {
String[] testKeys = {
"FYKJ-YK7F-MEVX-RYKK-JYWE-77WW-3TKN-PJRU", "EFDB57-92B4C2-EDFA20-51146E-E1AF4A-V3"
};
for (String licenseKey : testKeys) {
System.out.println("Testing license key: " + licenseKey);
boolean isValid = verifyLicense(licenseKey);
System.out.println("License is valid: " + isValid);
System.out.println("--------------------");
}
}
}

View File

@ -1,18 +1,22 @@
package stirling.software.SPDF.EE;
import java.io.IOException;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Component
public class LicenseKeyChecker implements CommandLineRunner {
public class LicenseKeyChecker {
private final KeygenLicenseVerifier licenseService;
private final ApplicationProperties applicationProperties;
private static boolean enterpriseEnbaledResult = false;
// Inject your license service or configuration
public LicenseKeyChecker(
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
@ -20,39 +24,28 @@ public class LicenseKeyChecker implements CommandLineRunner {
this.applicationProperties = new ApplicationProperties();
}
// Validate on startup
@Override
public void run(String... args) throws Exception {
checkLicense();
}
// Periodic license check - runs every 7 days
@Scheduled(fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() {
checkLicense();
}
// License validation logic
private void checkLicense() {
boolean isValid =
licenseService.verifyLicense(applicationProperties.getEnterpriseEdition().getKey());
if (!isValid) {
// Handle invalid license (shut down the app, log, etc.)
System.out.println("License key is invalid!");
// Optionally stop the application
// System.exit(1); // Uncomment if you want to stop the app
} else {
System.out.println("License key is valid.");
}
if(!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnbaledResult = false;
} else {
enterpriseEnbaledResult = licenseService.verifyLicense(applicationProperties.getEnterpriseEdition().getKey());
}
}
// Method to update the license key dynamically
public void updateLicenseKey(String newKey) {
// Update the key in ApplicationProperties
public void updateLicenseKey(String newKey) throws IOException {
applicationProperties.getEnterpriseEdition().setKey(newKey);
// Immediately validate the new key
System.out.println("License key has been updated. Checking new key...");
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
checkLicense();
}
public boolean getEnterpriseEnabledResult() {
return enterpriseEnbaledResult;
}
}

View File

@ -10,7 +10,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.pixee.security.SystemCommand;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LibreOfficeListener {
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
@ -31,7 +33,7 @@ public class LibreOfficeListener {
private LibreOfficeListener() {}
private boolean isListenerRunning() {
System.out.println("waiting for listener to start");
log.info("waiting for listener to start");
try (Socket socket = new Socket()) {
socket.connect(
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second

View File

@ -20,6 +20,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine;
import stirling.software.SPDF.EE.LicenseKeyChecker;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@ -30,6 +31,7 @@ public class AppConfig {
@Autowired ApplicationProperties applicationProperties;
@Bean
@ConditionalOnProperty(
name = "system.customHTMLFiles",
@ -169,7 +171,7 @@ public class AppConfig {
@Bean(name = "analyticsEnabled")
public boolean analyticsEnabled() {
if (applicationProperties.getEnterpriseEdition().getEnabled()) return true;
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
return applicationProperties.getSystem().getEnableAnalytics() != null
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
}
@ -178,4 +180,11 @@ public class AppConfig {
public String stirlingPDFLabel() {
return "Stirling-PDF" + " v" + appVersion();
}
@Bean(name = "UUID")
public String uuid() {
return applicationProperties.getAutomaticallyGenerated().getUUID();
}
}

View File

@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
import stirling.software.SPDF.model.ApplicationProperties;
@Service

View File

@ -0,0 +1,42 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class InitialSetup {
@Autowired private ApplicationProperties applicationProperties;
@PostConstruct
public void initUUIDKey() throws IOException {
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
if (!GeneralUtils.isValidUUID(uuid)) {
uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!GeneralUtils.isValidUUID(secretKey)) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
}
}
}

View File

@ -14,7 +14,7 @@ import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class Beans implements WebMvcConfigurer {
public class LocaleConfiguration implements WebMvcConfigurer {
@Autowired ApplicationProperties applicationProperties;

View File

@ -0,0 +1,34 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.posthog.java.PostHog;
import jakarta.annotation.PreDestroy;
@Configuration
public class PostHogConfig {
@Value("${posthog.api.key}")
private String posthogApiKey;
@Value("${posthog.host}")
private String posthogHost;
private PostHog postHogClient;
@Bean
public PostHog postHogClient() {
postHogClient = new PostHog.Builder(posthogApiKey).host(posthogHost).build();
return postHogClient;
}
@PreDestroy
public void shutdownPostHog() {
if (postHogClient != null) {
postHogClient.shutdown();
}
}
}

View File

@ -0,0 +1,66 @@
package stirling.software.SPDF.config.fingerprint;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component
@Slf4j
public class FingerprintBasedSessionFilter extends OncePerRequestFilter {
private final FingerprintGenerator fingerprintGenerator;
private final FingerprintBasedSessionManager sessionManager;
@Autowired
public FingerprintBasedSessionFilter(
FingerprintGenerator fingerprintGenerator,
FingerprintBasedSessionManager sessionManager) {
this.fingerprintGenerator = fingerprintGenerator;
this.sessionManager = sessionManager;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
String fingerprint = fingerprintGenerator.generateFingerprint(request);
log.debug("Generated fingerprint for request: {}", fingerprint);
HttpSession session = request.getSession();
boolean isNewSession = session.isNew();
String sessionId = session.getId();
if (isNewSession) {
log.info("New session created: {}", sessionId);
}
if (!sessionManager.isFingerPrintAllowed(fingerprint)) {
log.info("Blocked fingerprint detected, redirecting: {}", fingerprint);
response.sendRedirect(request.getContextPath() + "/too-many-requests");
return;
}
session.setAttribute("userFingerprint", fingerprint);
session.setAttribute(FingerprintBasedSessionManager.STARTUP_TIMESTAMP, FingerprintBasedSessionManager.APP_STARTUP_TIME);
sessionManager.registerFingerprint(fingerprint, sessionId);
log.debug("Proceeding with request: {}", request.getRequestURI());
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,130 @@
package stirling.software.SPDF.config.fingerprint;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionAttributeListener;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class FingerprintBasedSessionManager implements HttpSessionListener, HttpSessionAttributeListener {
private static final ConcurrentHashMap<String, FingerprintInfo> activeFingerprints = new ConcurrentHashMap<>();
// To be reduced in later version to 8~
private static final int MAX_ACTIVE_FINGERPRINTS = 30;
static final String STARTUP_TIMESTAMP = "appStartupTimestamp";
static final long APP_STARTUP_TIME = System.currentTimeMillis();
private static final long FINGERPRINT_EXPIRATION = TimeUnit.MINUTES.toMillis(30);
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
String sessionId = session.getId();
String fingerprint = (String) session.getAttribute("userFingerprint");
if (fingerprint == null) {
log.warn("Session created without fingerprint: {}", sessionId);
return;
}
synchronized (activeFingerprints) {
if (activeFingerprints.size() >= MAX_ACTIVE_FINGERPRINTS && !activeFingerprints.containsKey(fingerprint)) {
log.info("Max fingerprints reached. Marking session as blocked: {}", sessionId);
session.setAttribute("blocked", true);
} else {
activeFingerprints.put(fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
log.info(
"New fingerprint registered: {}. Total active fingerprints: {}",
fingerprint,
activeFingerprints.size()
);
}
session.setAttribute(STARTUP_TIMESTAMP, APP_STARTUP_TIME);
}
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
String fingerprint = (String) session.getAttribute("userFingerprint");
if (fingerprint != null) {
synchronized (activeFingerprints) {
activeFingerprints.remove(fingerprint);
log.info(
"Fingerprint removed: {}. Total active fingerprints: {}",
fingerprint,
activeFingerprints.size()
);
}
}
}
public boolean isFingerPrintAllowed(String fingerprint) {
synchronized (activeFingerprints) {
return activeFingerprints.size() < MAX_ACTIVE_FINGERPRINTS || activeFingerprints.containsKey(fingerprint);
}
}
public void registerFingerprint(String fingerprint, String sessionId) {
synchronized (activeFingerprints) {
activeFingerprints.put(fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
}
}
public void unregisterFingerprint(String fingerprint) {
synchronized (activeFingerprints) {
activeFingerprints.remove(fingerprint);
}
}
@Scheduled(fixedRate = 1800000) // Run every 30 mins
public void cleanupStaleFingerprints() {
log.info("Starting cleanup of stale fingerprints");
long now = System.currentTimeMillis();
int removedCount = 0;
synchronized (activeFingerprints) {
Iterator<Map.Entry<String, FingerprintInfo>> iterator = activeFingerprints.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, FingerprintInfo> entry = iterator.next();
FingerprintInfo info = entry.getValue();
if (now - info.getLastAccessTime() > FINGERPRINT_EXPIRATION) {
iterator.remove();
removedCount++;
log.info("Removed stale fingerprint: {}", entry.getKey());
}
}
}
log.info("Cleanup complete. Removed {} stale fingerprints", removedCount);
}
public void updateLastAccessTime(String fingerprint) {
FingerprintInfo info = activeFingerprints.get(fingerprint);
if (info != null) {
info.setLastAccessTime(System.currentTimeMillis());
}
}
@Data @AllArgsConstructor
private static class FingerprintInfo {
private String sessionId;
private long lastAccessTime;
}
}

View File

@ -0,0 +1,77 @@
package stirling.software.SPDF.config.fingerprint;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class FingerprintGenerator {
public String generateFingerprint(HttpServletRequest request) {
if (request == null) {
return "";
}
StringBuilder fingerprintBuilder = new StringBuilder();
// Add IP address
fingerprintBuilder.append(request.getRemoteAddr());
// Add X-Forwarded-For header if present (for clients behind proxies)
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null) {
fingerprintBuilder.append(forwardedFor);
}
// Add User-Agent
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
fingerprintBuilder.append(userAgent);
}
// Add Accept-Language header
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage != null) {
fingerprintBuilder.append(acceptLanguage);
}
// Add Accept header
String accept = request.getHeader("Accept");
if (accept != null) {
fingerprintBuilder.append(accept);
}
// Add Connection header
String connection = request.getHeader("Connection");
if (connection != null) {
fingerprintBuilder.append(connection);
}
// Add server port
fingerprintBuilder.append(request.getServerPort());
// Add secure flag
fingerprintBuilder.append(request.isSecure());
// Generate a hash of the fingerprint
return generateHash(fingerprintBuilder.toString());
}
private String generateHash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to generate fingerprint hash", e);
}
}
}

View File

@ -1,4 +1,4 @@
package stirling.software.SPDF.config;
package stirling.software.SPDF.config.interfaces;
import java.io.IOException;
import java.util.List;

View File

@ -1,4 +1,4 @@
package stirling.software.SPDF.config;
package stirling.software.SPDF.config.interfaces;
public interface ShowAdminInterface {
default boolean getShowUpdateOnlyAdmins() {

View File

@ -7,7 +7,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.ShowAdminInterface;
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;

View File

@ -1,19 +1,14 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role;
@ -39,15 +34,6 @@ public class InitialSecuritySetup {
initializeInternalApiUser();
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!isValidUUID(secretKey)) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void initializeAdminUser() throws IOException {
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
@ -89,33 +75,4 @@ public class InitialSecuritySetup {
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
}
}
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments();
settingsYml
.path("AutomaticallyGenerated.key")
.set(key)
.comment("# Automatically Generated Settings (Do Not Edit Directly)");
settingsYml.save();
}
private boolean isValidUUID(String uuid) {
if (uuid == null) {
return false;
}
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}

View File

@ -75,10 +75,9 @@ public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (loginEnabledValue) {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
@ -86,7 +85,7 @@ public class SecurityConfiguration {
sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10)
.maximumSessions(4)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true"));

View File

@ -19,7 +19,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.AuthenticationType;

View File

@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.DatabaseBackupInterface;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.utils.FileInfo;
@Slf4j

View File

@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.service.PostHogService;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -36,9 +37,13 @@ public class CropController {
private final CustomPDDocumentFactory pdfDocumentFactory;
private final PostHogService postHogService;
@Autowired
public CropController(CustomPDDocumentFactory pdfDocumentFactory) {
public CropController(
CustomPDDocumentFactory pdfDocumentFactory, PostHogService postHogService) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.postHogService = postHogService;
}
@PostMapping(value = "/crop", consumes = "multipart/form-data")

View File

@ -0,0 +1,37 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@Controller
@Tag(name = "Settings", description = "Settings APIs")
@RequestMapping("/api/v1/settings")
@Hidden
public class SettingsController {
@Autowired ApplicationProperties applicationProperties;
@PostMapping("/update-enable-analytics")
@Hidden
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body(
"Setting has already been set, To adjust please edit /config/settings.yml");
}
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
return ResponseEntity.ok("Updated");
}
}

View File

@ -60,8 +60,6 @@ public class SplitPDFController {
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
System.out.println(
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);

View File

@ -32,9 +32,9 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.config.PdfMetadataService;
import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.SPDF.service.PdfMetadataService;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController

View File

@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.AuthenticationType;
@ -40,6 +41,7 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass;
@Controller
@Tag(name = "User", description = "User APIs")
@RequestMapping("/api/v1/user")
@Slf4j
public class UserController {
@Autowired private UserService userService;
@ -191,13 +193,12 @@ public class UserController {
Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]);
}
System.out.println("Processed updates: " + updates);
log.debug("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates);

View File

@ -60,8 +60,6 @@ public class ExtractImagesController {
MultipartFile file = request.getFileInput();
String format = request.getFormat();
boolean allowDuplicates = request.isAllowDuplicates();
System.out.println(
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
PDDocument document = Loader.loadPDF(file.getBytes());
// Determine if multithreading should be used based on PDF size or number of pages

View File

@ -25,12 +25,13 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
@Slf4j
public class PrintFileController {
// TODO
@ -59,7 +60,7 @@ public class PrintFileController {
new IllegalArgumentException(
"No matching printer found"));
System.out.println("Selected Printer: " + selectedService.getName());
log.info("Selected Printer: " + selectedService.getName());
if ("application/pdf".equals(contentType)) {
PDDocument document = Loader.loadPDF(file.getBytes());

View File

@ -58,7 +58,6 @@ public class RedactController {
float customPadding = request.getCustomPadding();
boolean convertPDFToImage = request.isConvertPDFToImage();
System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n");
PDDocument document = pdfDocumentFactory.load(file);
@ -75,7 +74,6 @@ public class RedactController {
for (String text : listOfText) {
text = text.trim();
System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding, redactColor);

View File

@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import lombok.Data;
import lombok.ToString;
@ -24,6 +26,7 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@ConfigurationProperties(prefix = "")
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
@Data
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ApplicationProperties {
private Legal legal = new Legal();
@ -176,11 +179,12 @@ public class ApplicationProperties {
@Data
public static class AutomaticallyGenerated {
@ToString.Exclude private String key;
private String UUID;
}
@Data
public static class EnterpriseEdition {
private Boolean enabled;
private boolean enabled;
@ToString.Exclude private String key;
private CustomMetadata customMetadata = new CustomMetadata();

View File

@ -10,8 +10,10 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PDFText;
@Slf4j
public class TextFinder extends PDFTextStripper {
private final String searchText;
@ -92,7 +94,7 @@ public class TextFinder extends PDFTextStripper {
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
this.getText(document);
System.out.println(
log.debug(
"Found "
+ textOccurrences.size()
+ " occurrences of '"

View File

@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.config.PdfMetadataService;
import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.PDFFile;

View File

@ -0,0 +1,55 @@
package stirling.software.SPDF.service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.search.Search;
@Service
public class MetricsAggregatorService {
private final MeterRegistry meterRegistry;
private final PostHogService postHogService;
private final Map<String, Double> lastSentMetrics = new ConcurrentHashMap<>();
@Autowired
public MetricsAggregatorService(MeterRegistry meterRegistry, PostHogService postHogService) {
this.meterRegistry = meterRegistry;
this.postHogService = postHogService;
}
@Scheduled(fixedRate = 900000) // Run every 15 minutes
public void aggregateAndSendMetrics() {
Map<String, Object> metrics = new HashMap<>();
Search.in(meterRegistry)
.name("http.requests")
.counters()
.forEach(counter -> {
String key = String.format(
"http_requests_%s_%s",
counter.getId().getTag("method"),
counter.getId().getTag("uri").replace("/", "_"));
double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
double difference = currentCount - lastCount;
if (difference > 0) {
metrics.put(key, difference);
lastSentMetrics.put(key, currentCount);
}
});
// Send aggregated metrics to PostHog
if (!metrics.isEmpty()) {
postHogService.captureEvent("aggregated_metrics", metrics);
}
}
}

View File

@ -1,4 +1,4 @@
package stirling.software.SPDF.config;
package stirling.software.SPDF.service;
import java.util.Calendar;

View File

@ -0,0 +1,374 @@
package stirling.software.SPDF.service;
import java.io.File;
import java.lang.management.*;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.posthog.java.PostHog;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
public class PostHogService {
private final PostHog postHog;
private final String uniqueId;
private final ApplicationProperties applicationProperties;
@Autowired
public PostHogService(
PostHog postHog,
@Qualifier("UUID") String uuid,
ApplicationProperties applicationProperties) {
this.postHog = postHog;
this.uniqueId = uuid;
this.applicationProperties = applicationProperties;
captureSystemInfo();
}
private void captureSystemInfo() {
try {
postHog.capture(uniqueId, "system_info_captured", captureServerMetrics());
} catch (Exception e) {
// Handle exceptions
}
}
public void captureEvent(String eventName, Map<String, Object> properties) {
postHog.capture(uniqueId, eventName, properties);
}
public Map<String, Object> captureServerMetrics() {
Map<String, Object> metrics = new HashMap<>();
try {
// System info
metrics.put("os_name", System.getProperty("os.name"));
metrics.put("os_version", System.getProperty("os.version"));
metrics.put("java_version", System.getProperty("java.version"));
metrics.put("user_name", System.getProperty("user.name"));
metrics.put("user_home", System.getProperty("user.home"));
metrics.put("user_dir", System.getProperty("user.dir"));
// CPU and Memory
metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors());
metrics.put("total_memory", Runtime.getRuntime().totalMemory());
metrics.put("free_memory", Runtime.getRuntime().freeMemory());
// Network and Server Identity
InetAddress localHost = InetAddress.getLocalHost();
metrics.put("ip_address", localHost.getHostAddress());
metrics.put("hostname", localHost.getHostName());
metrics.put("mac_address", getMacAddress());
// JVM info
metrics.put("jvm_vendor", System.getProperty("java.vendor"));
metrics.put("jvm_version", System.getProperty("java.vm.version"));
// Locale and Timezone
metrics.put("system_language", System.getProperty("user.language"));
metrics.put("system_country", System.getProperty("user.country"));
metrics.put("timezone", TimeZone.getDefault().getID());
metrics.put("locale", Locale.getDefault().toString());
// Disk info
File root = new File(".");
metrics.put("total_disk_space", root.getTotalSpace());
metrics.put("free_disk_space", root.getFreeSpace());
// Process info
metrics.put("process_id", ProcessHandle.current().pid());
// JVM metrics
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
metrics.put("jvm_uptime_ms", runtimeMXBean.getUptime());
metrics.put("jvm_start_time", runtimeMXBean.getStartTime());
// Memory metrics
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
metrics.put("heap_memory_usage", memoryMXBean.getHeapMemoryUsage().getUsed());
metrics.put("non_heap_memory_usage", memoryMXBean.getNonHeapMemoryUsage().getUsed());
// CPU metrics
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
metrics.put("system_load_average", osMXBean.getSystemLoadAverage());
// Thread metrics
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
metrics.put("thread_count", threadMXBean.getThreadCount());
metrics.put("daemon_thread_count", threadMXBean.getDaemonThreadCount());
metrics.put("peak_thread_count", threadMXBean.getPeakThreadCount());
// Garbage collection metrics
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
metrics.put("gc_" + gcBean.getName() + "_count", gcBean.getCollectionCount());
metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime());
}
// Network interfaces
metrics.put("network_interfaces", getNetworkInterfacesInfo());
// Docker detection and stats
boolean isDocker = isRunningInDocker();
metrics.put("is_docker", isDocker);
if (isDocker) {
metrics.put("docker_metrics", getDockerMetrics());
}
metrics.put("application_properties", captureApplicationProperties());
} catch (Exception e) {
metrics.put("error", e.getMessage());
}
return metrics;
}
private boolean isRunningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
}
private Map<String, Object> getDockerMetrics() {
Map<String, Object> dockerMetrics = new HashMap<>();
// Network-related Docker info
dockerMetrics.put("docker_network_mode", System.getenv("DOCKER_NETWORK_MODE"));
// Container name (if set)
String containerName = System.getenv("CONTAINER_NAME");
if (containerName != null && !containerName.isEmpty()) {
dockerMetrics.put("container_name", containerName);
}
// Docker compose information
String composeProject = System.getenv("COMPOSE_PROJECT_NAME");
String composeService = System.getenv("COMPOSE_SERVICE_NAME");
if (composeProject != null && composeService != null) {
dockerMetrics.put("compose_project", composeProject);
dockerMetrics.put("compose_service", composeService);
}
// Kubernetes-specific info (if running in K8s)
String k8sPodName = System.getenv("KUBERNETES_POD_NAME");
if (k8sPodName != null) {
dockerMetrics.put("k8s_pod_name", k8sPodName);
dockerMetrics.put("k8s_namespace", System.getenv("KUBERNETES_NAMESPACE"));
dockerMetrics.put("k8s_node_name", System.getenv("KUBERNETES_NODE_NAME"));
}
// New environment variables
dockerMetrics.put("version_tag", System.getenv("VERSION_TAG"));
dockerMetrics.put("docker_enable_security", System.getenv("DOCKER_ENABLE_SECURITY"));
dockerMetrics.put("fat_docker", System.getenv("FAT_DOCKER"));
return dockerMetrics;
}
private void addIfNotEmpty(Map<String, Object> map, String key, Object value) {
if (value != null) {
if (value instanceof String) {
String strValue = (String) value;
if (!StringUtils.isBlank(strValue)) {
map.put(key, strValue.trim());
}
} else {
map.put(key, value);
}
}
}
public Map<String, Object> captureApplicationProperties() {
Map<String, Object> properties = new HashMap<>();
// Capture Legal properties
addIfNotEmpty(
properties,
"legal_termsAndConditions",
applicationProperties.getLegal().getTermsAndConditions());
addIfNotEmpty(
properties,
"legal_privacyPolicy",
applicationProperties.getLegal().getPrivacyPolicy());
addIfNotEmpty(
properties,
"legal_accessibilityStatement",
applicationProperties.getLegal().getAccessibilityStatement());
addIfNotEmpty(
properties,
"legal_cookiePolicy",
applicationProperties.getLegal().getCookiePolicy());
addIfNotEmpty(
properties, "legal_impressum", applicationProperties.getLegal().getImpressum());
// Capture Security properties
addIfNotEmpty(
properties,
"security_enableLogin",
applicationProperties.getSecurity().getEnableLogin());
addIfNotEmpty(
properties,
"security_csrfDisabled",
applicationProperties.getSecurity().getCsrfDisabled());
addIfNotEmpty(
properties,
"security_loginAttemptCount",
applicationProperties.getSecurity().getLoginAttemptCount());
addIfNotEmpty(
properties,
"security_loginResetTimeMinutes",
applicationProperties.getSecurity().getLoginResetTimeMinutes());
addIfNotEmpty(
properties,
"security_loginMethod",
applicationProperties.getSecurity().getLoginMethod());
// Capture OAuth2 properties (excluding sensitive information)
addIfNotEmpty(
properties,
"security_oauth2_enabled",
applicationProperties.getSecurity().getOauth2().getEnabled());
if (applicationProperties.getSecurity().getOauth2().getEnabled()) {
addIfNotEmpty(
properties,
"security_oauth2_autoCreateUser",
applicationProperties.getSecurity().getOauth2().getAutoCreateUser());
addIfNotEmpty(
properties,
"security_oauth2_blockRegistration",
applicationProperties.getSecurity().getOauth2().getBlockRegistration());
addIfNotEmpty(
properties,
"security_oauth2_useAsUsername",
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
addIfNotEmpty(
properties,
"security_oauth2_provider",
applicationProperties.getSecurity().getOauth2().getProvider());
}
// Capture System properties
addIfNotEmpty(
properties,
"system_defaultLocale",
applicationProperties.getSystem().getDefaultLocale());
addIfNotEmpty(
properties,
"system_googlevisibility",
applicationProperties.getSystem().getGooglevisibility());
addIfNotEmpty(
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
addIfNotEmpty(
properties,
"system_showUpdateOnlyAdmin",
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
addIfNotEmpty(
properties,
"system_customHTMLFiles",
applicationProperties.getSystem().isCustomHTMLFiles());
addIfNotEmpty(
properties,
"system_tessdataDir",
applicationProperties.getSystem().getTessdataDir());
addIfNotEmpty(
properties,
"system_enableAlphaFunctionality",
applicationProperties.getSystem().getEnableAlphaFunctionality());
addIfNotEmpty(
properties,
"system_enableAnalytics",
applicationProperties.getSystem().getEnableAnalytics());
// Capture UI properties
addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName());
addIfNotEmpty(
properties,
"ui_homeDescription",
applicationProperties.getUi().getHomeDescription());
addIfNotEmpty(
properties, "ui_appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
// Capture Metrics properties
addIfNotEmpty(
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
// Capture EnterpriseEdition properties
addIfNotEmpty(
properties,
"enterpriseEdition_enabled",
applicationProperties.getEnterpriseEdition().isEnabled());
if (applicationProperties.getEnterpriseEdition().isEnabled()) {
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_autoUpdateMetadata",
applicationProperties
.getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_author",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_creator",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_producer",
applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer());
}
// Capture AutoPipeline properties
addIfNotEmpty(
properties,
"autoPipeline_outputFolder",
applicationProperties.getAutoPipeline().getOutputFolder());
return properties;
}
private String getMacAddress() {
try {
Enumeration<NetworkInterface> networkInterfaces =
NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface ni = networkInterfaces.nextElement();
byte[] hardwareAddress = ni.getHardwareAddress();
if (hardwareAddress != null) {
String[] hexadecimal = new String[hardwareAddress.length];
for (int i = 0; i < hardwareAddress.length; i++) {
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
}
return String.join("-", hexadecimal);
}
}
} catch (Exception e) {
// Handle exception
}
return "Unknown";
}
private Map<String, String> getNetworkInterfacesInfo() {
Map<String, String> interfacesInfo = new HashMap<>();
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface netint = nets.nextElement();
interfacesInfo.put(netint.getName(), netint.getDisplayName());
}
} catch (Exception e) {
interfacesInfo.put("error", e.getMessage());
}
return interfacesInfo;
}
}

View File

@ -16,7 +16,12 @@ import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.file.YamlFileWrapper;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
@ -262,4 +267,38 @@ public class GeneralUtils {
}
return true;
}
public static boolean isValidUUID(String uuid) {
if (uuid == null) {
return false;
}
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
public static void saveKeyToConfig(String id, String key) throws IOException {
saveKeyToConfig(id, key, true);
}
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments();
YamlFileWrapper writer = settingsYml.path(id).set(key);
if (autoGenerated) {
writer.comment("# Automatically Generated Settings (Do Not Edit Directly)");
}
settingsYml.save();
}
}

View File

@ -191,7 +191,6 @@ public class PDFToFile {
Files.deleteIfExists(tempInputFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
}
System.out.println("fileBytes=" + fileBytes.length);
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
}

View File

@ -49,3 +49,5 @@ springdoc.api-docs.path=/v1/api-docs
springdoc.swagger-ui.url=/v1/api-docs
posthog.api.key=phc_Bh95TRT3qZveAxJpBmJcPpSpW2dJeiKlgin8c7xXGna
posthog.host=https://eu.i.posthog.com

View File

@ -76,6 +76,7 @@ donate=Donate
color=Color
sponsor=Sponsor
info=Info
pro=Pro
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
@ -108,8 +109,24 @@ pipelineOptions.pipelineHeader=Pipeline:
pipelineOptions.saveButton=Download
pipelineOptions.validateButton=Validate
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
#############
# NAVBAR #
@ -383,7 +400,7 @@ home.scalePages.title=Adjust page size/scale
home.scalePages.desc=Change the size/scale of a page and/or its contents.
scalePages.tags=resize,modify,dimension,adapt
home.pipeline.title=Pipeline (Advanced)
home.pipeline.title=Pipeline
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
pipeline.tags=automate,sequence,scripted,batch-process
@ -510,7 +527,10 @@ login.oauth2AccessDenied=Access Denied
login.oauth2InvalidTokenResponse=Invalid Token Response
login.oauth2InvalidIdToken=Invalid Id Token
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redact

View File

@ -50,6 +50,7 @@ security:
# Enterprise edition settings unused for now please ignore!
EnterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
CustomMetadata:
autoUpdateMetadata: true # set to 'true' to automatically update metadata with below values
@ -72,6 +73,7 @@ system:
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
tessdataDir: /usr/share/tessdata # Path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: undefined # Set to 'true' to enable analytics, set to 'false' to disable analytics, for enterprise users this is set to true
ui:
appName: '' # Application's visible name
@ -88,3 +90,4 @@ metrics:
# Automatically Generated Settings (Do Not Edit Directly)
AutomaticallyGenerated:
key: example
UUID: example

View File

@ -71,7 +71,48 @@
<script th:src="@{'/js/darkmode.js'}"></script>
<script th:inline="javascript">
const stirlingPDFLabel = /*[[${@StirlingPDFLabel}]]*/ '';
const analyticsEnabled = /*[[${@analyticsEnabled}]]*/ false;
if (analyticsEnabled) {
!function (t, e) {
var o, n, p, r;
e.__SV || (window.posthog = e, e._i = [], e.init = function (i, s, a) {
function g(t, e) {
var o = e.split(".");
2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () {
t.push([e].concat(Array.prototype.slice.call(arguments, 0)))
}
}
(p = t.createElement("script")).type = "text/javascript", p.async = !0, p.src = s.api_host + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r);
var u = e;
for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) {
var e = "posthog";
return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
}, u.people.toString = function () {
return u.toString(1) + ".people (stub)"
}, o = "capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "), n = 0; n < o.length; n++) g(u, o[n]);
e._i.push([i, s, a])
}, e.__SV = 1)
}(document, window.posthog || []);
posthog.init('phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq', {
api_host: 'https://eu.i.posthog.com',
person_profiles: 'always',
mask_all_text: true,
mask_all_element_attributes: true
})
const baseUrl = window.location.hostname;
posthog.register_once({
'hostname': baseUrl,
'UUID': /*[[${@UUID}]]*/ ''
})
}
</script>
</th:block>
<th:block th:fragment="game">

View File

@ -353,7 +353,71 @@
</div>
</div>
<script>
<!-- Analytics Modal -->
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel" aria-hidden="true" th:if="${@analyticsPrompt}">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF better?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.</p>
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.</p>
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
</div>
</div>
</div>
</div>
<script th:inline="javascript">
/*<![CDATA[*/
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
document.addEventListener('DOMContentLoaded', function() {
if (analyticsPromptBoolean) {
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
analyticsModal.show();
}
});
/*]]>*/
function setAnalytics(enabled) {
fetch('api/v1/settings/update-enable-analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(enabled)
})
.then(response => {
if (response.status === 200) {
console.log('Analytics setting updated successfully');
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
} else if (response.status === 208) {
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
} else {
throw new Error('Unexpected response status: ' + response.status);
}
})
.catch(error => {
console.error('Error updating analytics setting:', error);
alert('An error occurred while updating the analytics setting. Please try again.');
});
}
document.addEventListener("DOMContentLoaded", function() {
const surveyVersion = "2.0";