mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a2b1d3f2 | ||
|
|
5b6eca28ba |
@@ -337,6 +337,33 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a URL is safe for server-side fetches without making a network request.
|
||||
*
|
||||
* @param urlStr the URL to validate
|
||||
* @return {@code true} if the URL uses http/https and resolves to a non-sensitive address
|
||||
*/
|
||||
public boolean isUrlAllowedForServerFetch(String urlStr) {
|
||||
if (urlStr == null || urlStr.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URL url = URI.create(urlStr).toURL();
|
||||
String protocol = url.getProtocol();
|
||||
if (!"http".equals(protocol) && !"https".equals(protocol)) {
|
||||
return false;
|
||||
}
|
||||
String host = url.getHost();
|
||||
if (host == null || host.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return !isDisallowedNetworkLocation(host);
|
||||
} catch (Exception e) {
|
||||
log.debug("URL {} is not allowed for server fetch: {}", urlStr, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the specified host resolves to a disallowed network location, such as
|
||||
* local, private, multicast, or reserved ranges. Excessive DNS results are also blocked.
|
||||
@@ -567,6 +594,7 @@ public class GeneralUtils {
|
||||
default -> toBytes(value, 2); // Default to MB
|
||||
};
|
||||
}
|
||||
return bytes;
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Failed to parse size string '{}': {}", sizeStr, e.getMessage());
|
||||
return null;
|
||||
@@ -613,6 +641,44 @@ public class GeneralUtils {
|
||||
return unit != null && VALID_SIZE_UNITS.contains(unit.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private Long parseSizeToBytes(String sizeStr, String defaultUnit) {
|
||||
if (sizeStr.endsWith("TB")) {
|
||||
return convertDecimalToBytes(sizeStr.substring(0, sizeStr.length() - 2), 1024L * 1024L
|
||||
* 1024L * 1024L);
|
||||
} else if (sizeStr.endsWith("GB")) {
|
||||
return convertDecimalToBytes(
|
||||
sizeStr.substring(0, sizeStr.length() - 2), 1024L * 1024L * 1024L);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
return convertDecimalToBytes(sizeStr.substring(0, sizeStr.length() - 2), 1024L * 1024L);
|
||||
} else if (sizeStr.endsWith("KB")) {
|
||||
return convertDecimalToBytes(sizeStr.substring(0, sizeStr.length() - 2), 1024L);
|
||||
} else if (!sizeStr.isEmpty() && sizeStr.charAt(sizeStr.length() - 1) == 'B') {
|
||||
return convertDecimalToBytes(sizeStr.substring(0, sizeStr.length() - 1), 1L);
|
||||
}
|
||||
// Use provided default unit or fall back to MB
|
||||
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
|
||||
return switch (unit) {
|
||||
case "TB" -> convertDecimalToBytes(sizeStr, 1024L * 1024L * 1024L * 1024L);
|
||||
case "GB" -> convertDecimalToBytes(sizeStr, 1024L * 1024L * 1024L);
|
||||
case "MB" -> convertDecimalToBytes(sizeStr, 1024L * 1024L);
|
||||
case "KB" -> convertDecimalToBytes(sizeStr, 1024L);
|
||||
case "B" -> convertDecimalToBytes(sizeStr, 1L);
|
||||
default -> convertDecimalToBytes(sizeStr, 1024L * 1024L); // Default to MB
|
||||
};
|
||||
}
|
||||
|
||||
private Long convertDecimalToBytes(String value, long multiplier) {
|
||||
BigDecimal parsed = new BigDecimal(value);
|
||||
if (parsed.signum() < 0) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal bytes = parsed.multiply(BigDecimal.valueOf(multiplier));
|
||||
if (bytes.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0) {
|
||||
return null;
|
||||
}
|
||||
return bytes.setScale(0, RoundingMode.DOWN).longValue();
|
||||
}
|
||||
|
||||
/* Enhanced byte formatting with TB/PB support and better precision. */
|
||||
public String formatBytes(long bytes) {
|
||||
if (bytes < 0) {
|
||||
|
||||
+7
-1
@@ -82,7 +82,13 @@ public class SplitPdfBySizeController {
|
||||
|
||||
if (type == 0) {
|
||||
log.debug("Processing split by size");
|
||||
long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
||||
Long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
||||
if (maxBytes == null || maxBytes <= 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument",
|
||||
"Invalid argument: {0}",
|
||||
"split value: " + value);
|
||||
}
|
||||
log.debug("Max bytes per document: {}", maxBytes);
|
||||
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
|
||||
} else if (type == 1) {
|
||||
|
||||
+54
-16
@@ -54,6 +54,7 @@ public class ConvertWebsiteToPDF {
|
||||
Pattern.compile("(?<![a-z0-9_])file\\s*:(?:/{1,3}|%2f|%5c|%3a|/|/)");
|
||||
|
||||
private static final Pattern NUMERIC_HTML_ENTITY_PATTERN = Pattern.compile("&#(x?[0-9a-f]+);");
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
|
||||
@Operation(
|
||||
@@ -86,6 +87,12 @@ public class ConvertWebsiteToPDF {
|
||||
.queryParam("error", "error.invalidUrlFormat")
|
||||
.build()
|
||||
.toUri();
|
||||
} else if (!GeneralUtils.isUrlAllowedForServerFetch(URL)) {
|
||||
location =
|
||||
uriComponentsBuilder
|
||||
.queryParam("error", "error.invalidUrlFormat")
|
||||
.build()
|
||||
.toUri();
|
||||
} else if (!GeneralUtils.isURLReachable(URL)) {
|
||||
// validate the URL is reachable
|
||||
location =
|
||||
@@ -166,29 +173,60 @@ public class ConvertWebsiteToPDF {
|
||||
private String fetchRemoteHtml(String url) throws IOException, InterruptedException {
|
||||
HttpClient client =
|
||||
HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.followRedirects(HttpClient.Redirect.NEVER)
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(20))
|
||||
.GET()
|
||||
.header("User-Agent", "Stirling-PDF/URL-to-PDF")
|
||||
.build();
|
||||
URI currentUri = URI.create(url);
|
||||
for (int redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
||||
String currentUrl = currentUri.toString();
|
||||
if (!GeneralUtils.isUrlAllowedForServerFetch(currentUrl)) {
|
||||
throw ExceptionUtils.createIOException(
|
||||
"error.invalidUrlFormat",
|
||||
"Disallowed URL target: {0}",
|
||||
null,
|
||||
currentUrl);
|
||||
}
|
||||
|
||||
HttpResponse<String> response =
|
||||
client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder(currentUri)
|
||||
.timeout(Duration.ofSeconds(20))
|
||||
.GET()
|
||||
.header("User-Agent", "Stirling-PDF/URL-to-PDF")
|
||||
.build();
|
||||
|
||||
if (response.statusCode() >= 400 || response.body() == null) {
|
||||
throw ExceptionUtils.createIOException(
|
||||
"error.httpRequestFailed",
|
||||
"Failed to retrieve remote HTML. Status: {0}",
|
||||
null,
|
||||
response.statusCode());
|
||||
HttpResponse<String> response =
|
||||
client.send(
|
||||
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
|
||||
int statusCode = response.statusCode();
|
||||
if (statusCode >= 300 && statusCode < 400) {
|
||||
String location = response.headers().firstValue("location").orElse(null);
|
||||
if (location == null || location.isBlank()) {
|
||||
throw ExceptionUtils.createIOException(
|
||||
"error.httpRequestFailed",
|
||||
"Redirect response missing Location header",
|
||||
null);
|
||||
}
|
||||
currentUri = currentUri.resolve(location);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statusCode >= 400 || response.body() == null) {
|
||||
throw ExceptionUtils.createIOException(
|
||||
"error.httpRequestFailed",
|
||||
"Failed to retrieve remote HTML. Status: {0}",
|
||||
null,
|
||||
statusCode);
|
||||
}
|
||||
|
||||
return response.body();
|
||||
}
|
||||
|
||||
return response.body();
|
||||
throw ExceptionUtils.createIOException(
|
||||
"error.httpRequestFailed",
|
||||
"Too many redirects while retrieving remote HTML",
|
||||
null);
|
||||
}
|
||||
|
||||
private boolean containsDisallowedUriScheme(String htmlContent) {
|
||||
|
||||
+6
@@ -948,6 +948,12 @@ public class CompressController {
|
||||
boolean autoMode = false;
|
||||
if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1) {
|
||||
expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString);
|
||||
if (expectedOutputSize == null || expectedOutputSize <= 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument",
|
||||
"Invalid argument: {0}",
|
||||
"expected output size: " + expectedOutputSizeString);
|
||||
}
|
||||
autoMode = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user