From 6f2c968925c28d7269e1dada1d97b7dff2d6cf07 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 3 Mar 2016 14:37:48 -0500 Subject: [PATCH] Support strict URI variable encoding The DefaulUriTemplateHandler now provides a strictEncoding property which if turned on encodes everything outside the reserved char set. This is in contrast to the default policy of encoding only illegal charaters depending on the URI component type. Issue: SPR-11652 --- .../web/util/DefaultUriTemplateHandler.java | 87 ++++++++++++++++--- .../util/DefaultUriTemplateHandlerTests.java | 36 +++++++- 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java index 00d62a624ad..3eb27c3009a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java @@ -16,8 +16,10 @@ package org.springframework.web.util; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,6 +38,8 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { private boolean parsePath; + private boolean strictEncoding; + /** * Configure a base URL to prepend URI templates with. The base URL must @@ -83,19 +87,45 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { return this.parsePath; } + /** + * Whether to encode characters outside the unreserved set as defined in + * RFC 3986 Section 2. + * This ensures a URI variable value will not contain any characters with a + * reserved purpose. + *

By default this is set to {@code false} in which case only characters + * illegal for the given URI component are encoded. For example when expanding + * a URI variable into a path segment the "/" character is illegal and + * encoded. The ";" character however is legal and not encoded even though + * it has a reserved purpose. + *

Note: this property supersedes the need to also set + * the {@link #setParsePath parsePath} property. + * @param strictEncoding whether to perform strict encoding + * @since 4.3 + */ + public void setStrictEncoding(boolean strictEncoding) { + this.strictEncoding = strictEncoding; + } + + /** + * Whether to strictly encode any character outside the unreserved set. + */ + public boolean isStrictEncoding() { + return this.strictEncoding; + } + @Override public URI expand(String uriTemplate, Map uriVariables) { - UriComponentsBuilder builder = initUriComponentsBuilder(uriTemplate); - UriComponents url = builder.build().expand(uriVariables).encode(); - return insertBaseUrl(url); + UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); + UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables); + return insertBaseUrl(uriComponents); } @Override public URI expand(String uriTemplate, Object... uriVariables) { - UriComponentsBuilder builder = initUriComponentsBuilder(uriTemplate); - UriComponents url = builder.build().expand(uriVariables).encode(); - return insertBaseUrl(url); + UriComponentsBuilder uriComponentsBuilder = initUriComponentsBuilder(uriTemplate); + UriComponents uriComponents = expandAndEncode(uriComponentsBuilder, uriVariables); + return insertBaseUrl(uriComponents); } /** @@ -105,7 +135,7 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { */ protected UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate); - if (shouldParsePath()) { + if (shouldParsePath() && !isStrictEncoding()) { List pathSegments = builder.build().getPathSegments(); builder.replacePath(null); for (String pathSegment : pathSegments) { @@ -115,6 +145,43 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { return builder; } + protected UriComponents expandAndEncode(UriComponentsBuilder builder, Map uriVariables) { + if (!isStrictEncoding()) { + return builder.build().expand(uriVariables).encode(); + } + else { + Map encodedUriVars = new HashMap(uriVariables.size()); + for (Map.Entry entry : uriVariables.entrySet()) { + encodedUriVars.put(entry.getKey(), encodeValue(entry.getValue())); + } + return builder.build().expand(encodedUriVars); + } + } + + protected UriComponents expandAndEncode(UriComponentsBuilder builder, Object[] uriVariables) { + if (!isStrictEncoding()) { + return builder.build().expand(uriVariables).encode(); + } + else { + Object[] encodedUriVars = new Object[uriVariables.length]; + for (int i = 0; i < uriVariables.length; i++) { + encodedUriVars[i] = encodeValue(uriVariables[i]); + } + return builder.build().expand(encodedUriVars); + } + } + + private String encodeValue(Object value) { + String stringValue = (value != null ? value.toString() : ""); + try { + return UriUtils.encode(stringValue, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + // Should never happen + throw new IllegalStateException("Failed to encode URI variable", ex); + } + } + /** * Invoked after the URI template has been expanded and encoded to prepend * the configured {@link #setBaseUrl(String) baseUrl} if any. @@ -122,10 +189,10 @@ public class DefaultUriTemplateHandler implements UriTemplateHandler { * @return the final URI */ protected URI insertBaseUrl(UriComponents uriComponents) { - if (getBaseUrl() == null || uriComponents.getHost() != null) { - return uriComponents.toUri(); + String url = uriComponents.toUriString(); + if (getBaseUrl() != null && uriComponents.getHost() == null) { + url = getBaseUrl() + url; } - String url = getBaseUrl() + uriComponents.toUriString(); try { return new URI(url); } diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java index dd6c67c3d50..691d6da285f 100644 --- a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java @@ -49,7 +49,8 @@ public class DefaultUriTemplateHandlerTests { } @Test - public void expandWithFullPath() throws Exception { + public void parsePathOff() throws Exception { + this.handler.setParsePath(false); Map vars = new HashMap<>(2); vars.put("hotel", "1"); vars.put("publicpath", "pics/logo.png"); @@ -60,7 +61,7 @@ public class DefaultUriTemplateHandlerTests { } @Test - public void expandWithFullPathAndParsePathEnabled() throws Exception { + public void parsePathOn() throws Exception { this.handler.setParsePath(true); Map vars = new HashMap<>(2); vars.put("hotel", "1"); @@ -72,4 +73,35 @@ public class DefaultUriTemplateHandlerTests { assertEquals(expected, actual); } + @Test + public void strictEncodingOff() throws Exception { + this.handler.setStrictEncoding(false); + Map vars = new HashMap<>(2); + vars.put("userId", "john;doe"); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, vars); + URI expected = new URI("http://www.example.com/user/john;doe/dashboard"); + assertEquals(expected, actual); + } + + @Test + public void strictEncodingOnWithMap() throws Exception { + this.handler.setStrictEncoding(true); + Map vars = new HashMap<>(2); + vars.put("userId", "john;doe"); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, vars); + URI expected = new URI("http://www.example.com/user/john%3Bdoe/dashboard"); + assertEquals(expected, actual); + } + + @Test + public void strictEncodingOnWithArray() throws Exception { + this.handler.setStrictEncoding(true); + String template = "http://www.example.com/user/{userId}/dashboard"; + URI actual = this.handler.expand(template, "john;doe"); + URI expected = new URI("http://www.example.com/user/john%3Bdoe/dashboard"); + assertEquals(expected, actual); + } + }