diff --git a/.gitignore b/.gitignore index ba9d755d..c5a8f2e7 100644 --- a/.gitignore +++ b/.gitignore @@ -110,7 +110,6 @@ watchedFolders/ *.war *.nar *.ear -*.zip *.tar.gz *.rar *.db diff --git a/build.gradle b/build.gradle index 860a574a..f4683882 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ ext { } group = "stirling.software" -version = "0.29.0" +version = "0.30.0" java { // 17 is lowest but we support and recommend 21 @@ -32,6 +32,12 @@ java { repositories { mavenCentral() maven { url "https://jitpack.io" } + maven { + url "https://build.shibboleth.net/nexus/content/repositories/releases/" + } + maven { + url "https://build.shibboleth.net/maven/releases/" + } } licenseReport { @@ -127,6 +133,9 @@ 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' + implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1' + if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" @@ -134,6 +143,8 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" + implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.3' + implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' //2.2.x requires rebuild of DB file.. need migration path runtimeOnly "com.h2database:h2:2.1.214" // implementation "com.h2database:h2:2.2.224" diff --git a/cucumber/exampleFiles/example.html b/cucumber/exampleFiles/example.html new file mode 100644 index 00000000..82e96100 --- /dev/null +++ b/cucumber/exampleFiles/example.html @@ -0,0 +1,11 @@ + + +
+ +My first paragraph.
+ + + + diff --git a/cucumber/exampleFiles/example.md b/cucumber/exampleFiles/example.md new file mode 100644 index 00000000..10bb117b --- /dev/null +++ b/cucumber/exampleFiles/example.md @@ -0,0 +1,16 @@ +header +============ + +Header2 +------------ +text + +text2 + +## **PDF Features** + +### **Page Operations** + +- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts) +- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages. +- Merge multiple PDFs together into a single resultant file. \ No newline at end of file diff --git a/cucumber/exampleFiles/example_html.zip b/cucumber/exampleFiles/example_html.zip new file mode 100644 index 00000000..23bd1950 Binary files /dev/null and b/cucumber/exampleFiles/example_html.zip differ diff --git a/cucumber/features/external.feature b/cucumber/features/external.feature index c1330f52..58c0a859 100644 --- a/cucumber/features/external.feature +++ b/cucumber/features/external.feature @@ -123,7 +123,7 @@ Feature: API Validation | odt | .odt | | doc | .doc | - @ocr + @ocr @pdfa1 Scenario: PDFA Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput" And the request data includes @@ -134,7 +134,7 @@ Feature: API Validation And the response file should have extension ".pdf" And the response file should have size greater than 100 - @ocr + @ocr @pdfa2 Scenario: PDFA1 Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput" And the request data includes @@ -218,6 +218,28 @@ Feature: API Validation | .odt | | .pptx | | .rtf | - - + @calibre @positive @htmltopdf + Scenario: Convert HTML to PDF + Given I use an example file at "exampleFiles/example.html" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/html/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + + @calibre @positive @zippedhtmltopdf + Scenario: Convert zipped HTML to PDF + Given I use an example file at "exampleFiles/example_html.zip" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/html/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + + @calibre @positive @markdowntopdf + Scenario: Convert Markdown to PDF + Given I use an example file at "exampleFiles/example.md" as parameter "fileInput" + When I send the API request to the endpoint "/api/v1/convert/markdown/pdf" + Then the response status code should be 200 + And the response file should have size greater than 100 + And the response file should have extension ".pdf" + \ No newline at end of file diff --git a/exampleYmlFiles/docker-compose-latest-fat-security.yml b/exampleYmlFiles/docker-compose-latest-fat-security.yml index f29a8a9f..44d5ebef 100644 --- a/exampleYmlFiles/docker-compose-latest-fat-security.yml +++ b/exampleYmlFiles/docker-compose-latest-fat-security.yml @@ -7,7 +7,7 @@ services: limits: memory: 4G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 @@ -19,7 +19,7 @@ services: - /stirling/latest/logs:/logs:rw environment: DOCKER_ENABLE_SECURITY: "true" - SECURITY_ENABLELOGIN: "true" + SECURITY_ENABLELOGIN: "false" PUID: 1002 PGID: 1002 UMASK: "022" diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index 0818aa7d..5b1b4070 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -17,9 +17,10 @@ public class EEAppConfig { @Autowired ApplicationProperties applicationProperties; - @Bean(name = "RunningEE") + @Autowired private LicenseKeyChecker licenseKeyChecker; + + @Bean(name = "runningEE") public boolean runningEnterpriseEdition() { - // TODO: Implement EE detection - return false; + return licenseKeyChecker.getEnterpriseEnabledResult(); } } diff --git a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java new file mode 100644 index 00000000..e9b14ac4 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java @@ -0,0 +1,204 @@ +package stirling.software.SPDF.EE; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +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.JSONObject; + +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.utils.GeneralUtils; + +@Service +@Slf4j +public class KeygenLicenseVerifier { + 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 final ApplicationProperties applicationProperties; + + @Autowired + public KeygenLicenseVerifier(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + + public boolean verifyLicense(String licenseKey) { + try { + log.info("Checking license key"); + String machineFingerprint = generateMachineFingerprint(); + + // First, try to validate the license + JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint); + 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 false; + } catch (Exception e) { + log.error("Error verifying license: " + e.getMessage()); + return false; + } + } + + private JsonNode validateLicense(String licenseKey, String machineFingerprint) + throws Exception { + HttpClient client = HttpClient.newHttpClient(); + String requestBody = + String.format( + "{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}", + licenseKey, machineFingerprint); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + BASE_URL + + "/" + + ACCOUNT_ID + + "/licenses/actions/validate-key")) + .header("Content-Type", "application/vnd.api+json") + .header("Accept", "application/vnd.api+json") + // .header("Authorization", "License " + licenseKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + HttpResponse