From 559581c59dd4b59368996dc4685f8fc344f40f4f Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.>
Date: Fri, 15 Nov 2024 13:12:20 +0000
Subject: [PATCH] Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
---
.../security/saml2/CertificateUtils.java | 87 +++++++++++++++++++
...stomSaml2AuthenticationFailureHandler.java | 50 +++++++----
.../LoggingSamlAuthenticationProvider.java | 67 ++++++++++++++
.../SPDF/model/ApplicationProperties.java | 36 ++------
.../software/SPDF/utils/GeneralUtils.java | 23 +++++
src/main/resources/static/js/downloader.js | 70 +++++++++++++--
src/main/resources/static/js/githubVersion.js | 2 +-
.../resources/templates/fragments/common.html | 1 +
8 files changed, 278 insertions(+), 58 deletions(-)
create mode 100644 src/main/java/stirling/software/SPDF/config/security/saml2/LoggingSamlAuthenticationProvider.java
diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java
index 4f0d2488a..b3af0e14b 100644
--- a/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java
+++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java
@@ -1,18 +1,25 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.io.InputStreamReader;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Scanner;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
@@ -39,4 +46,84 @@ public class CertificateUtils {
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
}
+
+
+ public static X509Certificate getIdPCertificate(Resource certificateResource) throws Exception {
+
+ if (certificateResource instanceof UrlResource) {
+ return extractCertificateFromMetadata(certificateResource);
+ } else {
+ // Treat as file resource
+ return readCertificate(certificateResource);
+ }
+ }
+
+ private static X509Certificate extractCertificateFromMetadata(Resource metadataResource) throws Exception {
+ log.info("Attempting to extract certificate from metadata resource: {}", metadataResource.getDescription());
+
+ try (InputStream is = metadataResource.getInputStream()) {
+ String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ log.info("Retrieved metadata content, length: {}", content.length());
+
+ // Find the certificate data
+ int startIndex = content.indexOf("");
+ int endIndex = content.indexOf("");
+
+ if (startIndex == -1 || endIndex == -1) {
+ log.error("Certificate tags not found in metadata");
+ throw new Exception("Certificate tags not found in metadata");
+ }
+
+ // Extract certificate data
+ String certData = content.substring(
+ startIndex + "".length(),
+ endIndex
+ ).trim();
+
+ log.info("Found certificate data, length: {}", certData.length());
+
+ // Remove any whitespace and newlines from cert data
+ certData = certData.replaceAll("\\s+", "");
+
+ // Reconstruct PEM format with proper line breaks
+ StringBuilder pemBuilder = new StringBuilder();
+ pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
+
+ // Insert line breaks every 64 characters
+ int lineLength = 64;
+ for (int i = 0; i < certData.length(); i += lineLength) {
+ int end = Math.min(i + lineLength, certData.length());
+ pemBuilder.append(certData, i, end).append('\n');
+ }
+
+ pemBuilder.append("-----END CERTIFICATE-----");
+ String pemCert = pemBuilder.toString();
+
+ log.debug("Reconstructed PEM certificate:\n{}", pemCert);
+
+ try {
+ ByteArrayInputStream pemStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
+
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate cert = (X509Certificate) cf.generateCertificate(pemStream);
+
+ log.info("Successfully parsed certificate. Subject: {}", cert.getSubjectX500Principal());
+
+ // Optional: check validity dates
+ cert.checkValidity(); // Throws CertificateExpiredException if expired
+ log.info("Certificate is valid (not expired)");
+
+ return cert;
+
+ } catch (Exception e) {
+ log.error("Failed to parse certificate", e);
+ throw new Exception("Failed to parse X509 certificate from metadata", e);
+ }
+ } catch (Exception e) {
+ log.error("Error processing metadata resource", e);
+ throw e;
+ }
+ }
+
+
}
diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationFailureHandler.java
index 3b652d358..d60f70ce3 100644
--- a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationFailureHandler.java
+++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationFailureHandler.java
@@ -16,23 +16,35 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
- @Override
- public void onAuthenticationFailure(
- HttpServletRequest request,
- HttpServletResponse response,
- AuthenticationException exception)
- throws IOException, ServletException {
- if (exception instanceof Saml2AuthenticationException) {
- Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
- getRedirectStrategy()
- .sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
- } else if (exception instanceof ProviderNotFoundException) {
- getRedirectStrategy()
- .sendRedirect(
- request,
- response,
- "/login?erroroauth=not_authentication_provider_found");
- }
- log.error("AuthenticationException: " + exception);
- }
+ @Override
+ public void onAuthenticationFailure(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException exception)
+ throws IOException, ServletException {
+
+ if (exception instanceof Saml2AuthenticationException saml2Exception) {
+ Saml2Error error = saml2Exception.getSaml2Error();
+
+ // Log detailed information about the SAML error
+ log.error("SAML Authentication failed with error code: {}", error.getErrorCode());
+ log.error("Error description: {}", error.getDescription());
+
+ // Redirect to login with specific error code
+ getRedirectStrategy()
+ .sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
+ } else if (exception instanceof ProviderNotFoundException) {
+ log.error("Authentication failed: No authentication provider found");
+
+ getRedirectStrategy()
+ .sendRedirect(
+ request,
+ response,
+ "/login?erroroauth=not_authentication_provider_found");
+ } else {
+ log.error("Unknown AuthenticationException: {}", exception.getMessage());
+ getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=unknown_error");
+ }
+ }
+
}
diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/LoggingSamlAuthenticationProvider.java b/src/main/java/stirling/software/SPDF/config/security/saml2/LoggingSamlAuthenticationProvider.java
new file mode 100644
index 000000000..18421a1a6
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/config/security/saml2/LoggingSamlAuthenticationProvider.java
@@ -0,0 +1,67 @@
+package stirling.software.SPDF.config.security.saml2;
+import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.saml2.core.Saml2ErrorCodes;
+
+public class LoggingSamlAuthenticationProvider implements AuthenticationProvider {
+
+ private static final Logger log = LoggerFactory.getLogger(LoggingSamlAuthenticationProvider.class);
+ private final OpenSaml4AuthenticationProvider delegate;
+
+ public LoggingSamlAuthenticationProvider(OpenSaml4AuthenticationProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ if (authentication instanceof Saml2AuthenticationToken token) {
+ String samlResponse = token.getSaml2Response();
+
+ // Log the raw SAML response
+ log.info("Raw SAML Response (Base64): {}", samlResponse);
+
+ // Decode and log the SAML response XML
+ try {
+ String decodedResponse = new String(Base64.getDecoder().decode(samlResponse), StandardCharsets.UTF_8);
+ log.info("Decoded SAML Response XML:\n{}", decodedResponse);
+ } catch (IllegalArgumentException e) {
+ // If decoding fails, it’s likely already plain XML
+ log.warn("SAML Response appears to be different format, not Base64-encoded.");
+ log.debug("SAML Response XML:\n{}", samlResponse);
+ }
+ // Delegate the actual authentication to the wrapped OpenSaml4AuthenticationProvider
+ try {
+ return delegate.authenticate(authentication);
+ } catch (Saml2AuthenticationException e) {
+ log.error("SAML authentication failed: {}");
+ log.error("Detailed error message: {}", e);
+ throw e;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean supports(Class> authentication) {
+ // Only support Saml2AuthenticationToken
+ return Saml2AuthenticationToken.class.isAssignableFrom(authentication);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java
index 16397965a..4753a2df4 100644
--- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java
+++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java
@@ -20,6 +20,7 @@ import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
import lombok.Data;
import lombok.Getter;
@@ -30,6 +31,7 @@ import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
+import stirling.software.SPDF.utils.GeneralUtils;
@Configuration
@ConfigurationProperties(prefix = "")
@@ -133,44 +135,20 @@ public class ApplicationProperties {
private String privateKey;
private String spCert;
- public InputStream getIdpMetadataUri() throws IOException {
- if (idpMetadataUri.startsWith("classpath:")) {
- return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
- .getInputStream();
- }
- try {
- URI uri = new URI(idpMetadataUri);
- URL url = uri.toURL();
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setRequestMethod("GET");
- return connection.getInputStream();
- } catch (URISyntaxException e) {
- throw new IOException("Invalid URI format: " + idpMetadataUri, e);
- }
+ public Resource getIdpMetadataUri() throws IOException {
+ return GeneralUtils.filePathToResource(idpMetadataUri);
}
public Resource getSpCert() {
- if (spCert.startsWith("classpath:")) {
- return new ClassPathResource(spCert.substring("classpath:".length()));
- } else {
- return new FileSystemResource(spCert);
- }
+ return GeneralUtils.filePathToResource(spCert);
}
public Resource getidpCert() {
- if (idpCert.startsWith("classpath:")) {
- return new ClassPathResource(idpCert.substring("classpath:".length()));
- } else {
- return new FileSystemResource(idpCert);
- }
+ return GeneralUtils.filePathToResource(idpCert);
}
public Resource getPrivateKey() {
- if (privateKey.startsWith("classpath:")) {
- return new ClassPathResource(privateKey.substring("classpath:".length()));
- } else {
- return new FileSystemResource(privateKey);
- }
+ return GeneralUtils.filePathToResource(privateKey);
}
}
diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java
index 8e56c8df6..ed62e213c 100644
--- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java
+++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java
@@ -29,6 +29,10 @@ import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
import org.springframework.web.multipart.MultipartFile;
import com.fathzer.soft.javaluator.DoubleEvaluator;
@@ -349,4 +353,23 @@ public class GeneralUtils {
return "GenericID";
}
}
+
+ public static Resource filePathToResource(String resourceFile) {
+ if (resourceFile == null) {
+ throw new IllegalStateException("file is not configured");
+ }
+
+ if (resourceFile.startsWith("classpath:")) {
+ return new ClassPathResource(resourceFile.substring("classpath:".length()));
+ } else if (resourceFile.startsWith("http://") || resourceFile.startsWith("https://")) {
+ try {
+ return new UrlResource(resourceFile);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create URL resource: " + resourceFile, e);
+ }
+ } else {
+ return new FileSystemResource(resourceFile);
+ }
+ }
+
}
diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js
index 1933c4fbf..7dda085c7 100644
--- a/src/main/resources/static/js/downloader.js
+++ b/src/main/resources/static/js/downloader.js
@@ -93,20 +93,71 @@
});
});
+ function getPDFPageCount(file) {
+ try {
+ if (file.type !== 'application/pdf') {
+ return null;
+ }
+
+ // Create a URL for the file
+ const url = URL.createObjectURL(file);
+
+ try {
+ // Ensure the worker is properly set
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
+ pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
+ }
+
+ // Load the PDF document
+ const loadingTask = pdfjsLib.getDocument(url);
+ const pdf = await loadingTask.promise;
+
+ // Get the page count
+ const pageCount = pdf.numPages;
+ return pageCount;
+ } finally {
+ // Clean up the URL
+ URL.revokeObjectURL(url);
+ }
+ } catch (error) {
+ console.error('Error counting PDF pages:', error);
+ return null;
+ }
+ }
+
+ function trackFileProcessing(file, startTime, success, errorMessage = null) {
+ const endTime = performance.now();
+ const processingTime = endTime - startTime;
+
+ posthog.capture('file_processing', {
+ success: success,
+ file_type: file.type || 'unknown',
+ file_size: file.size,
+ processing_time: processingTime,
+ error_message: errorMessage,
+ pdf_pages: file.type === 'application/pdf' ? getPDFPageCount(file) : null
+ });
+ }
+
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
+ const startTime = performance.now();
+ const file = formData.get('fileInput');
+
try {
const response = await fetch(url, { method: "POST", body: formData });
const contentType = response.headers.get("content-type");
if (!response.ok) {
if (response.status === 401) {
- // Handle 401 Unauthorized error
showSessionExpiredPrompt();
+ trackFileProcessing(file, startTime, false, 'unauthorized');
return;
}
if (contentType && contentType.includes("application/json")) {
console.error("Throwing error banner, response was not okay");
- return handleJsonResponse(response);
+ const jsonResponse = await handleJsonResponse(response);
+ trackFileProcessing(file, startTime, false, jsonResponse?.error || 'unknown_error');
+ return jsonResponse;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -115,13 +166,15 @@
let filename = getFilenameFromContentDisposition(contentDisposition);
const blob = await response.blob();
- if (contentType.includes("application/pdf") || contentType.includes("image/")) {
- return handleResponse(blob, filename, !isMulti, isZip);
- } else {
- return handleResponse(blob, filename, false, isZip);
- }
+ const result = await handleResponse(blob, filename, !isMulti, isZip);
+
+ // Track successful processing
+ trackFileProcessing(file, startTime, true, null);
+
+ return result;
} catch (error) {
console.error("Error in handleSingleDownload:", error);
+ trackFileProcessing(file, startTime, false, error.message);
throw error;
}
}
@@ -162,8 +215,7 @@
if (considerViewOptions) {
if (downloadOption === "sameWindow") {
const url = URL.createObjectURL(blob);
- window.location.href = url;
- return;
+return { filename, blob, url };
} else if (downloadOption === "newWindow") {
const url = URL.createObjectURL(blob);
window.open(url, "_blank");
diff --git a/src/main/resources/static/js/githubVersion.js b/src/main/resources/static/js/githubVersion.js
index eebb0ae88..3a564ecba 100644
--- a/src/main/resources/static/js/githubVersion.js
+++ b/src/main/resources/static/js/githubVersion.js
@@ -76,7 +76,7 @@ async function checkForUpdate() {
document.getElementById("update-btn").style.display = "block";
}
if (updateLink !== null) {
- document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + latestVersion + '');
+ document.getElementById("app-update").innerText = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + latestVersion + '');
if (updateLink.classList.contains("visually-hidden")) {
updateLink.classList.remove("visually-hidden");
}
diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html
index dd74d3882..e710cae81 100644
--- a/src/main/resources/templates/fragments/common.html
+++ b/src/main/resources/templates/fragments/common.html
@@ -201,6 +201,7 @@
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
})();
+