diff --git a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java index 0333e4fe33..f3d10e3529 100644 --- a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java +++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java @@ -44,7 +44,8 @@ void testTemplate() throws Exception { fw.write("Value=${value}"); fw.close(); - final Configuration fmc = new Configuration(DEFAULT_INCOMPATIBLE_IMPROVEMENTS); + final Configuration fmc = + new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); fmc.setDirectoryForTemplateLoading(testDir); final Map map = Map.of("value", "myValue"); diff --git a/org.restlet/src/main/java/org/restlet/data/Reference.java b/org.restlet/src/main/java/org/restlet/data/Reference.java index 6d0a74d7d4..a4646c62d2 100644 --- a/org.restlet/src/main/java/org/restlet/data/Reference.java +++ b/org.restlet/src/main/java/org/restlet/data/Reference.java @@ -11,15 +11,14 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.logging.Level; +import java.util.regex.Pattern; import org.restlet.Context; -import org.restlet.engine.util.StringUtils; /** * Reference to a Uniform Resource Identifier (URI). Contrary to the java.net.URI class, this * interface represents mutable references. It strictly conforms to the RFC 3986 specifying URIs and - * follow its naming conventions.
+ * follows its naming conventions.
* *
  * URI reference        = absolute-reference | relative-reference
@@ -47,7 +46,7 @@
  *
  * 

The fundamental point to underline is the difference between a URI "reference" and a URI. * Contrary to a URI (the target identifier of a REST resource), a URI reference can be relative - * (with or without query and fragment part). This relative URI reference can then be resolved + * (with or without a query and fragment part). This relative URI reference can then be resolved * against a base reference via the getTargetRef() method which will return a new resolved Reference * instance, an absolute URI reference with no base reference and with no dot-segments (the path * segments "." and ".."). @@ -85,6 +84,8 @@ public class Reference { /** Helps to map characters and their validity as URI characters. */ private static final boolean[] charValidityMap = new boolean[127]; + private static final Pattern SCHEME_REGEXP = Pattern.compile("[a-zA-Z][a-zA-Z0-9+-.]*"); + static { // Initialize the map of valid characters. for (int character = 0; character < 127; character++) { @@ -129,8 +130,11 @@ public static String decode(String toDecode, CharacterSet characterSet) { Context.getCurrentLogger() .log( Level.WARNING, - "Unable to decode the string with the UTF-8 character set.", - uee); + uee, + () -> + "Unable to decode the string with the " + + characterSet.getName() + + " character set."); } return result; @@ -149,10 +153,10 @@ public static String encode(String toEncode) { /** * Encodes a given string using the standard URI encoding mechanism and the UTF-8 character set. * Useful to prevent the usage of '+' to encode spaces (%20 instead). The '*' characters are - * encoded as %2A and %7E are replaced by '~'. + * encoded as '%2A', and '%7E' are replaced by '~'. * * @param toEncode The string to encode. - * @param queryString True if the string to encode is part of a query string instead of a HTML + * @param queryString True if the string to encode is part of a query string instead of an HTML * form post. * @return The encoded string. */ @@ -163,10 +167,10 @@ public static String encode(String toEncode, boolean queryString) { /** * Encodes a given string using the standard URI encoding mechanism and the UTF-8 character set. * Useful to prevent the usage of '+' to encode spaces (%20 instead). The '*' characters are - * encoded as %2A and %7E are replaced by '~'. + * encoded as '%2A', and '%7E' are replaced by '~'. * * @param toEncode The string to encode. - * @param queryString True if the string to encode is part of a query string instead of a HTML + * @param queryString True if the string to encode is part of a query string instead of an HTML * form post. * @param characterSet The supported character encoding. * @return The encoded string. @@ -232,6 +236,18 @@ private static boolean isDigit(int character) { return (character >= '0') && (character <= '9'); } + /** + * Indicates if the given character is a hexadecimal digit (0-9, a-f, A-F). + * + * @param character The character to test. + * @return True if the given character is a hexadecimal digit. + */ + private static boolean isHexDigit(int character) { + return isDigit(character) + || (character >= 'a' && character <= 'f') + || (character >= 'A' && character <= 'F'); + } + /** * Indicates if the given character is a generic URI component delimiter character. * @@ -652,6 +668,15 @@ public Reference addSegment(String value) { return this; } + /** + * @deprecated Use the {@code copy} method instead. + */ + @Override + @Deprecated(since = "2.7", forRemoval = true) + public Reference clone() { + return copy(); + } + public Reference copy() { final Reference newRef = new Reference(); @@ -677,93 +702,109 @@ public Reference copy() { * @return The original reference, eventually with invalid URI characters encoded. */ private String encodeInvalidCharacters(String uriRef) throws IllegalArgumentException { - if (uriRef == null) { - return null; - } + String result = uriRef; - if (containsOnlyValidCharacters(uriRef)) { - return uriRef; - } - - StringBuilder sb = new StringBuilder(); + if (uriRef != null) { + boolean valid = true; - for (int i = 0; i < uriRef.length(); i++) { - final char character = uriRef.charAt(i); - if (isValid(character)) { - if ((character == '%') && (i > uriRef.length() - 2)) { - sb.append("%25"); - } else { - sb.append(character); + // Ensure that all characters are valid, otherwise encode them + for (int i = 0; valid && (i < uriRef.length()); i++) { + char character = uriRef.charAt(i); + if (!isValid(character)) { + valid = false; + Context.getCurrentLogger() + .log( + Level.FINE, + "Invalid character detected in URI reference at index \"{0}\": \"{1}\". It will be automatically encoded.", + new Object[] {i, character}); + } else if ((character == '%') && (i > uriRef.length() - 2)) { + // A percent encoding character has been detected but + // without the necessary two hexadecimal digits following + valid = false; + Context.getCurrentLogger() + .log( + Level.FINE, + "Invalid percent encoding detected in URI reference at index \"{0}\": \"{1}\". It will be automatically encoded.", + new Object[] {i, character}); } - } else { - sb.append(encode(String.valueOf(character))); } - } - - return sb.toString(); - } - private static boolean containsOnlyValidCharacters(final String uriRef) { - boolean valid = true; + if (!valid) { + StringBuilder sb = new StringBuilder(); - // Ensure that all characters are valid, otherwise encode them - for (int i = 0; valid && (i < uriRef.length()); i++) { - final char character = uriRef.charAt(i); + for (int i = 0; (i < uriRef.length()); i++) { + if (isValid(uriRef.charAt(i))) { + if ((uriRef.charAt(i) == '%') && (i > uriRef.length() - 2)) { + sb.append("%25"); + } else { + sb.append(uriRef.charAt(i)); + } + } else { + sb.append(encode(String.valueOf(uriRef.charAt(i)))); + } + } - if (!isValid(character) - || ((character == '%') - && (i - > uriRef.length() - - 2))) { // missing the 2 necessary trailing characters - valid = false; - Context.getCurrentLogger() - .log( - Level.FINE, - "Invalid character \"{0}\" detected in URI at index {1} will be encoded.", - new Object[] {character, i}); + result = sb.toString(); } } - return valid; + + return result; } - /** {@inheritDoc} */ + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object The object to compare to. + * @return True if this object is the same as the obj argument. + */ @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Reference that)) { - return false; + public boolean equals(Object object) { + if (object instanceof Reference ref) { + if (this.internalRef == null) { + return ref.internalRef == null; + } + return this.internalRef.equals(ref.internalRef); } - return Objects.equals(this.internalRef, that.internalRef); + + return false; } /** * Returns the authority component for hierarchical identifiers. Includes the user info, host - * name, and the host port number.
- * Note that this method does no URI decoding. + * name and the host port number.
+ * Note that no URI decoding is done by this method. * * @return The authority component for hierarchical identifiers. */ public String getAuthority() { + final String result; + final String part = isRelative() ? getRelativePart() : getSchemeSpecificPart(); if ((part != null) && part.startsWith("//")) { - int index = part.indexOf('/', 2); - - if (index != -1) { - return part.substring(2, index); - } - - index = part.indexOf('?'); - if (index != -1) { - return part.substring(2, index); + // At this point, no fragment, but a query string may come from + // getSchemeSpecificPart() + int indexSlash = part.indexOf('/', 2); + int indexQuery = part.indexOf('?', 2); + + if (indexSlash != -1) { + if (indexQuery != -1) { + result = part.substring(2, Math.min(indexSlash, indexQuery)); + } else { + result = part.substring(2, indexSlash); + } + } else if (indexQuery != -1) { + result = part.substring(2, indexQuery); + } else { + result = part.substring(2); } - - return part.substring(2); + } else { + result = null; } - return null; + validateAuthority(result); + + return result; } /** @@ -792,7 +833,7 @@ public Reference getBaseRef() { * the first '.' character of the last path segment and ends with either the end of the segment * of with the first ';' character (matrix start). It is a token similar to file extensions * separated by '.' characters. The value can be omitted.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The extensions or null. * @see #getExtensionsAsArray() @@ -839,7 +880,7 @@ public String[] getExtensionsAsArray() { /** * Returns the fragment identifier.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The fragment identifier. */ @@ -866,7 +907,7 @@ public String getFragment(boolean decode) { /** * Returns the hierarchical part which is equivalent to the scheme-specific part less the query * component.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The hierarchical part. */ @@ -917,7 +958,7 @@ public String getHierarchicalPart(boolean decode) { /** * Returns the host domain name component for server-based hierarchical identifiers. It can also * be replaced by an IP address when no domain name was registered.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The host domain name component for server-based hierarchical identifiers. */ @@ -969,9 +1010,8 @@ public String getHostDomain(boolean decode) { } /** - * Returns the host identifier. Includes the scheme, the host name, and the host port number. - *
- * Note that this method does no URI decoding. + * Returns the host identifier. Includes the scheme, the host name and the host port number.
+ * Note that no URI decoding is done by this method. * * @return The host identifier. */ @@ -1009,15 +1049,7 @@ public int getHostPort() { int index = authority.indexOf(':', (indexIPV6 == -1) ? indexUI : indexIPV6); if (index != -1) { - try { - result = Integer.parseInt(authority.substring(index + 1)); - } catch (NumberFormatException nfe) { - Context.getCurrentLogger() - .log( - Level.WARNING, - "Can''t parse hostPort : [hostRef,requestUri]=[{0},{1}]", - new Object[] {getBaseRef(), this.internalRef}); - } + result = Integer.parseInt(authority.substring(index + 1)); } } @@ -1026,7 +1058,7 @@ public int getHostPort() { /** * Returns the absolute resource identifier, without the fragment.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The absolute resource identifier, without the fragment. */ @@ -1055,7 +1087,7 @@ public String getIdentifier(boolean decode) { /** * Returns the last segment of a hierarchical path.
* For example, the "/a/b/c" and "/a/b/c/" paths have the same segments: "a", "b", "c.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The last segment of a hierarchical path. */ @@ -1117,7 +1149,7 @@ public String getLastSegment(boolean decode, boolean excludeMatrix) { * Returns the optional matrix for hierarchical identifiers. A matrix part starts after the * first ';' character of the last path segment. It is a sequence of 'name=value' parameters * separated by ';' characters. The value can be omitted.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The matrix or null. */ @@ -1203,45 +1235,53 @@ public Reference getParentRef() { /** * Returns the path component for hierarchical identifiers. If no path is available, it returns * null.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The path component for hierarchical identifiers. */ public String getPath() { - String result = null; - String part = isRelative() ? getRelativePart() : getSchemeSpecificPart(); + final String result; + final String part = isRelative() ? getRelativePart() : getSchemeSpecificPart(); if (part != null) { if (part.startsWith("//")) { // Authority found - int index1 = part.indexOf('/', 2); + int indexSlash = part.indexOf('/', 2); - if (index1 != -1) { + if (indexSlash != -1) { // Path found - int index2 = part.indexOf('?'); + int indexQuery = part.indexOf('?', 2); - if (index2 != -1) { + if (indexQuery != -1) { // Query found - result = part.substring(Math.min(index1, index2), index2); + if (indexSlash < indexQuery) { + result = part.substring(indexSlash, indexQuery); + } else { + // '/' inside the query: No path found + result = null; + } } else { // No query found - result = part.substring(index1); + result = part.substring(indexSlash); } } else { - // Path must be empty in this case + // Path must be null in this case + result = null; } } else { // No authority found - int index = part.indexOf('?'); + int indexQuery = part.indexOf('?'); - if (index != -1) { + if (indexQuery != -1) { // Query found - result = part.substring(0, index); + result = part.substring(0, indexQuery); } else { // No query found result = part; } } + } else { + result = null; } return result; @@ -1261,7 +1301,7 @@ public String getPath(boolean decode) { /** * Returns the optional query component for hierarchical identifiers.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The query component or null. */ @@ -1270,7 +1310,7 @@ public String getQuery() { // Query found if (hasFragment()) { if (this.queryIndex < this.fragmentIndex) { - // Fragment found and query sign not inside fragment + // Fragment found and query sign not inside the fragment return this.internalRef.substring(this.queryIndex + 1, this.fragmentIndex); } @@ -1329,7 +1369,7 @@ public Form getQueryAsForm(CharacterSet characterSet) { /** * Returns the relative part of relative references, without the query and fragment. If the * reference is absolute, then null is returned.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The relative part. */ @@ -1366,9 +1406,9 @@ public Reference getRelativeRef() { * invoked for absolute references, otherwise an IllegalArgumentException will be raised. * * @param base The base reference to use. - * @return The current reference relatively to a base reference. * @throws IllegalArgumentException If the relative reference is computed, although the * reference or the base reference are not absolute or not hierarchical. + * @return The current reference relatively to a base reference. */ public Reference getRelativeRef(Reference base) { Reference result = null; @@ -1414,7 +1454,7 @@ public Reference getRelativeRef(Reference base) { // Both paths are strictly equivalent relativePath = "."; } else if (i == localPath.length()) { - // End of local path reached + // End of a local path reached if (basePath.charAt(i) == '/') { if ((i + 1) == basePath.length()) { // Both paths are strictly equivalent @@ -1433,7 +1473,7 @@ public Reference getRelativeRef(Reference base) { j = basePath.indexOf('/', j + 1)) segments++; // Build relative path - sb.repeat("../", Math.max(0, segments)); + for (int j = 0; j < segments; j++) sb.append("../"); int lastLocalSlash = localPath.lastIndexOf('/'); sb.append(localPath.substring(lastLocalSlash + 1)); @@ -1441,8 +1481,9 @@ public Reference getRelativeRef(Reference base) { relativePath = sb.toString(); } } else { - // The base path has a segment that starts like the last local path - // segment, but that is longer. Situation similar to a junction + // The base path has a segment that starts like + // the last local path segment, but that is longer. + // Situation similar to a junction final StringBuilder sb = new StringBuilder(); // Count segments @@ -1495,7 +1536,7 @@ public Reference getRelativeRef(Reference base) { j = basePath.indexOf('/', j + 1)) segments++; // Build relative path - sb.repeat("../", Math.max(0, segments)); + for (int j = 0; j < segments; j++) sb.append("../"); sb.append(localPath.substring(lastSlashIndex + 1)); @@ -1528,8 +1569,8 @@ public Reference getRelativeRef(Reference base) { } /** - * Returns the part of the resource identifier remaining after the base reference. Note that - * this method does not return the optional fragment. Must be used with the following + * Returns the part of the resource identifier remaining after the base reference. Note that the + * optional fragment is not returned by this method. Must be used with the following * prerequisites: * *

* *
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * - * @return The remaining resource parts or null if the prerequisites are not satisfied. + * @return The remaining resource part or null if the prerequisites are not satisfied. * @see #getRemainingPart(boolean) */ public String getRemainingPart() { @@ -1588,14 +1629,16 @@ public String getRemainingPart(boolean decode, boolean query) { /** * Returns the scheme component.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The scheme component. */ public String getScheme() { if (hasScheme()) { // Scheme found - return this.internalRef.substring(0, this.schemeIndex); + final String scheme = this.internalRef.substring(0, this.schemeIndex); + validateScheme(scheme); + return scheme; } // No scheme found @@ -1661,46 +1704,44 @@ public String getSchemeSpecificPart(boolean decode) { /** * Returns the list of segments in a hierarchical path.
* A new list is created for each call.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The segments of a hierarchical path. */ public List getSegments() { - final String path = getPath(); - if (StringUtils.isNullOrEmpty(path)) { - return new ArrayList<>(); - } - final List result = new ArrayList<>(); + final String path = getPath(); int start = -2; // The index of the slash starting the segment char current; - for (int i = 0; i < path.length(); i++) { - current = path.charAt(i); - - if (current == '/') { - if (start == -2) { - // Beginning of an absolute path or sequence of two - // separators - start = i; - } else { - // End of a segment - result.add(path.substring(start + 1, i)); - start = i; - } - } else { - if (start == -2) { - // Starting a new segment for a relative path - start = -1; + if (path != null) { + for (int i = 0; i < path.length(); i++) { + current = path.charAt(i); + + if (current == '/') { + if (start == -2) { + // Beginning of an absolute path or sequence of two + // separators + start = i; + } else { + // End of a segment + result.add(path.substring(start + 1, i)); + start = i; + } } else { - // Looking for the next character + if (start == -2) { + // Starting a new segment for a relative path + start = -1; + } else { + // Looking for the next character + } } } - } - if (start != -2) { - // Add the last segment - result.add(path.substring(start + 1)); + if (start != -2) { + // Add the last segment + result.add(path.substring(start + 1)); + } } return result; @@ -1718,9 +1759,7 @@ public List getSegments(boolean decode) { final List result = getSegments(); if (decode) { - for (int i = 0; i < result.size(); i++) { - result.set(i, decode(result.get(i))); - } + result.replaceAll(Reference::decode); } return result; @@ -1730,17 +1769,17 @@ public List getSegments(boolean decode) { * Returns the target reference. This method resolves relative references against the base * reference, then normalizes them. * - * @return The target reference. * @throws IllegalArgumentException If the base reference (after resolution) is not absolute. * @throws IllegalArgumentException If the reference is relative and not base reference has been * provided. + * @return The target reference. */ public Reference getTargetRef() { - Reference result = null; + final Reference result; // Step 1 - Resolve relative reference against their base reference if (isRelative() && (this.baseRef != null)) { - final Reference baseReference; + Reference baseReference = null; if (this.baseRef.isAbsolute()) { baseReference = this.baseRef; @@ -1824,7 +1863,7 @@ public Reference getTargetRef() { /** * Returns the user info component for server-based hierarchical identifiers.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @return The user info component for server-based hierarchical identifiers. */ @@ -1858,13 +1897,13 @@ public String getUserInfo(boolean decode) { /** * Indicates if this reference has file-like extensions on its last path segment. * - * @return True if there is are extensions. + * @return True if there are extensions. * @see #getExtensions() */ public boolean hasExtensions() { boolean result = false; - // If this reference ends with a "/", it cannot be a file. + // If these references end with a "/", it cannot be a file. final String path = getPath(); if (!((path != null) && path.endsWith("/"))) { final String lastSegment = getLastSegment(); @@ -1993,7 +2032,7 @@ public boolean isRelative() { /** * Normalizes the reference. Useful before comparison between references or when building a - * target reference from a base and a relative reference. + * target reference from a base reference and a relative reference. * * @return The current reference. */ @@ -2011,7 +2050,7 @@ public Reference normalize() { // 2. While the input buffer is not empty, the loop is as follows: while (!input.isEmpty()) { // A. If the input buffer begins with a prefix of "../" or "./", - // then remove that prefix from the input buffer. + // then remove that prefix from the input buffer; otherwise. if ((input.length() >= 3) && input.substring(0, 3).equals("../")) { input.delete(0, 3); } else if ((input.length() >= 2) && input.substring(0, 2).equals("./")) { @@ -2049,7 +2088,7 @@ else if ((input.length() == 1) && input.substring(0, 1).equals(".")) { // E. move the first path segment in the input buffer to the end of // the output buffer, including the initial "/" character (if any) - // and any later characters up to, but not including, the next + // and any subsequent characters up to, but not including, the next // "/" character or the end of the input buffer. else { int max = -1; @@ -2074,7 +2113,7 @@ else if ((input.length() == 1) && input.substring(0, 1).equals(".")) { // Finally, the output buffer is returned as the result setPath(output.toString()); - // Ensure that the scheme and host names are reset in lower case + // Ensure that the scheme and host names are reset in the lower case setScheme(getScheme()); setHostDomain(getHostDomain()); @@ -2120,23 +2159,25 @@ private void removeLastSegment(StringBuilder output) { */ public void setAuthority(String authority) { final String oldPart = isRelative() ? getRelativePart() : getSchemeSpecificPart(); - String newPart; + final String newPart; final String newAuthority = (authority == null) ? "" : "//" + authority; if (oldPart == null) { newPart = newAuthority; } else if (oldPart.startsWith("//")) { - int index = oldPart.indexOf('/', 2); + int indexSlash = oldPart.indexOf('/', 2); + int indexQuery = oldPart.indexOf('?', 2); - if (index != -1) { - newPart = newAuthority + oldPart.substring(index); - } else { - index = oldPart.indexOf('?'); - if (index != -1) { - newPart = newAuthority + oldPart.substring(index); + if (indexSlash != -1) { + if (indexQuery != -1) { + newPart = newAuthority + oldPart.substring(Math.min(indexSlash, indexQuery)); } else { - newPart = newAuthority; + newPart = newAuthority + oldPart.substring(indexSlash); } + } else if (indexQuery != -1) { + newPart = newAuthority + oldPart.substring(indexQuery); + } else { + newPart = newAuthority; } } else { newPart = newAuthority + oldPart; @@ -2172,7 +2213,7 @@ public void setBaseRef(String baseUri) { * '.' character of the last path segment and ends with either the end of the segment of with * the first ';' character (matrix start). It is a token similar to file extensions separated by * '.' characters. The value can be omitted.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @param extensions The extensions to set or null (without leading or trailing dots). * @see #getExtensions() @@ -2182,9 +2223,7 @@ public void setBaseRef(String baseUri) { public void setExtensions(String extensions) { final String lastSegment = getLastSegment(); - if (lastSegment == null) { - setLastSegment('.' + extensions); - } else { + if (lastSegment != null) { final int extensionIndex = lastSegment.indexOf('.'); final int matrixIndex = lastSegment.indexOf(';'); final StringBuilder sb = new StringBuilder(); @@ -2220,8 +2259,10 @@ public void setExtensions(String extensions) { } } - // Finally update the last segment + // Finally, update the last segment setLastSegment(sb.toString()); + } else { + setLastSegment('.' + extensions); } } @@ -2270,17 +2311,17 @@ public void setFragment(String fragment) { if (hasFragment()) { // Existing fragment if (fragment != null) { - this.internalRef = this.internalRef.substring(0, this.fragmentIndex + 1) + fragment; + setInternalRef(this.internalRef.substring(0, this.fragmentIndex + 1) + fragment); } else { - this.internalRef = this.internalRef.substring(0, this.fragmentIndex); + setInternalRef(this.internalRef.substring(0, this.fragmentIndex)); } } else { // No existing fragment if (fragment != null) { if (this.internalRef != null) { - this.internalRef = this.internalRef + '#' + fragment; + setInternalRef(this.internalRef + '#' + fragment); } else { - this.internalRef = '#' + fragment; + setInternalRef('#' + fragment); } } else { // Do nothing @@ -2305,7 +2346,7 @@ public void setHostDomain(String domain) { domain = ""; } else { // URI specification indicates that host names should be - // produced in lower case + // produced in the lower case domain = domain.toLowerCase(); } @@ -2400,7 +2441,7 @@ public void setIdentifier(String identifier) { /** * Sets the last segment of the path. If no path is available, then it creates one and adds a * slash in front of the given last segment.
- * Note that this method does no URI decoding. + * Note that no URI decoding is done by this method. * * @param lastSegment The last segment of a hierarchical path. */ @@ -2426,7 +2467,7 @@ public void setLastSegment(String lastSegment) { */ public void setPath(String path) { final String oldPart = isRelative() ? getRelativePart() : getSchemeSpecificPart(); - String newPart = null; + final String newPart; if (oldPart != null) { if (path == null) { @@ -2435,26 +2476,36 @@ public void setPath(String path) { if (oldPart.startsWith("//")) { // Authority found - final int index1 = oldPart.indexOf('/', 2); - - if (index1 != -1) { - // Path found - final int index2 = oldPart.indexOf('?'); + final int indexSlash = oldPart.indexOf('/', 2); + final int indexQuery = oldPart.indexOf('?', 2); - if (index2 != -1) { + if (indexSlash != -1) { + if (indexQuery != -1) { // Query found - newPart = oldPart.substring(0, index1) + path + oldPart.substring(index2); + if (indexSlash < indexQuery) { + newPart = + oldPart.substring(0, indexSlash) + + path + + oldPart.substring(indexQuery); + } else { + // '/' inside the query string: no path found + newPart = + oldPart.substring(0, indexQuery) + + path + + oldPart.substring(indexQuery); + } } else { // No query found - newPart = oldPart.substring(0, index1) + path; + newPart = oldPart.substring(0, indexSlash) + path; } } else { // No path found - final int index2 = oldPart.indexOf('?'); - - if (index2 != -1) { + if (indexQuery != -1) { // Query found - newPart = oldPart.substring(0, index2) + path + oldPart.substring(index2); + newPart = + oldPart.substring(0, indexQuery) + + path + + oldPart.substring(indexQuery); } else { // No query found newPart = oldPart + path; @@ -2462,11 +2513,11 @@ public void setPath(String path) { } } else { // No authority found - final int index = oldPart.indexOf('?'); + final int indexQuery = oldPart.indexOf('?'); - if (index != -1) { + if (indexQuery != -1) { // Query found - newPart = path + oldPart.substring(index); + newPart = path + oldPart.substring(indexQuery); } else { // No query found newPart = path; @@ -2506,21 +2557,21 @@ public void setQuery(String query) { if (hasFragment()) { // Fragment found if (!emptyQueryString) { - this.internalRef = + setInternalRef( this.internalRef.substring(0, this.queryIndex + 1) + query - + this.internalRef.substring(this.fragmentIndex); + + this.internalRef.substring(this.fragmentIndex)); } else { - this.internalRef = + setInternalRef( this.internalRef.substring(0, this.queryIndex) - + this.internalRef.substring(this.fragmentIndex); + + this.internalRef.substring(this.fragmentIndex)); } } else { // No fragment found if (!emptyQueryString) { - this.internalRef = this.internalRef.substring(0, this.queryIndex + 1) + query; + setInternalRef(this.internalRef.substring(0, this.queryIndex + 1) + query); } else { - this.internalRef = this.internalRef.substring(0, this.queryIndex); + setInternalRef(this.internalRef.substring(0, this.queryIndex)); } } } else { @@ -2528,24 +2579,24 @@ public void setQuery(String query) { if (hasFragment()) { // Fragment found if (!emptyQueryString) { - this.internalRef = + setInternalRef( this.internalRef.substring(0, this.fragmentIndex) + '?' + query - + this.internalRef.substring(this.fragmentIndex); + + this.internalRef.substring(this.fragmentIndex)); } else { - // Do nothing; + // Do nothing } } else { // No fragment found if (!emptyQueryString) { if (this.internalRef != null) { - this.internalRef = this.internalRef + '?' + query; + setInternalRef(this.internalRef + '?' + query); } else { - this.internalRef = '?' + query; + setInternalRef('?' + query); } } else { - // Do nothing; + // Do nothing } } } @@ -2569,13 +2620,13 @@ public void setRelativePart(String relativePart) { // This is a relative reference, no scheme found if (hasQuery()) { // Query found - this.internalRef = relativePart + this.internalRef.substring(this.queryIndex); + setInternalRef(relativePart + this.internalRef.substring(this.queryIndex)); } else if (hasFragment()) { // Fragment found - this.internalRef = relativePart + this.internalRef.substring(this.fragmentIndex); + setInternalRef(relativePart + this.internalRef.substring(this.fragmentIndex)); } else { // No fragment found - this.internalRef = relativePart; + setInternalRef(relativePart); } } @@ -2599,17 +2650,17 @@ public void setScheme(String scheme) { if (hasScheme()) { // Scheme found if (scheme != null) { - this.internalRef = scheme + this.internalRef.substring(this.schemeIndex); + setInternalRef(scheme + this.internalRef.substring(this.schemeIndex)); } else { - this.internalRef = this.internalRef.substring(this.schemeIndex + 1); + setInternalRef(this.internalRef.substring(this.schemeIndex + 1)); } } else { // No scheme found if (scheme != null) { if (this.internalRef == null) { - this.internalRef = scheme + ':'; + setInternalRef(scheme + ':'); } else { - this.internalRef = scheme + ':' + this.internalRef; + setInternalRef(scheme + ':' + this.internalRef); } } } @@ -2633,24 +2684,23 @@ public void setSchemeSpecificPart(String schemeSpecificPart) { // Scheme found if (hasFragment()) { // Fragment found - this.internalRef = + setInternalRef( this.internalRef.substring(0, this.schemeIndex + 1) + schemeSpecificPart - + this.internalRef.substring(this.fragmentIndex); + + this.internalRef.substring(this.fragmentIndex)); } else { // No fragment found - this.internalRef = - this.internalRef.substring(0, this.schemeIndex + 1) + schemeSpecificPart; + setInternalRef( + this.internalRef.substring(0, this.schemeIndex + 1) + schemeSpecificPart); } } else { // No scheme found if (hasFragment()) { // Fragment found - this.internalRef = - schemeSpecificPart + this.internalRef.substring(this.fragmentIndex); + setInternalRef(schemeSpecificPart + this.internalRef.substring(this.fragmentIndex)); } else { // No fragment found - this.internalRef = schemeSpecificPart; + setInternalRef(schemeSpecificPart); } } @@ -2773,15 +2823,11 @@ public java.net.URI toUri() { * @return A {@link java.net.URL} instance. */ public java.net.URL toUrl() { - java.net.URL result = null; - try { - result = toUri().toURL(); + return toUri().toURL(); } catch (java.net.MalformedURLException e) { throw new IllegalArgumentException("Malformed URL exception", e); } - - return result; } /** Updates internal indexes. */ @@ -2789,27 +2835,29 @@ private void updateIndexes() { if (this.internalRef != null) { // Compute the indexes final int firstSlashIndex = this.internalRef.indexOf('/'); - this.schemeIndex = this.internalRef.indexOf(':'); + final int firstColonIndex = this.internalRef.indexOf(':'); - if ((firstSlashIndex != -1) && (this.schemeIndex > firstSlashIndex)) { + if ((firstSlashIndex != -1) && (firstColonIndex > firstSlashIndex)) { // We are in the rare case of a relative reference where one of // the path segments contains a colon character. In this case, // we ignore the colon as a valid scheme index. // Note that this colon can't be in the first segment as it is // forbidden by the URI RFC. this.schemeIndex = -1; + } else { + this.schemeIndex = firstColonIndex; } this.queryIndex = this.internalRef.indexOf('?'); this.fragmentIndex = this.internalRef.indexOf('#'); if (hasQuery() && hasFragment() && (this.queryIndex > this.fragmentIndex)) { - // Query sign inside the fragment + // Query sign inside a fragment this.queryIndex = -1; } if (hasQuery() && this.schemeIndex > this.queryIndex) { - // Colon sign inside the query + // Colon sign inside a query this.schemeIndex = -1; } @@ -2823,4 +2871,219 @@ private void updateIndexes() { this.fragmentIndex = -1; } } + + /** + * Validate an authority according to RFC 3986. + * + * @param authority The authority to validate. + */ + private void validateAuthority(String authority) { + if (authority == null) { + return; + } + + authority = validateUserInfoAndReturnRemaining(authority); + authority = validateIpV6AndReturnRemaining(authority); + + int portIndex = authority.indexOf(':'); + if (portIndex != -1) { + validateHostPort(authority.substring(portIndex + 1)); + } + } + + /** + * Validate the user info part of the authority, and return the remaining part of the authority. + */ + private String validateUserInfoAndReturnRemaining(final String authority) { + int atIndex = authority.indexOf('@'); + + if (atIndex != -1) { + if (authority.indexOf('@', atIndex + 1) != -1) { + throw new IllegalArgumentException( + "Invalid authority format: multiple '@' signs in userinfo"); + } + + final int ipV6StartIndex = authority.indexOf('['); + + if (ipV6StartIndex != -1 && ipV6StartIndex < atIndex) { + throw new IllegalArgumentException("Invalid authority format"); + } + + return authority.substring(atIndex + 1); + } + + return authority; + } + + /** + * Validate an host port. + * + * @param hostPort The port to validate. + */ + private void validateHostPort(final String hostPort) { + if (hostPort != null) { + try { + Integer.parseInt(hostPort); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid port number format"); + } + } + } + + private String validateIpV6AndReturnRemaining(String authority) { + int ipV6StartIndex = authority.indexOf('['); + int ipV6EndIndex = authority.indexOf(']'); + + if (ipV6StartIndex > 0) { + throw new IllegalArgumentException( + "Invalid IPv6 address format: unexpected character before opening bracket"); + } + if (ipV6StartIndex == -1) { + return authority; + } + + if (ipV6EndIndex == -1) { + throw new IllegalArgumentException("Invalid IPv6 address format: no closing bracket"); + } + validateIpV6(authority.substring(1, ipV6EndIndex)); // trim brackets + + if (ipV6EndIndex + 1 < authority.length() && (authority.charAt(ipV6EndIndex + 1) != ':')) { + throw new IllegalArgumentException( + "Invalid authority format: unexpected character after closing bracket"); + } + + return authority.substring(ipV6EndIndex + 1); + } + + /** Validate an IPv6 address according to RFC 4291, including mixed IPv4 tail notation. */ + private void validateIpV6(String ipV6) { + if (ipV6 == null || ipV6.isEmpty()) { + throw new IllegalArgumentException("Invalid IPv6 address"); + } + + int doubleColonCount = countDoubleColons(ipV6); + if (doubleColonCount > 1) { + throw new IllegalArgumentException("Invalid IPv6 address format"); + } + + String[] parts = ipV6.split(":", -1); + boolean hasIpV4Tail = parts[parts.length - 1].contains("."); + + if (hasIpV4Tail) { + // Mixed notation (RFC 4291 §2.2): validate the IPv4 tail separately. + validateIpV4(parts[parts.length - 1]); + } + + validateIpV6GroupCount(parts, doubleColonCount, hasIpV4Tail); + + int hexPartCount = hasIpV4Tail ? parts.length - 1 : parts.length; + for (int i = 0; i < hexPartCount; i++) { + validateIpV6Part(parts[i], doubleColonCount); + } + } + + /** Returns the number of "::" occurrences in the given IPv6 string. */ + private int countDoubleColons(String ipV6) { + int count = 0; + int idx = ipV6.indexOf("::"); + while (idx != -1) { + count++; + idx = ipV6.indexOf("::", idx + 2); + } + return count; + } + + /** + * Validates the number of hex groups in an IPv6 address. + * Without "::" compression, exactly {@code maxHexGroups} groups are required. + * With "::", at most {@code maxHexGroups - 1} explicit groups are allowed. + */ + private void validateIpV6GroupCount( + String[] parts, int doubleColonCount, boolean hasIpV4Tail) { + int maxHexGroups = hasIpV4Tail ? 6 : 8; + int hexPartCount = hasIpV4Tail ? parts.length - 1 : parts.length; + + if (doubleColonCount == 0) { + if (hexPartCount != maxHexGroups) { + throw new IllegalArgumentException("Invalid IPv6 address format"); + } + } else { + int explicitGroups = 0; + for (int i = 0; i < hexPartCount; i++) { + if (!parts[i].isEmpty()) { + explicitGroups++; + } + } + if (explicitGroups > maxHexGroups - 1) { + throw new IllegalArgumentException("Invalid IPv6 address format"); + } + } + } + + /** Validate a single hex group of an IPv6 address. */ + private void validateIpV6Part(final String part, final int doubleColonCount) { + if (part.isEmpty() && doubleColonCount == 0) { + throw new IllegalArgumentException("Invalid IPv6 address format"); + } + if (!part.isEmpty()) { + if (part.length() > 4) { + throw new IllegalArgumentException("Invalid IPv6 address format"); + } + for (char c : part.toCharArray()) { + if (!isHexDigit(c)) { + throw new IllegalArgumentException("Invalid IPv6 address format"); + } + } + } + } + + /** Validate an embedded IPv4 address: four decimal octets each in [0, 255]. */ + private void validateIpV4(final String ipV4) { + String[] octets = ipV4.split("\\.", -1); + if (octets.length != 4) { + throw new IllegalArgumentException("Invalid embedded IPv4 address format"); + } + for (String octet : octets) { + if (octet.isEmpty()) { + throw new IllegalArgumentException("Invalid embedded IPv4 address format"); + } + if (octet.length() > 1 && octet.charAt(0) == '0') { + throw new IllegalArgumentException( + "Invalid embedded IPv4 address format: leading zero in octet"); + } + try { + int value = Integer.parseInt(octet); + if (value < 0 || value > 255) { + throw new IllegalArgumentException( + "Invalid embedded IPv4 address format: octet out of range"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid embedded IPv4 address format"); + } + } + } + + /** + * Validate a scheme according to RFC 3986. + * + * @param scheme The scheme to validate. + */ + private void validateScheme(final String scheme) { + if (scheme == null || scheme.isEmpty()) { + return; + } + + if (!SCHEME_REGEXP.matcher(scheme).matches()) { + throw new IllegalArgumentException("Invalid scheme format"); + } + } + + /** + * Tackle non-atomic operation on volatile field 'internalRef'. + * + * @param internalRef The new value of field 'internalRef'. + */ + private void setInternalRef(final String internalRef) { + this.internalRef = internalRef; + } } diff --git a/org.restlet/src/main/java/org/restlet/resource/ClientResource.java b/org.restlet/src/main/java/org/restlet/resource/ClientResource.java index eccb9a60f6..e968c40100 100644 --- a/org.restlet/src/main/java/org/restlet/resource/ClientResource.java +++ b/org.restlet/src/main/java/org/restlet/resource/ClientResource.java @@ -1,5 +1,5 @@ /** - * Copyright 2005-2024 Qlik + * Copyright 2005-2026 Qlik *

* The content of this file is subject to the terms of the Apache 2.0 open * source license available at https://www.opensource.org/licenses/apache-2.0 diff --git a/org.restlet/src/test/java/org/restlet/data/ReferenceTestCase.java b/org.restlet/src/test/java/org/restlet/data/ReferenceTestCase.java index 133a383131..cf6069df7f 100644 --- a/org.restlet/src/test/java/org/restlet/data/ReferenceTestCase.java +++ b/org.restlet/src/test/java/org/restlet/data/ReferenceTestCase.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -29,7 +30,7 @@ class ReferenceTestCase { protected static final String DEFAULT_SCHEME = "http"; - protected static final String DEFAULT_SCHEME_PART = "//"; + protected static final String DEFAULT_SCHEMEPART = "//"; /** * Returns a reference initialized with http://restlet.org. @@ -50,7 +51,7 @@ protected Reference getDefaultReference() { protected Reference getReference() { final Reference ref = new Reference(); ref.setScheme(DEFAULT_SCHEME); - ref.setSchemeSpecificPart(DEFAULT_SCHEME_PART); + ref.setSchemeSpecificPart(DEFAULT_SCHEMEPART); return ref; } @@ -124,7 +125,8 @@ void testEmptyRef() { "http://localhost/abc#fragment,/def", "http://localhost/abc?query#fragment,/def", "http://localhost/abc#fragment?query,/def", - "http://localhost#fragment/abc?query,/def" + "http://localhost#fragment/abc?query,/def", + "http://localhost?query/abc,/def" }) void testSetPath(String reference, String path) { final Reference ref = new Reference(reference); @@ -229,215 +231,6 @@ void testParentRef() { assertEquals("/foo/", parentRef.toString()); } - /** Test port getting/setting. */ - @ParameterizedTest - @ValueSource(ints = {8080, 9090}) - void testPort(int port) { - Reference ref = getDefaultReference(); - ref.setHostPort(port); - assertEquals(port, ref.getHostPort()); - } - - @Test - void testPortIPv6() { - Reference ref = new Reference("http://[::1]:8182"); - assertEquals(8182, ref.getHostPort()); - } - - @Test - void testProtocolConstructors() { - assertEquals("http://restlet.org", new Reference(Protocol.HTTP, "restlet.org").toString()); - assertEquals( - "https://restlet.org:8443", - new Reference(Protocol.HTTPS, "restlet.org", 8443).toString()); - - final Reference ref = new Reference(Protocol.HTTP, "restlet.org"); - ref.addQueryParameter("abc", "123"); - assertEquals("http://restlet.org?abc=123", ref.toString()); - } - - @Test - void testQuery() { - - Reference ref1 = new Reference("http://localhost/search?q=anythingelse%"); - String query = ref1.getQuery(); - assertEquals("q=anythingelse%25", query); - - Form queryForm = ref1.getQueryAsForm(); - assertEquals("anythingelse%", queryForm.getFirstValue("q")); - - Form extJsQuery = new Form("&_dc=1244741620627&callback=stcCallback1001"); - assertEquals("1244741620627", extJsQuery.getFirstValue("_dc")); - assertEquals("stcCallback1001", extJsQuery.getFirstValue("callback")); - - Reference ref = new Reference("http://localhost/v1/projects/13404"); - ref.addQueryParameter("dyn", "true"); - assertEquals("http://localhost/v1/projects/13404?dyn=true", ref.toString()); - } - - @Test - void testQueryWithUri() { - Reference ref = - new Reference( - new Reference("http://localhost:8111/"), - "http://localhost:8111/contrats/123?srvgwt=localhost:9997"); - assertEquals("contrats/123?srvgwt=localhost:9997", ref.getRelativeRef().toString()); - } - - @Test - void testRiap() { - Reference baseRef = new Reference("riap://component/exist/db/"); - Reference ref = new Reference(baseRef, "something.xq"); - assertEquals("riap://component/exist/db/something.xq", ref.getTargetRef().toString()); - } - - /** Test scheme getting/setting. */ - @Test - void testScheme() { - final Reference ref = getDefaultReference(); - assertEquals(DEFAULT_SCHEME, ref.getScheme()); - final String scheme = "https"; - ref.setScheme(scheme); - assertEquals(scheme, ref.getScheme()); - ref.setScheme(DEFAULT_SCHEME); - assertEquals(DEFAULT_SCHEME, ref.getScheme()); - } - - /** Test scheme specific part getting/setting. */ - @Test - void testSchemeSpecificPart() { - final Reference ref = getDefaultReference(); - String part = "//restlet.org"; - assertEquals(part, ref.getSchemeSpecificPart()); - part = "//restlet.net"; - ref.setSchemeSpecificPart(part); - assertEquals(part, ref.getSchemeSpecificPart()); - } - - /** Test setting of the last segment. */ - @Test - void testSetLastSegment() { - Reference ref = new Reference("http://localhost:1234"); - ref.addSegment("test"); - assertEquals("http://localhost:1234/test", ref.toString()); - - ref.setLastSegment("last"); - assertEquals("http://localhost:1234/last", ref.toString()); - - ref = new Reference("http://localhost:1234"); - ref.setLastSegment("last"); - assertEquals("http://localhost:1234/last", ref.toString()); - - ref.setLastSegment("test"); - assertEquals("http://localhost:1234/test", ref.toString()); - - ref.addSegment("last"); - assertEquals("http://localhost:1234/test/last", ref.toString()); - } - - @ParameterizedTest - @CsvSource({ - "http://localhost:81,//localhost:81", - "http://localhost:81?query,//localhost:81?query", - "http://localhost:81?query#fragment,//localhost:81?query", - "http://localhost:81#fragment,//localhost:81", - "http://localhost:81/#fragment,//localhost:81/", - "http://localhost:81/?query,//localhost:81/?query", - "http://localhost:81/?query=https://perdu.com,//localhost:81/?query=https://perdu.com", - }) - void testSchemeSpecificPart(final String uri, final String expected) { - Reference ref = new Reference(uri); - assertEquals(expected, ref.getSchemeSpecificPart()); - } - - @ParameterizedTest - @CsvSource({ - "http://localhost:81,localhost:81", - "http://localhost:81?query,localhost:81", - "http://localhost:81?query#fragment,localhost:81", - "http://localhost:81#fragment,localhost:81", - "http://localhost:81/#fragment,localhost:81", - "http://localhost:81/?query,localhost:81", - "http://localhost:81/?query=https://perdu.com,localhost:81" - }) - void testAuthority(final String uri, final String expected) { - Reference ref = new Reference(uri); - assertEquals(expected, ref.getAuthority()); - } - - @ParameterizedTest - @CsvSource({ - "http://localhost:81,", - "http://localhost:81/a,/a", - "http://localhost:81?query,", - "http://localhost:81/a?query,/a", - "http://localhost:81?query#fragment,", - "http://localhost:81#fragment/1234,", - "http://localhost:81/a#fragment/1234,/a", - "http://localhost:81/?query,/", - "http://localhost:81/?query=https://perdu.com/1234,/" - }) - void testPath(final String uri, final String expected) { - Reference ref = new Reference(uri); - assertEquals(expected, ref.getPath()); - } - - /** Test references that are unequal. */ - @Test - void testUnEquals() { - final String uri1 = "http://restlet.org/"; - final String uri2 = "http://restlet.net/"; - final Reference ref1 = new Reference(uri1); - final Reference ref2 = new Reference(uri2); - assertNotEquals(ref1, ref2); - } - - @Test - void testUserinfo() { - final Reference reference = new Reference("http://localhost:81"); - // This format is deprecated; however, we may prevent failures. - reference.setUserInfo("login:password"); - assertEquals("login:password@localhost:81", reference.getAuthority()); - assertEquals("localhost", reference.getHostDomain()); - assertEquals(81, reference.getHostPort()); - assertEquals("login:password", reference.getUserInfo()); - - reference.setHostDomain("[::1]"); - assertEquals("login:password@[::1]:81", reference.getAuthority()); - assertEquals("[::1]", reference.getHostDomain()); - assertEquals(81, reference.getHostPort()); - assertEquals("login:password", reference.getUserInfo()); - - reference.setHostDomain("www.example.com"); - assertEquals("login:password@www.example.com:81", reference.getAuthority()); - assertEquals("www.example.com", reference.getHostDomain()); - assertEquals(81, reference.getHostPort()); - assertEquals("login:password", reference.getUserInfo()); - - reference.setHostPort(82); - assertEquals("login:password@www.example.com:82", reference.getAuthority()); - assertEquals("www.example.com", reference.getHostDomain()); - assertEquals(82, reference.getHostPort()); - assertEquals("login:password", reference.getUserInfo()); - - reference.setUserInfo("login"); - assertEquals("login@www.example.com:82", reference.getAuthority()); - assertEquals("www.example.com", reference.getHostDomain()); - assertEquals(82, reference.getHostPort()); - assertEquals("login", reference.getUserInfo()); - } - - @Test - void testValidity() { - String uri = "http ://domain.tld/whatever/"; - Reference ref = new Reference(uri); - assertEquals("http%20://domain.tld/whatever/", ref.toString()); - - uri = "file:///C|/wherever\\whatever.swf"; - ref = new Reference(uri); - assertEquals("file:///C%7C/wherever%5Cwhatever.swf", ref.toString()); - } - @Nested class TestParsing { @@ -710,4 +503,351 @@ void testRelativizeAbsoluteReference( assertEquals(expectedRelativeUri, relativeRef.toString()); } } + + /** Test port getting/setting. */ + @ParameterizedTest + @ValueSource(ints = {8080, 9090}) + void testPort(int port) { + Reference ref = getDefaultReference(); + ref.setHostPort(port); + assertEquals(port, ref.getHostPort()); + } + + @Test + void testPortIPv6() { + Reference ref = new Reference("http://[::1]:8182"); + assertEquals(8182, ref.getHostPort()); + } + + @Test + void testProtocolConstructors() { + assertEquals("http://restlet.org", new Reference(Protocol.HTTP, "restlet.org").toString()); + assertEquals( + "https://restlet.org:8443", + new Reference(Protocol.HTTPS, "restlet.org", 8443).toString()); + + final Reference ref = new Reference(Protocol.HTTP, "restlet.org"); + ref.addQueryParameter("abc", "123"); + assertEquals("http://restlet.org?abc=123", ref.toString()); + } + + @Test + void testQuery() { + + Reference ref1 = new Reference("http://localhost/search?q=anythingelse%"); + String query = ref1.getQuery(); + assertEquals("q=anythingelse%25", query); + + Form queryForm = ref1.getQueryAsForm(); + assertEquals("anythingelse%", queryForm.getFirstValue("q")); + + Form extJsQuery = new Form("&_dc=1244741620627&callback=stcCallback1001"); + assertEquals("1244741620627", extJsQuery.getFirstValue("_dc")); + assertEquals("stcCallback1001", extJsQuery.getFirstValue("callback")); + + Reference ref = new Reference("http://localhost/v1/projects/13404"); + ref.addQueryParameter("dyn", "true"); + assertEquals("http://localhost/v1/projects/13404?dyn=true", ref.toString()); + } + + @Test + void testQueryWithUri() { + Reference ref = + new Reference( + new Reference("http://localhost:8111/"), + "http://localhost:8111/contrats/123?srvgwt=localhost:9997"); + assertEquals("contrats/123?srvgwt=localhost:9997", ref.getRelativeRef().toString()); + } + + @Test + void testRiap() { + Reference baseRef = new Reference("riap://component/exist/db/"); + Reference ref = new Reference(baseRef, "something.xq"); + assertEquals("riap://component/exist/db/something.xq", ref.getTargetRef().toString()); + } + + /** Test scheme getting/setting. */ + @Test + void testScheme() { + final Reference ref = getDefaultReference(); + assertEquals(DEFAULT_SCHEME, ref.getScheme()); + final String scheme = "https"; + ref.setScheme(scheme); + assertEquals(scheme, ref.getScheme()); + ref.setScheme(DEFAULT_SCHEME); + assertEquals(DEFAULT_SCHEME, ref.getScheme()); + } + + /** Test scheme specific part getting/setting. */ + @Test + void testSchemeSpecificPart() { + final Reference ref = getDefaultReference(); + String part = "//restlet.org"; + assertEquals(part, ref.getSchemeSpecificPart()); + part = "//restlet.net"; + ref.setSchemeSpecificPart(part); + assertEquals(part, ref.getSchemeSpecificPart()); + } + + /** Test setting of the last segment. */ + @Test + void testSetLastSegment() { + Reference ref = new Reference("http://localhost:1234"); + ref.addSegment("test"); + assertEquals("http://localhost:1234/test", ref.toString()); + + ref.setLastSegment("last"); + assertEquals("http://localhost:1234/last", ref.toString()); + + ref = new Reference("http://localhost:1234"); + ref.setLastSegment("last"); + assertEquals("http://localhost:1234/last", ref.toString()); + + ref.setLastSegment("test"); + assertEquals("http://localhost:1234/test", ref.toString()); + + ref.addSegment("last"); + assertEquals("http://localhost:1234/test/last", ref.toString()); + } + + @ParameterizedTest + @CsvSource({ + "http://localhost:81,//localhost:81", + "http://localhost:81?query,//localhost:81?query", + "http://localhost:81?query#fragment,//localhost:81?query", + "http://localhost:81#fragment,//localhost:81", + "http://localhost:81/#fragment,//localhost:81/", + "http://localhost:81/?query,//localhost:81/?query", + "http://localhost:81/?query=https://perdu.com,//localhost:81/?query=https://perdu.com", + }) + void testSchemeSpecificPart(final String uri, final String expected) { + Reference ref = new Reference(uri); + assertEquals(expected, ref.getSchemeSpecificPart()); + } + + @ParameterizedTest + @CsvSource({ + "http://localhost:81,localhost:81", + "http://localhost:81?query,localhost:81", + "http://localhost:81?query#fragment,localhost:81", + "http://localhost:81#fragment,localhost:81", + "http://localhost:81/#fragment,localhost:81", + "http://localhost:81/?query,localhost:81", + "http://localhost:81/?query=https://perdu.com,localhost:81", + "http://localhost:81?query=https://perdu.com,localhost:81", + }) + void testAuthority(final String uri, final String expected) { + Reference ref = new Reference(uri); + assertEquals(expected, ref.getAuthority()); + } + + @ParameterizedTest + @CsvSource({ + "http://localhost:81,", + "http://localhost:81/a,/a", + "http://localhost:81?query,", + "http://localhost:81/a?query,/a", + "http://localhost:81?query#fragment,", + "http://localhost:81#fragment/1234,", + "http://localhost:81/a#fragment/1234,/a", + "http://localhost:81/?query,/", + "http://localhost:81/?query=https://perdu.com/1234,/", + "http://localhost:81?query=https://perdu.com/1234," + }) + void testPath(final String uri, final String expected) { + Reference ref = new Reference(uri); + assertEquals(expected, ref.getPath()); + } + + @Test + void testTargetRef() { + Reference ref = + new Reference( + "http://twitter.com?status=RT @gamasutra: Devil May Cry : Born Again http://www.gamasutra.com/view/feature/177267/"); + Reference targetRef = + new Reference( + new Reference( + "http://www.gamasutra.com/view/feature/177267/devil_may_cry_born_again.php"), + ref) + .getTargetRef(); + assertEquals( + "http://twitter.com?status=RT%20@gamasutra:%20%20Devil%20May%20Cry%20:%20Born%20Again%20http://www.gamasutra.com/view/feature/177267/", + targetRef.toString()); + } + + /** Test references that are unequal. */ + @Test + void testUnEquals() { + final String uri1 = "http://restlet.org/"; + final String uri2 = "http://restlet.net/"; + final Reference ref1 = new Reference(uri1); + final Reference ref2 = new Reference(uri2); + assertNotEquals(ref1, ref2); + } + + @Test + void testUserinfo() { + final Reference reference = new Reference("http://localhost:81"); + // This format is deprecated; however, we may prevent failures. + reference.setUserInfo("login:password"); + assertEquals("login:password@localhost:81", reference.getAuthority()); + assertEquals("localhost", reference.getHostDomain()); + assertEquals(81, reference.getHostPort()); + assertEquals("login:password", reference.getUserInfo()); + + reference.setHostDomain("[::1]"); + assertEquals("login:password@[::1]:81", reference.getAuthority()); + assertEquals("[::1]", reference.getHostDomain()); + assertEquals(81, reference.getHostPort()); + assertEquals("login:password", reference.getUserInfo()); + + reference.setHostDomain("www.example.com"); + assertEquals("login:password@www.example.com:81", reference.getAuthority()); + assertEquals("www.example.com", reference.getHostDomain()); + assertEquals(81, reference.getHostPort()); + assertEquals("login:password", reference.getUserInfo()); + + reference.setHostPort(82); + assertEquals("login:password@www.example.com:82", reference.getAuthority()); + assertEquals("www.example.com", reference.getHostDomain()); + assertEquals(82, reference.getHostPort()); + assertEquals("login:password", reference.getUserInfo()); + + reference.setUserInfo("login"); + assertEquals("login@www.example.com:82", reference.getAuthority()); + assertEquals("www.example.com", reference.getHostDomain()); + assertEquals(82, reference.getHostPort()); + assertEquals("login", reference.getUserInfo()); + } + + @Test + void testValidity() { + String uri = "http ://domain.tld/whatever/"; + Reference ref = new Reference(uri); + assertEquals("http%20://domain.tld/whatever/", ref.toString()); + + uri = "file:///C|/wherever\\whatever.swf"; + ref = new Reference(uri); + assertEquals("file:///C%7C/wherever%5Cwhatever.swf", ref.toString()); + } + + @Nested + class TestFailures { + @ParameterizedTest + @ValueSource( + strings = { + "https://[192.168.0.1]127.0.0.1/", + "https://[192.168.0.1]vulndetector.com/", + "https://[normal.com@]vulndetector.com/", + "https://normal.com[user@vulndetector].com/", + "https://normal.com[@]vulndetector.com/", + "https://user:pwd@a[1:2:3:4]/", + "https://[1:2:3:4:5:6:7:8:9]", + "https://[1::1::1]", + "https://[1:2:3:]", + "https://[ffff::127.0.0.4000]", + "https://[0:0::vulndetector.com]:80", + "https://[2001:db8::vulndetector.com]", + "http://localhost:18:19", + "http://localhost:18ab" + }) + void shouldFailWhenParsingIncorrectHosts(String url) { + final Reference reference = new Reference(url); + assertThrows(IllegalArgumentException.class, reference::getAuthority); + } + + @ParameterizedTest + @ValueSource( + strings = {"https>://vulndetector.com/path", "https%25://vulndetector.com/path"}) + void shouldFailWhenParsingIncorrectScheme(String url) { + final Reference reference = new Reference(url); + assertThrows(IllegalArgumentException.class, reference::getScheme); + } + + // Issue: isAlpha accepts g-z / G-Z which are not valid hex digits in IPv6 groups. + @ParameterizedTest + @ValueSource( + strings = { + "https://[g:0:0:0:0:0:0:1]", + "https://[0:0:0:0:0:0:0:z]", + "https://[1:2:3:4:5:6:7:g]", + "https://[::g]", + "https://[G:0::1]", + }) + void shouldFailWhenIpV6GroupContainsNonHexChar(String url) { + final Reference reference = new Reference(url); + assertThrows(IllegalArgumentException.class, reference::getAuthority); + } + + // Issue: IPv4-embedded tail validation is missing; invalid octets must be rejected. + @ParameterizedTest + @ValueSource( + strings = { + "https://[::ffff:256.0.0.1]", + "https://[::ffff:192.168.1.1000]", + "https://[::ffff:192.168.1]", + "https://[::ffff:192.168.1.1.1]", + "https://[::ffff:-1.0.0.1]", + }) + void shouldFailWhenEmbeddedIpV4TailIsInvalid(String url) { + final Reference reference = new Reference(url); + assertThrows(IllegalArgumentException.class, reference::getAuthority); + } + + // Issue: multiple '@' in the userinfo section must be rejected. + @ParameterizedTest + @ValueSource( + strings = { + "https://user@evil.com@real.com/", + "https://user@attacker.com@legitimate.com/path", + "https://a@b@c/", + }) + void shouldFailWhenUserInfoContainsMultipleAtSigns(String url) { + final Reference reference = new Reference(url); + assertThrows(IllegalArgumentException.class, reference::getAuthority); + } + } + + // Issue: valid IPv4-embedded IPv6 addresses (RFC 4291 mixed notation) must be accepted. + @ParameterizedTest + @CsvSource({ + "https://[::ffff:192.168.1.1]/, [::ffff:192.168.1.1]", + "https://[::192.168.1.1]/, [::192.168.1.1]", + "https://[2001:db8::192.0.2.1]/, [2001:db8::192.0.2.1]", + "https://[::ffff:10.0.0.1]:8080/,[::ffff:10.0.0.1]:8080", + }) + void shouldAcceptValidEmbeddedIpV4(String url, String expectedAuthority) { + final Reference reference = new Reference(url); + assertEquals(expectedAuthority.strip(), reference.getAuthority()); + } + + // Issue: compressed IPv6 addresses with 7 explicit groups and :: must be accepted. + // split(":",−1) on e.g. "::1:2:3:4:5:6:7" produces 9 tokens; the group−count check + // must account for the extra empty token introduced by ::. + @ParameterizedTest + @CsvSource({ + "https://[::1:2:3:4:5:6:7]/, [::1:2:3:4:5:6:7]", + "https://[1:2:3:4:5:6:7::]/, [1:2:3:4:5:6:7::]", + "https://[1:2:3::4:5:6:7]/, [1:2:3::4:5:6:7]", + "https://[fe80::1:2:3:4:5:6]/, [fe80::1:2:3:4:5:6]", + }) + void shouldAcceptCompressedIpV6With7ExplicitGroups(String url, String expectedAuthority) { + final Reference reference = new Reference(url); + assertEquals(expectedAuthority.strip(), reference.getAuthority()); + } + + // Issue: leading zeros in IPv4 octets are historically interpreted as octal by some parsers + // and must be rejected to avoid address confusion. + @ParameterizedTest + @ValueSource( + strings = { + "https://[::ffff:192.168.01.1]", + "https://[::ffff:192.168.1.01]", + "https://[::01.0.0.1]", + "https://[::ffff:010.0.0.1]", + }) + void shouldFailWhenIpV4OctetHasLeadingZero(String url) { + final Reference reference = new Reference(url); + assertThrows(IllegalArgumentException.class, reference::getAuthority); + } }