diff --git a/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java b/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java index fe8e7d5525aa652e1a7c3a5af7b7b4d5e07899e4..dc6668b3503d141faa88f21869b19c45f8aa3843 100644 --- a/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java +++ b/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java @@ -1,28 +1,63 @@ package it.inaf.oats.vospace.datamodel; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import net.ivoa.xml.vospace.v2.Node; public class NodeUtils { + /** + * Forbidden path chars are non printable characters and some symbols that + * could create issues to scripts that manipulates files. Other UTF-8 + * characters are allowed. Front end needs to pay attention to other allowed + * characters like & and parenthesis in any case, also to avoid XSS attacks. + */ + private static final Pattern FORBIDDEN_CHARS = Pattern.compile("[\\x00\\x08\\x0B\\x0C\\x0E-\\x1F" + Pattern.quote("<>?\":\\|'*") + "]"); + /** * Slash is a special character in defining REST endpoints and trying to * define a PathVariable containing slashes doesn't work, so the endpoint * has been defined using "/nodes/**" instead of "/nodes/{path}" and the - * path is extracted manually parsing the request URL. + * path is extracted manually parsing the request URL. Proper URL encoding + * handling is needed, considering also that slashes mustn't be escaped. */ public static String getPathFromRequestURLString(String requestUrlString) { + return getPathFromRequestURLString(requestUrlString, "/nodes/"); + } - String[] split = requestUrlString.split("/nodes/"); + public static String getPathFromRequestURLString(String requestUrlString, String prefix) { + + String[] split = requestUrlString.split(prefix); String path = "/"; if (split.length == 2) { - path += split[1]; + String[] parts = split[1].split("/"); + path += String.join("/", Arrays.stream(parts) + .map(p -> { + String decoded = URLDecoder.decode(p, StandardCharsets.UTF_8); + if (FORBIDDEN_CHARS.matcher(decoded).find()) { + throw new IllegalArgumentException("Path segment " + decoded + " contains an illegal character"); + } + return decoded; + }) + .collect(Collectors.toList())); } return path; } + public static String urlEncodePath(String path) { + String[] parts = path.split("/"); + return String.join("/", Arrays.stream(parts) + .map(p -> URLEncoder.encode(p, StandardCharsets.UTF_8)) + .collect(Collectors.toList())); + } + // This method assumes that URL is in the format /node1/node2/... // multiple slashes as a single separator are allowed // But the output has only single slash separators diff --git a/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java b/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1b3f4f4164c04e9c4eb7d2287731cac98ea23cdd --- /dev/null +++ b/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java @@ -0,0 +1,54 @@ +package it.inaf.oats.vospace.datamodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +public class NodeUtilsTest { + + @Test + public void testGetPathFromRequestURLString() { + + String requestUrl = "http://localhost/vospace/nodes/a/b/c/"; + assertEquals("/a/b/c", NodeUtils.getPathFromRequestURLString(requestUrl)); + } + + @Test + public void testGetPathWithSpacesFromRequestURLString() { + + String requestUrl = "http://localhost/vospace/nodes/a/b/c%20d%20%C3%A4.pdf"; + assertEquals("/a/b/c d ä.pdf", NodeUtils.getPathFromRequestURLString(requestUrl)); + } + + @Test + public void testEncodePathSpecialChars() { + + String specialChars = "ä è#+ /other/+-ò@"; + assertEquals("%C3%A4+%C3%A8%23%2B+/other/%2B-%C3%B2%40", NodeUtils.urlEncodePath(specialChars)); + } + + @Test + public void testIllegalBrakets() { + testIllegalChars("<no>.pdf"); + } + + @Test + public void testIllegalQuestionMark() { + testIllegalChars("???.pdf"); + } + + @Test + public void testIllegalQuotes() { + testIllegalChars("\"'.pdf"); + } + + private void testIllegalChars(String illegalString) { + boolean exception = false; + try { + NodeUtils.getPathFromRequestURLString("http://localhost/vospace/nodes/path/to/" + illegalString); + } catch (IllegalArgumentException ex) { + exception = true; + } + assertTrue(exception); + } +}