diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java index b281069f5..66288a198 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java @@ -17,8 +17,10 @@ package com.squareup.okhttp; import com.squareup.okhttp.UrlComponentEncodingTester.Component; import com.squareup.okhttp.UrlComponentEncodingTester.Encoding; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; @@ -949,6 +951,27 @@ public final class HttpUrlTest { assertEquals(null, HttpUrl.get(uri)); } + @Test public void fromJavaNetUrl_checked() throws Exception { + HttpUrl httpUrl = HttpUrl.getChecked("http://username:password@host/path?query#fragment"); + assertEquals("http://username:password@host/path?query#fragment", httpUrl.toString()); + } + + @Test public void fromJavaNetUrlUnsupportedScheme_checked() throws Exception { + try { + HttpUrl.getChecked("mailto:user@example.com"); + fail(); + } catch (MalformedURLException e) { + } + } + + @Test public void fromJavaNetUrlBadHost_checked() throws Exception { + try { + HttpUrl.getChecked("http://hostw ithspace/"); + fail(); + } catch (UnknownHostException expected) { + } + } + @Test public void composeQueryWithComponents() throws Exception { HttpUrl base = HttpUrl.parse("http://host/"); HttpUrl url = base.newBuilder().addQueryParameter("a+=& b", "c+=& d").build(); diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java index 2ea599707..65c1c057a 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java @@ -3178,6 +3178,37 @@ public final class URLConnectionTest { assertContent("abc", client.open(server.getUrl("/"))); } + @Test public void urlWithSpaceInHost() throws Exception { + URLConnection urlConnection = client.open(new URL("http://and roid.com/")); + try { + urlConnection.getInputStream(); + fail(); + } catch (UnknownHostException expected) { + } + } + + @Test public void urlWithSpaceInHostViaHttpProxy() throws Exception { + server.enqueue(new MockResponse()); + URLConnection urlConnection = + client.open(new URL("http://and roid.com/"), server.toProxyAddress()); + + try { + // This test is to check that a NullPointerException is not thrown. + urlConnection.getInputStream(); + fail(); // the RI makes a bogus proxy request for "GET http://and roid.com/ HTTP/1.1" + } catch (UnknownHostException expected) { + } + } + + @Test public void urlHostWithNul() throws Exception { + URLConnection urlConnection = client.open(new URL("http://host\u0000/")); + try { + urlConnection.getInputStream(); + fail(); + } catch (UnknownHostException expected) { + } + } + /** Returns a gzipped copy of {@code bytes}. */ public Buffer gzip(String data) throws IOException { Buffer result = new Buffer(); diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java index 8db26d75f..1cddd3ecb 100644 --- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java +++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java @@ -46,10 +46,12 @@ import java.io.OutputStream; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.InetSocketAddress; +import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.Proxy; import java.net.SocketPermission; import java.net.URL; +import java.net.UnknownHostException; import java.security.Permission; import java.util.ArrayList; import java.util.Arrays; @@ -321,14 +323,16 @@ public class HttpURLConnectionImpl extends HttpURLConnection { } } - private HttpEngine newHttpEngine(String method, Connection connection, - RetryableSink requestBody, Response priorResponse) { + private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody, + Response priorResponse) throws MalformedURLException, UnknownHostException { // OkHttp's Call API requires a placeholder body; the real body will be streamed separately. RequestBody placeholderBody = HttpMethod.requiresRequestBody(method) ? EMPTY_REQUEST_BODY : null; + URL url = getURL(); + HttpUrl httpUrl = Internal.instance.getHttpUrlChecked(url.toString()); Request.Builder builder = new Request.Builder() - .url(getURL()) + .url(httpUrl) .method(method, placeholderBody); Headers headers = requestHeaders.build(); for (int i = 0, size = headers.size(); i < size; i++) { diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java index dc8c7f2e0..2f0abfa1f 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java @@ -561,7 +561,9 @@ public final class HttpUrl { /** Returns the URL that would be retrieved by following {@code link} from this URL. */ public HttpUrl resolve(String link) { - return new Builder().parse(this, link); + Builder builder = new Builder(); + Builder.ParseResult result = builder.parse(this, link); + return result == Builder.ParseResult.SUCCESS ? builder.build() : null; } public Builder newBuilder() { @@ -584,11 +586,13 @@ public final class HttpUrl { } /** - * Returns a new {@code OkUrl} representing {@code url} if it is a well-formed HTTP or HTTPS URL, - * or null if it isn't. + * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS + * URL, or null if it isn't. */ public static HttpUrl parse(String url) { - return new Builder().parse(null, url); + Builder builder = new Builder(); + Builder.ParseResult result = builder.parse(null, url); + return result == Builder.ParseResult.SUCCESS ? builder.build() : null; } /** @@ -599,6 +603,29 @@ public final class HttpUrl { return parse(url.toString()); } + /** + * Returns a new {@code HttpUrl} representing {@code url} if it is a well-formed HTTP or HTTPS + * URL, or throws an exception if it isn't. + * + * @throws MalformedURLException if there was a non-host related URL issue + * @throws UnknownHostException if the host was invalid + */ + static HttpUrl getChecked(String url) throws MalformedURLException, UnknownHostException { + Builder builder = new Builder(); + Builder.ParseResult result = builder.parse(null, url); + switch (result) { + case SUCCESS: + return builder.build(); + case INVALID_HOST: + throw new UnknownHostException("Invalid host: " + url); + case UNSUPPORTED_SCHEME: + case MISSING_SCHEME: + case INVALID_PORT: + default: + throw new MalformedURLException("Invalid URL: " + result + " for " + url); + } + } + public static HttpUrl get(URI uri) { return parse(uri.toString()); } @@ -883,7 +910,15 @@ public final class HttpUrl { return result.toString(); } - HttpUrl parse(HttpUrl base, String input) { + enum ParseResult { + SUCCESS, + MISSING_SCHEME, + UNSUPPORTED_SCHEME, + INVALID_PORT, + INVALID_HOST, + } + + ParseResult parse(HttpUrl base, String input) { int pos = skipLeadingAsciiWhitespace(input, 0, input.length()); int limit = skipTrailingAsciiWhitespace(input, pos, input.length()); @@ -897,12 +932,12 @@ public final class HttpUrl { this.scheme = "http"; pos += "http:".length(); } else { - return null; // Not an HTTP scheme. + return ParseResult.UNSUPPORTED_SCHEME; // Not an HTTP scheme. } } else if (base != null) { this.scheme = base.scheme; } else { - return null; // No scheme. + return ParseResult.MISSING_SCHEME; // No scheme. } // Authority. @@ -960,12 +995,12 @@ public final class HttpUrl { if (portColonOffset + 1 < componentDelimiterOffset) { this.host = canonicalizeHost(input, pos, portColonOffset); this.port = parsePort(input, portColonOffset + 1, componentDelimiterOffset); - if (this.port == -1) return null; // Invalid port. + if (this.port == -1) return ParseResult.INVALID_PORT; // Invalid port. } else { this.host = canonicalizeHost(input, pos, portColonOffset); this.port = defaultPort(this.scheme); } - if (this.host == null) return null; // Invalid host. + if (this.host == null) return ParseResult.INVALID_HOST; // Invalid host. pos = componentDelimiterOffset; break authority; } @@ -1002,7 +1037,7 @@ public final class HttpUrl { input, pos + 1, limit, FRAGMENT_ENCODE_SET, true, false); } - return build(); + return ParseResult.SUCCESS; } private void resolvePath(String input, int pos, int limit) { diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java index 1ef79b0f6..4ed8000a6 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -27,9 +27,11 @@ import com.squareup.okhttp.internal.http.Transport; import com.squareup.okhttp.internal.tls.OkHostnameVerifier; import java.io.IOException; import java.net.CookieHandler; +import java.net.MalformedURLException; import java.net.Proxy; import java.net.ProxySelector; import java.net.URLConnection; +import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; @@ -157,6 +159,11 @@ public class OkHttpClient implements Cloneable { public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback) { tlsConfiguration.apply(sslSocket, isFallback); } + + @Override public HttpUrl getHttpUrlChecked(String url) + throws MalformedURLException, UnknownHostException { + return HttpUrl.getChecked(url); + } }; } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java index 1e583ba1b..21bcbf5c3 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java @@ -21,6 +21,7 @@ import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.ConnectionSpec; import com.squareup.okhttp.Headers; +import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Protocol; import com.squareup.okhttp.Request; @@ -28,6 +29,8 @@ import com.squareup.okhttp.internal.http.HttpEngine; import com.squareup.okhttp.internal.http.RouteException; import com.squareup.okhttp.internal.http.Transport; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.UnknownHostException; import java.util.logging.Logger; import javax.net.ssl.SSLSocket; import okio.BufferedSink; @@ -85,6 +88,9 @@ public abstract class Internal { public abstract void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback); + public abstract HttpUrl getHttpUrlChecked(String url) + throws MalformedURLException, UnknownHostException; + // TODO delete the following when web sockets move into the main package. public abstract void callEnqueue(Call call, Callback responseCallback, boolean forWebSocket); public abstract void callEngineReleaseConnection(Call call) throws IOException;