1/*
2 * Copyright (C) 2015 Square, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.squareup.okhttp;
17
18import com.squareup.okhttp.UrlComponentEncodingTester.Component;
19import com.squareup.okhttp.UrlComponentEncodingTester.Encoding;
20import java.util.Arrays;
21import org.junit.Test;
22
23import static org.junit.Assert.assertEquals;
24
25public final class HttpUrlTest {
26  @Test public void parseTrimsAsciiWhitespace() throws Exception {
27    HttpUrl expected = HttpUrl.parse("http://host/");
28    assertEquals(expected, HttpUrl.parse("http://host/\f\n\t \r")); // Leading.
29    assertEquals(expected, HttpUrl.parse("\r\n\f \thttp://host/")); // Trailing.
30    assertEquals(expected, HttpUrl.parse(" http://host/ ")); // Both.
31    assertEquals(expected, HttpUrl.parse("    http://host/    ")); // Both.
32    assertEquals(expected, HttpUrl.parse("http://host/").resolve("   "));
33    assertEquals(expected, HttpUrl.parse("http://host/").resolve("  .  "));
34  }
35
36  @Test public void parseDoesNotTrimOtherWhitespaceCharacters() throws Exception {
37    // Whitespace characters list from Google's Guava team: http://goo.gl/IcR9RD
38    assertEquals("/%0B", HttpUrl.parse("http://h/\u000b").path()); // line tabulation
39    assertEquals("/%1C", HttpUrl.parse("http://h/\u001c").path()); // information separator 4
40    assertEquals("/%1D", HttpUrl.parse("http://h/\u001d").path()); // information separator 3
41    assertEquals("/%1E", HttpUrl.parse("http://h/\u001e").path()); // information separator 2
42    assertEquals("/%1F", HttpUrl.parse("http://h/\u001f").path()); // information separator 1
43    assertEquals("/%C2%85", HttpUrl.parse("http://h/\u0085").path()); // next line
44    assertEquals("/%C2%A0", HttpUrl.parse("http://h/\u00a0").path()); // non-breaking space
45    assertEquals("/%E1%9A%80", HttpUrl.parse("http://h/\u1680").path()); // ogham space mark
46    assertEquals("/%E1%A0%8E", HttpUrl.parse("http://h/\u180e").path()); // mongolian vowel separator
47    assertEquals("/%E2%80%80", HttpUrl.parse("http://h/\u2000").path()); // en quad
48    assertEquals("/%E2%80%81", HttpUrl.parse("http://h/\u2001").path()); // em quad
49    assertEquals("/%E2%80%82", HttpUrl.parse("http://h/\u2002").path()); // en space
50    assertEquals("/%E2%80%83", HttpUrl.parse("http://h/\u2003").path()); // em space
51    assertEquals("/%E2%80%84", HttpUrl.parse("http://h/\u2004").path()); // three-per-em space
52    assertEquals("/%E2%80%85", HttpUrl.parse("http://h/\u2005").path()); // four-per-em space
53    assertEquals("/%E2%80%86", HttpUrl.parse("http://h/\u2006").path()); // six-per-em space
54    assertEquals("/%E2%80%87", HttpUrl.parse("http://h/\u2007").path()); // figure space
55    assertEquals("/%E2%80%88", HttpUrl.parse("http://h/\u2008").path()); // punctuation space
56    assertEquals("/%E2%80%89", HttpUrl.parse("http://h/\u2009").path()); // thin space
57    assertEquals("/%E2%80%8A", HttpUrl.parse("http://h/\u200a").path()); // hair space
58    assertEquals("/%E2%80%8B", HttpUrl.parse("http://h/\u200b").path()); // zero-width space
59    assertEquals("/%E2%80%8C", HttpUrl.parse("http://h/\u200c").path()); // zero-width non-joiner
60    assertEquals("/%E2%80%8D", HttpUrl.parse("http://h/\u200d").path()); // zero-width joiner
61    assertEquals("/%E2%80%8E", HttpUrl.parse("http://h/\u200e").path()); // left-to-right mark
62    assertEquals("/%E2%80%8F", HttpUrl.parse("http://h/\u200f").path()); // right-to-left mark
63    assertEquals("/%E2%80%A8", HttpUrl.parse("http://h/\u2028").path()); // line separator
64    assertEquals("/%E2%80%A9", HttpUrl.parse("http://h/\u2029").path()); // paragraph separator
65    assertEquals("/%E2%80%AF", HttpUrl.parse("http://h/\u202f").path()); // narrow non-breaking space
66    assertEquals("/%E2%81%9F", HttpUrl.parse("http://h/\u205f").path()); // medium mathematical space
67    assertEquals("/%E3%80%80", HttpUrl.parse("http://h/\u3000").path()); // ideographic space
68  }
69
70  @Test public void scheme() throws Exception {
71    assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("http://host/"));
72    assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("Http://host/"));
73    assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("http://host/"));
74    assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("HTTP://host/"));
75    assertEquals(HttpUrl.parse("https://host/"), HttpUrl.parse("https://host/"));
76    assertEquals(HttpUrl.parse("https://host/"), HttpUrl.parse("HTTPS://host/"));
77    assertEquals(null, HttpUrl.parse("httpp://host/"));
78    assertEquals(null, HttpUrl.parse("0ttp://host/"));
79    assertEquals(null, HttpUrl.parse("ht+tp://host/"));
80    assertEquals(null, HttpUrl.parse("ht.tp://host/"));
81    assertEquals(null, HttpUrl.parse("ht-tp://host/"));
82    assertEquals(null, HttpUrl.parse("ht1tp://host/"));
83    assertEquals(null, HttpUrl.parse("httpss://host/"));
84  }
85
86  @Test public void parseNoScheme() throws Exception {
87    assertEquals(null, HttpUrl.parse("//host"));
88    assertEquals(null, HttpUrl.parse("/path"));
89    assertEquals(null, HttpUrl.parse("path"));
90    assertEquals(null, HttpUrl.parse("?query"));
91    assertEquals(null, HttpUrl.parse("#fragment"));
92  }
93
94  @Test public void resolveNoScheme() throws Exception {
95    HttpUrl base = HttpUrl.parse("http://host/a/b");
96    assertEquals(HttpUrl.parse("http://host2/"), base.resolve("//host2"));
97    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("/path"));
98    assertEquals(HttpUrl.parse("http://host/a/path"), base.resolve("path"));
99    assertEquals(HttpUrl.parse("http://host/a/b?query"), base.resolve("?query"));
100    assertEquals(HttpUrl.parse("http://host/a/b#fragment"), base.resolve("#fragment"));
101    assertEquals(HttpUrl.parse("http://host/a/b"), base.resolve(""));
102    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("\\path"));
103  }
104
105  @Test public void resolveUnsupportedScheme() throws Exception {
106    HttpUrl base = HttpUrl.parse("http://a/");
107    assertEquals(null, base.resolve("ftp://b"));
108    assertEquals(null, base.resolve("ht+tp://b"));
109    assertEquals(null, base.resolve("ht-tp://b"));
110    assertEquals(null, base.resolve("ht.tp://b"));
111  }
112
113  @Test public void resolveSchemeLikePath() throws Exception {
114    HttpUrl base = HttpUrl.parse("http://a/");
115    assertEquals(HttpUrl.parse("http://a/http//b/"), base.resolve("http//b/"));
116    assertEquals(HttpUrl.parse("http://a/ht+tp//b/"), base.resolve("ht+tp//b/"));
117    assertEquals(HttpUrl.parse("http://a/ht-tp//b/"), base.resolve("ht-tp//b/"));
118    assertEquals(HttpUrl.parse("http://a/ht.tp//b/"), base.resolve("ht.tp//b/"));
119  }
120
121  @Test public void parseAuthoritySlashCountDoesntMatter() throws Exception {
122    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:host/path"));
123    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:/host/path"));
124    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:\\host/path"));
125    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http://host/path"));
126    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:\\/host/path"));
127    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:/\\host/path"));
128    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:\\\\host/path"));
129    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:///host/path"));
130    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:\\//host/path"));
131    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:/\\/host/path"));
132    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http://\\host/path"));
133    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:\\\\/host/path"));
134    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:/\\\\host/path"));
135    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:\\\\\\host/path"));
136    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http:////host/path"));
137  }
138
139  @Test public void resolveAuthoritySlashCountDoesntMatterWithDifferentScheme() throws Exception {
140    HttpUrl base = HttpUrl.parse("https://a/b/c");
141    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:host/path"));
142    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/host/path"));
143    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\host/path"));
144    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http://host/path"));
145    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\/host/path"));
146    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/\\host/path"));
147    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\\\host/path"));
148    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:///host/path"));
149    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\//host/path"));
150    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/\\/host/path"));
151    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http://\\host/path"));
152    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\\\/host/path"));
153    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/\\\\host/path"));
154    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\\\\\host/path"));
155    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:////host/path"));
156  }
157
158  @Test public void resolveAuthoritySlashCountMattersWithSameScheme() throws Exception {
159    HttpUrl base = HttpUrl.parse("http://a/b/c");
160    assertEquals(HttpUrl.parse("http://a/b/host/path"), base.resolve("http:host/path"));
161    assertEquals(HttpUrl.parse("http://a/host/path"), base.resolve("http:/host/path"));
162    assertEquals(HttpUrl.parse("http://a/host/path"), base.resolve("http:\\host/path"));
163    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http://host/path"));
164    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\/host/path"));
165    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/\\host/path"));
166    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\\\host/path"));
167    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:///host/path"));
168    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\//host/path"));
169    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/\\/host/path"));
170    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http://\\host/path"));
171    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\\\/host/path"));
172    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:/\\\\host/path"));
173    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:\\\\\\host/path"));
174    assertEquals(HttpUrl.parse("http://host/path"), base.resolve("http:////host/path"));
175  }
176
177  @Test public void username() throws Exception {
178    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http://@host/path"));
179    assertEquals(HttpUrl.parse("http://user@host/path"), HttpUrl.parse("http://user@host/path"));
180  }
181
182  @Test public void authorityWithMultipleAtSigns() throws Exception {
183    assertEquals(HttpUrl.parse("http://foo%40bar@baz/path"),
184        HttpUrl.parse("http://foo@bar@baz/path"));
185    assertEquals(HttpUrl.parse("http://foo:pass1%40bar%3Apass2@baz/path"),
186        HttpUrl.parse("http://foo:pass1@bar:pass2@baz/path"));
187  }
188
189  @Test public void usernameAndPassword() throws Exception {
190    assertEquals(HttpUrl.parse("http://username:password@host/path"),
191        HttpUrl.parse("http://username:password@host/path"));
192    assertEquals(HttpUrl.parse("http://username@host/path"),
193        HttpUrl.parse("http://username:@host/path"));
194  }
195
196  @Test public void passwordWithEmptyUsername() throws Exception {
197    // Chrome doesn't mind, but Firefox rejects URLs with empty usernames and non-empty passwords.
198    assertEquals(HttpUrl.parse("http://host/path"), HttpUrl.parse("http://:@host/path"));
199    assertEquals("password%40", HttpUrl.parse("http://:password@@host/path").password());
200  }
201
202  @Test public void unprintableCharactersArePercentEncoded() throws Exception {
203    assertEquals("/%00", HttpUrl.parse("http://host/\u0000").path());
204    assertEquals("/%08", HttpUrl.parse("http://host/\u0008").path());
205    assertEquals("/%EF%BF%BD", HttpUrl.parse("http://host/\ufffd").path());
206  }
207
208  @Test public void usernameCharacters() throws Exception {
209    new UrlComponentEncodingTester()
210        .override(Encoding.PERCENT, '[', ']', '{', '}', '|', '^', '\'', ';', '=', '@')
211        .override(Encoding.SKIP, ':', '/', '\\', '?', '#')
212        .test(Component.USER);
213  }
214
215  @Test public void passwordCharacters() throws Exception {
216    new UrlComponentEncodingTester()
217        .override(Encoding.PERCENT, '[', ']', '{', '}', '|', '^', '\'', ':', ';', '=', '@')
218        .override(Encoding.SKIP, '/', '\\', '?', '#')
219        .test(Component.PASSWORD);
220  }
221
222  @Test public void hostContainsIllegalCharacter() throws Exception {
223    assertEquals(null, HttpUrl.parse("http://\n/"));
224    assertEquals(null, HttpUrl.parse("http:// /"));
225    assertEquals(null, HttpUrl.parse("http://%20/"));
226  }
227
228  @Test public void hostIpv6() throws Exception {
229    // Square braces are absent from host()...
230    assertEquals("::1", HttpUrl.parse("http://[::1]/").host());
231
232    // ... but they're included in toString().
233    assertEquals("http://[::1]/", HttpUrl.parse("http://[::1]/").toString());
234
235    // IPv6 colons don't interfere with port numbers or passwords.
236    assertEquals(8080, HttpUrl.parse("http://[::1]:8080/").port());
237    assertEquals("password", HttpUrl.parse("http://user:password@[::1]/").password());
238    assertEquals("::1", HttpUrl.parse("http://user:password@[::1]:8080/").host());
239
240    // Permit the contents of IPv6 addresses to be percent-encoded...
241    assertEquals("::1", HttpUrl.parse("http://[%3A%3A%31]/").host());
242
243    // Including the Square braces themselves! (This is what Chrome does.)
244    assertEquals("::1", HttpUrl.parse("http://%5B%3A%3A1%5D/").host());
245  }
246
247  @Test public void port() throws Exception {
248    assertEquals(HttpUrl.parse("http://host/"), HttpUrl.parse("http://host:80/"));
249    assertEquals(HttpUrl.parse("http://host:99/"), HttpUrl.parse("http://host:99/"));
250    assertEquals(65535, HttpUrl.parse("http://host:65535/").port());
251    assertEquals(null, HttpUrl.parse("http://host:0/"));
252    assertEquals(null, HttpUrl.parse("http://host:65536/"));
253    assertEquals(null, HttpUrl.parse("http://host:-1/"));
254    assertEquals(null, HttpUrl.parse("http://host:a/"));
255    assertEquals(null, HttpUrl.parse("http://host:%39%39/"));
256  }
257
258  @Test public void pathCharacters() throws Exception {
259    new UrlComponentEncodingTester()
260        .override(Encoding.PERCENT, '^', '{', '}', '|')
261        .override(Encoding.SKIP, '\\', '?', '#')
262        .test(Component.PATH);
263  }
264
265  @Test public void queryCharacters() throws Exception {
266    new UrlComponentEncodingTester()
267        .override(Encoding.IDENTITY, '?', '`')
268        .override(Encoding.PERCENT, '\'')
269        .override(Encoding.SKIP, '#')
270        .test(Component.QUERY);
271  }
272
273  @Test public void fragmentCharacters() throws Exception {
274    new UrlComponentEncodingTester()
275        .override(Encoding.IDENTITY, ' ', '"', '#', '<', '>', '?', '`')
276        .test(Component.FRAGMENT);
277    // TODO(jwilson): don't percent-encode non-ASCII characters. (But do encode control characters!)
278  }
279
280  @Test public void relativePath() throws Exception {
281    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
282    assertEquals(HttpUrl.parse("http://host/a/b/d/e/f"), base.resolve("d/e/f"));
283    assertEquals(HttpUrl.parse("http://host/d/e/f"), base.resolve("../../d/e/f"));
284    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve(".."));
285    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../.."));
286    assertEquals(HttpUrl.parse("http://host/"), base.resolve("../../.."));
287    assertEquals(HttpUrl.parse("http://host/a/b/"), base.resolve("."));
288    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("././.."));
289    assertEquals(HttpUrl.parse("http://host/a/b/c/"), base.resolve("c/d/../e/../"));
290    assertEquals(HttpUrl.parse("http://host/a/b/..e/"), base.resolve("..e/"));
291    assertEquals(HttpUrl.parse("http://host/a/b/e/f../"), base.resolve("e/f../"));
292    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("%2E."));
293    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve(".%2E"));
294    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("%2E%2E"));
295    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("%2e."));
296    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve(".%2e"));
297    assertEquals(HttpUrl.parse("http://host/a/"), base.resolve("%2e%2e"));
298    assertEquals(HttpUrl.parse("http://host/a/b/"), base.resolve("%2E"));
299    assertEquals(HttpUrl.parse("http://host/a/b/"), base.resolve("%2e"));
300  }
301
302  @Test public void pathWithBackslash() throws Exception {
303    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
304    assertEquals(HttpUrl.parse("http://host/a/b/d/e/f"), base.resolve("d\\e\\f"));
305    assertEquals(HttpUrl.parse("http://host/d/e/f"), base.resolve("../..\\d\\e\\f"));
306    assertEquals(HttpUrl.parse("http://host/"), base.resolve("..\\.."));
307  }
308
309  @Test public void relativePathWithSameScheme() throws Exception {
310    HttpUrl base = HttpUrl.parse("http://host/a/b/c");
311    assertEquals(HttpUrl.parse("http://host/a/b/d/e/f"), base.resolve("http:d/e/f"));
312    assertEquals(HttpUrl.parse("http://host/d/e/f"), base.resolve("http:../../d/e/f"));
313  }
314
315  @Test public void decodeUsername() {
316    assertEquals("user", HttpUrl.parse("http://user@host/").decodeUsername());
317    assertEquals("\uD83C\uDF69", HttpUrl.parse("http://%F0%9F%8D%A9@host/").decodeUsername());
318  }
319
320  @Test public void decodePassword() {
321    assertEquals("password", HttpUrl.parse("http://user:password@host/").decodePassword());
322    assertEquals(null, HttpUrl.parse("http://user:@host/").decodePassword());
323    assertEquals("\uD83C\uDF69", HttpUrl.parse("http://user:%F0%9F%8D%A9@host/").decodePassword());
324  }
325
326  @Test public void decodeSlashCharacterInDecodedPathSegment() {
327    assertEquals(Arrays.asList("a/b/c"),
328        HttpUrl.parse("http://host/a%2Fb%2Fc").decodePathSegments());
329  }
330
331  @Test public void decodeEmptyPathSegments() {
332    assertEquals(Arrays.asList(""),
333        HttpUrl.parse("http://host/").decodePathSegments());
334  }
335
336  @Test public void percentDecode() throws Exception {
337    assertEquals(Arrays.asList("\u0000"),
338        HttpUrl.parse("http://host/%00").decodePathSegments());
339    assertEquals(Arrays.asList("a", "\u2603", "c"),
340        HttpUrl.parse("http://host/a/%E2%98%83/c").decodePathSegments());
341    assertEquals(Arrays.asList("a", "\uD83C\uDF69", "c"),
342        HttpUrl.parse("http://host/a/%F0%9F%8D%A9/c").decodePathSegments());
343    assertEquals(Arrays.asList("a", "b", "c"),
344        HttpUrl.parse("http://host/a/%62/c").decodePathSegments());
345    assertEquals(Arrays.asList("a", "z", "c"),
346        HttpUrl.parse("http://host/a/%7A/c").decodePathSegments());
347    assertEquals(Arrays.asList("a", "z", "c"),
348        HttpUrl.parse("http://host/a/%7a/c").decodePathSegments());
349  }
350
351  @Test public void malformedPercentEncoding() {
352    assertEquals(Arrays.asList("a%f", "b"),
353        HttpUrl.parse("http://host/a%f/b").decodePathSegments());
354    assertEquals(Arrays.asList("%", "b"),
355        HttpUrl.parse("http://host/%/b").decodePathSegments());
356    assertEquals(Arrays.asList("%"),
357        HttpUrl.parse("http://host/%").decodePathSegments());
358  }
359
360  @Test public void malformedUtf8Encoding() {
361    // Replace a partial UTF-8 sequence with the Unicode replacement character.
362    assertEquals(Arrays.asList("a", "\ufffdx", "c"),
363        HttpUrl.parse("http://host/a/%E2%98x/c").decodePathSegments());
364  }
365}
366