Skip to content

Commit 080e753

Browse files
committed
Encode enough to make URI happy.
Plus a bunch of test cases around this unfortunate case. Closes square#1872
1 parent 071efc6 commit 080e753

File tree

4 files changed

+177
-53
lines changed

4 files changed

+177
-53
lines changed

okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java

+105-12
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,20 @@ public final class HttpUrlTest {
196196
assertEquals(HttpUrl.parse("http://user@host/path"), HttpUrl.parse("http://user@host/path"));
197197
}
198198

199+
/** Given multiple '@' characters, the last one is the delimiter. */
199200
@Test public void authorityWithMultipleAtSigns() throws Exception {
200-
assertEquals(HttpUrl.parse("http://foo%40bar@baz/path"),
201-
HttpUrl.parse("http://foo@bar@baz/path"));
202-
assertEquals(HttpUrl.parse("http://foo:pass1%40bar%3Apass2@baz/path"),
203-
HttpUrl.parse("http://foo:pass1@bar:pass2@baz/path"));
201+
HttpUrl httpUrl = HttpUrl.parse("http://foo@bar@baz/path");
202+
assertEquals("foo@bar", httpUrl.username());
203+
assertEquals("", httpUrl.password());
204+
assertEquals(HttpUrl.parse("http://foo%40bar@baz/path"), httpUrl);
205+
}
206+
207+
/** Given multiple ':' characters, the first one is the delimiter. */
208+
@Test public void authorityWithMultipleColons() throws Exception {
209+
HttpUrl httpUrl = HttpUrl.parse("http://foo:pass1@bar:pass2@baz/path");
210+
assertEquals("foo", httpUrl.username());
211+
assertEquals("pass1@bar:pass2", httpUrl.password());
212+
assertEquals(HttpUrl.parse("http://foo:pass1%40bar%3Apass2@baz/path"), httpUrl);
204213
}
205214

