feat(back-end): add ClamAV service remember to add env and compose.deploy on server
This commit is contained in:
@@ -30,6 +30,7 @@ dependencies {
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
implementation 'xyz.capybara:clamav-client:2.1.2'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
}
|
||||
|
||||
|
||||
@@ -25,14 +25,17 @@ public class CustomQuoteRequestController {
|
||||
|
||||
private final CustomQuoteRequestRepository requestRepo;
|
||||
private final CustomQuoteRequestAttachmentRepository attachmentRepo;
|
||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
||||
|
||||
// TODO: Inject Storage Service
|
||||
private static final String STORAGE_ROOT = "storage_requests";
|
||||
|
||||
public CustomQuoteRequestController(CustomQuoteRequestRepository requestRepo,
|
||||
CustomQuoteRequestAttachmentRepository attachmentRepo) {
|
||||
CustomQuoteRequestAttachmentRepository attachmentRepo,
|
||||
com.printcalculator.service.ClamAVService clamAVService) {
|
||||
this.requestRepo = requestRepo;
|
||||
this.attachmentRepo = attachmentRepo;
|
||||
this.clamAVService = clamAVService;
|
||||
}
|
||||
|
||||
// 1. Create Custom Quote Request
|
||||
@@ -68,6 +71,9 @@ public class CustomQuoteRequestController {
|
||||
for (MultipartFile file : files) {
|
||||
if (file.isEmpty()) continue;
|
||||
|
||||
// Scan for virus
|
||||
clamAVService.scan(file.getInputStream());
|
||||
|
||||
CustomQuoteRequestAttachment attachment = new CustomQuoteRequestAttachment();
|
||||
attachment.setRequest(request);
|
||||
attachment.setOriginalFilename(file.getOriginalFilename());
|
||||
|
||||
@@ -28,6 +28,7 @@ public class OrderController {
|
||||
private final QuoteSessionRepository quoteSessionRepo;
|
||||
private final QuoteLineItemRepository quoteLineItemRepo;
|
||||
private final CustomerRepository customerRepo;
|
||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
||||
|
||||
// TODO: Inject Storage Service or use a base path property
|
||||
private static final String STORAGE_ROOT = "storage_orders";
|
||||
@@ -36,12 +37,14 @@ public class OrderController {
|
||||
OrderItemRepository orderItemRepo,
|
||||
QuoteSessionRepository quoteSessionRepo,
|
||||
QuoteLineItemRepository quoteLineItemRepo,
|
||||
CustomerRepository customerRepo) {
|
||||
CustomerRepository customerRepo,
|
||||
com.printcalculator.service.ClamAVService clamAVService) {
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderItemRepo = orderItemRepo;
|
||||
this.quoteSessionRepo = quoteSessionRepo;
|
||||
this.quoteLineItemRepo = quoteLineItemRepo;
|
||||
this.customerRepo = customerRepo;
|
||||
this.clamAVService = clamAVService;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +233,9 @@ public class OrderController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Scan for virus
|
||||
clamAVService.scan(file.getInputStream());
|
||||
|
||||
// Ensure path logic
|
||||
String relativePath = item.getStoredRelativePath();
|
||||
if (relativePath == null || relativePath.equals("PENDING")) {
|
||||
|
||||
@@ -22,15 +22,17 @@ public class QuoteController {
|
||||
private final SlicerService slicerService;
|
||||
private final QuoteCalculator quoteCalculator;
|
||||
private final PrinterMachineRepository machineRepo;
|
||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
||||
|
||||
// Defaults (using aliases defined in ProfileManager)
|
||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
||||
private static final String DEFAULT_PROCESS = "standard";
|
||||
|
||||
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo) {
|
||||
public QuoteController(SlicerService slicerService, QuoteCalculator quoteCalculator, PrinterMachineRepository machineRepo, com.printcalculator.service.ClamAVService clamAVService) {
|
||||
this.slicerService = slicerService;
|
||||
this.quoteCalculator = quoteCalculator;
|
||||
this.machineRepo = machineRepo;
|
||||
this.clamAVService = clamAVService;
|
||||
}
|
||||
|
||||
@PostMapping("/api/quote")
|
||||
@@ -99,6 +101,9 @@ public class QuoteController {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Scan for virus
|
||||
clamAVService.scan(file.getInputStream());
|
||||
|
||||
// Fetch Default Active Machine
|
||||
PrinterMachine machine = machineRepo.findFirstByIsActiveTrue()
|
||||
.orElseThrow(() -> new IOException("No active printer found in database"));
|
||||
|
||||
@@ -40,6 +40,7 @@ public class QuoteSessionController {
|
||||
private final QuoteCalculator quoteCalculator;
|
||||
private final PrinterMachineRepository machineRepo;
|
||||
private final com.printcalculator.repository.PricingPolicyRepository pricingRepo;
|
||||
private final com.printcalculator.service.ClamAVService clamAVService;
|
||||
|
||||
// Defaults
|
||||
private static final String DEFAULT_FILAMENT = "pla_basic";
|
||||
@@ -50,13 +51,15 @@ public class QuoteSessionController {
|
||||
SlicerService slicerService,
|
||||
QuoteCalculator quoteCalculator,
|
||||
PrinterMachineRepository machineRepo,
|
||||
com.printcalculator.repository.PricingPolicyRepository pricingRepo) {
|
||||
com.printcalculator.repository.PricingPolicyRepository pricingRepo,
|
||||
com.printcalculator.service.ClamAVService clamAVService) {
|
||||
this.sessionRepo = sessionRepo;
|
||||
this.lineItemRepo = lineItemRepo;
|
||||
this.slicerService = slicerService;
|
||||
this.quoteCalculator = quoteCalculator;
|
||||
this.machineRepo = machineRepo;
|
||||
this.pricingRepo = pricingRepo;
|
||||
this.clamAVService = clamAVService;
|
||||
}
|
||||
|
||||
// 1. Start a new empty session
|
||||
@@ -99,6 +102,9 @@ public class QuoteSessionController {
|
||||
private QuoteLineItem addItemToSession(QuoteSession session, MultipartFile file, com.printcalculator.dto.PrintSettingsDto settings) throws IOException {
|
||||
if (file.isEmpty()) throw new IOException("File is empty");
|
||||
|
||||
// Scan for virus
|
||||
clamAVService.scan(file.getInputStream());
|
||||
|
||||
// 1. Define Persistent Storage Path
|
||||
// Structure: storage_quotes/{sessionId}/{uuid}.{ext}
|
||||
String storageDir = "storage_quotes/" + session.getId();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.printcalculator.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(VirusDetectedException.class)
|
||||
public ResponseEntity<Object> handleVirusDetectedException(
|
||||
VirusDetectedException ex, WebRequest request) {
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("timestamp", LocalDateTime.now());
|
||||
body.put("message", ex.getMessage());
|
||||
body.put("error", "Virus Detected");
|
||||
|
||||
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.printcalculator.exception;
|
||||
|
||||
public class VirusDetectedException extends RuntimeException {
|
||||
public VirusDetectedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import com.printcalculator.exception.VirusDetectedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xyz.capybara.clamav.ClamavClient;
|
||||
import xyz.capybara.clamav.commands.scan.result.ScanResult;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
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:clamav}") String host,
|
||||
@Value("${clamav.port:3310}") int port,
|
||||
@Value("${clamav.enabled:true}") boolean enabled
|
||||
) {
|
||||
this.enabled = enabled;
|
||||
ClamavClient client = null;
|
||||
try {
|
||||
if (enabled) {
|
||||
logger.info("Initializing ClamAV client at {}:{}", host, port);
|
||||
client = new ClamavClient(host, port);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to initialize ClamAV client: " + e.getMessage());
|
||||
}
|
||||
this.clamavClient = client;
|
||||
}
|
||||
|
||||
public boolean scan(InputStream inputStream) {
|
||||
if (!enabled || clamavClient == null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
ScanResult result = clamavClient.scan(inputStream);
|
||||
if (result instanceof ScanResult.OK) {
|
||||
return true;
|
||||
} else if (result instanceof ScanResult.VirusFound) {
|
||||
Map<String, Collection<String>> viruses = ((ScanResult.VirusFound) result).getFoundViruses();
|
||||
logger.warn("VIRUS DETECTED: {}", viruses);
|
||||
throw new VirusDetectedException("Virus detected in the uploaded file: " + viruses);
|
||||
} else {
|
||||
logger.warn("Unknown scan result: {}. Allowing file (FAIL-OPEN)", result);
|
||||
return true;
|
||||
}
|
||||
} catch (VirusDetectedException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error scanning file with ClamAV. Allowing file (FAIL-OPEN)", e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,7 @@ TAG=dev
|
||||
BACKEND_PORT=18002
|
||||
FRONTEND_PORT=18082
|
||||
|
||||
CLAMAV_HOST=192.168.1.147
|
||||
CLAMAV_PORT=3310
|
||||
CLAMAV_ENABLED=true
|
||||
|
||||
|
||||
@@ -7,4 +7,7 @@ TAG=int
|
||||
BACKEND_PORT=18001
|
||||
FRONTEND_PORT=18081
|
||||
|
||||
CLAMAV_HOST=192.168.1.147
|
||||
CLAMAV_PORT=3310
|
||||
CLAMAV_ENABLED=true
|
||||
|
||||
|
||||
@@ -7,4 +7,7 @@ TAG=prod
|
||||
BACKEND_PORT=8000
|
||||
FRONTEND_PORT=80
|
||||
|
||||
CLAMAV_HOST=192.168.1.147
|
||||
CLAMAV_PORT=3310
|
||||
CLAMAV_ENABLED=true
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ services:
|
||||
- DB_URL=${DB_URL}
|
||||
- DB_USERNAME=${DB_USERNAME}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- CLAMAV_HOST=${CLAMAV_HOST}
|
||||
- CLAMAV_PORT=${CLAMAV_PORT}
|
||||
- CLAMAV_ENABLED=${CLAMAV_ENABLED}
|
||||
- TEMP_DIR=/app/temp
|
||||
- PROFILES_DIR=/app/profiles
|
||||
restart: always
|
||||
|
||||
@@ -20,8 +20,11 @@ services:
|
||||
- MARKUP_PERCENT=20
|
||||
- TEMP_DIR=/app/temp
|
||||
- PROFILES_DIR=/app/profiles
|
||||
- CLAMAV_HOST=clamav
|
||||
- CLAMAV_PORT=3310
|
||||
depends_on:
|
||||
- db
|
||||
- clamav
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
@@ -49,5 +52,13 @@ services:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
clamav:
|
||||
platform: linux/amd64
|
||||
image: clamav/clamav:latest
|
||||
container_name: print-calculator-clamav
|
||||
ports:
|
||||
- "3310:3310"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
Reference in New Issue
Block a user