package stirling.software.SPDF.utils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Deque; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.springframework.web.multipart.MultipartFile; import com.fathzer.soft.javaluator.DoubleEvaluator; import io.github.pixee.security.HostValidator; import io.github.pixee.security.Urls; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.InstallationPathConfig; @Slf4j public class GeneralUtils { public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { File tempFile = Files.createTempFile("temp", null).toFile(); try (FileOutputStream os = new FileOutputStream(tempFile)) { os.write(multipartFile.getBytes()); } return tempFile; } public static void deleteDirectory(Path path) throws IOException { Files.walkFileTree( path, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.deleteIfExists(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.deleteIfExists(dir); return FileVisitResult.CONTINUE; } }); } public static String convertToFileName(String name) { String safeName = name.replaceAll("[^a-zA-Z0-9]", "_"); if (safeName.length() > 50) { safeName = safeName.substring(0, 50); } return safeName; } public static boolean isValidURL(String urlStr) { try { Urls.create( urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS); return true; } catch (MalformedURLException e) { return false; } } public static boolean isURLReachable(String urlStr) { try { // Parse the URL URL url = URI.create(urlStr).toURL(); // Allow only http and https protocols String protocol = url.getProtocol(); if (!"http".equals(protocol) && !"https".equals(protocol)) { return false; // Disallow other protocols } // Check if the host is a local address String host = url.getHost(); if (isLocalAddress(host)) { return false; // Exclude local addresses } // Check if the URL is reachable HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("HEAD"); // connection.setConnectTimeout(5000); // Set connection timeout // connection.setReadTimeout(5000); // Set read timeout int responseCode = connection.getResponseCode(); return (200 <= responseCode && responseCode <= 399); } catch (Exception e) { return false; // Return false in case of any exception } } private static boolean isLocalAddress(String host) { try { // Resolve DNS to IP address InetAddress address = InetAddress.getByName(host); // Check for local addresses return address.isAnyLocalAddress() || // Matches 0.0.0.0 or similar address.isLoopbackAddress() || // Matches 127.0.0.1 or ::1 address.isSiteLocalAddress() || // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to // 172.31.x.x address.getHostAddress() .startsWith("fe80:"); // Matches link-local IPv6 addresses } catch (Exception e) { return false; // Return false for invalid or unresolved addresses } } public static File multipartToFile(MultipartFile multipart) throws IOException { Path tempFile = Files.createTempFile("overlay-", ".pdf"); try (InputStream in = multipart.getInputStream(); FileOutputStream out = new FileOutputStream(tempFile.toFile())) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } return tempFile.toFile(); } public static Long convertSizeToBytes(String sizeStr) { if (sizeStr == null) { return null; } sizeStr = sizeStr.trim().toUpperCase(); sizeStr = sizeStr.replace(",", ".").replace(" ", ""); try { if (sizeStr.endsWith("KB")) { return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); } else if (sizeStr.endsWith("MB")) { return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024); } else if (sizeStr.endsWith("GB")) { return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024); } else if (sizeStr.endsWith("B")) { return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); } else { // Assume MB if no unit is specified return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); } } catch (NumberFormatException e) { // The numeric part of the input string cannot be parsed, handle this case } return null; } public static List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) { if (pages == null) { return List.of(1); // Default to first page if input is null } try { return parsePageList(pages.split(","), totalPages, oneBased); } catch (NumberFormatException e) { return List.of(1); // Default to first page if input is invalid } } public static List<Integer> parsePageList(String[] pages, int totalPages) { return parsePageList(pages, totalPages, false); } public static List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) { List<Integer> result = new ArrayList<>(); int offset = oneBased ? 1 : 0; for (String page : pages) { if ("all".equalsIgnoreCase(page)) { for (int i = 0; i < totalPages; i++) { result.add(i + offset); } } else if (page.contains(",")) { // Split the string into parts, could be single pages or ranges String[] parts = page.split(","); for (String part : parts) { result.addAll(handlePart(part, totalPages, offset)); } } else { result.addAll(handlePart(page, totalPages, offset)); } } return result; } public static List<Integer> evaluateNFunc(String expression, int maxValue) { List<Integer> results = new ArrayList<>(); DoubleEvaluator evaluator = new DoubleEvaluator(); // Validate the expression if (!expression.matches("[0-9n+\\-*/() ]+")) { throw new IllegalArgumentException("Invalid expression"); } for (int n = 1; n <= maxValue; n++) { // Replace 'n' with the current value of n, correctly handling numbers before // 'n' String sanitizedExpression = sanitizeNFunction(expression, n); Double result = evaluator.evaluate(sanitizedExpression); // Check if the result is null or not within bounds if (result == null) break; if (result.intValue() > 0 && result.intValue() <= maxValue) results.add(result.intValue()); } return results; } private static String sanitizeNFunction(String expression, int nValue) { String sanitizedExpression = expression.replace(" ", ""); String multiplyByOpeningRoundBracketPattern = "([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2) sanitizedExpression = sanitizedExpression.replaceAll(multiplyByOpeningRoundBracketPattern, "$1*("); String multiplyByClosingRoundBracketPattern = "\\)([0-9n)])"; // example: (n-1)n, (n-1)9, (n-1)(n-2) sanitizedExpression = sanitizedExpression.replaceAll(multiplyByClosingRoundBracketPattern, ")*$1"); sanitizedExpression = insertMultiplicationBeforeN(sanitizedExpression, nValue); return sanitizedExpression; } private static String insertMultiplicationBeforeN(String expression, int nValue) { // Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") String withMultiplication = expression.replaceAll("(\\d)n", "$1*n"); withMultiplication = formatConsecutiveNsForNFunction(withMultiplication); // Now replace 'n' with its current value return withMultiplication.replace("n", String.valueOf(nValue)); } private static String formatConsecutiveNsForNFunction(String expression) { String text = expression; while (text.matches(".*n{2,}.*")) { text = text.replaceAll("(?<!n)n{2}", "n*n"); } return text; } private static List<Integer> handlePart(String part, int totalPages, int offset) { List<Integer> partResult = new ArrayList<>(); // First check for n-syntax because it should not be processed as a range if (part.contains("n")) { partResult = evaluateNFunc(part, totalPages); // Adjust the results according to the offset for (int i = 0; i < partResult.size(); i++) { int adjustedValue = partResult.get(i) - 1 + offset; partResult.set(i, adjustedValue); } } else if (part.contains("-")) { // Process ranges only if it's not n-syntax String[] rangeParts = part.split("-"); try { int start = Integer.parseInt(rangeParts[0]); int end = (rangeParts.length > 1 && !rangeParts[1].isEmpty()) ? Integer.parseInt(rangeParts[1]) : totalPages; for (int i = start; i <= end; i++) { if (i >= 1 && i <= totalPages) { partResult.add(i - 1 + offset); } } } catch (NumberFormatException e) { // Range is invalid, ignore this part } } else { // This is a single page number try { int pageNum = Integer.parseInt(part.trim()); if (pageNum >= 1 && pageNum <= totalPages) { partResult.add(pageNum - 1 + offset); } } catch (NumberFormatException ignored) { // Ignore invalid numbers } } return partResult; } public static boolean createDir(String path) { Path folder = Paths.get(path); if (!Files.exists(folder)) { try { Files.createDirectories(folder); } catch (IOException e) { log.error("exception", e); return false; } } return true; } public static boolean isValidUUID(String uuid) { if (uuid == null) { return false; } try { UUID.fromString(uuid); return true; } catch (IllegalArgumentException e) { return false; } } public static void saveKeyToConfig(String id, String key) throws IOException { saveKeyToConfig(id, key, true); } public static void saveKeyToConfig(String id, boolean key) throws IOException { saveKeyToConfig(id, key, true); } public static void saveKeyToConfig(String id, String key, boolean autoGenerated) throws IOException { doSaveKeyToConfig(id, (key == null ? "" : key), autoGenerated); } public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) throws IOException { doSaveKeyToConfig(id, String.valueOf(key), autoGenerated); } /*------------------------------------------------------------------------* * Internal Implementation Details * *------------------------------------------------------------------------*/ /** * Actually performs the line-based update for the given path (e.g. "security.csrfDisabled") to * a new string value (e.g. "true"), possibly marking it as auto-generated. */ private static void doSaveKeyToConfig(String fullPath, String newValue, boolean autoGenerated) throws IOException { // 1) Load the file (settings.yml) Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); if (!Files.exists(settingsPath)) { log.warn("Settings file not found at {}, creating a new empty file...", settingsPath); Files.createDirectories(settingsPath.getParent()); Files.createFile(settingsPath); } List<String> lines = Files.readAllLines(settingsPath); // 2) Build a map of "nestedKeyPath -> lineIndex" by parsing indentation // Also track each line's indentation so we can preserve it when rewriting. Map<String, LineInfo> pathToLine = parseNestedYamlKeys(lines); // 3) If the path is found, rewrite its line. Else, append at the bottom (no indentation). boolean changed = false; if (pathToLine.containsKey(fullPath)) { // Rewrite existing line LineInfo info = pathToLine.get(fullPath); String oldLine = lines.get(info.lineIndex); String newLine = rewriteLine(oldLine, info.indentSpaces, fullPath, newValue, autoGenerated); if (!newLine.equals(oldLine)) { lines.set(info.lineIndex, newLine); changed = true; } } else { // Append a new line at the bottom, with zero indentation String appended = fullPath + ": " + newValue; if (autoGenerated) { appended += " # Automatically Generated Settings (Do Not Edit Directly)"; } lines.add(appended); changed = true; } // 4) If changed, write back to file if (changed) { Files.write(settingsPath, lines); log.info( "Updated '{}' to '{}' (autoGenerated={}) in {}", fullPath, newValue, autoGenerated, settingsPath); } else { log.info("No changes for '{}' (already set to '{}').", fullPath, newValue); } } /** A small record-like class that holds: - lineIndex - indentSpaces */ private static class LineInfo { int lineIndex; int indentSpaces; public LineInfo(int lineIndex, int indentSpaces) { this.lineIndex = lineIndex; this.indentSpaces = indentSpaces; } } /** * Parse the YAML lines to build a map: "full.nested.key" -> (lineIndex, indentSpaces). We do a * naive indentation-based path stacking: - 2 spaces = 1 indent level - lines that start with * fewer or equal indentation pop the stack - lines that look like "key:" or "key: value" cause * a push */ private static Map<String, LineInfo> parseNestedYamlKeys(List<String> lines) { Map<String, LineInfo> result = new HashMap<>(); // We'll maintain a stack of (keyName, indentLevel). // Each line that looks like "myKey:" or "myKey: value" is a new "child" of the top of the // stack if indent is deeper. Deque<String> pathStack = new ArrayDeque<>(); Deque<Integer> indentStack = new ArrayDeque<>(); indentStack.push(-1); // sentinel for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); String trimmed = line.trim(); // skip blank lines, comment lines, or list items if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) { continue; } // check if there's a colon int colonIdx = trimmed.indexOf(':'); if (colonIdx <= 0) { // must have at least one char before ':' continue; } // parse out key String keyPart = trimmed.substring(0, colonIdx).trim(); if (keyPart.isEmpty()) { continue; } // count leading spaces for indentation int leadingSpaces = countLeadingSpaces(line); int indentLevel = leadingSpaces / 2; // assume 2 spaces per level // pop from stack until we get to a shallower indentation while (indentStack.peek() != null && indentStack.peek() >= indentLevel) { indentStack.pop(); pathStack.pop(); } // push the new key pathStack.push(keyPart); indentStack.push(indentLevel); // build the full path String[] arr = pathStack.toArray(new String[0]); List<String> reversed = Arrays.asList(arr); Collections.reverse(reversed); String fullPath = String.join(".", reversed); // store line info result.put(fullPath, new LineInfo(i, leadingSpaces)); } return result; } /** * Rewrite a single line to set a new value, preserving indentation and (optionally) the * existing or auto-generated inline comment. * * <p>For example, oldLine might be: " csrfDisabled: false # set to 'true' to disable CSRF * protection" newValue = "true" autoGenerated = false * * <p>We'll produce something like: " csrfDisabled: true # set to 'true' to disable CSRF * protection" */ private static String rewriteLine( String oldLine, int indentSpaces, String path, String newValue, boolean autoGenerated) { // We'll keep the exact leading indentation (indentSpaces). // Then "key: newValue". We'll try to preserve any existing inline comment unless // autoGenerated is true. // 1) Extract leading spaces from the old line (just in case they differ from indentSpaces). int actualLeadingSpaces = countLeadingSpaces(oldLine); String leading = oldLine.substring(0, actualLeadingSpaces); // 2) Remove leading spaces from the rest String trimmed = oldLine.substring(actualLeadingSpaces); // 3) Check for existing comment int hashIndex = trimmed.indexOf('#'); String lineWithoutComment = (hashIndex >= 0) ? trimmed.substring(0, hashIndex).trim() : trimmed.trim(); String oldComment = (hashIndex >= 0) ? trimmed.substring(hashIndex).trim() : ""; // 4) Rebuild "key: newValue" // The "key" here is everything before ':' in lineWithoutComment int colonIdx = lineWithoutComment.indexOf(':'); String existingKey = (colonIdx >= 0) ? lineWithoutComment.substring(0, colonIdx).trim() : path; // fallback if line is malformed StringBuilder sb = new StringBuilder(); sb.append(leading); // restore original leading spaces // "key: newValue" sb.append(existingKey).append(": ").append(newValue); // 5) If autoGenerated, add/replace comment if (autoGenerated) { sb.append(" # Automatically Generated Settings (Do Not Edit Directly)"); } else { // preserve the old comment if it exists if (!oldComment.isEmpty()) { sb.append(" ").append(oldComment); } } return sb.toString(); } private static int countLeadingSpaces(String line) { int count = 0; for (char c : line.toCharArray()) { if (c == ' ') count++; else break; } return count; } public static String generateMachineFingerprint() { try { // Get the MAC address StringBuilder sb = new StringBuilder(); InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); if (network == null) { Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces(); while (networks.hasMoreElements()) { NetworkInterface net = networks.nextElement(); byte[] mac = net.getHardwareAddress(); if (mac != null) { for (int i = 0; i < mac.length; i++) { sb.append(String.format("%02X", mac[i])); } break; // Use the first network interface with a MAC address } } } else { byte[] mac = network.getHardwareAddress(); if (mac != null) { for (int i = 0; i < mac.length; i++) { sb.append(String.format("%02X", mac[i])); } } } // Hash the MAC address for privacy and consistency MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8)); StringBuilder fingerprint = new StringBuilder(); for (byte b : hash) { fingerprint.append(String.format("%02x", b)); } return fingerprint.toString(); } catch (Exception e) { return "GenericID"; } } public static boolean isVersionHigher(String currentVersion, String compareVersion) { if (currentVersion == null || compareVersion == null) { return false; } // Split versions into components String[] current = currentVersion.split("\\."); String[] compare = compareVersion.split("\\."); // Get the length of the shorter version array int length = Math.min(current.length, compare.length); // Compare each component for (int i = 0; i < length; i++) { int currentPart = Integer.parseInt(current[i]); int comparePart = Integer.parseInt(compare[i]); if (currentPart > comparePart) { return true; } if (currentPart < comparePart) { return false; } } // If all components so far are equal, the longer version is considered higher return current.length > compare.length; } }