mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
Security fixes, enterprise stuff and more (#3241)
# Description of Changes Please provide a summary of the changes, including: - Enable user to add custom JAVA ops with env JAVA_CUSTOM_OPTS - Added support for prometheus (enabled via JAVA_CUSTOM_OPTS + enterprise license) - Changed settings from enterprise naming to 'Premium' - KeygenLicense Check to support offline licenses - Disable URL-to-PDF due to huge security bug - Remove loud Split PDF logs - addUsers renamed to adminSettings - Added Usage analytics page - Add user button to only be enabled based on total users free - Improve Merge memory usage Closes #(issue_number) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a <a> Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import org.springframework.core.annotation.Order;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.EnterpriseEdition;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Premium;
|
||||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@@ -22,6 +24,7 @@ public class EEAppConfig {
|
||||
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.licenseKeyChecker = licenseKeyChecker;
|
||||
migrateEnterpriseSettingsToPremium(this.applicationProperties);
|
||||
}
|
||||
|
||||
@Bean(name = "runningEE")
|
||||
@@ -31,6 +34,74 @@ public class EEAppConfig {
|
||||
|
||||
@Bean(name = "SSOAutoLogin")
|
||||
public boolean ssoAutoLogin() {
|
||||
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
|
||||
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin();
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
public void migrateEnterpriseSettingsToPremium(ApplicationProperties applicationProperties) {
|
||||
EnterpriseEdition enterpriseEdition = applicationProperties.getEnterpriseEdition();
|
||||
Premium premium = applicationProperties.getPremium();
|
||||
|
||||
// Only proceed if both objects exist
|
||||
if (enterpriseEdition == null || premium == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the license key if it's set in enterprise but not in premium
|
||||
if (premium.getKey() == null
|
||||
|| premium.getKey().equals("00000000-0000-0000-0000-000000000000")) {
|
||||
if (enterpriseEdition.getKey() != null
|
||||
&& !enterpriseEdition.getKey().equals("00000000-0000-0000-0000-000000000000")) {
|
||||
premium.setKey(enterpriseEdition.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// Copy enabled state if enterprise is enabled but premium is not
|
||||
if (!premium.isEnabled() && enterpriseEdition.isEnabled()) {
|
||||
premium.setEnabled(true);
|
||||
}
|
||||
|
||||
// Copy SSO auto login setting
|
||||
if (!premium.getProFeatures().isSsoAutoLogin() && enterpriseEdition.isSsoAutoLogin()) {
|
||||
premium.getProFeatures().setSsoAutoLogin(true);
|
||||
}
|
||||
|
||||
// Copy CustomMetadata settings
|
||||
Premium.ProFeatures.CustomMetadata premiumMetadata =
|
||||
premium.getProFeatures().getCustomMetadata();
|
||||
EnterpriseEdition.CustomMetadata enterpriseMetadata = enterpriseEdition.getCustomMetadata();
|
||||
|
||||
if (enterpriseMetadata != null && premiumMetadata != null) {
|
||||
// Copy autoUpdateMetadata setting
|
||||
if (!premiumMetadata.isAutoUpdateMetadata()
|
||||
&& enterpriseMetadata.isAutoUpdateMetadata()) {
|
||||
premiumMetadata.setAutoUpdateMetadata(true);
|
||||
}
|
||||
|
||||
// Copy author if not set in premium but set in enterprise
|
||||
if ((premiumMetadata.getAuthor() == null
|
||||
|| premiumMetadata.getAuthor().trim().isEmpty()
|
||||
|| "username".equals(premiumMetadata.getAuthor()))
|
||||
&& enterpriseMetadata.getAuthor() != null
|
||||
&& !enterpriseMetadata.getAuthor().trim().isEmpty()) {
|
||||
premiumMetadata.setAuthor(enterpriseMetadata.getAuthor());
|
||||
}
|
||||
|
||||
// Copy creator if not set in premium but set in enterprise and different from default
|
||||
if ((premiumMetadata.getCreator() == null
|
||||
|| "Stirling-PDF".equals(premiumMetadata.getCreator()))
|
||||
&& enterpriseMetadata.getCreator() != null
|
||||
&& !"Stirling-PDF".equals(enterpriseMetadata.getCreator())) {
|
||||
premiumMetadata.setCreator(enterpriseMetadata.getCreator());
|
||||
}
|
||||
|
||||
// Copy producer if not set in premium but set in enterprise and different from default
|
||||
if ((premiumMetadata.getProducer() == null
|
||||
|| "Stirling-PDF".equals(premiumMetadata.getProducer()))
|
||||
&& enterpriseMetadata.getProducer() != null
|
||||
&& !"Stirling-PDF".equals(enterpriseMetadata.getProducer())) {
|
||||
premiumMetadata.setProducer(enterpriseMetadata.getProducer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.Ed25519Signer;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.posthog.java.shaded.org.json.JSONException;
|
||||
import com.posthog.java.shaded.org.json.JSONObject;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -20,11 +25,19 @@ import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@Service
|
||||
@Slf4j
|
||||
public class KeygenLicenseVerifier {
|
||||
// todo: place in config files?
|
||||
// License verification configuration
|
||||
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
||||
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static final String PUBLIC_KEY =
|
||||
"9fbc0d78593dcfcf03c945146edd60083bf5fae77dbc08aaa3935f03ce94a58d";
|
||||
|
||||
private static final String CERT_PREFIX = "-----BEGIN LICENSE FILE-----";
|
||||
private static final String CERT_SUFFIX = "-----END LICENSE FILE-----";
|
||||
|
||||
private static final String JWT_PREFIX = "key/";
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
@@ -32,9 +45,367 @@ public class KeygenLicenseVerifier {
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
public boolean verifyLicense(String licenseKey) {
|
||||
public boolean verifyLicense(String licenseKeyOrCert) {
|
||||
if (isCertificateLicense(licenseKeyOrCert)) {
|
||||
log.info("Detected certificate-based license. Processing...");
|
||||
return verifyCertificateLicense(licenseKeyOrCert);
|
||||
} else if (isJWTLicense(licenseKeyOrCert)) {
|
||||
log.info("Detected JWT-style license key. Processing...");
|
||||
return verifyJWTLicense(licenseKeyOrCert);
|
||||
} else {
|
||||
log.info("Detected standard license key. Processing...");
|
||||
return verifyStandardLicense(licenseKeyOrCert);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCertificateLicense(String license) {
|
||||
return license != null && license.trim().startsWith(CERT_PREFIX);
|
||||
}
|
||||
|
||||
private boolean isJWTLicense(String license) {
|
||||
return license != null && license.trim().startsWith(JWT_PREFIX);
|
||||
}
|
||||
|
||||
private boolean verifyCertificateLicense(String licenseFile) {
|
||||
try {
|
||||
log.info("Checking license key");
|
||||
log.info("Verifying certificate-based license");
|
||||
|
||||
String encodedPayload = licenseFile;
|
||||
// Remove the header
|
||||
encodedPayload = encodedPayload.replace(CERT_PREFIX, "");
|
||||
// Remove the footer
|
||||
encodedPayload = encodedPayload.replace(CERT_SUFFIX, "");
|
||||
// Remove all newlines
|
||||
encodedPayload = encodedPayload.replaceAll("\\r?\\n", "");
|
||||
|
||||
byte[] payloadBytes = Base64.getDecoder().decode(encodedPayload);
|
||||
String payload = new String(payloadBytes);
|
||||
|
||||
log.info("Decoded certificate payload: {}", payload);
|
||||
|
||||
String encryptedData = "";
|
||||
String encodedSignature = "";
|
||||
String algorithm = "";
|
||||
|
||||
try {
|
||||
JSONObject attrs = new JSONObject(payload);
|
||||
encryptedData = (String) attrs.get("enc");
|
||||
encodedSignature = (String) attrs.get("sig");
|
||||
algorithm = (String) attrs.get("alg");
|
||||
|
||||
log.info("Certificate algorithm: {}", algorithm);
|
||||
} catch (JSONException e) {
|
||||
log.error("Failed to parse license file: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify license file algorithm
|
||||
if (!algorithm.equals("base64+ed25519")) {
|
||||
log.error(
|
||||
"Unsupported algorithm: {}. Only base64+ed25519 is supported.", algorithm);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
boolean isSignatureValid = verifyEd25519Signature(encryptedData, encodedSignature);
|
||||
if (!isSignatureValid) {
|
||||
log.error("License file signature is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("License file signature is valid");
|
||||
|
||||
// Decode the base64 data
|
||||
String decodedData;
|
||||
try {
|
||||
decodedData = new String(Base64.getDecoder().decode(encryptedData));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Failed to decode license data: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process the certificate data
|
||||
boolean isValid = processCertificateData(decodedData);
|
||||
|
||||
return isValid;
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying certificate license: {}", e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyEd25519Signature(String encryptedData, String encodedSignature) {
|
||||
try {
|
||||
log.info("Signature to verify: {}", encodedSignature);
|
||||
log.info("Public key being used: {}", PUBLIC_KEY);
|
||||
|
||||
byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature);
|
||||
|
||||
// Create the signing data format - prefix with "license/"
|
||||
String signingData = String.format("license/%s", encryptedData);
|
||||
byte[] signingDataBytes = signingData.getBytes();
|
||||
|
||||
log.info("Signing data length: {} bytes", signingDataBytes.length);
|
||||
|
||||
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
|
||||
|
||||
Ed25519PublicKeyParameters verifierParams =
|
||||
new Ed25519PublicKeyParameters(publicKeyBytes, 0);
|
||||
Ed25519Signer verifier = new Ed25519Signer();
|
||||
|
||||
verifier.init(false, verifierParams);
|
||||
verifier.update(signingDataBytes, 0, signingDataBytes.length);
|
||||
|
||||
// Verify the signature
|
||||
boolean result = verifier.verifySignature(signatureBytes);
|
||||
if (!result) {
|
||||
log.error("Signature verification failed with standard public key");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying Ed25519 signature: {}", e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processCertificateData(String certData) {
|
||||
try {
|
||||
log.info("Processing certificate data: {}", certData);
|
||||
|
||||
JSONObject licenseData = new JSONObject(certData);
|
||||
JSONObject metaObj = licenseData.optJSONObject("meta");
|
||||
if (metaObj != null) {
|
||||
String issuedStr = metaObj.optString("issued", null);
|
||||
String expiryStr = metaObj.optString("expiry", null);
|
||||
|
||||
if (issuedStr != null && expiryStr != null) {
|
||||
java.time.Instant issued = java.time.Instant.parse(issuedStr);
|
||||
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
|
||||
if (issued.isAfter(now)) {
|
||||
log.error(
|
||||
"License file issued date is in the future. Please adjust system time or request a new license");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the license file has expired
|
||||
if (expiry.isBefore(now)) {
|
||||
log.error("License file has expired on {}", expiryStr);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("License file valid until {}", expiryStr);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the main license data
|
||||
JSONObject dataObj = licenseData.optJSONObject("data");
|
||||
if (dataObj == null) {
|
||||
log.error("No data object found in certificate");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract license or machine information
|
||||
JSONObject attributesObj = dataObj.optJSONObject("attributes");
|
||||
if (attributesObj != null) {
|
||||
log.info("Found attributes in certificate data");
|
||||
|
||||
// Extract metadata
|
||||
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
||||
if (metadataObj != null) {
|
||||
int users = metadataObj.optInt("users", 0);
|
||||
if (users > 0) {
|
||||
applicationProperties.getPremium().setMaxUsers(users);
|
||||
log.info("License allows for {} users", users);
|
||||
}
|
||||
}
|
||||
|
||||
// Check maxUsers directly in attributes if present from policy definition
|
||||
// if (attributesObj.has("maxUsers")) {
|
||||
// int maxUsers = attributesObj.optInt("maxUsers", 0);
|
||||
// if (maxUsers > 0) {
|
||||
// applicationProperties.getPremium().setMaxUsers(maxUsers);
|
||||
// log.info("License directly specifies {} max users",
|
||||
// maxUsers);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check license status if available
|
||||
String status = attributesObj.optString("status", null);
|
||||
if (status != null
|
||||
&& !status.equals("ACTIVE")
|
||||
&& !status.equals("EXPIRING")) { // Accept "EXPIRING" status as valid
|
||||
log.error("License status is not active: {}", status);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing certificate data: {}", e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyJWTLicense(String licenseKey) {
|
||||
try {
|
||||
log.info("Verifying ED25519_SIGN format license key");
|
||||
|
||||
// Remove the "key/" prefix
|
||||
String licenseData = licenseKey.substring(JWT_PREFIX.length());
|
||||
|
||||
// Split into payload and signature
|
||||
String[] parts = licenseData.split("\\.", 2);
|
||||
if (parts.length != 2) {
|
||||
log.error(
|
||||
"Invalid ED25519_SIGN license format. Expected format: key/payload.signature");
|
||||
return false;
|
||||
}
|
||||
|
||||
String encodedPayload = parts[0];
|
||||
String encodedSignature = parts[1];
|
||||
|
||||
// Verify signature
|
||||
boolean isSignatureValid = verifyJWTSignature(encodedPayload, encodedSignature);
|
||||
if (!isSignatureValid) {
|
||||
log.error("ED25519_SIGN license signature is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("ED25519_SIGN license signature is valid");
|
||||
|
||||
// Decode and process payload - first convert from URL-safe base64 if needed
|
||||
String base64Payload = encodedPayload.replace('-', '+').replace('_', '/');
|
||||
byte[] payloadBytes = Base64.getDecoder().decode(base64Payload);
|
||||
String payload = new String(payloadBytes);
|
||||
|
||||
// Process the license payload
|
||||
boolean isValid = processJWTLicensePayload(payload);
|
||||
|
||||
return isValid;
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying ED25519_SIGN license: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyJWTSignature(String encodedPayload, String encodedSignature) {
|
||||
try {
|
||||
// Decode base64 signature
|
||||
byte[] signatureBytes =
|
||||
Base64.getDecoder()
|
||||
.decode(encodedSignature.replace('-', '+').replace('_', '/'));
|
||||
|
||||
// For ED25519_SIGN format, the signing data is "key/" + encodedPayload
|
||||
String signingData = String.format("key/%s", encodedPayload);
|
||||
byte[] dataBytes = signingData.getBytes();
|
||||
|
||||
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
|
||||
Ed25519PublicKeyParameters verifierParams =
|
||||
new Ed25519PublicKeyParameters(publicKeyBytes, 0);
|
||||
Ed25519Signer verifier = new Ed25519Signer();
|
||||
|
||||
verifier.init(false, verifierParams);
|
||||
verifier.update(dataBytes, 0, dataBytes.length);
|
||||
|
||||
// Verify the signature
|
||||
return verifier.verifySignature(signatureBytes);
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying JWT signature: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processJWTLicensePayload(String payload) {
|
||||
try {
|
||||
log.info("Processing license payload: {}", payload);
|
||||
|
||||
JSONObject licenseData = new JSONObject(payload);
|
||||
|
||||
JSONObject licenseObj = licenseData.optJSONObject("license");
|
||||
if (licenseObj == null) {
|
||||
String id = licenseData.optString("id", null);
|
||||
if (id != null) {
|
||||
log.info("Found license ID: {}", id);
|
||||
licenseObj = licenseData; // Use the root object as the license object
|
||||
} else {
|
||||
log.error("License data not found in payload");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String licenseId = licenseObj.optString("id", "unknown");
|
||||
log.info("Processing license with ID: {}", licenseId);
|
||||
|
||||
// Check expiry date
|
||||
String expiryStr = licenseObj.optString("expiry", null);
|
||||
if (expiryStr != null && !expiryStr.equals("null")) {
|
||||
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
|
||||
if (now.isAfter(expiry)) {
|
||||
log.error("License has expired on {}", expiryStr);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("License valid until {}", expiryStr);
|
||||
} else {
|
||||
log.info("License has no expiration date");
|
||||
}
|
||||
|
||||
// Extract account, product, policy info
|
||||
JSONObject accountObj = licenseData.optJSONObject("account");
|
||||
if (accountObj != null) {
|
||||
String accountId = accountObj.optString("id", "unknown");
|
||||
log.info("License belongs to account: {}", accountId);
|
||||
|
||||
// Verify this matches your expected account ID
|
||||
if (!ACCOUNT_ID.equals(accountId)) {
|
||||
log.warn("License account ID does not match expected account ID");
|
||||
// You might want to fail verification here depending on your requirements
|
||||
}
|
||||
}
|
||||
|
||||
// Extract policy information if available
|
||||
JSONObject policyObj = licenseData.optJSONObject("policy");
|
||||
if (policyObj != null) {
|
||||
String policyId = policyObj.optString("id", "unknown");
|
||||
log.info("License uses policy: {}", policyId);
|
||||
|
||||
// Extract max users from policy if available (customize based on your policy
|
||||
// structure)
|
||||
int users = policyObj.optInt("users", 0);
|
||||
if (users > 0) {
|
||||
applicationProperties.getPremium().setMaxUsers(users);
|
||||
log.info("License allows for {} users", users);
|
||||
} else {
|
||||
// Try to get users from metadata if present
|
||||
Object metadataObj = policyObj.opt("metadata");
|
||||
if (metadataObj instanceof JSONObject) {
|
||||
JSONObject metadata = (JSONObject) metadataObj;
|
||||
users = metadata.optInt("users", 1);
|
||||
applicationProperties.getPremium().setMaxUsers(users);
|
||||
log.info("License allows for {} users (from metadata)", users);
|
||||
} else {
|
||||
// Default value
|
||||
applicationProperties.getPremium().setMaxUsers(1);
|
||||
log.info("Using default of 1 user for license");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing license payload: {}", e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyStandardLicense(String licenseKey) {
|
||||
try {
|
||||
log.info("Checking standard license key");
|
||||
String machineFingerprint = generateMachineFingerprint();
|
||||
|
||||
// First, try to validate the license
|
||||
@@ -44,7 +415,7 @@ public class KeygenLicenseVerifier {
|
||||
String licenseId = validationResponse.path("data").path("id").asText();
|
||||
if (!isValid) {
|
||||
String code = validationResponse.path("meta").path("code").asText();
|
||||
log.debug(code);
|
||||
log.info(code);
|
||||
if ("NO_MACHINE".equals(code)
|
||||
|| "NO_MACHINES".equals(code)
|
||||
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
|
||||
@@ -69,7 +440,7 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying license: {}", e.getMessage());
|
||||
log.error("Error verifying standard license: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +467,7 @@ public class KeygenLicenseVerifier {
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
log.debug("ValidateLicenseResponse body: {}", response.body());
|
||||
log.info("ValidateLicenseResponse body: {}", response.body());
|
||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||
if (response.statusCode() == 200) {
|
||||
JsonNode metaNode = jsonResponse.path("meta");
|
||||
@@ -105,9 +476,9 @@ public class KeygenLicenseVerifier {
|
||||
String detail = metaNode.path("detail").asText();
|
||||
String code = metaNode.path("code").asText();
|
||||
|
||||
log.debug("License validity: " + isValid);
|
||||
log.debug("Validation detail: " + detail);
|
||||
log.debug("Validation code: " + code);
|
||||
log.info("License validity: " + isValid);
|
||||
log.info("Validation detail: " + detail);
|
||||
log.info("Validation code: " + code);
|
||||
|
||||
int users =
|
||||
jsonResponse
|
||||
@@ -116,7 +487,7 @@ public class KeygenLicenseVerifier {
|
||||
.path("metadata")
|
||||
.path("users")
|
||||
.asInt(0);
|
||||
applicationProperties.getEnterpriseEdition().setMaxUsers(users);
|
||||
applicationProperties.getPremium().setMaxUsers(users);
|
||||
log.info(applicationProperties.toString());
|
||||
|
||||
} else {
|
||||
@@ -148,13 +519,8 @@ public class KeygenLicenseVerifier {
|
||||
.put("fingerprint", machineFingerprint)
|
||||
.put(
|
||||
"platform",
|
||||
System.getProperty(
|
||||
"os.name")) // Added
|
||||
// platform
|
||||
// parameter
|
||||
.put(
|
||||
"name",
|
||||
hostname)) // Added name parameter
|
||||
System.getProperty("os.name"))
|
||||
.put("name", hostname))
|
||||
.put(
|
||||
"relationships",
|
||||
new JSONObject()
|
||||
@@ -176,16 +542,12 @@ public class KeygenLicenseVerifier {
|
||||
.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
|
||||
.header("Authorization", "License " + licenseKey)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
log.debug("activateMachine Response body: " + response.body());
|
||||
log.info("activateMachine Response body: " + response.body());
|
||||
if (response.statusCode() == 201) {
|
||||
log.info("Machine activated successfully");
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package stirling.software.SPDF.EE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@@ -15,11 +18,13 @@ import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@Slf4j
|
||||
public class LicenseKeyChecker {
|
||||
|
||||
private static final String FILE_PREFIX = "file:";
|
||||
|
||||
private final KeygenLicenseVerifier licenseService;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private boolean enterpriseEnabledResult = false;
|
||||
private boolean premiumEnabledResult = false;
|
||||
|
||||
@Autowired
|
||||
public LicenseKeyChecker(
|
||||
@@ -35,27 +40,58 @@ public class LicenseKeyChecker {
|
||||
}
|
||||
|
||||
private void checkLicense() {
|
||||
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||
enterpriseEnabledResult = false;
|
||||
if (!applicationProperties.getPremium().isEnabled()) {
|
||||
premiumEnabledResult = false;
|
||||
} else {
|
||||
enterpriseEnabledResult =
|
||||
licenseService.verifyLicense(
|
||||
applicationProperties.getEnterpriseEdition().getKey());
|
||||
if (enterpriseEnabledResult) {
|
||||
log.info("License key is valid.");
|
||||
String licenseKey = getLicenseKeyContent(applicationProperties.getPremium().getKey());
|
||||
if (licenseKey != null) {
|
||||
premiumEnabledResult = licenseService.verifyLicense(licenseKey);
|
||||
if (premiumEnabledResult) {
|
||||
log.info("License key is valid.");
|
||||
} else {
|
||||
log.info("License key is invalid.");
|
||||
}
|
||||
} else {
|
||||
log.info("License key is invalid.");
|
||||
log.error("Failed to obtain license key content.");
|
||||
premiumEnabledResult = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getLicenseKeyContent(String keyOrFilePath) {
|
||||
if (keyOrFilePath == null || keyOrFilePath.trim().isEmpty()) {
|
||||
log.error("License key is not specified");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's a file reference
|
||||
if (keyOrFilePath.startsWith(FILE_PREFIX)) {
|
||||
String filePath = keyOrFilePath.substring(FILE_PREFIX.length());
|
||||
try {
|
||||
Path path = Paths.get(filePath);
|
||||
if (!Files.exists(path)) {
|
||||
log.error("License file does not exist: {}", filePath);
|
||||
return null;
|
||||
}
|
||||
log.info("Reading license from file: {}", filePath);
|
||||
return Files.readString(path);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read license file: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// It's a direct license key
|
||||
return keyOrFilePath;
|
||||
}
|
||||
|
||||
public void updateLicenseKey(String newKey) throws IOException {
|
||||
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
||||
applicationProperties.getPremium().setKey(newKey);
|
||||
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
|
||||
checkLicense();
|
||||
}
|
||||
|
||||
public boolean getEnterpriseEnabledResult() {
|
||||
return enterpriseEnabledResult;
|
||||
return premiumEnabledResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ public class AppConfig {
|
||||
@Bean(name = "analyticsEnabled")
|
||||
@Scope("request")
|
||||
public boolean analyticsEnabled() {
|
||||
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
||||
if (applicationProperties.getPremium().isEnabled()) return true;
|
||||
return applicationProperties.getSystem().isAnalyticsEnabled();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -56,6 +57,8 @@ public class ConfigInitializer {
|
||||
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
|
||||
YamlHelper settingsFile = new YamlHelper(settingTempPath);
|
||||
|
||||
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
|
||||
|
||||
boolean changesMade =
|
||||
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
||||
if (changesMade) {
|
||||
@@ -76,4 +79,46 @@ public class ConfigInitializer {
|
||||
log.info("Created custom_settings file: {}", customSettingsPath.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
private void migrateEnterpriseEditionToPremium(YamlHelper yaml, YamlHelper template) {
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "enabled") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "enabled"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "enabled"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "key") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "key"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "key"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "SSOAutoLogin"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")
|
||||
!= null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "autoUpdateMetadata"),
|
||||
yaml.getValueByExactKeyPath(
|
||||
"enterpriseEdition", "CustomMetadata", "autoUpdateMetadata"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "author"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "creator"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator"));
|
||||
}
|
||||
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")
|
||||
!= null) {
|
||||
template.updateValue(
|
||||
List.of("premium", "proFeatures", "CustomMetadata", "producer"),
|
||||
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -22,10 +23,14 @@ public class EndpointConfiguration {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
private final boolean runningEE;
|
||||
|
||||
@Autowired
|
||||
public EndpointConfiguration(ApplicationProperties applicationProperties) {
|
||||
public EndpointConfiguration(
|
||||
ApplicationProperties applicationProperties,
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.runningEE = runningEE;
|
||||
init();
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
@@ -281,6 +286,13 @@ public class EndpointConfiguration {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!runningEE) {
|
||||
disableGroup("enterprise");
|
||||
}
|
||||
|
||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
||||
disableEndpoint("url-to-pdf");
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getEndpointsForGroup(String group) {
|
||||
|
||||
@@ -36,7 +36,6 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
|
||||
if (!endpointsDiscovered) {
|
||||
discoverEndpoints();
|
||||
endpointsDiscovered = true;
|
||||
logger.info("Discovered {} valid GET endpoints", validGetEndpoints.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
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;
|
||||
|
||||
@Component
|
||||
public class EnterpriseEndpointFilter extends OncePerRequestFilter {
|
||||
private final boolean runningEE;
|
||||
|
||||
public EnterpriseEndpointFilter(@Qualifier("runningEE") boolean runningEE) {
|
||||
this.runningEE = runningEE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (!runningEE && isPrometheusEndpointRequest(request)) {
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean isPrometheusEndpointRequest(HttpServletRequest request) {
|
||||
return request.getRequestURI().contains("/actuator/");
|
||||
}
|
||||
}
|
||||
@@ -125,8 +125,7 @@ public class MergeController {
|
||||
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
|
||||
throws IOException {
|
||||
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
|
||||
ByteArrayOutputStream docOutputstream =
|
||||
new ByteArrayOutputStream(); // Stream for the merged document
|
||||
File mergedTempFile = null;
|
||||
PDDocument mergedDocument = null;
|
||||
|
||||
boolean removeCertSign = form.isRemoveCertSign();
|
||||
@@ -139,21 +138,24 @@ public class MergeController {
|
||||
form.getSortType())); // Sort files based on the given sort type
|
||||
|
||||
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
||||
long totalSize = 0;
|
||||
for (MultipartFile multipartFile : files) {
|
||||
totalSize += multipartFile.getSize();
|
||||
File tempFile =
|
||||
GeneralUtils.convertMultipartFileToFile(
|
||||
multipartFile); // Convert MultipartFile to File
|
||||
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
||||
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
||||
}
|
||||
mergerUtility.setDestinationStream(
|
||||
docOutputstream); // Set the output stream for the merged document
|
||||
mergerUtility.mergeDocuments(null); // Merge the documents
|
||||
|
||||
byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes
|
||||
|
||||
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
|
||||
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
|
||||
|
||||
mergerUtility.mergeDocuments(
|
||||
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
|
||||
|
||||
// Load the merged PDF document
|
||||
mergedDocument = pdfDocumentFactory.load(mergedPdfBytes);
|
||||
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
|
||||
|
||||
// Remove signatures if removeCertSign is true
|
||||
if (removeCertSign) {
|
||||
@@ -180,21 +182,23 @@ public class MergeController {
|
||||
String mergedFileName =
|
||||
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_merged_unsigned.pdf";
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
baos.toByteArray(), mergedFileName); // Return the modified PDF
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos, mergedFileName); // Return the modified PDF
|
||||
|
||||
} catch (Exception ex) {
|
||||
log.error("Error in merge pdf process", ex);
|
||||
throw ex;
|
||||
} finally {
|
||||
if (mergedDocument != null) {
|
||||
mergedDocument.close(); // Close the merged document
|
||||
}
|
||||
for (File file : filesToDelete) {
|
||||
if (file != null) {
|
||||
Files.deleteIfExists(file.toPath()); // Delete temporary files
|
||||
}
|
||||
}
|
||||
docOutputstream.close();
|
||||
if (mergedDocument != null) {
|
||||
mergedDocument.close(); // Close the merged document
|
||||
}
|
||||
if (mergedTempFile != null) {
|
||||
Files.deleteIfExists(mergedTempFile.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ public class SplitPdfBySizeController {
|
||||
@Autowired
|
||||
public SplitPdfBySizeController(CustomPDFDocumentFactory pdfDocumentFactory) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
log.info(
|
||||
"SplitPdfBySizeController initialized with pdfDocumentFactory: {}",
|
||||
pdfDocumentFactory.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||
@@ -57,53 +54,49 @@ public class SplitPdfBySizeController {
|
||||
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
|
||||
throws Exception {
|
||||
|
||||
log.info("Starting PDF split process with request: {}", request);
|
||||
log.debug("Starting PDF split process with request: {}", request);
|
||||
MultipartFile file = request.getFileInput();
|
||||
log.info(
|
||||
"File received: name={}, size={} bytes",
|
||||
file.getOriginalFilename(),
|
||||
file.getSize());
|
||||
|
||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||
log.info("Created temporary zip file: {}", zipFile);
|
||||
log.debug("Created temporary zip file: {}", zipFile);
|
||||
|
||||
String filename =
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "");
|
||||
log.info("Base filename for output: {}", filename);
|
||||
log.debug("Base filename for output: {}", filename);
|
||||
|
||||
byte[] data = null;
|
||||
try {
|
||||
log.info("Reading input file bytes");
|
||||
log.debug("Reading input file bytes");
|
||||
byte[] pdfBytes = file.getBytes();
|
||||
log.info("Successfully read {} bytes from input file", pdfBytes.length);
|
||||
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
|
||||
|
||||
log.info("Creating ZIP output stream");
|
||||
log.debug("Creating ZIP output stream");
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||
log.info("Loading PDF document");
|
||||
log.debug("Loading PDF document");
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Successfully loaded PDF with {} pages",
|
||||
sourceDocument.getNumberOfPages());
|
||||
|
||||
int type = request.getSplitType();
|
||||
String value = request.getSplitValue();
|
||||
log.info("Split type: {}, Split value: {}", type, value);
|
||||
log.debug("Split type: {}, Split value: {}", type, value);
|
||||
|
||||
if (type == 0) {
|
||||
log.info("Processing split by size");
|
||||
log.debug("Processing split by size");
|
||||
long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
||||
log.info("Max bytes per document: {}", maxBytes);
|
||||
log.debug("Max bytes per document: {}", maxBytes);
|
||||
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
|
||||
} else if (type == 1) {
|
||||
log.info("Processing split by page count");
|
||||
log.debug("Processing split by page count");
|
||||
int pageCount = Integer.parseInt(value);
|
||||
log.info("Pages per document: {}", pageCount);
|
||||
log.debug("Pages per document: {}", pageCount);
|
||||
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
|
||||
} else if (type == 2) {
|
||||
log.info("Processing split by document count");
|
||||
log.debug("Processing split by document count");
|
||||
int documentCount = Integer.parseInt(value);
|
||||
log.info("Total number of documents: {}", documentCount);
|
||||
log.debug("Total number of documents: {}", documentCount);
|
||||
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
|
||||
} else {
|
||||
log.error("Invalid split type: {}", type);
|
||||
@@ -111,7 +104,7 @@ public class SplitPdfBySizeController {
|
||||
"Invalid argument for split type: " + type);
|
||||
}
|
||||
|
||||
log.info("PDF splitting completed successfully");
|
||||
log.debug("PDF splitting completed successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading or processing PDF document", e);
|
||||
throw e;
|
||||
@@ -126,23 +119,23 @@ public class SplitPdfBySizeController {
|
||||
throw e; // Re-throw to ensure proper error response
|
||||
} finally {
|
||||
try {
|
||||
log.info("Reading ZIP file data");
|
||||
log.debug("Reading ZIP file data");
|
||||
data = Files.readAllBytes(zipFile);
|
||||
log.info("Successfully read {} bytes from ZIP file", data.length);
|
||||
log.debug("Successfully read {} bytes from ZIP file", data.length);
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading ZIP file data", e);
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Deleting temporary ZIP file");
|
||||
log.debug("Deleting temporary ZIP file");
|
||||
boolean deleted = Files.deleteIfExists(zipFile);
|
||||
log.info("Temporary ZIP file deleted: {}", deleted);
|
||||
log.debug("Temporary ZIP file deleted: {}", deleted);
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting temporary ZIP file", e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Returning response with {} bytes of data", data != null ? data.length : 0);
|
||||
log.debug("Returning response with {} bytes of data", data != null ? data.length : 0);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
@@ -150,7 +143,7 @@ public class SplitPdfBySizeController {
|
||||
private void handleSplitBySize(
|
||||
PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename)
|
||||
throws IOException {
|
||||
log.info("Starting handleSplitBySize with maxBytes={}", maxBytes);
|
||||
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes);
|
||||
|
||||
PDDocument currentDoc =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
||||
@@ -163,7 +156,7 @@ public class SplitPdfBySizeController {
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
PDPage page = sourceDocument.getPage(pageIndex);
|
||||
log.info("Processing page {} of {}", pageIndex + 1, totalPages);
|
||||
log.debug("Processing page {} of {}", pageIndex + 1, totalPages);
|
||||
|
||||
// Add the page to current document
|
||||
PDPage newPage = new PDPage(page.getCOSObject());
|
||||
@@ -177,21 +170,21 @@ public class SplitPdfBySizeController {
|
||||
|| (pageAdded >= 20); // Always check after 20 pages
|
||||
|
||||
if (shouldCheckSize) {
|
||||
log.info("Performing size check after {} pages", pageAdded);
|
||||
log.debug("Performing size check after {} pages", pageAdded);
|
||||
ByteArrayOutputStream checkSizeStream = new ByteArrayOutputStream();
|
||||
currentDoc.save(checkSizeStream);
|
||||
long actualSize = checkSizeStream.size();
|
||||
log.info("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
|
||||
log.debug("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
|
||||
|
||||
if (actualSize > maxBytes) {
|
||||
// We exceeded the limit - remove the last page and save
|
||||
if (currentDoc.getNumberOfPages() > 1) {
|
||||
currentDoc.removePage(currentDoc.getNumberOfPages() - 1);
|
||||
pageIndex--; // Process this page again in the next document
|
||||
log.info("Size limit exceeded - removed last page");
|
||||
log.debug("Size limit exceeded - removed last page");
|
||||
}
|
||||
|
||||
log.info(
|
||||
log.debug(
|
||||
"Saving document with {} pages as part {}",
|
||||
currentDoc.getNumberOfPages(),
|
||||
fileIndex);
|
||||
@@ -206,7 +199,7 @@ public class SplitPdfBySizeController {
|
||||
int pagesToLookAhead = Math.min(5, totalPages - pageIndex - 1);
|
||||
|
||||
if (pagesToLookAhead > 0) {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Testing {} upcoming pages for potential addition",
|
||||
pagesToLookAhead);
|
||||
|
||||
@@ -231,12 +224,12 @@ public class SplitPdfBySizeController {
|
||||
|
||||
if (testSize <= maxBytes) {
|
||||
extraPagesAdded++;
|
||||
log.info(
|
||||
log.debug(
|
||||
"Test: Can add page {} (size would be {})",
|
||||
testPageIndex + 1,
|
||||
testSize);
|
||||
} else {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Test: Cannot add page {} (size would be {})",
|
||||
testPageIndex + 1,
|
||||
testSize);
|
||||
@@ -248,7 +241,7 @@ public class SplitPdfBySizeController {
|
||||
|
||||
// Add the pages we verified would fit
|
||||
if (extraPagesAdded > 0) {
|
||||
log.info("Adding {} verified pages ahead", extraPagesAdded);
|
||||
log.debug("Adding {} verified pages ahead", extraPagesAdded);
|
||||
for (int i = 0; i < extraPagesAdded; i++) {
|
||||
int extraPageIndex = pageIndex + 1 + i;
|
||||
PDPage extraPage = sourceDocument.getPage(extraPageIndex);
|
||||
@@ -265,26 +258,26 @@ public class SplitPdfBySizeController {
|
||||
|
||||
// Save final document if it has any pages
|
||||
if (currentDoc.getNumberOfPages() > 0) {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Saving final document with {} pages as part {}",
|
||||
currentDoc.getNumberOfPages(),
|
||||
fileIndex);
|
||||
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||
}
|
||||
|
||||
log.info("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
|
||||
log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
|
||||
}
|
||||
|
||||
private void handleSplitByPageCount(
|
||||
PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename)
|
||||
throws IOException {
|
||||
log.info("Starting handleSplitByPageCount with pageCount={}", pageCount);
|
||||
log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount);
|
||||
int currentPageCount = 0;
|
||||
log.info("Creating initial output document");
|
||||
log.debug("Creating initial output document");
|
||||
PDDocument currentDoc = null;
|
||||
try {
|
||||
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
||||
log.info("Successfully created initial output document");
|
||||
log.debug("Successfully created initial output document");
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating initial output document", e);
|
||||
throw new IOException("Failed to create initial output document", e);
|
||||
@@ -293,49 +286,49 @@ public class SplitPdfBySizeController {
|
||||
int fileIndex = 1;
|
||||
int pageIndex = 0;
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
log.info("Processing {} pages", totalPages);
|
||||
log.debug("Processing {} pages", totalPages);
|
||||
|
||||
try {
|
||||
for (PDPage page : sourceDocument.getPages()) {
|
||||
pageIndex++;
|
||||
log.info("Processing page {} of {}", pageIndex, totalPages);
|
||||
log.debug("Processing page {} of {}", pageIndex, totalPages);
|
||||
|
||||
try {
|
||||
log.info("Adding page {} to current document", pageIndex);
|
||||
log.debug("Adding page {} to current document", pageIndex);
|
||||
currentDoc.addPage(page);
|
||||
log.info("Successfully added page {} to current document", pageIndex);
|
||||
log.debug("Successfully added page {} to current document", pageIndex);
|
||||
} catch (Exception e) {
|
||||
log.error("Error adding page {} to current document", pageIndex, e);
|
||||
throw new IOException("Failed to add page to document", e);
|
||||
}
|
||||
|
||||
currentPageCount++;
|
||||
log.info("Current page count: {}/{}", currentPageCount, pageCount);
|
||||
log.debug("Current page count: {}/{}", currentPageCount, pageCount);
|
||||
|
||||
if (currentPageCount == pageCount) {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Reached target page count ({}), saving current document as part {}",
|
||||
pageCount,
|
||||
fileIndex);
|
||||
try {
|
||||
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||
log.info("Successfully saved document part {}", fileIndex - 1);
|
||||
log.debug("Successfully saved document part {}", fileIndex - 1);
|
||||
} catch (Exception e) {
|
||||
log.error("Error saving document part {}", fileIndex - 1, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Creating new document for next part");
|
||||
log.debug("Creating new document for next part");
|
||||
currentDoc = new PDDocument();
|
||||
log.info("Successfully created new document");
|
||||
log.debug("Successfully created new document");
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating new document for next part", e);
|
||||
throw new IOException("Failed to create new document", e);
|
||||
}
|
||||
|
||||
currentPageCount = 0;
|
||||
log.info("Reset current page count to 0");
|
||||
log.debug("Reset current page count to 0");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -346,34 +339,34 @@ public class SplitPdfBySizeController {
|
||||
// Add the last document if it contains any pages
|
||||
try {
|
||||
if (currentDoc.getPages().getCount() != 0) {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Saving final document with {} pages as part {}",
|
||||
currentDoc.getPages().getCount(),
|
||||
fileIndex);
|
||||
try {
|
||||
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||
log.info("Successfully saved final document part {}", fileIndex - 1);
|
||||
log.debug("Successfully saved final document part {}", fileIndex - 1);
|
||||
} catch (Exception e) {
|
||||
log.error("Error saving final document part {}", fileIndex - 1, e);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log.info("Final document has no pages, skipping");
|
||||
log.debug("Final document has no pages, skipping");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error checking or saving final document", e);
|
||||
throw new IOException("Failed to process final document", e);
|
||||
} finally {
|
||||
try {
|
||||
log.info("Closing final document");
|
||||
log.debug("Closing final document");
|
||||
currentDoc.close();
|
||||
log.info("Successfully closed final document");
|
||||
log.debug("Successfully closed final document");
|
||||
} catch (Exception e) {
|
||||
log.error("Error closing final document", e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
|
||||
log.debug("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
|
||||
}
|
||||
|
||||
private void handleSplitByDocCount(
|
||||
@@ -382,40 +375,40 @@ public class SplitPdfBySizeController {
|
||||
ZipOutputStream zipOut,
|
||||
String baseFilename)
|
||||
throws IOException {
|
||||
log.info("Starting handleSplitByDocCount with documentCount={}", documentCount);
|
||||
log.debug("Starting handleSplitByDocCount with documentCount={}", documentCount);
|
||||
int totalPageCount = sourceDocument.getNumberOfPages();
|
||||
log.info("Total pages in source document: {}", totalPageCount);
|
||||
log.debug("Total pages in source document: {}", totalPageCount);
|
||||
|
||||
int pagesPerDocument = totalPageCount / documentCount;
|
||||
int extraPages = totalPageCount % documentCount;
|
||||
log.info("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
|
||||
log.debug("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
|
||||
|
||||
int currentPageIndex = 0;
|
||||
int fileIndex = 1;
|
||||
|
||||
for (int i = 0; i < documentCount; i++) {
|
||||
log.info("Creating document {} of {}", i + 1, documentCount);
|
||||
log.debug("Creating document {} of {}", i + 1, documentCount);
|
||||
PDDocument currentDoc = null;
|
||||
try {
|
||||
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
||||
log.info("Successfully created document {} of {}", i + 1, documentCount);
|
||||
log.debug("Successfully created document {} of {}", i + 1, documentCount);
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating document {} of {}", i + 1, documentCount, e);
|
||||
throw new IOException("Failed to create document", e);
|
||||
}
|
||||
|
||||
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
||||
log.info("Adding {} pages to document {}", pagesToAdd, i + 1);
|
||||
log.debug("Adding {} pages to document {}", pagesToAdd, i + 1);
|
||||
|
||||
for (int j = 0; j < pagesToAdd; j++) {
|
||||
try {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Adding page {} (index {}) to document {}",
|
||||
j + 1,
|
||||
currentPageIndex,
|
||||
i + 1);
|
||||
currentDoc.addPage(sourceDocument.getPage(currentPageIndex));
|
||||
log.info("Successfully added page {} to document {}", j + 1, i + 1);
|
||||
log.debug("Successfully added page {} to document {}", j + 1, i + 1);
|
||||
currentPageIndex++;
|
||||
} catch (Exception e) {
|
||||
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
|
||||
@@ -424,37 +417,37 @@ public class SplitPdfBySizeController {
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Saving document {} with {} pages", i + 1, pagesToAdd);
|
||||
log.debug("Saving document {} with {} pages", i + 1, pagesToAdd);
|
||||
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
|
||||
log.info("Successfully saved document {}", i + 1);
|
||||
log.debug("Successfully saved document {}", i + 1);
|
||||
} catch (Exception e) {
|
||||
log.error("Error saving document {}", i + 1, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Completed handleSplitByDocCount with {} documents created", documentCount);
|
||||
log.debug("Completed handleSplitByDocCount with {} documents created", documentCount);
|
||||
}
|
||||
|
||||
private void saveDocumentToZip(
|
||||
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
|
||||
throws IOException {
|
||||
log.info("Starting saveDocumentToZip for document part {}", index);
|
||||
log.debug("Starting saveDocumentToZip for document part {}", index);
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
log.info("Saving document part {} to byte array", index);
|
||||
log.debug("Saving document part {} to byte array", index);
|
||||
document.save(outStream);
|
||||
log.info("Successfully saved document part {} ({} bytes)", index, outStream.size());
|
||||
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
|
||||
} catch (Exception e) {
|
||||
log.error("Error saving document part {} to byte array", index, e);
|
||||
throw new IOException("Failed to save document to byte array", e);
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Closing document part {}", index);
|
||||
log.debug("Closing document part {}", index);
|
||||
document.close();
|
||||
log.info("Successfully closed document part {}", index);
|
||||
log.debug("Successfully closed document part {}", index);
|
||||
} catch (Exception e) {
|
||||
log.error("Error closing document part {}", index, e);
|
||||
// Continue despite close error
|
||||
@@ -463,17 +456,17 @@ public class SplitPdfBySizeController {
|
||||
try {
|
||||
// Create a new zip entry
|
||||
String entryName = baseFilename + "_" + index + ".pdf";
|
||||
log.info("Creating ZIP entry: {}", entryName);
|
||||
log.debug("Creating ZIP entry: {}", entryName);
|
||||
ZipEntry zipEntry = new ZipEntry(entryName);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
byte[] bytes = outStream.toByteArray();
|
||||
log.info("Writing {} bytes to ZIP entry", bytes.length);
|
||||
log.debug("Writing {} bytes to ZIP entry", bytes.length);
|
||||
zipOut.write(bytes);
|
||||
|
||||
log.info("Closing ZIP entry");
|
||||
log.debug("Closing ZIP entry");
|
||||
zipOut.closeEntry();
|
||||
log.info("Successfully added document part {} to ZIP", index);
|
||||
log.debug("Successfully added document part {} to ZIP", index);
|
||||
} catch (Exception e) {
|
||||
log.error("Error adding document part {} to ZIP", index, e);
|
||||
throw new IOException("Failed to add document to ZIP file", e);
|
||||
|
||||
@@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.User;
|
||||
@@ -47,10 +48,15 @@ public class UserController {
|
||||
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
|
||||
private final UserService userService;
|
||||
private final SessionPersistentRegistry sessionRegistry;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
|
||||
public UserController(
|
||||
UserService userService,
|
||||
SessionPersistentRegistry sessionRegistry,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.userService = userService;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@@ -194,39 +200,44 @@ public class UserController {
|
||||
boolean forceChange)
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!userService.isUsernameValid(username)) {
|
||||
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
||||
return new RedirectView("/adminSettings?messageType=invalidUsername", true);
|
||||
}
|
||||
if (applicationProperties.getPremium().isEnabled()
|
||||
&& applicationProperties.getPremium().getMaxUsers()
|
||||
<= userService.getTotalUsersCount()) {
|
||||
return new RedirectView("/adminSettings?messageType=maxUsersReached", true);
|
||||
}
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
if (user.getUsername().equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||
return new RedirectView("/adminSettings?messageType=usernameExists", true);
|
||||
}
|
||||
}
|
||||
if (userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||
return new RedirectView("/adminSettings?messageType=usernameExists", true);
|
||||
}
|
||||
try {
|
||||
// Validate the role
|
||||
Role roleEnum = Role.fromString(role);
|
||||
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||
// If the role is INTERNAL_API_USER, reject the request
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the role ID is not valid, redirect with an error message
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
||||
}
|
||||
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
||||
userService.saveUser(username, AuthenticationType.SSO, role);
|
||||
} else {
|
||||
if (password.isBlank()) {
|
||||
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
||||
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
|
||||
}
|
||||
userService.saveUser(username, password, role, forceChange);
|
||||
}
|
||||
return new RedirectView(
|
||||
"/addUsers", // Redirect to account page after adding the user
|
||||
"/adminSettings", // Redirect to account page after adding the user
|
||||
true);
|
||||
}
|
||||
|
||||
@@ -239,32 +250,32 @@ public class UserController {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (!userOpt.isPresent()) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
return new RedirectView("/adminSettings?messageType=userNotFound", true);
|
||||
}
|
||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
return new RedirectView("/adminSettings?messageType=userNotFound", true);
|
||||
}
|
||||
// Get the currently authenticated username
|
||||
String currentUsername = authentication.getName();
|
||||
// Check if the provided username matches the current session's username
|
||||
if (currentUsername.equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
|
||||
return new RedirectView("/adminSettings?messageType=downgradeCurrentUser", true);
|
||||
}
|
||||
try {
|
||||
// Validate the role
|
||||
Role roleEnum = Role.fromString(role);
|
||||
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||
// If the role is INTERNAL_API_USER, reject the request
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the role ID is not valid, redirect with an error message
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
userService.changeRole(user, role);
|
||||
return new RedirectView(
|
||||
"/addUsers", // Redirect to account page after adding the user
|
||||
"/adminSettings", // Redirect to account page after adding the user
|
||||
true);
|
||||
}
|
||||
|
||||
@@ -277,16 +288,16 @@ public class UserController {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (userOpt.isEmpty()) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
return new RedirectView("/adminSettings?messageType=userNotFound", true);
|
||||
}
|
||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
return new RedirectView("/adminSettings?messageType=userNotFound", true);
|
||||
}
|
||||
// Get the currently authenticated username
|
||||
String currentUsername = authentication.getName();
|
||||
// Check if the provided username matches the current session's username
|
||||
if (currentUsername.equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
|
||||
return new RedirectView("/adminSettings?messageType=disabledCurrentUser", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
userService.changeUserEnabled(user, enabled);
|
||||
@@ -314,7 +325,7 @@ public class UserController {
|
||||
}
|
||||
}
|
||||
return new RedirectView(
|
||||
"/addUsers", // Redirect to account page after adding the user
|
||||
"/adminSettings", // Redirect to account page after adding the user
|
||||
true);
|
||||
}
|
||||
|
||||
@@ -323,13 +334,13 @@ public class UserController {
|
||||
public RedirectView deleteUser(
|
||||
@PathVariable("username") String username, Authentication authentication) {
|
||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
||||
return new RedirectView("/adminSettings?messageType=deleteUsernameExists", true);
|
||||
}
|
||||
// Get the currently authenticated username
|
||||
String currentUsername = authentication.getName();
|
||||
// Check if the provided username matches the current session's username
|
||||
if (currentUsername.equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||
return new RedirectView("/adminSettings?messageType=deleteCurrentUser", true);
|
||||
}
|
||||
// Invalidate all sessions before deleting the user
|
||||
List<SessionInformation> sessionsInformations =
|
||||
@@ -339,7 +350,7 @@ public class UserController {
|
||||
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
|
||||
}
|
||||
userService.deleteUser(username);
|
||||
return new RedirectView("/addUsers", true);
|
||||
return new RedirectView("/adminSettings", true);
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.RuntimePathConfig;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@@ -35,12 +36,16 @@ public class ConvertWebsiteToPDF {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
public ConvertWebsiteToPDF(
|
||||
CustomPDFDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
RuntimePathConfig runtimePathConfig,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||
@@ -53,6 +58,9 @@ public class ConvertWebsiteToPDF {
|
||||
throws IOException, InterruptedException {
|
||||
String URL = request.getUrlInput();
|
||||
|
||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
||||
throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
|
||||
}
|
||||
// Validate the URL format
|
||||
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||
|
||||
@@ -118,11 +118,11 @@ public class AccountWebController {
|
||||
|
||||
if (securityProps.isSaml2Active()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
&& applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||
&& applicationProperties.getPremium().isEnabled()) {
|
||||
String samlIdp = saml2.getProvider();
|
||||
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||
|
||||
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
|
||||
if (applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
|
||||
return "redirect:" + request.getRequestURL() + saml2AuthenticationPath;
|
||||
} else {
|
||||
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
|
||||
@@ -195,7 +195,13 @@ public class AccountWebController {
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/addUsers")
|
||||
@GetMapping("/usage")
|
||||
public String showUsage() {
|
||||
return "usage";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/adminSettings")
|
||||
public String showAddUserForm(
|
||||
HttpServletRequest request, Model model, Authentication authentication) {
|
||||
List<User> allUsers = userRepository.findAll();
|
||||
@@ -318,7 +324,9 @@ public class AccountWebController {
|
||||
model.addAttribute("totalUsers", allUsers.size());
|
||||
model.addAttribute("activeUsers", activeUsers);
|
||||
model.addAttribute("disabledUsers", disabledUsers);
|
||||
return "addUsers";
|
||||
|
||||
model.addAttribute("maxEnterpriseUsers", applicationProperties.getPremium().getMaxUsers());
|
||||
return "adminSettings";
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
|
||||
@@ -22,6 +22,7 @@ import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointInspector;
|
||||
import stirling.software.SPDF.config.StartupApplicationListener;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@@ -32,15 +33,17 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
||||
public class MetricsController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
private final EndpointInspector endpointInspector;
|
||||
private boolean metricsEnabled;
|
||||
|
||||
public MetricsController(
|
||||
ApplicationProperties applicationProperties, MeterRegistry meterRegistry) {
|
||||
ApplicationProperties applicationProperties,
|
||||
MeterRegistry meterRegistry,
|
||||
EndpointInspector endpointInspector) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.endpointInspector = endpointInspector;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@@ -208,25 +211,43 @@ public class MetricsController {
|
||||
}
|
||||
|
||||
private double getRequestCount(String method, Optional<String> endpoint) {
|
||||
log.info(
|
||||
"Getting request count for method: {}, endpoint: {}",
|
||||
method,
|
||||
endpoint.orElse("all"));
|
||||
double count =
|
||||
meterRegistry.find("http.requests").tag("method", method).counters().stream()
|
||||
.filter(
|
||||
counter ->
|
||||
!endpoint.isPresent()
|
||||
|| endpoint.get()
|
||||
.equals(counter.getId().getTag("uri")))
|
||||
.mapToDouble(Counter::count)
|
||||
.sum();
|
||||
log.info("Request count: {}", count);
|
||||
return count;
|
||||
return meterRegistry.find("http.requests").tag("method", method).counters().stream()
|
||||
.filter(
|
||||
counter -> {
|
||||
String uri = counter.getId().getTag("uri");
|
||||
|
||||
// Apply filtering logic - Skip if uri is null
|
||||
if (uri == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For POST requests, only include if they start with /api/v1
|
||||
if ("POST".equals(method) && !uri.contains("api/v1")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.contains(".txt")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For GET requests, validate if we have a list of valid endpoints
|
||||
final boolean validateGetEndpoints =
|
||||
endpointInspector.getValidGetEndpoints().size() != 0;
|
||||
if ("GET".equals(method)
|
||||
&& validateGetEndpoints
|
||||
&& !endpointInspector.isValidGetEndpoint(uri)) {
|
||||
log.debug("Skipping invalid GET endpoint: {}", uri);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter for specific endpoint if provided
|
||||
return !endpoint.isPresent() || endpoint.get().equals(uri);
|
||||
})
|
||||
.mapToDouble(Counter::count)
|
||||
.sum();
|
||||
}
|
||||
|
||||
private List<EndpointCount> getEndpointCounts(String method) {
|
||||
log.info("Getting endpoint counts for method: {}", method);
|
||||
Map<String, Double> counts = new HashMap<>();
|
||||
meterRegistry
|
||||
.find("http.requests")
|
||||
@@ -235,28 +256,72 @@ public class MetricsController {
|
||||
.forEach(
|
||||
counter -> {
|
||||
String uri = counter.getId().getTag("uri");
|
||||
|
||||
// Skip if uri is null
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For POST requests, only include if they start with /api/v1
|
||||
if ("POST".equals(method) && !uri.contains("api/v1")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri.contains(".txt")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For GET requests, validate if we have a list of valid endpoints
|
||||
final boolean validateGetEndpoints =
|
||||
endpointInspector.getValidGetEndpoints().size() != 0;
|
||||
if ("GET".equals(method)
|
||||
&& validateGetEndpoints
|
||||
&& !endpointInspector.isValidGetEndpoint(uri)) {
|
||||
log.debug("Skipping invalid GET endpoint: {}", uri);
|
||||
return;
|
||||
}
|
||||
|
||||
counts.merge(uri, counter.count(), Double::sum);
|
||||
});
|
||||
List<EndpointCount> result =
|
||||
counts.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
log.info("Found {} endpoints with counts", result.size());
|
||||
return result;
|
||||
|
||||
return counts.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private double getUniqueUserCount(String method, Optional<String> endpoint) {
|
||||
log.info(
|
||||
"Getting unique user count for method: {}, endpoint: {}",
|
||||
method,
|
||||
endpoint.orElse("all"));
|
||||
Set<String> uniqueUsers = new HashSet<>();
|
||||
meterRegistry.find("http.requests").tag("method", method).counters().stream()
|
||||
.filter(
|
||||
counter ->
|
||||
!endpoint.isPresent()
|
||||
|| endpoint.get().equals(counter.getId().getTag("uri")))
|
||||
counter -> {
|
||||
String uri = counter.getId().getTag("uri");
|
||||
|
||||
// Skip if uri is null
|
||||
if (uri == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For POST requests, only include if they start with /api/v1
|
||||
if ("POST".equals(method) && !uri.contains("api/v1")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.contains(".txt")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For GET requests, validate if we have a list of valid endpoints
|
||||
final boolean validateGetEndpoints =
|
||||
endpointInspector.getValidGetEndpoints().size() != 0;
|
||||
if ("GET".equals(method)
|
||||
&& validateGetEndpoints
|
||||
&& !endpointInspector.isValidGetEndpoint(uri)) {
|
||||
log.debug("Skipping invalid GET endpoint: {}", uri);
|
||||
return false;
|
||||
}
|
||||
return !endpoint.isPresent() || endpoint.get().equals(uri);
|
||||
})
|
||||
.forEach(
|
||||
counter -> {
|
||||
String session = counter.getId().getTag("session");
|
||||
@@ -264,12 +329,10 @@ public class MetricsController {
|
||||
uniqueUsers.add(session);
|
||||
}
|
||||
});
|
||||
log.info("Unique user count: {}", uniqueUsers.size());
|
||||
return uniqueUsers.size();
|
||||
}
|
||||
|
||||
private List<EndpointCount> getUniqueUserCounts(String method) {
|
||||
log.info("Getting unique user counts for method: {}", method);
|
||||
Map<String, Set<String>> uniqueUsers = new HashMap<>();
|
||||
meterRegistry
|
||||
.find("http.requests")
|
||||
@@ -283,13 +346,10 @@ public class MetricsController {
|
||||
uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session);
|
||||
}
|
||||
});
|
||||
List<EndpointCount> result =
|
||||
uniqueUsers.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
log.info("Found {} endpoints with unique user counts", result.size());
|
||||
return result;
|
||||
return uniqueUsers.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/uptime")
|
||||
|
||||
@@ -81,6 +81,8 @@ public class ApplicationProperties {
|
||||
private Endpoints endpoints = new Endpoints();
|
||||
private Metrics metrics = new Metrics();
|
||||
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
||||
|
||||
private Premium premium = new Premium();
|
||||
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
||||
private AutoPipeline autoPipeline = new AutoPipeline();
|
||||
private ProcessExecutor processExecutor = new ProcessExecutor();
|
||||
@@ -287,6 +289,7 @@ public class ApplicationProperties {
|
||||
private Boolean enableAnalytics;
|
||||
private Datasource datasource;
|
||||
private Boolean disableSanitize;
|
||||
private Boolean enableUrlToPDF;
|
||||
private CustomPaths customPaths = new CustomPaths();
|
||||
|
||||
public boolean isAnalyticsEnabled() {
|
||||
@@ -390,6 +393,7 @@ public class ApplicationProperties {
|
||||
private String appVersion;
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
@Data
|
||||
public static class EnterpriseEdition {
|
||||
private boolean enabled;
|
||||
@@ -415,6 +419,50 @@ public class ApplicationProperties {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Premium {
|
||||
private boolean enabled;
|
||||
@ToString.Exclude private String key;
|
||||
private int maxUsers;
|
||||
private ProFeatures proFeatures = new ProFeatures();
|
||||
private EnterpriseFeatures enterpriseFeatures = new EnterpriseFeatures();
|
||||
|
||||
@Data
|
||||
public static class ProFeatures {
|
||||
private boolean ssoAutoLogin;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
|
||||
@Data
|
||||
public static class CustomMetadata {
|
||||
private boolean autoUpdateMetadata;
|
||||
private String author;
|
||||
private String creator;
|
||||
private String producer;
|
||||
|
||||
public String getCreator() {
|
||||
return creator == null || creator.trim().isEmpty() ? "Stirling-PDF" : creator;
|
||||
}
|
||||
|
||||
public String getProducer() {
|
||||
return producer == null || producer.trim().isEmpty()
|
||||
? "Stirling-PDF"
|
||||
: producer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class EnterpriseFeatures {
|
||||
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
||||
|
||||
@Data
|
||||
public static class PersistentMetrics {
|
||||
private boolean enabled;
|
||||
private int retentionDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProcessExecutor {
|
||||
private SessionLimit sessionLimit = new SessionLimit();
|
||||
|
||||
@@ -139,7 +139,7 @@ public class CustomPDFDocumentFactory {
|
||||
* Determine the appropriate caching strategy based on file size and available memory. This
|
||||
* common method is used by both password and non-password loading paths.
|
||||
*/
|
||||
private StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
||||
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
||||
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||
long freeMemory = Runtime.getRuntime().freeMemory();
|
||||
long totalMemory = Runtime.getRuntime().totalMemory();
|
||||
|
||||
@@ -64,10 +64,19 @@ public class PdfMetadataService {
|
||||
|
||||
String creator = stirlingPDFLabel;
|
||||
|
||||
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
|
||||
if (applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata()
|
||||
&& runningEE) {
|
||||
|
||||
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
||||
creator =
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getCreator();
|
||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||
}
|
||||
|
||||
@@ -84,9 +93,18 @@ public class PdfMetadataService {
|
||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||
|
||||
String author = pdfMetadata.getAuthor();
|
||||
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
|
||||
if (applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata()
|
||||
&& runningEE) {
|
||||
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
||||
author =
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getAuthor();
|
||||
|
||||
if (userService != null) {
|
||||
author = author.replace("username", userService.getCurrentUsername());
|
||||
|
||||
@@ -334,27 +334,40 @@ public class PostHogService {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_enabled",
|
||||
applicationProperties.getEnterpriseEdition().isEnabled());
|
||||
if (applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||
applicationProperties.getPremium().isEnabled());
|
||||
if (applicationProperties.getPremium().isEnabled()) {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_autoUpdateMetadata",
|
||||
applicationProperties
|
||||
.getEnterpriseEdition()
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_author",
|
||||
applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor());
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getAuthor());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_creator",
|
||||
applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator());
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getCreator());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"enterpriseEdition_customMetadata_producer",
|
||||
applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer());
|
||||
applicationProperties
|
||||
.getPremium()
|
||||
.getProFeatures()
|
||||
.getCustomMetadata()
|
||||
.getProducer());
|
||||
}
|
||||
// Capture AutoPipeline properties
|
||||
addIfNotEmpty(
|
||||
|
||||
@@ -39,6 +39,7 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith(".css")
|
||||
|| requestURI.endsWith(".map")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith("popularity.txt")
|
||||
|| requestURI.endsWith(".js")
|
||||
|| requestURI.contains("swagger")
|
||||
|| requestURI.startsWith("/api/v1/info")
|
||||
|
||||
Reference in New Issue
Block a user