mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
feat(docker): update base images to Java 25, Spring 4, Jackson 3, Gradle 9 and optimize JVM options (Project Lilliput) (#5725)
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
24128dd318
commit
1f9b90ad57
@@ -27,8 +27,8 @@ spotless {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
@@ -42,7 +42,7 @@ dependencies {
|
||||
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15"
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1"
|
||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
||||
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
||||
|
||||
+2
-2
@@ -91,9 +91,9 @@ public class RuntimePathConfig {
|
||||
|
||||
// Initialize Operation paths
|
||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||
String defaultUnoConvertPath = isDocker ? "/usr/local/bin/unoconvert" : "unoconvert";
|
||||
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
|
||||
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
|
||||
String defaultOcrMyPdfPath = isDocker ? "/opt/venv/bin/ocrmypdf" : "ocrmypdf";
|
||||
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
|
||||
|
||||
Operations operations = customPaths.getOperations();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
|
||||
|
||||
/**
|
||||
* Configures the scheduler used by all {@code @Scheduled} methods. Uses virtual threads so that
|
||||
* long-running scheduled tasks (e.g. cleanup, license checks, file monitoring) never block each
|
||||
* other — each runs on its own lightweight virtual thread.
|
||||
*/
|
||||
@Configuration
|
||||
public class SchedulingConfig {
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
|
||||
scheduler.setVirtualThreads(true);
|
||||
scheduler.setThreadNamePrefix("scheduled-vt-");
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
+607
-448
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ public class JobExecutorService {
|
||||
private final HttpServletRequest request;
|
||||
private final ResourceMonitor resourceMonitor;
|
||||
private final JobQueue jobQueue;
|
||||
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||
private final ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
|
||||
private final long effectiveTimeoutMs;
|
||||
|
||||
@Autowired(required = false)
|
||||
|
||||
@@ -44,8 +44,10 @@ public class JobQueue implements SmartLifecycle {
|
||||
|
||||
private volatile BlockingQueue<QueuedJob> jobQueue;
|
||||
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||
private final ScheduledExecutorService scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
Thread.ofVirtual().name("job-queue-scheduler-", 0).factory());
|
||||
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualThreadExecutor();
|
||||
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
|
||||
|
||||
private boolean shuttingDown = false;
|
||||
|
||||
@@ -43,7 +43,9 @@ public class ResourceMonitor {
|
||||
@Value("${stirling.resource.monitor.interval-ms:60000}")
|
||||
private long monitorIntervalMs = 60000; // 60 seconds
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
Thread.ofVirtual().name("resource-monitor-", 0).factory());
|
||||
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ public class TaskManager {
|
||||
|
||||
private final FileStorage fileStorage;
|
||||
private final ScheduledExecutorService cleanupExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
Thread.ofVirtual().name("task-cleanup-", 0).factory());
|
||||
|
||||
/** Initialize the task manager and start the cleanup scheduler */
|
||||
public TaskManager(FileStorage fileStorage) {
|
||||
|
||||
@@ -2,30 +2,28 @@ package stirling.software.common.util;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
/**
|
||||
* Factory for creating executors backed by virtual threads (Java 21+). Virtual threads are
|
||||
* lightweight, managed by the JVM, and ideal for I/O-bound tasks. They eliminate the need for
|
||||
* thread pool sizing since thousands can run concurrently with minimal overhead.
|
||||
*/
|
||||
public final class ExecutorFactory {
|
||||
|
||||
@Slf4j
|
||||
public class ExecutorFactory {
|
||||
private ExecutorFactory() {}
|
||||
|
||||
/** Creates an {@link ExecutorService} that starts a new virtual thread for each task. */
|
||||
public static ExecutorService newVirtualThreadExecutor() {
|
||||
return Executors.newVirtualThreadPerTaskExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
|
||||
* cached thread pool on older Java versions.
|
||||
* Creates a {@link ScheduledExecutorService} backed by a single virtual thread. Useful for
|
||||
* periodic/delayed tasks that should not pin a platform thread.
|
||||
*/
|
||||
public static ExecutorService newVirtualOrCachedThreadExecutor() {
|
||||
try {
|
||||
ExecutorService executor =
|
||||
(ExecutorService)
|
||||
Executors.class
|
||||
.getMethod("newVirtualThreadPerTaskExecutor")
|
||||
.invoke(null);
|
||||
return executor;
|
||||
} catch (NoSuchMethodException e) {
|
||||
log.debug("Virtual threads not available; falling back to cached thread pool.");
|
||||
} catch (Exception e) {
|
||||
log.debug("Error initializing virtual thread executor: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
return Executors.newCachedThreadPool();
|
||||
public static ScheduledExecutorService newSingleVirtualThreadScheduledExecutor() {
|
||||
return Executors.newSingleThreadScheduledExecutor(
|
||||
Thread.ofVirtual().name("scheduled-vt-", 0).factory());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,17 @@ public class FileMonitor {
|
||||
|
||||
if (this.rootDirs.isEmpty()) {
|
||||
log.error("No valid directories to monitor - FileMonitor will not function");
|
||||
} else {
|
||||
// Register directories eagerly so the first @Scheduled tick does not warn.
|
||||
for (Path rootDir : this.rootDirs) {
|
||||
if (Files.exists(rootDir)) {
|
||||
try {
|
||||
recursivelyRegisterEntry(rootDir);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to register monitoring for {}", rootDir, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -649,7 +649,8 @@ public class GeneralUtils {
|
||||
}
|
||||
|
||||
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
// Use LinkedHashSet to prevent duplicates from inflating size and triggering maxSize guard
|
||||
Set<Integer> result = new LinkedHashSet<>();
|
||||
int offset = oneBased ? 1 : 0;
|
||||
int maxSize = Math.max(1000, totalPages * 3);
|
||||
for (String page : pages) {
|
||||
@@ -672,7 +673,7 @@ public class GeneralUtils {
|
||||
"Page list exceeds maximum allowed size of " + maxSize);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return new ArrayList<>(result);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -226,50 +226,54 @@ public class ProcessExecutor {
|
||||
List<String> outputLines = new ArrayList<>();
|
||||
|
||||
Thread errorReaderThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try (BufferedReader errorReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
errorReader, 5_000_000))
|
||||
!= null) {
|
||||
errorLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn("Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.unstarted(
|
||||
() -> {
|
||||
try (BufferedReader errorReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
errorReader, 5_000_000))
|
||||
!= null) {
|
||||
errorLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn(
|
||||
"Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
|
||||
Thread outputReaderThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try (BufferedReader outputReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
outputReader, 5_000_000))
|
||||
!= null) {
|
||||
outputLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn("Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.unstarted(
|
||||
() -> {
|
||||
try (BufferedReader outputReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
outputReader, 5_000_000))
|
||||
!= null) {
|
||||
outputLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn(
|
||||
"Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
|
||||
errorReaderThread.start();
|
||||
outputReaderThread.start();
|
||||
@@ -370,7 +374,7 @@ public class ProcessExecutor {
|
||||
}
|
||||
// Match common unoconvert variants (but NOT soffice)
|
||||
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
|
||||
if (lowerBasename.contains("unoconvert") || "unoconv".equals(lowerBasename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -4,18 +4,23 @@ import java.beans.PropertyEditorSupport;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.security.RedactionArea;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.DeserializationFeature;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@Slf4j
|
||||
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper =
|
||||
JsonMapper.builder()
|
||||
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
@@ -24,7 +29,6 @@ public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
TypeReference<ArrayList<RedactionArea>> typeRef = new TypeReference<>() {};
|
||||
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
|
||||
setValue(list);
|
||||
|
||||
+4
-3
@@ -4,12 +4,13 @@ import java.beans.PropertyEditorSupport;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
|
||||
+4
-3
@@ -12,11 +12,12 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||
public StrategyType lastStrategyUsed;
|
||||
|
||||
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
||||
super(service);
|
||||
super(service); // TempFileManager falls back to Files.createTempFile in tests
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
||||
protected StreamCacheCreateFunction getStreamCacheFunction(
|
||||
long contentSize, MemorySnapshot mem) {
|
||||
StrategyType type;
|
||||
if (contentSize < 10 * 1024 * 1024) {
|
||||
type = StrategyType.MEMORY_ONLY;
|
||||
@@ -26,6 +27,6 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||
type = StrategyType.TEMP_FILE;
|
||||
}
|
||||
this.lastStrategyUsed = type;
|
||||
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
|
||||
return super.getStreamCacheFunction(contentSize, mem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,17 +121,19 @@ public class UnoServerPoolTest {
|
||||
|
||||
// Try to acquire a third endpoint in separate thread (should block)
|
||||
Thread blockingThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try {
|
||||
acquireLatch.countDown(); // Signal we're about to block
|
||||
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint();
|
||||
acquired.incrementAndGet();
|
||||
lease3.close();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.unstarted(
|
||||
() -> {
|
||||
try {
|
||||
acquireLatch.countDown(); // Signal we're about to block
|
||||
UnoServerPool.UnoServerLease lease3 =
|
||||
pool.acquireEndpoint();
|
||||
acquired.incrementAndGet();
|
||||
lease3.close();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
blockingThread.start();
|
||||
acquireLatch.await(); // Wait for thread to start
|
||||
|
||||
+14
-4
@@ -143,11 +143,21 @@ class StringToArrayListPropertyEditorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_InvalidStructure() {
|
||||
// Arrange - this JSON doesn't match RedactionArea structure
|
||||
void testSetAsText_UnknownProperties() {
|
||||
// Arrange - this JSON contains properties not in RedactionArea
|
||||
// With FAIL_ON_UNKNOWN_PROPERTIES disabled, this should ignore the unknown properties
|
||||
String json = "[{\"invalid\":\"structure\"}]";
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
|
||||
// Act
|
||||
editor.setAsText(json);
|
||||
Object value = editor.getValue();
|
||||
|
||||
// Assert
|
||||
assertNotNull(value, "Value should not be null");
|
||||
assertInstanceOf(List.class, value, "Value should be a List");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||
assertEquals(1, list.size(), "List should have 1 entry (empty object)");
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -46,8 +46,23 @@ dependencies {
|
||||
|
||||
implementation project(':common')
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
implementation 'com.posthog.java:posthog:1.2.0'
|
||||
implementation 'org.telegram:telegrambots:6.9.7.1'
|
||||
implementation 'org.eclipse.jetty.http2:jetty-http2-server'
|
||||
implementation ('org.telegram:telegrambots:6.9.7.1') {
|
||||
// Grizzly server + Jersey JAX-RS stack: only used for webhook mode;
|
||||
// Stirling-PDF uses long-polling mode so these are dead weight (~3 MB)
|
||||
exclude group: 'org.glassfish.jersey.inject'
|
||||
exclude group: 'org.glassfish.jersey.media'
|
||||
exclude group: 'org.glassfish.jersey.containers'
|
||||
exclude group: 'org.glassfish.jersey.core'
|
||||
exclude group: 'org.glassfish.jersey.ext'
|
||||
exclude group: 'org.glassfish.grizzly'
|
||||
exclude group: 'org.glassfish.hk2'
|
||||
exclude group: 'org.glassfish.hk2.external'
|
||||
exclude group: 'org.javassist', module: 'javassist'
|
||||
// Old javax JAX-RS Jackson bindings (not needed, project uses jakarta)
|
||||
exclude group: 'com.fasterxml.jackson.jaxrs'
|
||||
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations'
|
||||
}
|
||||
implementation 'commons-io:commons-io:2.21.0'
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||
@@ -78,8 +93,9 @@ dependencies {
|
||||
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||
implementation 'org.apache.poi:poi-ooxml:5.5.1'
|
||||
|
||||
// Batik
|
||||
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
||||
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
|
||||
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
|
||||
implementation 'org.apache.xmlgraphics:batik-bridge:1.19'
|
||||
|
||||
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
|
||||
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
|
||||
|
||||
@@ -49,8 +49,8 @@ public class LibreOfficeListener {
|
||||
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
|
||||
lastActivityTime = System.currentTimeMillis();
|
||||
|
||||
// Start a background thread to monitor the activity timeout
|
||||
executorService = Executors.newSingleThreadExecutor();
|
||||
// Start a virtual thread to monitor the activity timeout
|
||||
executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||
executorService.submit(
|
||||
() -> {
|
||||
while (true) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@@ -176,11 +176,14 @@ public class SPDFApplication {
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onWebServerInitialized(WebServerInitializedEvent event) {
|
||||
int actualPort = event.getWebServer().getPort();
|
||||
serverPortStatic = String.valueOf(actualPort);
|
||||
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||
String port =
|
||||
event.getApplicationContext().getEnvironment().getProperty("local.server.port");
|
||||
if (port != null) {
|
||||
serverPortStatic = port;
|
||||
}
|
||||
// Log the actual runtime port for Tauri to parse
|
||||
log.info("Stirling-PDF running on port: {}", actualPort);
|
||||
log.info("Stirling-PDF running on port: {}", serverPortStatic);
|
||||
}
|
||||
|
||||
private static void printStartupLogs() {
|
||||
|
||||
@@ -51,9 +51,7 @@ public class ExternalAppDepConfig {
|
||||
*/
|
||||
private final Map<String, List<String>> commandToGroupMapping;
|
||||
|
||||
private final ExecutorService pool =
|
||||
Executors.newFixedThreadPool(
|
||||
Math.max(2, Runtime.getRuntime().availableProcessors() / 2));
|
||||
private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
public ExternalAppDepConfig(
|
||||
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.web.servlet.MultipartConfigFactory;
|
||||
import org.springframework.boot.servlet.MultipartConfigFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
|
||||
@@ -54,8 +54,8 @@ public class TauriProcessMonitor {
|
||||
scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
r -> {
|
||||
Thread t = new Thread(r, "tauri-process-monitor");
|
||||
t.setDaemon(true);
|
||||
Thread t =
|
||||
Thread.ofVirtual().name("tauri-process-monitor").unstarted(r);
|
||||
return t;
|
||||
});
|
||||
|
||||
|
||||
@@ -154,8 +154,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.maxAge(3600);
|
||||
} else {
|
||||
// Default to allowing all origins when nothing is configured
|
||||
logger.info(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
||||
logger.debug(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); WebMvcConfig allowing all origins.");
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
|
||||
+3
-3
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Getter;
|
||||
@@ -32,6 +29,9 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -16,9 +16,6 @@ import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
@@ -35,6 +32,9 @@ import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@UiDataApi
|
||||
public class UIDataController {
|
||||
@@ -44,18 +44,21 @@ public class UIDataController {
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SharedSignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
RuntimePathConfig runtimePathConfig,
|
||||
ObjectMapper objectMapper) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/footer-info")
|
||||
@@ -93,9 +96,8 @@ public class UIDataController {
|
||||
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
mapper.readValue(json, new TypeReference<>() {});
|
||||
objectMapper.readValue(json, new TypeReference<>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
@@ -127,8 +129,8 @@ public class UIDataController {
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
objectMapper.readValue(
|
||||
config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
|
||||
+36
-19
@@ -91,22 +91,32 @@ public class ConvertOfficeController {
|
||||
|
||||
Path libreOfficeProfile = null;
|
||||
try {
|
||||
ProcessExecutorResult result;
|
||||
// Run Unoconvert command
|
||||
if (isUnoconvertAvailable()) {
|
||||
// Unoconvert: schreibe direkt in outputPath innerhalb des workDir
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--convert-to");
|
||||
command.add("pdf");
|
||||
command.add(inputPath.toString());
|
||||
command.add(outputPath.toString());
|
||||
ProcessExecutorResult result = null;
|
||||
IOException unoconvertException = null;
|
||||
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} // Run soffice command
|
||||
else {
|
||||
// Try unoconvert first if available
|
||||
if (isUnoconvertAvailable()) {
|
||||
try {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--convert-to");
|
||||
command.add("pdf");
|
||||
command.add(inputPath.toString());
|
||||
command.add(outputPath.toString());
|
||||
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (IOException e) {
|
||||
unoconvertException = e;
|
||||
log.warn(
|
||||
"Unoconvert command failed ({}). Falling back to soffice command.",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to soffice if unoconvert was unavailable or failed
|
||||
if (result == null) {
|
||||
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getSOfficePath());
|
||||
@@ -114,14 +124,21 @@ public class ConvertOfficeController {
|
||||
command.add("--headless");
|
||||
command.add("--nologo");
|
||||
command.add("--convert-to");
|
||||
command.add("pdf:writer_pdf_Export");
|
||||
command.add("pdf");
|
||||
command.add("--outdir");
|
||||
command.add(workDir.toString());
|
||||
command.add(inputPath.toString());
|
||||
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
try {
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (IOException e) {
|
||||
if (unoconvertException != null) {
|
||||
e.addSuppressed(unoconvertException);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the result
|
||||
|
||||
+1
-4
@@ -70,10 +70,7 @@ public class ConvertPDFToExcelController {
|
||||
tables.size() == 1
|
||||
? String.format(Locale.ROOT, "Page %d", pageNum)
|
||||
: String.format(
|
||||
Locale.ROOT,
|
||||
"Page %d Table %d",
|
||||
pageNum,
|
||||
tableIdx + 1);
|
||||
Locale.ROOT, "Page %d Table %d", pageNum, tableIdx + 1);
|
||||
|
||||
sheetName = getUniqueSheetName(workbook, sheetName);
|
||||
Sheet sheet = workbook.createSheet(sheetName);
|
||||
|
||||
+2
-2
@@ -9,13 +9,13 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.exception.CacheUnavailableException;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@ControllerAdvice(assignableTypes = ConvertPdfJsonController.class)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+3
-2
@@ -19,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.opencsv.CSVWriter;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
@@ -38,6 +36,9 @@ import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/form")
|
||||
@Tag(
|
||||
|
||||
+16
-17
@@ -1,6 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api.form;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -8,13 +7,14 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
final class FormPayloadParser {
|
||||
|
||||
private static final String KEY_FIELDS = "fields";
|
||||
@@ -32,8 +32,7 @@ final class FormPayloadParser {
|
||||
|
||||
private FormPayloadParser() {}
|
||||
|
||||
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json)
|
||||
throws IOException {
|
||||
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return Map.of();
|
||||
}
|
||||
@@ -41,7 +40,7 @@ final class FormPayloadParser {
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(json);
|
||||
} catch (IOException e) {
|
||||
} catch (JacksonException e) {
|
||||
// Fallback to legacy direct map parse (will throw again if invalid)
|
||||
return objectMapper.readValue(json, MAP_TYPE);
|
||||
}
|
||||
@@ -88,14 +87,14 @@ final class FormPayloadParser {
|
||||
}
|
||||
|
||||
static List<FormUtils.ModifyFormFieldDefinition> parseModificationDefinitions(
|
||||
ObjectMapper objectMapper, String json) throws IOException {
|
||||
ObjectMapper objectMapper, String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return objectMapper.readValue(json, MODIFY_FIELD_LIST_TYPE);
|
||||
}
|
||||
|
||||
static List<String> parseNameList(ObjectMapper objectMapper, String json) throws IOException {
|
||||
static List<String> parseNameList(ObjectMapper objectMapper, String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -119,7 +118,7 @@ final class FormPayloadParser {
|
||||
}
|
||||
}
|
||||
} else if (root.isTextual()) {
|
||||
final String single = trimToNull(root.asText());
|
||||
final String single = trimToNull(root.asText(""));
|
||||
if (single != null) {
|
||||
names.add(single);
|
||||
}
|
||||
@@ -131,7 +130,7 @@ final class FormPayloadParser {
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(json, STRING_LIST_TYPE);
|
||||
} catch (IOException e) {
|
||||
} catch (JacksonException e) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat",
|
||||
"Invalid {0} format: {1}",
|
||||
@@ -199,7 +198,7 @@ final class FormPayloadParser {
|
||||
return null;
|
||||
}
|
||||
if (node.isTextual()) {
|
||||
return trimToEmpty(node.asText());
|
||||
return trimToEmpty(node.asText(""));
|
||||
}
|
||||
if (node.isNumber()) {
|
||||
return node.numberValue().toString();
|
||||
@@ -208,7 +207,7 @@ final class FormPayloadParser {
|
||||
return Boolean.toString(node.booleanValue());
|
||||
}
|
||||
// Fallback for other scalar-like nodes
|
||||
return trimToEmpty(node.asText());
|
||||
return trimToEmpty(node.asText(""));
|
||||
}
|
||||
|
||||
private static void collectNames(JsonNode arrayNode, Set<String> sink) {
|
||||
@@ -229,7 +228,7 @@ final class FormPayloadParser {
|
||||
}
|
||||
|
||||
if (node.isTextual()) {
|
||||
return trimToNull(node.asText());
|
||||
return trimToNull(node.asText(""));
|
||||
}
|
||||
|
||||
if (node.isObject()) {
|
||||
@@ -264,8 +263,8 @@ final class FormPayloadParser {
|
||||
private static Map<String, Object> objectToLinkedMap(JsonNode objectNode) {
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
objectNode
|
||||
.fieldNames()
|
||||
.forEachRemaining(
|
||||
.propertyNames()
|
||||
.forEach(
|
||||
key -> {
|
||||
final JsonNode v = objectNode.get(key);
|
||||
if (v == null || v.isNull()) {
|
||||
|
||||
+2
-3
@@ -82,9 +82,8 @@ public class ExtractImagesController {
|
||||
Set<byte[]> processedImages = new HashSet<>();
|
||||
|
||||
if (useMultithreading) {
|
||||
// Executor service to handle multithreading
|
||||
ExecutorService executor =
|
||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
// Virtual thread executor — lightweight threads ideal for I/O-bound image extraction
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
Set<Future<Void>> futures = new HashSet<>();
|
||||
|
||||
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
|
||||
|
||||
+20
-5
@@ -246,10 +246,12 @@ public class OCRController {
|
||||
command.add("--clean-final");
|
||||
}
|
||||
if (ocrType != null && !ocrType.isEmpty()) {
|
||||
if ("skip-text".equals(ocrType)) {
|
||||
command.add("--skip-text");
|
||||
} else if ("force-ocr".equals(ocrType)) {
|
||||
if ("force-ocr".equals(ocrType)) {
|
||||
command.add("--force-ocr");
|
||||
} else {
|
||||
// Default for 'Normal' and 'skip-text': use --skip-text
|
||||
// ocrmypdf 17+ requires explicit flag when PDF already contains text
|
||||
command.add("--skip-text");
|
||||
}
|
||||
}
|
||||
command.add("--invalidate-digital-signatures");
|
||||
@@ -378,11 +380,12 @@ public class OCRController {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("tesseract");
|
||||
command.add(imagePath.toString());
|
||||
command.add(
|
||||
String outputBase =
|
||||
new File(
|
||||
tempOutputDir,
|
||||
String.format(Locale.ROOT, "page_%d", pageNum))
|
||||
.toString());
|
||||
.toString();
|
||||
command.add(outputBase);
|
||||
command.add("-l");
|
||||
command.add(String.join("+", selectedLanguages));
|
||||
command.add("pdf"); // Always output PDF
|
||||
@@ -400,6 +403,18 @@ public class OCRController {
|
||||
result.getRc());
|
||||
}
|
||||
|
||||
// Verify the OCR'd PDF was created
|
||||
if (!pageOutputPath.exists()) {
|
||||
log.warn(
|
||||
"Tesseract did not create expected output file: {}. Page may be blank or unreadable.",
|
||||
pageOutputPath.getAbsolutePath());
|
||||
// Save original page without OCR as fallback
|
||||
try (PDDocument pageDoc = new PDDocument()) {
|
||||
pageDoc.addPage(page);
|
||||
pageDoc.save(pageOutputPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Add OCR'd PDF to merger
|
||||
merger.addSource(pageOutputPath);
|
||||
} else {
|
||||
|
||||
+5
-5
@@ -14,10 +14,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -34,6 +30,10 @@ import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.DatabindException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@PipelineApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -54,7 +54,7 @@ public class PipelineController {
|
||||
+ "Users provide files and a JSON configuration defining the sequence of operations to perform. "
|
||||
+ "Input:PDF Output:PDF/ZIP Type:MIMO")
|
||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||
throws JsonMappingException, JsonProcessingException {
|
||||
throws DatabindException, JacksonException {
|
||||
MultipartFile[] files = request.getFileInput();
|
||||
String jsonString = request.getJson();
|
||||
if (files == null) {
|
||||
|
||||
+2
-2
@@ -28,8 +28,6 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
@@ -40,6 +38,8 @@ import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.util.FileMonitor;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PipelineDirectoryProcessor {
|
||||
|
||||
+6
-5
@@ -47,10 +47,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -66,6 +62,11 @@ import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
@SecurityApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -77,7 +78,7 @@ public class GetInfoOnPDF {
|
||||
private static final String PAGE_PREFIX = "Page ";
|
||||
private static final long MAX_FILE_SIZE = 100L * 1024 * 1024;
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private static final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final VeraPDFService veraPDFService;
|
||||
|
||||
+20
-1
@@ -1,5 +1,7 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
@@ -54,10 +56,27 @@ public class MetricsController {
|
||||
}
|
||||
Map<String, String> status = new HashMap<>();
|
||||
status.put("status", "UP");
|
||||
status.put("version", getClass().getPackage().getImplementationVersion());
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
if (version == null) {
|
||||
version = getVersionFromProperties();
|
||||
}
|
||||
status.put("version", version);
|
||||
return ResponseEntity.ok(status);
|
||||
}
|
||||
|
||||
private String getVersionFromProperties() {
|
||||
try (InputStream is = getClass().getResourceAsStream("/version.properties")) {
|
||||
if (is != null) {
|
||||
Properties props = new Properties();
|
||||
props.load(is);
|
||||
return props.getProperty("version");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load version.properties", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GetMapping("/load")
|
||||
@Operation(
|
||||
summary = "GET request count",
|
||||
|
||||
@@ -190,6 +190,31 @@ public class GlobalExceptionHandler {
|
||||
return problemDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given IOException indicates that the client disconnected before the
|
||||
* response could be written (broken pipe, connection reset, etc.). When this happens there is
|
||||
* no point in serialising a {@link ProblemDetail} body because the socket is already closed —
|
||||
* and attempting to do so may trigger a secondary {@code HttpMessageNotWritableException} if
|
||||
* the response Content-Type was already committed as a non-JSON type (e.g. image/png).
|
||||
*/
|
||||
private static boolean isClientDisconnectException(IOException ex) {
|
||||
// Walk the causal chain — Jetty/Tomcat may wrap the low-level SocketException
|
||||
Throwable current = ex;
|
||||
while (current != null) {
|
||||
String msg = current.getMessage();
|
||||
if (msg != null) {
|
||||
String lower = msg.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lower.contains("broken pipe")
|
||||
|| lower.contains("connection reset")
|
||||
|| lower.contains("an established connection was aborted")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a standardized ProblemDetail response for exceptions with error
|
||||
* codes.
|
||||
@@ -1107,6 +1132,15 @@ public class GlobalExceptionHandler {
|
||||
public ResponseEntity<ProblemDetail> handleIOException(
|
||||
IOException ex, HttpServletRequest request) {
|
||||
|
||||
// Broken pipe / connection reset means the client disconnected.
|
||||
// Attempting to write a ProblemDetail response will fail because the
|
||||
// response Content-Type may already be committed (e.g. image/png) and
|
||||
// the client is gone anyway. Log at WARN and return an empty body.
|
||||
if (isClientDisconnectException(ex)) {
|
||||
log.warn("Client disconnected at {}: {}", request.getRequestURI(), ex.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
// Check if this is a PDF-specific error and wrap it appropriately
|
||||
IOException processedException =
|
||||
ExceptionUtils.handlePdfException(ex, request.getRequestURI());
|
||||
|
||||
@@ -3,10 +3,10 @@ package stirling.software.SPDF.model;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
public class ApiEndpoint {
|
||||
private final String name;
|
||||
private Map<String, JsonNode> parameters;
|
||||
@@ -18,10 +18,10 @@ public class ApiEndpoint {
|
||||
postNode.path("parameters")
|
||||
.forEach(
|
||||
paramNode -> {
|
||||
String paramName = paramNode.path("name").asText();
|
||||
String paramName = paramNode.path("name").asText("");
|
||||
parameters.put(paramName, paramNode);
|
||||
});
|
||||
this.description = postNode.path("description").asText();
|
||||
this.description = postNode.path("description").asText("");
|
||||
}
|
||||
|
||||
public boolean areParametersValid(Map<String, Object> providedParams) {
|
||||
|
||||
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -28,6 +25,9 @@ import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ApiDocService {
|
||||
@@ -36,12 +36,15 @@ public class ApiDocService {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final UserServiceInterface userService;
|
||||
private final ObjectMapper objectMapper;
|
||||
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
||||
JsonNode apiDocsJsonRootNode;
|
||||
|
||||
public ApiDocService(
|
||||
ObjectMapper objectMapper,
|
||||
ServletContext servletContext,
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.servletContext = servletContext;
|
||||
this.userService = userService;
|
||||
}
|
||||
@@ -116,8 +119,7 @@ public class ApiDocService {
|
||||
ResponseEntity<String> response =
|
||||
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||
apiDocsJson = response.getBody();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
||||
apiDocsJsonRootNode = objectMapper.readTree(apiDocsJson);
|
||||
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||
paths.propertyStream()
|
||||
.forEach(
|
||||
|
||||
+2
-1
@@ -427,7 +427,8 @@ public class CertificateValidationService {
|
||||
try (InputStream certStream =
|
||||
getClass().getClassLoader().getResourceAsStream("certs/cacert.pem")) {
|
||||
if (certStream == null) {
|
||||
log.warn("Bundled Mozilla CA certificate file not found in resources");
|
||||
log.debug(
|
||||
"Bundled Mozilla CA certificate file not found in resources — using Java system trust store only");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,6 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -129,6 +127,8 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -6834,7 +6834,8 @@ public class PdfJsonConversionService {
|
||||
|
||||
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
||||
private void scheduleDocumentCleanup(String jobId) {
|
||||
new Thread(
|
||||
Thread.ofVirtual()
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
||||
@@ -6843,7 +6844,6 @@ public class PdfJsonConversionService {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
@@ -24,6 +22,8 @@ import stirling.software.SPDF.model.api.signature.SavedSignatureRequest;
|
||||
import stirling.software.SPDF.model.api.signature.SavedSignatureResponse;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SharedSignatureService {
|
||||
@@ -33,9 +33,9 @@ public class SharedSignatureService {
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SharedSignatureService() {
|
||||
public SharedSignatureService(ObjectMapper objectMapper) {
|
||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||
|
||||
+5
-5
@@ -18,8 +18,6 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -38,6 +36,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.TaskManager;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Service for lazy loading PDF pages. Caches PDF documents and extracts pages on-demand to reduce
|
||||
* memory usage for large PDFs.
|
||||
@@ -255,7 +255,8 @@ public class PdfLazyLoadingService {
|
||||
|
||||
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
||||
private void scheduleDocumentCleanup(String jobId) {
|
||||
new Thread(
|
||||
Thread.ofVirtual()
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
||||
@@ -264,8 +265,7 @@ public class PdfLazyLoadingService {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+23
-9
@@ -18,14 +18,14 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -202,20 +202,34 @@ public class Type3FontLibrary {
|
||||
if (payload == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] data = null;
|
||||
String base64;
|
||||
if (payload.base64 != null && !payload.base64.isBlank()) {
|
||||
// Validate the base64 string without wasteful full decode
|
||||
// Only decode a small prefix to verify encoding is valid
|
||||
try {
|
||||
data = Base64.getDecoder().decode(payload.base64);
|
||||
byte[] probe =
|
||||
Base64.getDecoder()
|
||||
.decode(
|
||||
payload.base64.substring(
|
||||
0, Math.min(4, payload.base64.length())));
|
||||
if (probe.length == 0 && payload.base64.length() <= 4) {
|
||||
return null;
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("[TYPE3] Invalid base64 payload in Type3 library: {}", ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
// Keep the original base64 string directly — avoids 3x memory pressure
|
||||
base64 = payload.base64;
|
||||
} else if (payload.resource != null && !payload.resource.isBlank()) {
|
||||
data = loadResourceBytes(payload.resource);
|
||||
}
|
||||
if (data == null || data.length == 0) {
|
||||
byte[] data = loadResourceBytes(payload.resource);
|
||||
if (data == null || data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
base64 = Base64.getEncoder().encodeToString(data);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
String base64 = Base64.getEncoder().encodeToString(data);
|
||||
return new Type3FontLibraryPayload(base64, normalizeFormat(payload.format));
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -26,14 +26,15 @@ import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
||||
import stirling.software.SPDF.service.pdfjson.type3.Type3GlyphExtractor;
|
||||
import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectWriter;
|
||||
import tools.jackson.databind.SerializationFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
/**
|
||||
* Small CLI helper that scans a PDF for Type3 fonts, computes their signatures, and optionally
|
||||
* emits JSON describing the glyph coverage. This allows Type3 library entries to be added without
|
||||
@@ -48,7 +49,7 @@ import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
|
||||
public final class Type3SignatureTool {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
|
||||
|
||||
private Type3SignatureTool() {}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class SvgToPdf {
|
||||
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
|
||||
throws IOException {
|
||||
GVTBuilder builder = new GVTBuilder();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
|
||||
Future<GraphicsNode> future = executor.submit(buildTask);
|
||||
|
||||
@@ -13,6 +13,18 @@ logging.level.stirling.software.common.service.JobExecutorService=INFO
|
||||
logging.level.stirling.software.common.service.TaskManager=INFO
|
||||
spring.jpa.open-in-view=false
|
||||
server.forward-headers-strategy=NATIVE
|
||||
|
||||
# Enable HTTP/2 for improved performance (multiplexed streams, header compression)
|
||||
server.http2.enabled=true
|
||||
|
||||
# Enable virtual threads (Java 21+, pinning fix in Java 25)
|
||||
spring.threads.virtual.enabled=true
|
||||
|
||||
# Response compression
|
||||
server.compression.enabled=true
|
||||
server.compression.min-response-size=1024
|
||||
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript
|
||||
|
||||
server.error.path=/error
|
||||
server.error.whitelabel.enabled=false
|
||||
server.error.include-stacktrace=always
|
||||
@@ -38,7 +50,7 @@ spring.devtools.livereload.enabled=true
|
||||
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||
server.tomcat.max-http-header-size=32768
|
||||
server.jetty.max-http-request-header-size=32768
|
||||
|
||||
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
|
||||
+3
-3
@@ -27,13 +27,13 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem;
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EditTableOfContentsControllerTest {
|
||||
|
||||
|
||||
+18
-16
@@ -40,14 +40,15 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
||||
import stirling.software.SPDF.service.VeraPDFService;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@DisplayName("GetInfoOnPDF Controller Tests")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GetInfoOnPDFTest {
|
||||
@@ -64,7 +65,7 @@ class GetInfoOnPDFTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper = JsonMapper.builder().build();
|
||||
}
|
||||
|
||||
/** Helper method to load a PDF file from test resources */
|
||||
@@ -216,8 +217,8 @@ class GetInfoOnPDFTest {
|
||||
Assertions.assertTrue(jsonNode.has("Permissions"));
|
||||
|
||||
JsonNode metadata = jsonNode.get("Metadata");
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,12 +316,12 @@ class GetInfoOnPDFTest {
|
||||
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
||||
JsonNode metadata = jsonNode.get("Metadata");
|
||||
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
|
||||
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText());
|
||||
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText());
|
||||
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText());
|
||||
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText());
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
|
||||
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText(""));
|
||||
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText(""));
|
||||
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText(""));
|
||||
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText(""));
|
||||
Assertions.assertTrue(metadata.has("CreationDate"));
|
||||
Assertions.assertTrue(metadata.has("ModificationDate"));
|
||||
|
||||
@@ -512,10 +513,10 @@ class GetInfoOnPDFTest {
|
||||
JsonNode page1 = perPageInfo.get("Page 1");
|
||||
Assertions.assertTrue(page1.has("Size"));
|
||||
Assertions.assertTrue(page1.get("Size").has("Standard Page"));
|
||||
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText());
|
||||
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText(""));
|
||||
|
||||
JsonNode page2 = perPageInfo.get("Page 2");
|
||||
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText());
|
||||
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText(""));
|
||||
|
||||
loadedDoc.close();
|
||||
}
|
||||
@@ -569,7 +570,8 @@ class GetInfoOnPDFTest {
|
||||
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
||||
|
||||
Assertions.assertTrue(jsonNode.has("error"));
|
||||
Assertions.assertTrue(jsonNode.get("error").asText().contains("PDF file is required"));
|
||||
Assertions.assertTrue(
|
||||
jsonNode.get("error").asText("").contains("PDF file is required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -645,7 +647,7 @@ class GetInfoOnPDFTest {
|
||||
|
||||
Assertions.assertTrue(jsonNode.has("error"));
|
||||
Assertions.assertTrue(
|
||||
jsonNode.get("error").asText().contains("exceeds maximum allowed size"));
|
||||
jsonNode.get("error").asText("").contains("exceeds maximum allowed size"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,15 @@ import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
class ApiEndpointTest {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final ObjectMapper mapper = JsonMapper.builder().build();
|
||||
|
||||
private JsonNode postNodeWithParams(String description, String... names) {
|
||||
ObjectNode post = mapper.createObjectNode();
|
||||
|
||||
@@ -12,14 +12,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
||||
import stirling.software.SPDF.model.ApiEndpoint;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ApiDocServiceTest {
|
||||
|
||||
@@ -27,11 +28,11 @@ class ApiDocServiceTest {
|
||||
@Mock UserServiceInterface userService;
|
||||
|
||||
ApiDocService apiDocService;
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectMapper mapper = JsonMapper.builder().build();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
apiDocService = new ApiDocService(servletContext, userService);
|
||||
apiDocService = new ApiDocService(mapper, servletContext, userService);
|
||||
}
|
||||
|
||||
private void setApiDocumentation(Map<String, ApiEndpoint> docs) throws Exception {
|
||||
|
||||
@@ -20,6 +20,8 @@ import org.mockito.MockedStatic;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
class SignatureServiceTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
@@ -53,7 +55,7 @@ class SignatureServiceTest {
|
||||
.thenReturn(tempDir.toString());
|
||||
|
||||
// Initialize the service with our temp directory
|
||||
signatureService = new SharedSignatureService();
|
||||
signatureService = new SharedSignatureService(JsonMapper.builder().build());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
api 'org.springframework.boot:spring-boot-starter-security'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||
api 'org.springframework.boot:spring-boot-starter-security-oauth2-client'
|
||||
api 'org.springframework.boot:spring-boot-starter-mail'
|
||||
api 'org.springframework.boot:spring-boot-starter-cache'
|
||||
api 'com.github.ben-manes.caffeine:caffeine'
|
||||
|
||||
+8
-14
@@ -2,21 +2,22 @@ package stirling.software.proprietary.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.TaskDecorator;
|
||||
import org.springframework.core.task.support.TaskExecutorAdapter;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
/**
|
||||
* MDC context-propagating task decorator Copies MDC context from the caller thread to the async
|
||||
* executor thread
|
||||
* MDC context-propagating task decorator. Copies MDC context from the caller thread to the
|
||||
* virtual thread executing the task.
|
||||
*/
|
||||
static class MDCContextTaskDecorator implements TaskDecorator {
|
||||
@Override
|
||||
@@ -42,16 +43,9 @@ public class AsyncConfig {
|
||||
|
||||
@Bean(name = "auditExecutor")
|
||||
public Executor auditExecutor() {
|
||||
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
||||
exec.setCorePoolSize(2);
|
||||
exec.setMaxPoolSize(8);
|
||||
exec.setQueueCapacity(1_000);
|
||||
exec.setThreadNamePrefix("audit-");
|
||||
|
||||
// Set the task decorator to propagate MDC context
|
||||
exec.setTaskDecorator(new MDCContextTaskDecorator());
|
||||
|
||||
exec.initialize();
|
||||
return exec;
|
||||
TaskExecutorAdapter adapter =
|
||||
new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
|
||||
adapter.setTaskDecorator(new MDCContextTaskDecorator());
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -1,15 +1,12 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/** Configuration to enable scheduling for the audit system. */
|
||||
/** Configuration for audit system transaction management. */
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableScheduling
|
||||
public class AuditJpaConfig {
|
||||
// This configuration enables scheduling for audit cleanup tasks
|
||||
// JPA repositories are now managed by DatabaseConfig to avoid conflicts
|
||||
// No additional beans or methods needed
|
||||
// Scheduling is enabled on SPDFApplication — no duplicate @EnableScheduling needed.
|
||||
// JPA repositories are now managed by DatabaseConfig to avoid conflicts.
|
||||
}
|
||||
|
||||
+6
-3
@@ -12,8 +12,6 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -21,6 +19,8 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.util.SecretMasker;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Component
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
@@ -68,7 +68,10 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
.build();
|
||||
repo.save(ent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // fail-open
|
||||
log.error(
|
||||
"Failed to persist audit event (fail-open); principal={}",
|
||||
ev.getPrincipal(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -28,9 +28,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -47,6 +44,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** REST endpoints for the audit dashboard. */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@@ -266,7 +266,7 @@ public class AuditDashboardController {
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
|
||||
+5
-5
@@ -20,9 +20,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -32,6 +29,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** REST API controller for audit data used by React frontend. */
|
||||
@Slf4j
|
||||
@ProprietaryUiDataApi
|
||||
@@ -332,7 +332,7 @@ public class AuditRestController {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> parsed = objectMapper.readValue(event.getData(), Map.class);
|
||||
details = parsed;
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.warn("Failed to parse audit event data as JSON: {}", event.getData());
|
||||
details.put("rawData", event.getData());
|
||||
}
|
||||
@@ -380,7 +380,7 @@ public class AuditRestController {
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
|
||||
+4
-4
@@ -15,9 +15,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
@@ -54,6 +51,9 @@ import stirling.software.proprietary.security.service.TeamService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@ProprietaryUiDataApi
|
||||
public class ProprietaryUIDataController {
|
||||
@@ -414,7 +414,7 @@ public class ProprietaryUIDataController {
|
||||
String settingsJson;
|
||||
try {
|
||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Error converting settings map", e);
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
+4
-4
@@ -8,9 +8,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -20,6 +17,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** REST API controller for usage analytics data used by React frontend. */
|
||||
@Slf4j
|
||||
@ProprietaryUiDataApi
|
||||
@@ -135,7 +135,7 @@ public class UsageRestController {
|
||||
return normalizeEndpoint(requestUri.toString());
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.debug("Failed to parse audit data JSON: {}", dataJson, e);
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -6,9 +6,9 @@ import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.boot.jdbc.DatabaseDriver;
|
||||
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
@@ -75,10 +75,18 @@ public class DatabaseConfig {
|
||||
}
|
||||
|
||||
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
|
||||
// Support AOT training: override URL via system property to avoid H2 file lock
|
||||
// conflicts when the AOT RECORD phase starts a second Spring context
|
||||
String overrideUrl = System.getProperty("stirling.datasource.url");
|
||||
String url =
|
||||
(overrideUrl != null && !overrideUrl.isBlank())
|
||||
? overrideUrl
|
||||
: DATASOURCE_DEFAULT_URL;
|
||||
|
||||
log.info("Using default H2 database");
|
||||
|
||||
dataSourceBuilder
|
||||
.url(DATASOURCE_DEFAULT_URL)
|
||||
.url(url)
|
||||
.driverClassName(DatabaseDriver.H2.getDriverClassName())
|
||||
.username(DEFAULT_USERNAME);
|
||||
|
||||
|
||||
+16
-13
@@ -14,15 +14,17 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
@@ -82,7 +84,7 @@ public class SecurityConfiguration {
|
||||
private final PersistentLoginRepository persistentLoginRepository;
|
||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
private final OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
||||
licenseSettingsService;
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
@@ -105,7 +107,7 @@ public class SecurityConfiguration {
|
||||
@Autowired(required = false)
|
||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||
@Autowired(required = false)
|
||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||
OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
|
||||
stirling.software.proprietary.service.UserLicenseSettingsService
|
||||
licenseSettingsService) {
|
||||
@@ -151,7 +153,8 @@ public class SecurityConfiguration {
|
||||
// Default to allowing all origins when nothing is configured
|
||||
cfg.setAllowedOriginPatterns(List.of("*"));
|
||||
log.info(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
||||
"No CORS allowed origins configured in settings.yml"
|
||||
+ " (system.corsAllowedOrigins); allowing all origins.");
|
||||
}
|
||||
|
||||
// Explicitly configure supported HTTP methods (include OPTIONS for preflight)
|
||||
@@ -225,7 +228,7 @@ public class SecurityConfiguration {
|
||||
http.cors(cors -> cors.configurationSource(corsSource));
|
||||
} else {
|
||||
// Explicitly disable CORS when no origins are configured
|
||||
http.cors(cors -> cors.disable());
|
||||
http.cors(CorsConfigurer::disable);
|
||||
}
|
||||
|
||||
http.csrf(CsrfConfigurer::disable);
|
||||
@@ -233,24 +236,24 @@ public class SecurityConfiguration {
|
||||
// Configure X-Frame-Options based on settings.yml configuration
|
||||
// When login is disabled, automatically disable X-Frame-Options to allow embedding
|
||||
if (!loginEnabledValue) {
|
||||
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
|
||||
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::disable));
|
||||
} else {
|
||||
String xFrameOption = securityProperties.getXFrameOptions();
|
||||
if (xFrameOption != null) {
|
||||
http.headers(
|
||||
headers -> {
|
||||
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
|
||||
headers.frameOptions(frameOptions -> frameOptions.disable());
|
||||
headers.frameOptions(FrameOptionsConfig::disable);
|
||||
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
|
||||
headers.frameOptions(frameOptions -> frameOptions.sameOrigin());
|
||||
headers.frameOptions(FrameOptionsConfig::sameOrigin);
|
||||
} else {
|
||||
// Default to DENY
|
||||
headers.frameOptions(frameOptions -> frameOptions.deny());
|
||||
headers.frameOptions(FrameOptionsConfig::deny);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If not configured, use default DENY
|
||||
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.deny()));
|
||||
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::deny));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,8 +384,8 @@ public class SecurityConfiguration {
|
||||
}
|
||||
// Handle SAML
|
||||
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
OpenSaml5AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml5AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
|
||||
+90
-87
@@ -12,11 +12,6 @@ import org.bouncycastle.crypto.signers.Ed25519Signer;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
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.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -24,6 +19,9 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -47,7 +45,7 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
private static final String JWT_PREFIX = "key/";
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
// Shared HTTP client for connection pooling
|
||||
@@ -135,11 +133,11 @@ public class KeygenLicenseVerifier {
|
||||
String algorithm = "";
|
||||
|
||||
try {
|
||||
JSONObject attrs = new JSONObject(payload);
|
||||
encryptedData = (String) attrs.get("enc");
|
||||
encodedSignature = (String) attrs.get("sig");
|
||||
algorithm = (String) attrs.get("alg");
|
||||
} catch (JSONException e) {
|
||||
JsonNode attrs = objectMapper.readTree(payload);
|
||||
encryptedData = attrs.path("enc").asText("");
|
||||
encodedSignature = attrs.path("sig").asText("");
|
||||
algorithm = attrs.path("alg").asText("");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse license file: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
@@ -215,11 +213,17 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
private boolean processCertificateData(String certData, LicenseContext context) {
|
||||
try {
|
||||
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);
|
||||
JsonNode licenseData = objectMapper.readTree(certData);
|
||||
JsonNode metaObj = licenseData.path("meta");
|
||||
if (!metaObj.isMissingNode() && metaObj.isObject()) {
|
||||
String issuedStr =
|
||||
metaObj.path("issued").isNull()
|
||||
? null
|
||||
: metaObj.path("issued").asText(null);
|
||||
String expiryStr =
|
||||
metaObj.path("expiry").isNull()
|
||||
? null
|
||||
: metaObj.path("expiry").asText(null);
|
||||
|
||||
if (issuedStr != null && expiryStr != null) {
|
||||
java.time.Instant issued = java.time.Instant.parse(issuedStr);
|
||||
@@ -244,31 +248,32 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Get the main license data
|
||||
JSONObject dataObj = licenseData.optJSONObject("data");
|
||||
if (dataObj == null) {
|
||||
JsonNode dataObj = licenseData.path("data");
|
||||
if (dataObj.isMissingNode() || !dataObj.isObject()) {
|
||||
log.error("No data object found in certificate");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract license or machine information
|
||||
JSONObject attributesObj = dataObj.optJSONObject("attributes");
|
||||
if (attributesObj != null) {
|
||||
JsonNode attributesObj = dataObj.path("attributes");
|
||||
if (!attributesObj.isMissingNode() && attributesObj.isObject()) {
|
||||
log.info("Found attributes in certificate data");
|
||||
|
||||
// Check for floating license
|
||||
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
|
||||
context.maxMachines = attributesObj.optInt("maxMachines", 1);
|
||||
context.isFloatingLicense = attributesObj.path("floating").asBoolean(false);
|
||||
context.maxMachines = attributesObj.path("maxMachines").asInt(1);
|
||||
|
||||
// Extract metadata
|
||||
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
||||
if (metadataObj != null) {
|
||||
JsonNode metadataObj = attributesObj.path("metadata");
|
||||
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
|
||||
// Check if this is an old license (no planType) with isEnterprise flag
|
||||
context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
|
||||
context.isEnterpriseLicense = metadataObj.path("isEnterprise").asBoolean(false);
|
||||
|
||||
// Extract user count - default based on license type
|
||||
// Old licenses: Only had isEnterprise flag
|
||||
// New licenses: Have planType field with "server" or "enterprise"
|
||||
int users = metadataObj.optInt("users", context.isEnterpriseLicense ? 1 : 0);
|
||||
int users =
|
||||
metadataObj.path("users").asInt(context.isEnterpriseLicense ? 1 : 0);
|
||||
|
||||
// SERVER license (isEnterprise=false, users=0) = unlimited
|
||||
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
|
||||
@@ -282,7 +287,7 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Check license status if available
|
||||
String status = attributesObj.optString("status", null);
|
||||
String status = attributesObj.path("status").asText(null);
|
||||
if (status != null
|
||||
&& !"ACTIVE".equals(status)
|
||||
&& !"EXPIRING".equals(status)) { // Accept "EXPIRING" status as valid
|
||||
@@ -372,11 +377,11 @@ public class KeygenLicenseVerifier {
|
||||
try {
|
||||
log.info("Processing license payload: {}", payload);
|
||||
|
||||
JSONObject licenseData = new JSONObject(payload);
|
||||
JsonNode licenseData = objectMapper.readTree(payload);
|
||||
|
||||
JSONObject licenseObj = licenseData.optJSONObject("license");
|
||||
if (licenseObj == null) {
|
||||
String id = licenseData.optString("id", null);
|
||||
JsonNode licenseObj = licenseData.path("license");
|
||||
if (licenseObj.isMissingNode() || !licenseObj.isObject()) {
|
||||
String id = licenseData.path("id").asText(null);
|
||||
if (id != null) {
|
||||
log.info("Found license ID: {}", id);
|
||||
licenseObj = licenseData; // Use the root object as the license object
|
||||
@@ -386,18 +391,18 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
}
|
||||
|
||||
String licenseId = licenseObj.optString("id", "unknown");
|
||||
String licenseId = licenseObj.path("id").asText("unknown");
|
||||
log.info("Processing license with ID: {}", licenseId);
|
||||
|
||||
// Check for floating license in license object
|
||||
context.isFloatingLicense = licenseObj.optBoolean("floating", false);
|
||||
context.maxMachines = licenseObj.optInt("maxMachines", 1);
|
||||
context.isFloatingLicense = licenseObj.path("floating").asBoolean(false);
|
||||
context.maxMachines = licenseObj.path("maxMachines").asInt(1);
|
||||
if (context.isFloatingLicense) {
|
||||
log.info("Detected floating license with max machines: {}", context.maxMachines);
|
||||
}
|
||||
|
||||
// Check expiry date
|
||||
String expiryStr = licenseObj.optString("expiry", null);
|
||||
String expiryStr = licenseObj.path("expiry").asText(null);
|
||||
if (expiryStr != null && !"null".equals(expiryStr)) {
|
||||
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
@@ -413,9 +418,9 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Extract account, product, policy info
|
||||
JSONObject accountObj = licenseData.optJSONObject("account");
|
||||
if (accountObj != null) {
|
||||
String accountId = accountObj.optString("id", "unknown");
|
||||
JsonNode accountObj = licenseData.path("account");
|
||||
if (!accountObj.isMissingNode() && accountObj.isObject()) {
|
||||
String accountId = accountObj.path("id").asText("unknown");
|
||||
log.info("License belongs to account: {}", accountId);
|
||||
|
||||
// Verify this matches your expected account ID
|
||||
@@ -426,14 +431,14 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Extract policy information if available
|
||||
JSONObject policyObj = licenseData.optJSONObject("policy");
|
||||
if (policyObj != null) {
|
||||
String policyId = policyObj.optString("id", "unknown");
|
||||
JsonNode policyObj = licenseData.path("policy");
|
||||
if (!policyObj.isMissingNode() && policyObj.isObject()) {
|
||||
String policyId = policyObj.path("id").asText("unknown");
|
||||
log.info("License uses policy: {}", policyId);
|
||||
|
||||
// Check for floating license in policy
|
||||
boolean policyFloating = policyObj.optBoolean("floating", false);
|
||||
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
|
||||
boolean policyFloating = policyObj.path("floating").asBoolean(false);
|
||||
int policyMaxMachines = policyObj.path("maxMachines").asInt(1);
|
||||
|
||||
// Policy settings take precedence
|
||||
if (policyFloating) {
|
||||
@@ -445,16 +450,21 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Extract max users and isEnterprise from policy or metadata
|
||||
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
||||
int users = policyObj.optInt("users", -1);
|
||||
context.isEnterpriseLicense = policyObj.path("isEnterprise").asBoolean(false);
|
||||
int users = policyObj.path("users").asInt(-1);
|
||||
|
||||
if (users == -1) {
|
||||
// Try to get users from metadata if not at policy level
|
||||
Object metadataObj = policyObj.opt("metadata");
|
||||
if (metadataObj instanceof JSONObject metadata) {
|
||||
JsonNode metadataObj = policyObj.path("metadata");
|
||||
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
|
||||
context.isEnterpriseLicense =
|
||||
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
|
||||
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
|
||||
metadataObj
|
||||
.path("isEnterprise")
|
||||
.asBoolean(context.isEnterpriseLicense);
|
||||
users =
|
||||
metadataObj
|
||||
.path("users")
|
||||
.asInt(context.isEnterpriseLicense ? 1 : 0);
|
||||
} else {
|
||||
// Default based on license type
|
||||
users = context.isEnterpriseLicense ? 1 : 0;
|
||||
@@ -493,9 +503,9 @@ public class KeygenLicenseVerifier {
|
||||
validateLicense(licenseKey, machineFingerprint, context);
|
||||
if (validationResponse != null) {
|
||||
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
|
||||
String licenseId = validationResponse.path("data").path("id").asText();
|
||||
String licenseId = validationResponse.path("data").path("id").asText("");
|
||||
if (!isValid) {
|
||||
String code = validationResponse.path("meta").path("code").asText();
|
||||
String code = validationResponse.path("meta").path("code").asText("");
|
||||
log.info(code);
|
||||
if ("NO_MACHINE".equals(code)
|
||||
|| "NO_MACHINES".equals(code)
|
||||
@@ -579,8 +589,8 @@ public class KeygenLicenseVerifier {
|
||||
JsonNode metaNode = jsonResponse.path("meta");
|
||||
boolean isValid = metaNode.path("valid").asBoolean();
|
||||
|
||||
String detail = metaNode.path("detail").asText();
|
||||
String code = metaNode.path("code").asText();
|
||||
String detail = metaNode.path("detail").asText("");
|
||||
String code = metaNode.path("code").asText("");
|
||||
|
||||
log.info("License validity: {}", isValid);
|
||||
log.info("Validation detail: {}", detail);
|
||||
@@ -604,7 +614,7 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
if (includedNode.isArray()) {
|
||||
for (JsonNode node : includedNode) {
|
||||
if ("policies".equals(node.path("type").asText())) {
|
||||
if ("policies".equals(node.path("type").asText(""))) {
|
||||
policyNode = node;
|
||||
break;
|
||||
}
|
||||
@@ -690,9 +700,9 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
for (JsonNode machine : machines) {
|
||||
if (machineFingerprint.equals(
|
||||
machine.path("attributes").path("fingerprint").asText())) {
|
||||
machine.path("attributes").path("fingerprint").asText(""))) {
|
||||
isCurrentMachineActivated = true;
|
||||
currentMachineId = machine.path("id").asText();
|
||||
currentMachineId = machine.path("id").asText("");
|
||||
log.info(
|
||||
"Current machine is already activated with ID: {}",
|
||||
currentMachineId);
|
||||
@@ -726,7 +736,7 @@ public class KeygenLicenseVerifier {
|
||||
java.time.Instant.parse(createdStr);
|
||||
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
||||
oldestTime = createdTime;
|
||||
oldestMachineId = machine.path("id").asText();
|
||||
oldestMachineId = machine.path("id").asText("");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
@@ -740,7 +750,7 @@ public class KeygenLicenseVerifier {
|
||||
if (oldestMachineId == null) {
|
||||
log.warn(
|
||||
"Could not determine oldest machine by timestamp, using first machine in list");
|
||||
oldestMachineId = machines.path(0).path("id").asText();
|
||||
oldestMachineId = machines.path(0).path("id").asText("");
|
||||
}
|
||||
|
||||
log.info("Deregistering machine with ID: {}", oldestMachineId);
|
||||
@@ -770,35 +780,28 @@ public class KeygenLicenseVerifier {
|
||||
hostname = "Unknown";
|
||||
}
|
||||
|
||||
JSONObject body =
|
||||
new JSONObject()
|
||||
.put(
|
||||
"data",
|
||||
new JSONObject()
|
||||
.put("type", "machines")
|
||||
.put(
|
||||
"attributes",
|
||||
new JSONObject()
|
||||
.put("fingerprint", machineFingerprint)
|
||||
.put(
|
||||
"platform",
|
||||
System.getProperty("os.name"))
|
||||
.put("name", hostname))
|
||||
.put(
|
||||
"relationships",
|
||||
new JSONObject()
|
||||
.put(
|
||||
"license",
|
||||
new JSONObject()
|
||||
.put(
|
||||
"data",
|
||||
new JSONObject()
|
||||
.put(
|
||||
"type",
|
||||
"licenses")
|
||||
.put(
|
||||
"id",
|
||||
licenseId)))));
|
||||
tools.jackson.databind.node.ObjectNode attributes = objectMapper.createObjectNode();
|
||||
attributes.put("fingerprint", machineFingerprint);
|
||||
attributes.put("platform", System.getProperty("os.name"));
|
||||
attributes.put("name", hostname);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode licenseRef = objectMapper.createObjectNode();
|
||||
licenseRef.put("type", "licenses");
|
||||
licenseRef.put("id", licenseId);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode licenseRelation = objectMapper.createObjectNode();
|
||||
licenseRelation.set("data", licenseRef);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode relationships = objectMapper.createObjectNode();
|
||||
relationships.set("license", licenseRelation);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode data = objectMapper.createObjectNode();
|
||||
data.put("type", "machines");
|
||||
data.set("attributes", attributes);
|
||||
data.set("relationships", relationships);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode body = objectMapper.createObjectNode();
|
||||
body.set("data", data);
|
||||
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder()
|
||||
|
||||
+6
-6
@@ -27,9 +27,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
@@ -49,6 +46,9 @@ import stirling.software.proprietary.security.model.api.admin.SettingValueRespon
|
||||
import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest;
|
||||
import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@AdminApi
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@@ -543,7 +543,8 @@ public class AdminSettingsController {
|
||||
pendingChanges.clear();
|
||||
|
||||
// Give the HTTP response time to complete, then exit
|
||||
new Thread(
|
||||
Thread.ofVirtual()
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
@@ -554,8 +555,7 @@ public class AdminSettingsController {
|
||||
log.error("Restart interrupted: {}", e.getMessage(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
});
|
||||
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
|
||||
+6
-5
@@ -20,9 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
@@ -31,6 +28,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ui-data")
|
||||
@@ -39,6 +39,7 @@ public class UIDataTessdataController {
|
||||
|
||||
private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]");
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
private static volatile List<String> cachedRemoteTessdata = null;
|
||||
private static volatile long cachedRemoteTessdataExpiry = 0L;
|
||||
private static final long REMOTE_TESSDATA_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||
@@ -223,9 +224,9 @@ public class UIDataTessdataController {
|
||||
}
|
||||
|
||||
try (InputStream is = connection.getInputStream()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
List<Map<String, Object>> items =
|
||||
mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
|
||||
objectMapper.readValue(
|
||||
is, new TypeReference<List<Map<String, Object>>>() {});
|
||||
List<String> languages =
|
||||
items.stream()
|
||||
.map(item -> (String) item.get("name"))
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private Object credentials;
|
||||
|
||||
public ApiKeyAuthenticationToken(String apiKey) {
|
||||
super(null);
|
||||
super((Collection<? extends GrantedAuthority>) null);
|
||||
this.principal = null;
|
||||
this.credentials = apiKey;
|
||||
setAuthenticated(false);
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import org.opensaml.saml.saml2.core.AuthnStatement;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ import org.springframework.security.saml2.provider.service.registration.InMemory
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -151,10 +151,10 @@ public class Saml2Configuration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||
public OpenSaml5AuthenticationRequestResolver authenticationRequestResolver(
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
OpenSaml5AuthenticationRequestResolver resolver =
|
||||
new OpenSaml5AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
|
||||
resolver.setRelayStateResolver(
|
||||
request -> {
|
||||
|
||||
+8
-7
@@ -20,9 +20,6 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
@@ -40,21 +37,25 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
|
||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JwtService implements JwtServiceInterface {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final KeyPersistenceServiceInterface keyPersistenceService;
|
||||
private final boolean v2Enabled;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
|
||||
@Autowired
|
||||
public JwtService(
|
||||
ObjectMapper objectMapper,
|
||||
@Qualifier("v2Enabled") boolean v2Enabled,
|
||||
KeyPersistenceServiceInterface keyPersistenceService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.v2Enabled = v2Enabled;
|
||||
this.keyPersistenceService = keyPersistenceService;
|
||||
this.securityProperties = applicationProperties.getSecurity();
|
||||
@@ -374,14 +375,14 @@ public class JwtService implements JwtServiceInterface {
|
||||
|
||||
byte[] headerBytes = Base64.getUrlDecoder().decode(tokenParts[0]);
|
||||
Map<String, Object> header =
|
||||
OBJECT_MAPPER.readValue(
|
||||
objectMapper.readValue(
|
||||
headerBytes, new TypeReference<Map<String, Object>>() {});
|
||||
Object keyId = header.get("kid");
|
||||
return keyId instanceof String ? (String) keyId : null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("Failed to decode Base64 JWT header: {}", e.getMessage());
|
||||
return null;
|
||||
} catch (java.io.IOException e) {
|
||||
} catch (tools.jackson.core.JacksonException e) {
|
||||
log.debug("Failed to parse JWT header as JSON: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
+5
-4
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
@@ -24,6 +22,8 @@ import stirling.software.common.service.PersonalSignatureServiceInterface;
|
||||
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
|
||||
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Service for managing user signatures with authentication and storage limits. This proprietary
|
||||
* version enforces per-user quotas and requires authentication. Provides access to personal
|
||||
@@ -36,15 +36,16 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
||||
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
|
||||
private final String SIGNATURE_BASE_PATH;
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// Storage limits per user
|
||||
private static final int MAX_SIGNATURES_PER_USER = 20;
|
||||
private static final long MAX_SIGNATURE_SIZE_BYTES = 2_000_000; // 2MB per signature
|
||||
private static final long MAX_TOTAL_USER_STORAGE_BYTES = 20_000_000; // 20MB total per user
|
||||
|
||||
public SignatureService() {
|
||||
public SignatureService(ObjectMapper objectMapper) {
|
||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-3
@@ -16,8 +16,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
@@ -35,6 +33,9 @@ import stirling.software.proprietary.security.service.MfaService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProprietaryUIDataControllerTest {
|
||||
|
||||
@@ -63,7 +64,7 @@ class ProprietaryUIDataControllerTest {
|
||||
applicationProperties.getSecurity().getSaml2().setEnabled(false);
|
||||
|
||||
auditConfig = new AuditConfigurationProperties(applicationProperties);
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper = JsonMapper.builder().build();
|
||||
|
||||
controller =
|
||||
new ProprietaryUIDataController(
|
||||
|
||||
+4
-3
@@ -26,8 +26,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
@@ -42,10 +40,13 @@ import stirling.software.proprietary.security.service.RefreshRateLimitService;
|
||||
import stirling.software.proprietary.security.service.TotpService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthControllerLoginTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private ApplicationProperties.Security securityProperties;
|
||||
|
||||
+4
-3
@@ -26,8 +26,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||
@@ -37,12 +35,15 @@ import stirling.software.proprietary.security.service.MfaService;
|
||||
import stirling.software.proprietary.security.service.TotpService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthControllerMfaTest {
|
||||
|
||||
private static final String USERNAME = "[email protected]";
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private Authentication authentication;
|
||||
|
||||
+15
-13
@@ -19,6 +19,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
class UIDataTessdataControllerTest {
|
||||
|
||||
@Test
|
||||
@@ -27,7 +29,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn("ignored/path");
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -49,7 +51,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -77,7 +79,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -100,7 +102,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng", "fra");
|
||||
@@ -139,7 +141,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -164,7 +166,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected boolean isWritableDirectory(Path dir) {
|
||||
return false;
|
||||
@@ -186,7 +188,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -217,7 +219,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -258,7 +260,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng", "fra");
|
||||
@@ -282,7 +284,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -307,7 +309,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -330,7 +332,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(missingDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -352,7 +354,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected boolean isWritableDirectory(Path dir) {
|
||||
return false;
|
||||
|
||||
+4
-3
@@ -21,8 +21,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
@@ -35,10 +33,13 @@ import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserControllerTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
@Mock private UserService userService;
|
||||
@Mock private SessionPersistentRegistry sessionRegistry;
|
||||
|
||||
+5
-1
@@ -36,6 +36,9 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtServiceTest {
|
||||
|
||||
@@ -66,7 +69,8 @@ class JwtServiceTest {
|
||||
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
|
||||
|
||||
ApplicationProperties applicationProperties = new ApplicationProperties();
|
||||
jwtService = new JwtService(true, keystoreService, applicationProperties);
|
||||
ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
jwtService = new JwtService(objectMapper, true, keystoreService, applicationProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user