206215
@Test public void usernameAndPassword() throws Exception {
@@ -930,19 +939,103 @@ public final class HttpUrlTest {
930939
assertEquals("http://host/?d=abc!@[]%5E%60%7B%7D%7C%5C", uri.toString());
931940
}
932941

933-
@Test public void toUriSpecialPathCharacters() throws Exception {
942+
@Test public void toUriWithUsernameNoPassword() throws Exception {
943+
HttpUrl httpUrl = new HttpUrl.Builder()
944+
.scheme("http")
945+
.username("user")
946+
.host("host")
947+
.build();
948+
assertEquals("http://user@host/", httpUrl.toString());
949+
assertEquals("http://user@host/", httpUrl.uri().toString());
950+
}
951+
952+
@Test public void toUriUsernameSpecialCharacters() throws Exception {
934953
HttpUrl url = new HttpUrl.Builder()
935954
.scheme("http")
936-
.host("example.com")
937-
.addPathSegment("data=[out:json];node[\"name\"~\"Karlsruhe\"]" +
938-
"[\"place\"~\"city|village|town\"];out body;")
955+
.host("host")
956+
.username("=[]:;\"~|?#@^/$%*")
957+
.build();
958+
assertEquals("http://%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/", url.toString());
959+
assertEquals("http://%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/", url.uri().toString());
960+
}
961+
962+
@Test public void toUriPasswordSpecialCharacters() throws Exception {
963+
HttpUrl url = new HttpUrl.Builder()
964+
.scheme("http")
965+
.host("host")
966+
.username("user")
967+
.password("=[]:;\"~|?#@^/$%*")
939968
.build();
940-
URI uri = url.uri();
941-
assertEquals("http://example.com/data=%5Bout:json%5D;node%5B%22name%22~%22Karlsruhe%22%5D" +
942-
"%5B%22place%22~%22city%7Cvillage%7Ctown%22%5D;out%20body;",
943-
uri.toString());
969+
assertEquals("http://user:%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/", url.toString());
970+
assertEquals("http://user:%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/",
971+
url.uri().toString());
972+
}
973+
974+
@Test public void toUriPathSpecialCharacters() throws Exception {
975+
HttpUrl url = new HttpUrl.Builder()
976+
.scheme("http")
977+
.host("host")
978+
.addPathSegment("=[]:;\"~|?#@^/$%*")
979+
.build();
980+
assertEquals("http://host/=[]:;%22~%7C%3F%23@%5E%2F$%25*", url.toString());
981+
assertEquals("http://host/=%5B%5D:;%22~%7C%3F%23@%5E%2F$%25*", url.uri().toString());
944982
}
945983

984+
@Test public void toUriQueryParameterNameSpecialCharacters() throws Exception {
985+
HttpUrl url = new HttpUrl.Builder()
986+
.scheme("http")
987+
.host("host")
988+
.addQueryParameter("=[]:;\"~|?#@^/$%*", "a")
989+
.build();
990+
assertEquals("http://host/?%3D[]:;%22~|?%23@^/$%25*=a", url.toString());
991+
assertEquals("http://host/?%3D[]:;%22~%7C?%23@%5E/$%25*=a", url.uri().toString());
992+
}
993+
994+
@Test public void toUriQueryParameterValueSpecialCharacters() throws Exception {
995+
HttpUrl url = new HttpUrl.Builder()
996+
.scheme("http")
997+
.host("host")
998+
.addQueryParameter("a", "=[]:;\"~|?#@^/$%*")
999+
.build();
1000+
assertEquals("http://host/?a=%3D[]:;%22~|?%23@^/$%25*", url.toString());
1001+
assertEquals("http://host/?a=%3D[]:;%22~%7C?%23@%5E/$%25*", url.uri().toString());
1002+
}
1003+
1004+
@Test public void toUriQueryValueSpecialCharacters() throws Exception {
1005+
HttpUrl url = new HttpUrl.Builder()
1006+
.scheme("http")
1007+
.host("host")
1008+
.query("=[]:;\"~|?#@^/$%*")
1009+
.build();
1010+
assertEquals("http://host/?=[]:;%22~|?%23@^/$%25*", url.toString());
1011+
assertEquals("http://host/?=[]:;%22~%7C?%23@%5E/$%25*", url.uri().toString());
1012+
}
1013+
1014+
@Test public void toUriFragmentSpecialCharacters() throws Exception {
1015+
HttpUrl url = new HttpUrl.Builder()
1016+
.scheme("http")
1017+
.host("host")
1018+
.fragment("=[]:;\"~|?#@^/$%*")
1019+
.build();
1020+
assertEquals("http://host/#=[]:;\"~|?#@^/$%25*", url.toString());
1021+
assertEquals("http://host/#=[]:;%22~%7C?%23@%5E/$%25*", url.uri().toString());
1022+
}
1023+
1024+
@Test public void toUriWithMalformedPercentEscape() throws Exception {
1025+
HttpUrl url = new HttpUrl.Builder()
1026+
.scheme("http")
1027+
.host("host")
1028+
.encodedPath("/%xx")
1029+
.build();
1030+
assertEquals("http://host/%xx", url.toString());
1031+
try {
1032+
url.uri();
1033+
fail();
1034+
} catch (IllegalStateException expected) {
1035+
assertEquals("not valid as a java.net.URI: http://host/%xx", expected.getMessage());
1036+
}
1037+
}
1038+
9461039
@Test public void fromJavaNetUrl() throws Exception {
9471040
URL javaNetUrl = new URL("http://username:password@host/path?query#fragment");
9481041
HttpUrl httpUrl = HttpUrl.get(javaNetUrl);

okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Ex
395395
try {
396396
connection.connect();
397397
fail();
398-
} catch (IllegalStateException expected) {
398+
} catch (UnknownHostException expected) {
399399
}
400400
}
401401

@@ -2427,7 +2427,7 @@ private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws I
24272427
}
24282428

24292429
@Test public void malformedUrlThrowsUnknownHostException() throws IOException {
2430-
connection = client.open(new URL("http:///foo.html"));
2430+
connection = client.open(new URL("http://./foo.html"));
24312431
try {
24322432
connection.connect();
24332433
fail();

okhttp-tests/src/test/java/com/squareup/okhttp/UrlComponentEncodingTester.java

+32-21
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class UrlComponentEncodingTester {
168168
}
169169

170170
private final Map<Integer, Encoding> encodings;
171-
private final StringBuilder skipForUri = new StringBuilder();
171+
private final StringBuilder uriEscapedCodePoints = new StringBuilder();
172172

173173
public UrlComponentEncodingTester() {
174174
this.encodings = new LinkedHashMap<>(defaultEncodings);
@@ -186,7 +186,7 @@ public UrlComponentEncodingTester override(Encoding encoding, int... codePoints)
186186
* That class is more strict than the others.
187187
*/
188188
public UrlComponentEncodingTester skipForUri(int... codePoints) {
189-
skipForUri.append(new String(codePoints, 0, codePoints.length));
189+
uriEscapedCodePoints.append(new String(codePoints, 0, codePoints.length));
190190
return this;
191191
}
192192

@@ -202,9 +202,10 @@ public UrlComponentEncodingTester test(Component component) {
202202
testToUrl(codePoint, encoding, component);
203203
testFromUrl(codePoint, encoding, component);
204204

205-
if (skipForUri.indexOf(Encoding.IDENTITY.encode(codePoint)) == -1) {
206-
testToUri(codePoint, encoding, component);
207-
testFromUri(codePoint, encoding, component);
205+
if (codePoint != '%') {
206+
boolean uriEscaped = uriEscapedCodePoints.indexOf(
207+
Encoding.IDENTITY.encode(codePoint)) != -1;
208+
testUri(codePoint, encoding, component, uriEscaped);
208209
}
209210
}
210211
return this;
@@ -261,21 +262,29 @@ private void testFromUrl(int codePoint, Encoding encoding, Component component)
261262
}
262263
}
263264

264-
private void testToUri(int codePoint, Encoding encoding, Component component) {
265+
private void testUri(
266+
int codePoint, Encoding encoding, Component component, boolean uriEscaped) {
267+
String string = new String(new int[] { codePoint }, 0, 1);
265268
String encoded = encoding.encode(codePoint);
266269
HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
267270
URI uri = httpUrl.uri();
268-
if (!uri.toString().equals(uri.toString())) {
269-
fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
270-
}
271-
}
272-
273-
private void testFromUri(int codePoint, Encoding encoding, Component component) {
274-
String encoded = encoding.encode(codePoint);
275-
HttpUrl httpUrl = HttpUrl.parse(component.urlString(encoded));
276-
HttpUrl toAndFromUri = HttpUrl.get(httpUrl.uri());
277-
if (!toAndFromUri.equals(httpUrl)) {
278-
fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
271+
HttpUrl toAndFromUri = HttpUrl.get(uri);
272+
if (uriEscaped) {
273+
// The URI has more escaping than the HttpURL. Check that the decoded values still match.
274+
if (uri.toString().equals(httpUrl.toString())) {
275+
fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
276+
}
277+
if (!component.get(toAndFromUri).equals(string)) {
278+
fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
279+
}
280+
} else {
281+
// Check that the URI and HttpURL have the exact same escaping.
282+
if (!toAndFromUri.equals(httpUrl)) {
283+
fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
284+
}
285+
if (!uri.toString().equals(httpUrl.toString())) {
286+
fail(String.format("Encoding %s %#x using %s", component, codePoint, encoding));
287+
}
279288
}
280289
}
281290

@@ -358,10 +367,11 @@ public enum Component {
358367
return query.substring(1, query.length() - 1);
359368
}
360369
@Override public void set(HttpUrl.Builder builder, String value) {
361-
builder.query(value);
370+
builder.query("a" + value + "z");
362371
}
363372
@Override public String get(HttpUrl url) {
364-
return url.query();
373+
String query = url.query();
374+
return query.substring(1, query.length() - 1);
365375
}
366376
},
367377
FRAGMENT {
@@ -373,10 +383,11 @@ public enum Component {
373383
return fragment.substring(1, fragment.length() - 1);
374384
}
375385
@Override public void set(HttpUrl.Builder builder, String value) {
376-
builder.fragment(value);
386+
builder.fragment("a" + value + "z");
377387
}
378388
@Override public String get(HttpUrl url) {
379-
return url.fragment();
389+
String fragment = url.fragment();
390+
return fragment.substring(1, fragment.length() - 1);
380391
}
381392
};
382393

okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java

+38-18
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,13 @@ public final class HttpUrl {
258258
static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#";
259259
static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#";
260260
static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#";
261+
static final String PATH_SEGMENT_ENCODE_SET_URI = "[]";
261262
static final String QUERY_ENCODE_SET = " \"'<>#";
262263
static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&=";
264+
static final String QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}";
263265
static final String FORM_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#&!$(),~";
264266
static final String FRAGMENT_ENCODE_SET = "";
267+
static final String FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}";
265268

266269
/** Either "http" or "https". */
267270
private final String scheme;
@@ -324,19 +327,17 @@ public URL url() {
324327
}
325328

326329
/**
327-
* Attempt to convert this URL to a {@link URI java.net.URI}. This method throws an unchecked
328-
* {@link IllegalStateException} if the URL it holds isn't valid by URI's overly-stringent
329-
* standard. For example, URI rejects paths containing the '[' character. Consult that class for
330-
* the exact rules of what URLs are permitted.
330+
* Returns this URL as a {@link URI java.net.URI}. Because {@code URI} forbids certain characters
331+
* like {@code [} and {@code |}, the returned URI may escape more characters than this URL.
332+
*
333+
* <p>This method throws an unchecked {@link IllegalStateException} if it cannot be converted to a
334+
* URI even after escaping forbidden characters. In particular, URLs that contain malformed
335+
* percent escapes like {@code http://host/%xx} will trigger this exception.
331336
*/
332337
public URI uri() {
333338
try {
334-
String uriUserInfo = username + ":" + password;
335-
if (uriUserInfo.equals(":")) uriUserInfo = null;
336-
final int uriPort = port == defaultPort(scheme) ? -1 : port; // Don't include default port
337-
StringBuilder path = new StringBuilder();
338-
pathSegmentsToString(path, pathSegments);
339-
return new URI(scheme, uriUserInfo, host, uriPort, path.toString(), query(), fragment);
339+
String uri = newBuilder().reencodeForUri().toString();
340+
return new URI(uri);
340341
} catch (URISyntaxException e) {
341342
throw new IllegalStateException("not valid as a java.net.URI: " + url);
342343
}
@@ -577,12 +578,8 @@ public Builder newBuilder() {
577578
result.encodedUsername = encodedUsername();
578579
result.encodedPassword = encodedPassword();
579580
result.host = host;
580-
// If we're set to a default port, unset it, in case of a scheme change.
581-
if (port == defaultPort(scheme)) {
582-
result.port = -1;
583-
} else {
584-
result.port = port;
585-
}
581+
// If we're set to a default port, unset it in case of a scheme change.
582+
result.port = port != defaultPort(scheme) ? port : -1;
586583
result.encodedPathSegments.clear();
587584
result.encodedPathSegments.addAll(encodedPathSegments());
588585
result.encodedQuery(encodedQuery());
@@ -867,6 +864,31 @@ public Builder encodedFragment(String encodedFragment) {
867864
return this;
868865
}
869866

867+
/**
868+
* Re-encodes the components of this URL so that it satisfies (obsolete) RFC 2396, which is
869+
* particularly strict for certain components.
870+
*/
871+
Builder reencodeForUri() {
872+
for (int i = 0, size = encodedPathSegments.size(); i < size; i++) {
873+
String pathSegment = encodedPathSegments.get(i);
874+
encodedPathSegments.set(i,
875+
canonicalize(pathSegment, PATH_SEGMENT_ENCODE_SET_URI, true, false));
876+
}
877+
if (encodedQueryNamesAndValues != null) {
878+
for (int i = 0, size = encodedQueryNamesAndValues.size(); i < size; i++) {
879+
String component = encodedQueryNamesAndValues.get(i);
880+
if (component != null) {
881+
encodedQueryNamesAndValues.set(i,
882+
canonicalize(component, QUERY_COMPONENT_ENCODE_SET_URI, true, true));
883+
}
884+
}
885+
}
886+
if (encodedFragment != null) {
887+
encodedFragment = canonicalize(encodedFragment, FRAGMENT_ENCODE_SET_URI, true, false);
888+
}
889+
return this;
890+
}
891+
870892
public HttpUrl build() {
871893
if (scheme == null) throw new IllegalStateException("scheme == null");
872894
if (host == null) throw new IllegalStateException("host == null");
@@ -1363,8 +1385,6 @@ private static String domainToAscii(String input) {
13631385
String result = IDN.toASCII(input).toLowerCase(Locale.US);
13641386
if (result.isEmpty()) return null;
13651387

1366-
if (result == null) return null;
1367-
13681388
// Confirm that the IDN ToASCII result doesn't contain any illegal characters.
13691389
if (containsInvalidHostnameAsciiCodes(result)) {
13701390
return null;

0 commit comments

Comments
 (0)