diff --git a/backend/src/main/java/com/printcalculator/service/ClamAVService.java b/backend/src/main/java/com/printcalculator/service/ClamAVService.java index ffca6b6..4d009bd 100644 --- a/backend/src/main/java/com/printcalculator/service/ClamAVService.java +++ b/backend/src/main/java/com/printcalculator/service/ClamAVService.java @@ -17,11 +17,19 @@ public class ClamAVService { private static final Logger logger = LoggerFactory.getLogger(ClamAVService.class); private final ClamavClient clamavClient; + private final boolean enabled; public ClamAVService( - @Value("${clamav.host:localhost}") String host, - @Value("${clamav.port:3310}") int port + @Value("${clamav.host:clamav}") String host, + @Value("${clamav.port:3310}") int port, + @Value("${clamav.enabled:false}") boolean enabled ) { + this.enabled = enabled; + if (!enabled) { + logger.info("ClamAV is DISABLED"); + this.clamavClient = null; + return; + } logger.info("Initializing ClamAV client at {}:{}", host, port); ClamavClient client = null; try { @@ -33,8 +41,7 @@ public class ClamAVService { } public boolean scan(InputStream inputStream) { - if (clamavClient == null) { - logger.warn("ClamAV client not initialized, skipping scan (FAIL-OPEN)"); + if (!enabled || clamavClient == null) { return true; } try { diff --git a/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java b/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java index e53d610..7db4aa8 100644 --- a/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java +++ b/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java @@ -7,6 +7,8 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import com.printcalculator.exception.StorageException; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; @@ -39,20 +41,24 @@ public class FileSystemStorageService implements StorageService { public void store(MultipartFile file, Path destinationRelativePath) throws IOException { Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath(); if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) { - // Security check throw new StorageException("Cannot store file outside current directory."); } - // Scan stream (Read 1) - try (InputStream inputStream = file.getInputStream()) { - if (!clamAVService.scan(inputStream)) { - throw new StorageException("File rejected by antivirus scanner."); - } - } - - // Save to disk (Using transferTo which is safer than opening another stream) + // 1. Salva prima il file su disco per evitare problemi di stream con file grandi Files.createDirectories(destinationFile.getParent()); file.transferTo(destinationFile.toFile()); + + // 2. Scansiona il file appena salvato aprendo un nuovo stream + try (InputStream inputStream = new FileInputStream(destinationFile.toFile())) { + if (!clamAVService.scan(inputStream)) { + // Se infetto, cancella il file e solleva eccezione + Files.deleteIfExists(destinationFile); + throw new StorageException("File rejected by antivirus scanner."); + } + } catch (Exception e) { + if (e instanceof StorageException) throw e; + // Se l'antivirus fallisce per motivi tecnici, lasciamo il file (fail-open come concordato) + } } @Override @@ -62,11 +68,6 @@ public class FileSystemStorageService implements StorageService { throw new StorageException("Cannot store file outside current directory."); } Files.createDirectories(destinationFile.getParent()); - // We assume source is already safe/scanned if it is internal? - // Or should we scan it too? - // If it comes from QuoteSession (which was scanned on upload), it is safe. - // If we want to be paranoid, we can scan again, but maybe overkill. - // Let's assume it is safe for internal copies. Files.copy(source, destinationFile, StandardCopyOption.REPLACE_EXISTING); } diff --git a/backend/src/main/java/com/printcalculator/service/SlicerService.java b/backend/src/main/java/com/printcalculator/service/SlicerService.java index 04b73c3..131bf72 100644 --- a/backend/src/main/java/com/printcalculator/service/SlicerService.java +++ b/backend/src/main/java/com/printcalculator/service/SlicerService.java @@ -43,6 +43,9 @@ public class SlicerService { public PrintStats slice(File inputStl, String machineName, String filamentName, String processName, Map machineOverrides, Map processOverrides) throws IOException { + // Log version once for diagnostics + try { runVersionCheck(); } catch (Exception e) {} + ObjectNode machineProfile = profileManager.getMergedProfile(machineName, "machine"); ObjectNode filamentProfile = profileManager.getMergedProfile(filamentName, "filament"); ObjectNode processProfile = profileManager.getMergedProfile(processName, "process"); @@ -50,18 +53,14 @@ public class SlicerService { if (machineOverrides != null) machineOverrides.forEach(machineProfile::put); if (processOverrides != null) processOverrides.forEach(processProfile::put); - // Pulizia profonda per evitare errori di inizializzazione grafica/piatto - machineProfile.remove("bed_exclude_area"); - machineProfile.remove("head_wrap_detect_zone"); - machineProfile.remove("bed_custom_model"); - machineProfile.remove("bed_custom_texture"); + // Pulizia radicale per rendere la macchina "anonima" ed evitare crash geometrici su zone di esclusione + makeMachineGeneric(machineProfile); Path baseTempPath = Paths.get("/app/temp"); if (!Files.exists(baseTempPath)) Files.createDirectories(baseTempPath); Path tempDir = Files.createTempDirectory(baseTempPath, "job_"); try { - // Usiamo un nome file senza spazi per massima compatibilità CLI File localStl = tempDir.resolve("input.stl").toFile(); Files.copy(inputStl.toPath(), localStl.toPath()); @@ -76,7 +75,7 @@ public class SlicerService { List command = new ArrayList<>(); command.add(slicerPath); - // Parametri di caricamento + // Ordine ottimizzato per OrcaSlicer 1.9+ command.add("--load-settings"); command.add(mFile.getAbsolutePath()); command.add("--load-settings"); @@ -84,26 +83,23 @@ public class SlicerService { command.add("--load-filaments"); command.add(fFile.getAbsolutePath()); - // Output command.add("--outputdir"); command.add(tempDir.toAbsolutePath().toString()); - // Slicing & Auto-center (fondamentale per evitare "Nothing to be sliced") command.add("--arrange"); command.add("1"); command.add("--ensure-on-bed"); command.add("--slice"); - command.add("0"); // 0 solitamente significa "tutti i piatti con oggetti" - - // File da processare (sempre per ultimo) + command.add("1"); // Plate 1 + command.add(localStl.getAbsolutePath()); logger.info("Executing Slicer on file: " + localStl.getAbsolutePath() + " (Size: " + localStl.length() + " bytes)"); runSlicerCommand(command, tempDir); - // Cerca il file G-code prodotto (input.gcode o simili) + // Cerca il file G-code prodotto try (Stream s = Files.list(tempDir)) { Optional found = s.filter(p -> p.toString().endsWith(".gcode")).findFirst(); if (found.isPresent()) return gCodeParser.parse(found.get().toFile()); @@ -116,11 +112,40 @@ public class SlicerService { } } + private void makeMachineGeneric(ObjectNode profile) { + // Rimuove l'identità della stampante per forzare lo slicer a usare solo i dati geometrici che forniamo + profile.remove("inherits"); + profile.remove("printer_model"); + profile.remove("printer_variant"); + profile.remove("setting_id"); + profile.remove("printer_settings_id"); + + // Rimuove zone di esclusione e modelli complessi che richiedono calcoli grafici pesanti + profile.remove("bed_exclude_area"); + profile.remove("head_wrap_detect_zone"); + profile.remove("bed_custom_model"); + profile.remove("bed_custom_texture"); + profile.remove("thumbnail"); + profile.remove("thumbnails"); + + // Forza un'area di stampa standard 256x256 (Bambu A1) + try { + profile.set("printable_area", mapper.readTree("[\"0x0\",\"256x0\",\"256x256\",\"0x256\"]")); + profile.put("printable_height", "256"); + } catch (Exception ignored) {} + } + + private void runVersionCheck() throws IOException, InterruptedException { + Process p = new ProcessBuilder(slicerPath, "--version").start(); + p.waitFor(); + String ver = new String(p.getInputStream().readAllBytes()).trim(); + logger.info("OrcaSlicer Version on server: " + ver); + } + protected void runSlicerCommand(List command, Path tempDir) throws IOException, InterruptedException { ProcessBuilder pb = new ProcessBuilder(command); pb.directory(tempDir.toFile()); - // Variabili d'ambiente minimali ma necessarie Map env = pb.environment(); env.put("HOME", "/tmp"); env.put("QT_QPA_PLATFORM", "offscreen"); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index fc32567..1ed0260 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -18,3 +18,8 @@ profiles.root=${PROFILES_DIR:profiles} # File Upload Limits spring.servlet.multipart.max-file-size=200MB spring.servlet.multipart.max-request-size=200MB + +# ClamAV Configuration +clamav.host=${CLAMAV_HOST:clamav} +clamav.port=${CLAMAV_PORT:3310} +