diff --git a/src/main/java/com/mindee/input/URLInputSource.java b/src/main/java/com/mindee/input/URLInputSource.java index 352bc2834..0d5bae111 100644 --- a/src/main/java/com/mindee/input/URLInputSource.java +++ b/src/main/java/com/mindee/input/URLInputSource.java @@ -7,18 +7,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; -import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Base64; -import java.util.Locale; import lombok.Getter; /** @@ -62,78 +57,13 @@ public static Builder builder(URL url) { * Ensures the URL can be safely sent to the Mindee server. * *

- * Rejects any URL that could be used for Server-Side Request Forgery (SSRF): - *

+ * Rejects any URL that don't follow an HTTPS scheme. + *

*/ public void validateSecure() { if (!"https".equalsIgnoreCase(this.url.getProtocol())) { throw new MindeeException("Only HTTPS source URLs are allowed"); } - String userInfo = this.url.getUserInfo(); - if (userInfo != null && !userInfo.isEmpty()) { - throw new MindeeException("Source URLs must not embed user credentials"); - } - String host = this.url.getHost(); - if (host == null || host.isEmpty()) { - throw new MindeeException("Source URL is missing a host"); - } - String lowerHost = host.toLowerCase(Locale.ROOT); - if ( - "localhost".equals(lowerHost) - || lowerHost.endsWith(".localhost") - || "ip6-localhost".equals(lowerHost) - || "ip6-loopback".equals(lowerHost) - ) { - throw new MindeeException("Loopback hostnames are not allowed: " + host); - } - - InetAddress[] addresses; - try { - addresses = InetAddress.getAllByName(host); - } catch (UnknownHostException e) { - throw new MindeeException("Unable to resolve source URL host: " + host, e); - } - for (InetAddress addr : addresses) { - if (isDisallowedAddress(addr)) { - throw new MindeeException( - "Source URL host resolves to a disallowed address: " + addr.getHostAddress() - ); - } - } - } - - private static boolean isDisallowedAddress(InetAddress addr) { - return addr.isLoopbackAddress() - || addr.isLinkLocalAddress() - || addr.isSiteLocalAddress() - || addr.isAnyLocalAddress() - || addr.isMulticastAddress() - || isUniqueLocalIpv6(addr) - || isCarrierGradeNat(addr); - } - - private static boolean isUniqueLocalIpv6(InetAddress addr) { - if (!(addr instanceof Inet6Address)) { - return false; - } - byte[] raw = addr.getAddress(); - return (raw[0] & 0xFE) == 0xFC; - } - - private static boolean isCarrierGradeNat(InetAddress addr) { - if (!(addr instanceof Inet4Address)) { - return false; - } - byte[] raw = addr.getAddress(); - return (raw[0] & 0xFF) == 100 && (raw[1] & 0xC0) == 0x40; } /** @@ -191,7 +121,7 @@ private HttpURLConnection handleRedirects(HttpURLConnection connection) throws I connection.disconnect(); HttpURLConnection newConnection = createConnection(new URL(newUrl)); - return handleRedirects(newConnection); // Recursive call to handle multiple redirects + return handleRedirects(newConnection); } return connection; } diff --git a/src/main/java/com/mindee/v2/MindeeClient.java b/src/main/java/com/mindee/v2/MindeeClient.java index 10f1972aa..4b2bda9be 100644 --- a/src/main/java/com/mindee/v2/MindeeClient.java +++ b/src/main/java/com/mindee/v2/MindeeClient.java @@ -9,7 +9,6 @@ import com.mindee.v2.http.MindeeHttpExceptionV2; import com.mindee.v2.parsing.CommonResponse; import com.mindee.v2.parsing.JobResponse; -import com.mindee.v2.parsing.JobStatus; import com.mindee.v2.parsing.error.ErrorResponse; import com.mindee.v2.parsing.search.SearchResponse; import com.mindee.v2.product.extraction.ExtractionResponse; @@ -244,10 +243,10 @@ private TResponse pollAndFetch( interruptibleSleep((long) (currentIntervalSec * 1000), pollingOptions); resp = getJob(initialJob.getJob().getId()); - if (resp.getJob().getStatusEnum() == JobStatus.Failed) { + if (resp.getJob().getStatus().equals("Failed")) { attempts = max; } - if (resp.getJob().getStatusEnum() == JobStatus.Processed) { + if (resp.getJob().getStatus().equals("Processed")) { return getResult(responseClass, resp.getJob().getId()); } currentIntervalSec = Math.min(currentIntervalSec * backoffMultiplier, maxIntervalSec); diff --git a/src/main/java/com/mindee/v2/parsing/Job.java b/src/main/java/com/mindee/v2/parsing/Job.java index 56cb0bf20..3b67ee6d0 100644 --- a/src/main/java/com/mindee/v2/parsing/Job.java +++ b/src/main/java/com/mindee/v2/parsing/Job.java @@ -48,14 +48,6 @@ public final class Job { @JsonProperty("status") private String status; - /** - * Typed view of {@link #status}. Returns {@link JobStatus#Unknown} for values - * the SDK doesn't recognise, or {@code null} if the raw status is {@code null}. - */ - public JobStatus getStatusEnum() { - return JobStatus.fromValue(status); - } - /** * Status of the job. */ diff --git a/src/main/java/com/mindee/v2/parsing/JobStatus.java b/src/main/java/com/mindee/v2/parsing/JobStatus.java deleted file mode 100644 index b90897df7..000000000 --- a/src/main/java/com/mindee/v2/parsing/JobStatus.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.mindee.v2.parsing; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -/** - * Possible statuses returned by the API for an asynchronous {@link Job}. - */ -public enum JobStatus { - Processing("Processing"), - Processed("Processed"), - Failed("Failed"), - Unknown("Unknown"); - - private final String value; - - JobStatus(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - - /** - * Deserializes a status string coming from the API. Unknown values fall back to - * {@link #Unknown} rather than failing so newly added statuses don't break clients. - */ - @JsonCreator - public static JobStatus fromValue(String value) { - if (value == null) { - return null; - } - for (JobStatus status : values()) { - if (status.value.equalsIgnoreCase(value)) { - return status; - } - } - return Unknown; - } -} diff --git a/src/test/java/com/mindee/input/URLInputSourceTest.java b/src/test/java/com/mindee/input/URLInputSourceTest.java index 5845ec613..7db6053f0 100644 --- a/src/test/java/com/mindee/input/URLInputSourceTest.java +++ b/src/test/java/com/mindee/input/URLInputSourceTest.java @@ -101,77 +101,6 @@ void httpScheme_isRejected() throws MalformedURLException { MindeeException e = assertThrows(MindeeException.class, src::validateSecure); assertTrue(e.getMessage().contains("HTTPS")); } - - @Test - void userInfo_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://user:pass@example.com/file.pdf").build(); - MindeeException e = assertThrows(MindeeException.class, src::validateSecure); - assertTrue(e.getMessage().contains("credentials")); - } - - @Test - void loopbackHostname_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://localhost/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void subLocalhostHostname_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://foo.localhost/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void loopbackIpv4_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://127.0.0.1/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void loopbackIpv6_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://[::1]/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void anyLocalIpv4_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://0.0.0.0/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void privateRfc1918_isRejected() throws MalformedURLException { - for (String host : new String[] { "10.0.0.1", "172.16.0.1", "192.168.1.1" }) { - var src = URLInputSource.builder("https://" + host + "/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure, "expected rejection for " + host); - } - } - - @Test - void linkLocalIpv4_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://169.254.169.254/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void cgnat_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://100.64.0.1/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void uniqueLocalIpv6_isRejected() throws MalformedURLException { - var src = URLInputSource.builder("https://[fd00::1]/file.pdf").build(); - assertThrows(MindeeException.class, src::validateSecure); - } - - @Test - void unresolvableHost_isRejected() throws MalformedURLException { - var src = URLInputSource - .builder("https://this-host-should-not-exist.invalid/file.pdf") - .build(); - assertThrows(MindeeException.class, src::validateSecure); - } } static class TestableURLInputSource extends URLInputSource { diff --git a/src/test/java/com/mindee/v2/MindeeClientIT.java b/src/test/java/com/mindee/v2/MindeeClientIT.java index c0e069ed2..c8386c921 100644 --- a/src/test/java/com/mindee/v2/MindeeClientIT.java +++ b/src/test/java/com/mindee/v2/MindeeClientIT.java @@ -251,7 +251,7 @@ void getResultFromUrl_mustSucceed() throws IOException, InterruptedException { for (int i = 0; i < 80 && resultUrl == null; i++) { Thread.sleep(1500); var poll = mindeeClient.getJob(jobId); - if (poll.getJob().getStatusEnum() == com.mindee.v2.parsing.JobStatus.Processed) { + if (poll.getJob().getStatus().equals("Processed")) { resultUrl = poll.getJob().getResultUrl(); } } diff --git a/src/test/java/com/mindee/v2/parsing/JobTest.java b/src/test/java/com/mindee/v2/parsing/JobTest.java index 6a3f3f880..30996ee1a 100644 --- a/src/test/java/com/mindee/v2/parsing/JobTest.java +++ b/src/test/java/com/mindee/v2/parsing/JobTest.java @@ -28,7 +28,6 @@ void whenProcessing_mustHaveValidProperties() throws IOException { var job = response.getJob(); assertNotNull(job); assertEquals("Processing", job.getStatus()); - assertEquals(JobStatus.Processing, job.getStatusEnum()); assertNotNull(job.getCreatedAt()); assertNull(job.getCompletedAt()); assertNull(job.getResultUrl()); @@ -46,7 +45,6 @@ void whenProcessing_mustHaveValidProperties() throws IOException { var job = response.getJob(); assertNotNull(job); assertEquals("Processed", job.getStatus()); - assertEquals(JobStatus.Processed, job.getStatusEnum()); assertNotNull(job.getCreatedAt()); assertNotNull(job.getCompletedAt()); assertNotNull(job.getResultUrl());