Harden tachograph file session extraction
This commit is contained in:
parent
c85b657acf
commit
39714b90b3
|
|
@ -24,6 +24,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
|
|
@ -275,14 +276,45 @@ public class DriverCardXmlExtractionService {
|
|||
) {
|
||||
List<ExtractedCardActivityInterval> result = new ArrayList<>(activityIntervals.size());
|
||||
for (ExtractedCardActivityInterval interval : activityIntervals) {
|
||||
ExtractedCardVehicleUsageInterval covering = vehicleUsageIntervals.stream()
|
||||
.filter(usage -> !usage.from().isAfter(interval.from()) && !usage.to().isBefore(interval.from()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
result.add(new ExtractedCardActivityInterval(
|
||||
interval.intervalId(),
|
||||
interval.from(),
|
||||
interval.to(),
|
||||
result.addAll(splitByVehicleCoverage(interval, vehicleUsageIntervals));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ExtractedCardActivityInterval> splitByVehicleCoverage(
|
||||
ExtractedCardActivityInterval interval,
|
||||
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals
|
||||
) {
|
||||
Set<OffsetDateTime> cutPoints = new TreeSet<>();
|
||||
cutPoints.add(interval.from());
|
||||
cutPoints.add(interval.to());
|
||||
|
||||
for (ExtractedCardVehicleUsageInterval usage : vehicleUsageIntervals) {
|
||||
if (!usage.to().isBefore(interval.from()) && !usage.from().isAfter(interval.to())) {
|
||||
if (usage.from().isAfter(interval.from()) && usage.from().isBefore(interval.to())) {
|
||||
cutPoints.add(usage.from());
|
||||
}
|
||||
OffsetDateTime usageEndExclusive = usage.to().plusSeconds(1);
|
||||
if (usageEndExclusive.isAfter(interval.from()) && usageEndExclusive.isBefore(interval.to())) {
|
||||
cutPoints.add(usageEndExclusive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<OffsetDateTime> orderedCutPoints = List.copyOf(cutPoints);
|
||||
List<ExtractedCardActivityInterval> segments = new ArrayList<>(Math.max(1, orderedCutPoints.size() - 1));
|
||||
for (int i = 0; i < orderedCutPoints.size() - 1; i++) {
|
||||
OffsetDateTime segmentFrom = orderedCutPoints.get(i);
|
||||
OffsetDateTime segmentTo = orderedCutPoints.get(i + 1);
|
||||
if (!segmentFrom.isBefore(segmentTo)) {
|
||||
continue;
|
||||
}
|
||||
ExtractedCardVehicleUsageInterval covering = findVehicleCoverage(segmentFrom, vehicleUsageIntervals);
|
||||
String intervalId = orderedCutPoints.size() == 2 ? interval.intervalId() : interval.intervalId() + "-" + (i + 1);
|
||||
segments.add(new ExtractedCardActivityInterval(
|
||||
intervalId,
|
||||
segmentFrom,
|
||||
segmentTo,
|
||||
interval.activityType(),
|
||||
interval.slot(),
|
||||
interval.cardStatus(),
|
||||
|
|
@ -292,7 +324,17 @@ public class DriverCardXmlExtractionService {
|
|||
interval.rawRecordPath()
|
||||
));
|
||||
}
|
||||
return result;
|
||||
return segments;
|
||||
}
|
||||
|
||||
private ExtractedCardVehicleUsageInterval findVehicleCoverage(
|
||||
OffsetDateTime timestamp,
|
||||
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals
|
||||
) {
|
||||
return vehicleUsageIntervals.stream()
|
||||
.filter(usage -> !usage.from().isAfter(timestamp) && timestamp.isBefore(usage.to().plusSeconds(1)))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Element firstElement(Object node, String expression) {
|
||||
|
|
|
|||
|
|
@ -71,9 +71,11 @@ public class LegalRequirementsClient {
|
|||
throw new LegalRequirementsUploadException("LegalRequirements upload response did not contain DataPackageID.");
|
||||
}
|
||||
return dataPackageId;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LegalRequirementsUploadException("Failed to upload tachograph file to LegalRequirements.", e);
|
||||
} catch (IOException e) {
|
||||
throw new LegalRequirementsUploadException("Failed to upload tachograph file to LegalRequirements.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,9 +90,11 @@ public class LegalRequirementsClient {
|
|||
throw new LegalRequirementsXmlDownloadException("LegalRequirements XML download failed with status " + response.statusCode());
|
||||
}
|
||||
return response.body();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LegalRequirementsXmlDownloadException("Failed to download processed tachograph XML.", e);
|
||||
} catch (IOException e) {
|
||||
throw new LegalRequirementsXmlDownloadException("Failed to download processed tachograph XML.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +105,9 @@ public class LegalRequirementsClient {
|
|||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build();
|
||||
client.send(request, HttpResponse.BodyHandlers.discarding());
|
||||
} catch (IOException | InterruptedException ignored) {
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ public class TachographFileSessionService {
|
|||
String sessionLabel
|
||||
) {
|
||||
try {
|
||||
validateFile(file);
|
||||
byte[] fileBytes = file.getBytes();
|
||||
validateFile(file, fileBytes);
|
||||
validateFileBytes(fileBytes);
|
||||
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadDriverCard(fileBytes, file.getOriginalFilename());
|
||||
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parseDriverCardXml(uploadResult.xmlContent());
|
||||
Instant createdAt = Instant.now();
|
||||
|
|
@ -148,8 +149,17 @@ public class TachographFileSessionService {
|
|||
.toList();
|
||||
}
|
||||
|
||||
private void validateFile(MultipartFile file, byte[] fileBytes) {
|
||||
if (file == null || file.isEmpty() || fileBytes.length == 0) {
|
||||
private void validateFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty() || file.getSize() == 0L) {
|
||||
throw new IllegalArgumentException("Tachograph file must not be empty.");
|
||||
}
|
||||
if (file.getSize() > properties.getTachographFileSession().getMaxFileSizeBytes()) {
|
||||
throw new IllegalArgumentException("Tachograph file exceeds the configured size limit.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFileBytes(byte[] fileBytes) {
|
||||
if (fileBytes == null || fileBytes.length == 0) {
|
||||
throw new IllegalArgumentException("Tachograph file must not be empty.");
|
||||
}
|
||||
if (fileBytes.length > properties.getTachographFileSession().getMaxFileSizeBytes()) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ public class TachographXmlParser {
|
|||
validate(xmlContent);
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(false);
|
||||
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
||||
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
||||
factory.setXIncludeAware(false);
|
||||
factory.setExpandEntityReferences(false);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));
|
||||
String rootName = document.getDocumentElement() == null ? null : document.getDocumentElement().getNodeName();
|
||||
|
|
|
|||
|
|
@ -45,8 +45,11 @@ class DriverCardXmlExtractionServiceTest {
|
|||
assertThat(driver.vehicleRegistrations()).hasSize(2);
|
||||
assertThat(driver.vehicles()).hasSize(2);
|
||||
assertThat(driver.cardVehicleUsageIntervals()).hasSize(2);
|
||||
assertThat(driver.cardActivityIntervals()).hasSize(3);
|
||||
assertThat(driver.cardActivityIntervals()).hasSize(5);
|
||||
assertThat(driver.cardActivityIntervals().get(0).registrationKey()).isEqualTo("12:W-12345A");
|
||||
assertThat(driver.cardActivityIntervals().get(1).to()).isEqualTo(driver.cardVehicleUsageIntervals().get(0).to().plusSeconds(1));
|
||||
assertThat(driver.cardActivityIntervals().get(2).registrationKey()).isEqualTo("12:W-54321B");
|
||||
assertThat(driver.cardActivityIntervals().get(3).registrationKey()).isEqualTo("12:W-54321B");
|
||||
assertThat(driver.cardActivityIntervals().get(4).registrationKey()).isNull();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
|
|
@ -49,4 +51,23 @@ class TachographFileSessionServiceTest {
|
|||
assertThat(response.session().sessionId()).isEqualTo(extracted.sessionId());
|
||||
assertThat(service.getSession(extracted.sessionId()).sessionId()).isEqualTo(extracted.sessionId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOversizedFileBeforeUpload() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
properties.getTachographFileSession().setMaxFileSizeBytes(1024);
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||
DriverCardXmlExtractionService extractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
TachographFileSessionService service = new TachographFileSessionService(properties, repository, client, parser, extractor);
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", new byte[1025]);
|
||||
|
||||
assertThatThrownBy(() -> service.createSession(file, null, null, "sample"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("size limit");
|
||||
|
||||
verifyNoInteractions(client, parser, extractor);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,17 @@ class TachographXmlParserTest {
|
|||
assertThatThrownBy(() -> parser.parseDriverCardXml(invalid))
|
||||
.isInstanceOf(TachographXmlValidationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsXmlWithDoctype() {
|
||||
String xmlWithDoctype = """
|
||||
<!DOCTYPE DriverCard [
|
||||
<!ELEMENT DriverCard ANY>
|
||||
]>
|
||||
<DriverCard/>
|
||||
""";
|
||||
|
||||
assertThatThrownBy(() -> parser.parseDriverCardXml(xmlWithDoctype))
|
||||
.isInstanceOf(TachographXmlValidationException.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue