diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java index 22e6a9591..7b7e1125c 100644 --- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java +++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java @@ -15,8 +15,6 @@ */ package com.squareup.okhttp.mockwebserver; -import junit.framework.TestCase; - import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; @@ -24,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import junit.framework.TestCase; public class CustomDispatcherTest extends TestCase { diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java index 6bbb42587..121a78d96 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java @@ -20,7 +20,6 @@ import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.http.HttpAuthenticator; import com.squareup.okhttp.internal.http.HttpEngine; import com.squareup.okhttp.internal.http.HttpTransport; -import com.squareup.okhttp.internal.http.RawHeaders; import com.squareup.okhttp.internal.http.SpdyTransport; import com.squareup.okhttp.internal.spdy.SpdyConnection; import java.io.BufferedInputStream; @@ -32,7 +31,6 @@ import java.io.OutputStream; import java.net.Proxy; import java.net.Socket; import java.net.SocketTimeoutException; -import java.net.URL; import java.util.Arrays; import javax.net.ssl.SSLSocket; @@ -309,23 +307,23 @@ public final class Connection implements Closeable { * retried if the proxy requires authorization. */ private void makeTunnel(TunnelRequest tunnelRequest) throws IOException { - RawHeaders requestHeaders = tunnelRequest.getRequestHeaders(); + Request request = tunnelRequest.getRequest(); + String requestLine = tunnelRequest.requestLine(); while (true) { - out.write(requestHeaders.toBytes()); - RawHeaders responseHeaders = RawHeaders.readHttpHeaders(in); + out.write(request.rawHeaders().toBytes(requestLine)); + Response response = HttpTransport.readResponse(request, in).build(); - switch (responseHeaders.getResponseCode()) { + switch (response.code()) { case HTTP_OK: return; case HTTP_PROXY_AUTH: - URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); - requestHeaders = HttpAuthenticator.processAuthHeader(route.address.authenticator, - HTTP_PROXY_AUTH, responseHeaders, requestHeaders, route.proxy, url); - if (requestHeaders != null) continue; + request = HttpAuthenticator.processAuthHeader( + route.address.authenticator, response, route.proxy); + if (request != null) continue; throw new IOException("Failed to authenticate with proxy"); default: throw new IOException( - "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode()); + "Unexpected response code for CONNECT: " + response.code()); } } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java index f8e7aa9ab..bb39385e5 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java +++ b/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java @@ -358,6 +358,7 @@ public final class HttpResponseCache extends ResponseCache implements OkResponse private final String url; private final RawHeaders varyHeaders; private final String requestMethod; + private final String statusLine; private final RawHeaders responseHeaders; private final Handshake handshake; @@ -422,8 +423,8 @@ public final class HttpResponseCache extends ResponseCache implements OkResponse } varyHeaders = varyHeadersBuilder.build(); + statusLine = reader.readLine(); RawHeaders.Builder responseHeadersBuilder = new RawHeaders.Builder(); - responseHeadersBuilder.setStatusLine(reader.readLine()); int responseHeaderLineCount = reader.readInt(); for (int i = 0; i < responseHeaderLineCount; i++) { responseHeadersBuilder.addLine(reader.readLine()); @@ -451,6 +452,7 @@ public final class HttpResponseCache extends ResponseCache implements OkResponse this.url = response.request().urlString(); this.varyHeaders = response.request().rawHeaders().getAll(response.getVaryFields()); this.requestMethod = response.request().method(); + this.statusLine = response.statusLine(); this.responseHeaders = response.rawHeaders(); this.handshake = response.handshake(); } @@ -466,7 +468,7 @@ public final class HttpResponseCache extends ResponseCache implements OkResponse writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n'); } - writer.write(responseHeaders.getStatusLine() + '\n'); + writer.write(statusLine + '\n'); writer.write(Integer.toString(responseHeaders.length()) + '\n'); for (int i = 0; i < responseHeaders.length(); i++) { writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n'); @@ -525,7 +527,8 @@ public final class HttpResponseCache extends ResponseCache implements OkResponse public Response response(Request request, DiskLruCache.Snapshot snapshot) { String contentType = responseHeaders.get("Content-Type"); String contentLength = responseHeaders.get("Content-Length"); - return new Response.Builder(request, responseHeaders.getResponseCode()) + return new Response.Builder(request) + .statusLine(statusLine) .rawHeaders(responseHeaders) .body(new CacheResponseBody(snapshot, contentType, contentLength)) .handshake(handshake) diff --git a/okhttp/src/main/java/com/squareup/okhttp/Job.java b/okhttp/src/main/java/com/squareup/okhttp/Job.java index 33b43fc4e..eea12b5b0 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Job.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Job.java @@ -18,9 +18,7 @@ package com.squareup.okhttp; import com.squareup.okhttp.internal.http.HttpAuthenticator; import com.squareup.okhttp.internal.http.HttpEngine; import com.squareup.okhttp.internal.http.HttpTransport; -import com.squareup.okhttp.internal.http.HttpsEngine; import com.squareup.okhttp.internal.http.Policy; -import com.squareup.okhttp.internal.http.RawHeaders; import java.io.IOException; import java.net.ProtocolException; import java.net.Proxy; @@ -63,10 +61,6 @@ final class Job implements Runnable, Policy { return true; } - @Override public URL getURL() { - return request.url(); - } - @Override public long getIfModifiedSince() { return 0; // For HttpURLConnection only. We let the cache drive this. } @@ -146,15 +140,7 @@ final class Job implements Runnable, Policy { } HttpEngine newEngine(Connection connection) throws IOException { - String protocol = request.url().getProtocol(); - RawHeaders requestHeaders = request.rawHeaders(); - if (protocol.equals("http")) { - return new HttpEngine(client, this, request.method(), requestHeaders, connection, null); - } else if (protocol.equals("https")) { - return new HttpsEngine(client, this, request.method(), requestHeaders, connection, null); - } else { - throw new AssertionError(); - } + return new HttpEngine(client, this, request, connection, null); } /** @@ -177,12 +163,8 @@ final class Job implements Runnable, Policy { } // fall-through case HTTP_UNAUTHORIZED: - RawHeaders successorRequestHeaders = HttpAuthenticator.processAuthHeader( - client.getAuthenticator(), response.code(), response.rawHeaders(), request.rawHeaders(), - selectedProxy, this.request.url()); - return successorRequestHeaders != null - ? request.newBuilder().rawHeaders(successorRequestHeaders).build() - : null; + return HttpAuthenticator.processAuthHeader( + client.getAuthenticator(), response, selectedProxy); case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java index 18e7a640f..57d0d0f67 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Request.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java @@ -216,6 +216,10 @@ public final class Request { return result != null ? result : (parsedHeaders = new ParsedHeaders(headers)); } + public boolean isHttps() { + return url().getProtocol().equals("https"); + } + /** Parsed request headers, computed on-demand and cached. */ private static class ParsedHeaders { /** Don't use a cache to satisfy this request. */ @@ -445,12 +449,6 @@ public final class Request { return this; } - // TODO: this shouldn't be public. - public Builder setRequestLine(String requestLine) { - headers.setRequestLine(requestLine); - return this; - } - public Builder setChunked() { headers.set("Transfer-Encoding", "chunked"); return this; @@ -527,11 +525,11 @@ public final class Request { public Builder addSpdyRequestHeaders( String method, String path, String version, String host, String scheme) { // TODO: populate the statusLine for the client's benefit? - headers.add(":method", method); - headers.add(":scheme", scheme); - headers.add(":path", path); - headers.add(":version", version); - headers.add(":host", host); + headers.set(":method", method); + headers.set(":scheme", scheme); + headers.set(":path", path); + headers.set(":version", version); + headers.set(":host", host); return this; } diff --git a/okhttp/src/main/java/com/squareup/okhttp/Response.java b/okhttp/src/main/java/com/squareup/okhttp/Response.java index 1c6c536ae..e8cb46de9 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/Response.java +++ b/okhttp/src/main/java/com/squareup/okhttp/Response.java @@ -20,6 +20,7 @@ import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.http.HeaderParser; import com.squareup.okhttp.internal.http.HttpDate; import com.squareup.okhttp.internal.http.RawHeaders; +import com.squareup.okhttp.internal.http.StatusLine; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; @@ -62,7 +63,7 @@ public final class Response { = Platform.get().getPrefix() + "-Selected-Transport"; private final Request request; - private final int code; + private final StatusLine statusLine; private final Handshake handshake; private final RawHeaders headers; private final Body body; @@ -72,7 +73,7 @@ public final class Response { private Response(Builder builder) { this.request = builder.request; - this.code = builder.code; + this.statusLine = builder.statusLine; this.handshake = builder.handshake; this.headers = builder.headers.build(); this.body = builder.body; @@ -95,8 +96,20 @@ public final class Response { return request; } + public String statusLine() { + return statusLine.getStatusLine(); + } + public int code() { - return code; + return statusLine.code(); + } + + public String statusMessage() { + return statusLine.message(); + } + + public int httpMinorVersion() { + return statusLine.httpMinorVersion(); } /** @@ -146,7 +159,8 @@ public final class Response { } public Builder newBuilder() { - return new Builder(request, code) + return new Builder(request) + .statusLine(statusLine) .handshake(handshake) .rawHeaders(headers) .body(body) @@ -265,7 +279,7 @@ public final class Response { * network response should be used. */ public boolean validate(Response network) { - if (network.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + if (network.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { return true; } @@ -288,7 +302,6 @@ public final class Response { */ public Response combine(Response network) throws IOException { RawHeaders.Builder result = new RawHeaders.Builder(); - result.setStatusLine(headers.getStatusLine()); for (int i = 0; i < headers.length(); i++) { String fieldName = headers.getFieldName(i); @@ -604,17 +617,29 @@ public final class Response { public static class Builder { private final Request request; - private final int code; + private StatusLine statusLine; private Handshake handshake; private RawHeaders.Builder headers = new RawHeaders.Builder(); private Body body; private Response redirectedBy; - public Builder(Request request, int code) { + public Builder(Request request) { if (request == null) throw new IllegalArgumentException("request == null"); - if (code <= 0) throw new IllegalArgumentException("code <= 0"); this.request = request; - this.code = code; + } + + public Builder statusLine(StatusLine statusLine) { + if (statusLine == null) throw new IllegalArgumentException("statusLine == null"); + this.statusLine = statusLine; + return this; + } + + public Builder statusLine(String statusLine) { + try { + return statusLine(new StatusLine(statusLine)); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } } public Builder handshake(Handshake handshake) { @@ -677,7 +702,7 @@ public final class Response { // TODO: this shouldn't be public. public Builder setResponseSource(ResponseSource responseSource) { - headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + headers.getResponseCode()); + headers.set(RESPONSE_SOURCE, responseSource.toString() + " " + statusLine.code()); return this; } @@ -688,8 +713,8 @@ public final class Response { } public Response build() { - if (request == null) throw new IllegalStateException("Response has no request."); - if (code == -1) throw new IllegalStateException("Response has no code."); + if (request == null) throw new IllegalStateException("request == null"); + if (statusLine == null) throw new IllegalStateException("statusLine == null"); return new Response(this); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java b/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java index 261595349..7efc422db 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java +++ b/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java @@ -15,7 +15,8 @@ */ package com.squareup.okhttp; -import com.squareup.okhttp.internal.http.RawHeaders; +import java.io.IOException; +import java.net.URL; import static com.squareup.okhttp.internal.Util.getDefaultPort; @@ -49,27 +50,30 @@ public final class TunnelRequest { this.proxyAuthorization = proxyAuthorization; } + String requestLine() { + return "CONNECT " + host + ":" + port + " HTTP/1.1"; + } + /** * If we're creating a TLS tunnel, send only the minimum set of headers. * This avoids sending potentially sensitive data like HTTP cookies to * the proxy unencrypted. */ - RawHeaders getRequestHeaders() { - RawHeaders.Builder result = new RawHeaders.Builder() - .setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1"); + Request getRequest() throws IOException { + Request.Builder result = new Request.Builder(new URL("https", host, port, "/")); // Always set Host and User-Agent. - result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port)); - result.set("User-Agent", userAgent); + result.header("Host", port == getDefaultPort("https") ? host : (host + ":" + port)); + result.header("User-Agent", userAgent); // Copy over the Proxy-Authorization header if it exists. if (proxyAuthorization != null) { - result.set("Proxy-Authorization", proxyAuthorization); + result.header("Proxy-Authorization", proxyAuthorization); } // Always set the Proxy-Connection to Keep-Alive for the benefit of // HTTP/1.0 proxies like Squid. - result.set("Proxy-Connection", "Keep-Alive"); + result.header("Proxy-Connection", "Keep-Alive"); return result.build(); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java index 2e385106e..d8ffcba58 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java @@ -18,6 +18,8 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.OkAuthenticator; import com.squareup.okhttp.OkAuthenticator.Challenge; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; import java.io.IOException; import java.net.Authenticator; import java.net.InetAddress; @@ -84,32 +86,33 @@ public final class HttpAuthenticator { /** * React to a failed authorization response by looking up new credentials. - * Returns headers for a subsequent attempt, or null if no further attempts + * Returns a request for a subsequent attempt, or null if no further attempts * should be made. */ - public static RawHeaders processAuthHeader(OkAuthenticator authenticator, int responseCode, - RawHeaders responseHeaders, RawHeaders requestHeaders, Proxy proxy, URL url) - throws IOException { + public static Request processAuthHeader( + OkAuthenticator authenticator, Response response, Proxy proxy) throws IOException { String responseField; String requestField; - if (responseCode == HTTP_UNAUTHORIZED) { + if (response.code() == HTTP_UNAUTHORIZED) { responseField = "WWW-Authenticate"; requestField = "Authorization"; - } else if (responseCode == HTTP_PROXY_AUTH) { + } else if (response.code() == HTTP_PROXY_AUTH) { responseField = "Proxy-Authenticate"; requestField = "Proxy-Authorization"; } else { throw new IllegalArgumentException(); // TODO: ProtocolException? } - List challenges = parseChallenges(responseHeaders, responseField); + List challenges = parseChallenges(response.rawHeaders(), responseField); if (challenges.isEmpty()) return null; // Could not find a challenge so end the request cycle. - Credential credential = responseHeaders.getResponseCode() == HTTP_PROXY_AUTH - ? authenticator.authenticateProxy(proxy, url, challenges) - : authenticator.authenticate(proxy, url, challenges); + + Request request = response.request(); + Credential credential = response.code() == HTTP_PROXY_AUTH + ? authenticator.authenticateProxy(proxy, request.url(), challenges) + : authenticator.authenticate(proxy, request.url(), challenges); if (credential == null) return null; // Couldn't satisfy the challenge so end the request cycle. // Add authorization credentials, bypassing the already-connected check. - return requestHeaders.newBuilder().set(requestField, credential.getHeaderValue()).build(); + return request.newBuilder().header(requestField, credential.getHeaderValue()).build(); } /** diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 771be3dd9..8311ddf84 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -34,7 +34,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.CacheRequest; import java.net.CookieHandler; -import java.net.Proxy; import java.net.URL; import java.net.UnknownHostException; import java.util.Date; @@ -139,22 +138,18 @@ public class HttpEngine { private boolean connectionReleased; /** + * @param request the HTTP request without a body. The body must be + * written via the engine's request body stream. * @param connection the connection used for an intermediate response * immediately prior to this request/response pair, such as a same-host * redirect. This engine assumes ownership of the connection and must * release it when it is unneeded. */ - public HttpEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders, + public HttpEngine(OkHttpClient client, Policy policy, Request request, Connection connection, RetryableOutputStream requestBodyOut) throws IOException { this.client = client; this.policy = policy; - - // This doesn't have a body. When we're sending requests to the cache, we don't need it. - this.request = new Request.Builder(policy.getURL()) - .method(method, null) - .rawHeaders(requestHeaders) - .build(); - + this.request = request; this.connection = connection; this.requestBodyOut = requestBodyOut; } @@ -186,11 +181,8 @@ public class HttpEngine { } this.responseSource = ResponseSource.CACHE; - RawHeaders gatewayTimeoutHeaders = new RawHeaders.Builder() - .setStatusLine("HTTP/1.1 504 Gateway Timeout") - .build(); - this.validatingResponse = new Response.Builder(request, 504) - .rawHeaders(gatewayTimeoutHeaders) + this.validatingResponse = new Response.Builder(request) + .statusLine(new StatusLine("HTTP/1.1 504 Gateway Timeout")) .body(EMPTY_BODY) .build(); promoteValidatingResponse(); @@ -218,7 +210,8 @@ public class HttpEngine { Response candidate = responseCache.get(request); if (candidate == null) return; - if (!acceptCacheResponseType(candidate)) { + // Drop the cached response if it's missing a required handshake. + if (request.isHttps() && candidate.handshake() == null) { Util.closeQuietly(candidate.body()); return; } @@ -296,10 +289,6 @@ public class HttpEngine { connection.updateReadTimeout(client.getReadTimeout()); } connected(connection); - if (connection.getRoute().getProxy() != client.getProxy()) { - // Update the request line if the proxy changed; it may need a host name. - request = request.newBuilder().setRequestLine(getRequestLine()).build(); - } } /** @@ -364,7 +353,7 @@ public class HttpEngine { if (response == null) { throw new IllegalStateException(); } - return response.getHeaders().getResponseCode(); + return response.code(); } public final InputStream getResponseBody() { @@ -378,14 +367,6 @@ public class HttpEngine { return connection; } - /** - * Returns true if {@code response} is of the right type. This condition is - * necessary but not sufficient for the cached response to be used. - */ - protected boolean acceptCacheResponseType(Response response) { - return true; - } - private void maybeCache() throws IOException { // Are we caching at all? if (!policy.getUseCaches()) return; @@ -468,7 +449,7 @@ public class HttpEngine { * See RFC 2616 section 4.3. */ public final boolean hasResponseBody() { - int responseCode = response.getHeaders().getResponseCode(); + int responseCode = response.code(); // HEAD requests never yield a body regardless of the response headers. if (request.method().equals("HEAD")) { @@ -500,8 +481,6 @@ public class HttpEngine { private void prepareRawRequestHeaders() throws IOException { Request.Builder result = request.newBuilder(); - result.setRequestLine(getRequestLine()); - if (request.getUserAgent() == null) { result.setUserAgent(getDefaultUserAgent()); } @@ -531,63 +510,12 @@ public class HttpEngine { CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { - result.addCookies(cookieHandler.get( - request.uri(), request.getHeaders().toMultimap(false))); + result.addCookies(cookieHandler.get(request.uri(), request.getHeaders().toMultimap(null))); } request = result.build(); } - /** - * Returns the request status line, like "GET / HTTP/1.1". This is exposed - * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so - * it needs to be set even if the transport is SPDY. - */ - String getRequestLine() { - String protocol = - (connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0"; - return request.method() + " " + requestString() + " " + protocol; - } - - private String requestString() { - URL url = request.url(); - if (includeAuthorityInRequestLine()) { - return url.toString(); - } else { - return requestPath(url); - } - } - - /** - * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never - * empty, even if the request URL is. Includes the query component if it - * exists. - */ - public static String requestPath(URL url) { - String fileOnly = url.getFile(); - if (fileOnly == null) { - return "/"; - } else if (!fileOnly.startsWith("/")) { - return "/" + fileOnly; - } else { - return fileOnly; - } - } - - /** - * Returns true if the request line should contain the full URL with host - * and port (like "GET http://android.com/foo HTTP/1.1") or only the path - * (like "GET /foo HTTP/1.1"). - * - *

This is non-final because for HTTPS it's never necessary to supply the - * full URL, even if a proxy is in use. - */ - protected boolean includeAuthorityInRequestLine() { - return connection == null - ? policy.usingProxy() // A proxy was requested. - : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected. - } - /** * Returns the TLS handshake created when this engine connected, or null if * no TLS connection was made. @@ -680,14 +608,21 @@ public class HttpEngine { initContentStream(transport.getTransferStream(cacheRequest)); } - protected TunnelRequest getTunnelConfig() { - return null; + private TunnelRequest getTunnelConfig() { + if (!request.isHttps()) return null; + + String userAgent = request.getUserAgent(); + if (userAgent == null) userAgent = getDefaultUserAgent(); + + URL url = request.url(); + return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent, + request.getProxyAuthorization()); } public void receiveHeaders(RawHeaders headers) throws IOException { CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { - cookieHandler.put(request.uri(), headers.toMultimap(true)); + cookieHandler.put(request.uri(), headers.toMultimap(null)); } } diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java index 202effc11..f6b88531f 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java @@ -135,18 +135,38 @@ public final class HttpTransport implements Transport { public void writeRequestHeaders() throws IOException { httpEngine.writingRequestHeaders(); RawHeaders headersToSend = httpEngine.getRequest().getHeaders(); - byte[] bytes = headersToSend.toBytes(); + String requestLine = RequestLine.get(httpEngine.getRequest(), + httpEngine.connection.getRoute().getProxy().type(), + httpEngine.connection.getHttpMinorVersion()); + byte[] bytes = headersToSend.toBytes(requestLine); requestOut.write(bytes); } @Override public Response readResponseHeaders() throws IOException { - RawHeaders rawHeaders = RawHeaders.readHttpHeaders(socketIn); - httpEngine.connection.setHttpMinorVersion(rawHeaders.getHttpMinorVersion()); - httpEngine.receiveHeaders(rawHeaders); - return new Response.Builder(httpEngine.getRequest(), rawHeaders.getResponseCode()) + Response response = readResponse(httpEngine.getRequest(), socketIn) .handshake(httpEngine.connection.getHandshake()) - .rawHeaders(rawHeaders) .build(); + httpEngine.connection.setHttpMinorVersion(response.httpMinorVersion()); + httpEngine.receiveHeaders(response.rawHeaders()); + return response; + } + + /** Parses bytes of a response header from an HTTP transport. */ + public static Response.Builder readResponse(Request request, InputStream in) throws IOException { + while (true) { + String statusLineString = Util.readAsciiLine(in); + StatusLine statusLine = new StatusLine(statusLineString); + + Response.Builder responseBuilder = new Response.Builder(request); + responseBuilder.statusLine(statusLine); + responseBuilder.header(Response.SELECTED_TRANSPORT, "http/1.1"); + + RawHeaders.Builder headersBuilder = new RawHeaders.Builder(); + headersBuilder.readHeaders(in); + responseBuilder.rawHeaders(headersBuilder.build()); + + if (statusLine.code() != HttpEngine.HTTP_CONTINUE) return responseBuilder; + } } public boolean makeReusable(boolean streamCanceled, OutputStream requestBodyOut, diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java index 69a8f6e87..5d42c063b 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -19,6 +19,8 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.Util; import java.io.FileNotFoundException; @@ -142,8 +144,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { */ @Override public final String getHeaderField(String fieldName) { try { - RawHeaders rawHeaders = getResponse().getResponse().getHeaders(); - return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName); + Response response = getResponse().getResponse(); + return fieldName == null ? response.statusLine() : response.getHeaders().get(fieldName); } catch (IOException e) { return null; } @@ -159,7 +161,8 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { @Override public final Map> getHeaderFields() { try { - return getResponse().getResponse().getHeaders().toMultimap(true); + Response response = getResponse().getResponse(); + return response.getHeaders().toMultimap(response.statusLine()); } catch (IOException e) { return Collections.emptyMap(); } @@ -170,7 +173,11 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { throw new IllegalStateException( "Cannot access request header fields after connection is set"); } - return requestHeaders.build().toMultimap(false); + + // For the request line property assigned to the null key, just use no proxy and HTTP 1.1. + Request request = new Request.Builder(getURL()).method(method, null).build(); + String requestLine = RequestLine.get(request, null, 1); + return requestHeaders.build().toMultimap(requestLine); } @Override public final InputStream getInputStream() throws IOException { @@ -267,13 +274,11 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { private HttpEngine newHttpEngine(String method, Connection connection, RetryableOutputStream requestBody) throws IOException { - if (url.getProtocol().equals("http")) { - return new HttpEngine(client, this, method, requestHeaders.build(), connection, requestBody); - } else if (url.getProtocol().equals("https")) { - return new HttpsEngine(client, this, method, requestHeaders.build(), connection, requestBody); - } else { - throw new AssertionError(); - } + Request request = new Request.Builder(getURL()) + .method(method, null) // No body: that's provided later! + .rawHeaders(requestHeaders.build()) + .build(); + return new HttpEngine(client, this, request, connection, requestBody); } /** @@ -390,10 +395,6 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { return !sslFailure && !protocolFailure; } - public HttpEngine getHttpEngine() { - return httpEngine; - } - enum Retry { NONE, SAME_CONNECTION, @@ -417,12 +418,10 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { } // fall-through case HTTP_UNAUTHORIZED: - RawHeaders successorRequestHeaders = HttpAuthenticator.processAuthHeader( - client.getAuthenticator(), getResponseCode(), - httpEngine.getResponse().getHeaders(), requestHeaders.build(), selectedProxy, - url); - if (successorRequestHeaders == null) return Retry.NONE; - requestHeaders = successorRequestHeaders.newBuilder(); + Request successorRequest = HttpAuthenticator.processAuthHeader(client.getAuthenticator(), + httpEngine.getResponse(), selectedProxy); + if (successorRequest == null) return Retry.NONE; + requestHeaders = successorRequest.getHeaders().newBuilder(); return Retry.SAME_CONNECTION; case HTTP_MULT_CHOICE: @@ -494,7 +493,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection implements Policy { } @Override public String getResponseMessage() throws IOException { - return getResponse().getResponse().getHeaders().getResponseMessage(); + return getResponse().getResponse().statusMessage(); } @Override public final int getResponseCode() throws IOException { diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java deleted file mode 100644 index 909a4f40d..000000000 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsEngine.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.squareup.okhttp.internal.http; - -import com.squareup.okhttp.Connection; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Response; -import com.squareup.okhttp.TunnelRequest; -import java.io.IOException; -import java.net.URL; - -import static com.squareup.okhttp.internal.Util.getEffectivePort; - -public final class HttpsEngine extends HttpEngine { - public HttpsEngine(OkHttpClient client, Policy policy, String method, RawHeaders requestHeaders, - Connection connection, RetryableOutputStream requestBody) throws IOException { - super(client, policy, method, requestHeaders, connection, requestBody); - } - - @Override protected boolean acceptCacheResponseType(Response response) { - return response.handshake() != null; - } - - @Override protected boolean includeAuthorityInRequestLine() { - // Even if there is a proxy, it isn't involved. Always request just the path. - return false; - } - - @Override protected TunnelRequest getTunnelConfig() { - String userAgent = getRequest().getUserAgent(); - if (userAgent == null) { - userAgent = getDefaultUserAgent(); - } - - URL url = getRequest().url(); - return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent, - getRequest().getProxyAuthorization()); - } -} diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java index edc07cf33..7e9e568ab 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Policy.java @@ -16,15 +16,11 @@ package com.squareup.okhttp.internal.http; import java.net.Proxy; -import java.net.URL; public interface Policy { /** Returns true if HTTP response caches should be used. */ boolean getUseCaches(); - /** Returns the current destination URL, possibly a redirect. */ - URL getURL(); - /** Returns the If-Modified-Since timestamp, or 0 if none is set. */ long getIfModifiedSince(); diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java index 2b37c40d2..93cab49d6 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java @@ -17,12 +17,10 @@ package com.squareup.okhttp.internal.http; -import com.squareup.okhttp.Response; import com.squareup.okhttp.internal.Util; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.net.ProtocolException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -67,41 +65,9 @@ public final class RawHeaders { }; private final List namesAndValues; - private final String requestLine; - private final String statusLine; - private final int httpMinorVersion; - private final int responseCode; - private final String responseMessage; private RawHeaders(Builder builder) { this.namesAndValues = Util.immutableList(builder.namesAndValues); - this.requestLine = builder.requestLine; - this.statusLine = builder.statusLine; - this.httpMinorVersion = builder.httpMinorVersion; - this.responseCode = builder.responseCode; - this.responseMessage = builder.responseMessage; - } - - public String getStatusLine() { - return statusLine; - } - - /** - * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 - * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. - */ - public int getHttpMinorVersion() { - return httpMinorVersion != -1 ? httpMinorVersion : 1; - } - - /** Returns the HTTP status code or -1 if it is unknown. */ - public int getResponseCode() { - return responseCode; - } - - /** Returns the HTTP status message or null if it is unknown. */ - public String getResponseMessage() { - return responseMessage; } /** Returns the number of field values. */ @@ -168,7 +134,7 @@ public final class RawHeaders { } /** Returns bytes of a request header for sending on an HTTP transport. */ - public byte[] toBytes() throws UnsupportedEncodingException { + public byte[] toBytes(String requestLine) throws UnsupportedEncodingException { StringBuilder result = new StringBuilder(256); result.append(requestLine).append("\r\n"); for (int i = 0; i < namesAndValues.size(); i += 2) { @@ -181,23 +147,13 @@ public final class RawHeaders { return result.toString().getBytes("ISO-8859-1"); } - /** Parses bytes of a response header from an HTTP transport. */ - public static RawHeaders readHttpHeaders(InputStream in) throws IOException { - Builder builder; - do { - builder = new Builder(); - builder.set(Response.SELECTED_TRANSPORT, "http/1.1"); - builder.setStatusLine(Util.readAsciiLine(in)); - builder.readHeaders(in); - } while (builder.responseCode == HttpEngine.HTTP_CONTINUE); - return builder.build(); - } - /** - * Returns an immutable map containing each field to its list of values. The - * status line is mapped to null. + * Returns an immutable map containing each field to its list of values. + * + * @param valueForNullKey the request line for requests, or the status line + * for responses. If non-null, this value is mapped to the null key. */ - public Map> toMultimap(boolean response) { + public Map> toMultimap(String valueForNullKey) { Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); for (int i = 0; i < namesAndValues.size(); i += 2) { String fieldName = namesAndValues.get(i); @@ -211,10 +167,8 @@ public final class RawHeaders { allValues.add(value); result.put(fieldName, Collections.unmodifiableList(allValues)); } - if (response && statusLine != null) { - result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine))); - } else if (requestLine != null) { - result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine))); + if (valueForNullKey != null) { + result.put(null, Collections.unmodifiableList(Collections.singletonList(valueForNullKey))); } return Collections.unmodifiableMap(result); } @@ -258,49 +212,9 @@ public final class RawHeaders { return result; } - /** Returns headers for a name value block containing a SPDY response. */ - public static RawHeaders fromNameValueBlock(List nameValueBlock) throws IOException { - if (nameValueBlock.size() % 2 != 0) { - throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock); - } - String status = null; - String version = null; - Builder builder = new Builder(); - builder.set(Response.SELECTED_TRANSPORT, "spdy/3"); - for (int i = 0; i < nameValueBlock.size(); i += 2) { - String name = nameValueBlock.get(i); - String values = nameValueBlock.get(i + 1); - for (int start = 0; start < values.length(); ) { - int end = values.indexOf('\0', start); - if (end == -1) { - end = values.length(); - } - String value = values.substring(start, end); - if (":status".equals(name)) { - status = value; - } else if (":version".equals(name)) { - version = value; - } else { - builder.namesAndValues.add(name); - builder.namesAndValues.add(value); - } - start = end + 1; - } - } - if (status == null) throw new ProtocolException("Expected ':status' header not present"); - if (version == null) throw new ProtocolException("Expected ':version' header not present"); - builder.setStatusLine(version + " " + status); - return builder.build(); - } - public Builder newBuilder() { Builder result = new Builder(); result.namesAndValues.addAll(namesAndValues); - result.requestLine = requestLine; - result.statusLine = statusLine; - result.httpMinorVersion = httpMinorVersion; - result.responseCode = responseCode; - result.responseMessage = responseMessage; return result; } @@ -315,61 +229,12 @@ public final class RawHeaders { public static class Builder { private final List namesAndValues = new ArrayList(20); - private String requestLine; - private String statusLine; - private int httpMinorVersion = 1; - private int responseCode = -1; - private String responseMessage; - - /** Sets the request line (like "GET / HTTP/1.1"). */ - public Builder setRequestLine(String requestLine) { - this.requestLine = requestLine.trim(); - return this; - } /** Equivalent to {@code build().get(fieldName)}, but potentially faster. */ public String get(String fieldName) { return RawHeaders.get(namesAndValues, fieldName); } - /** Equivalent to {@code build().getResponseCode()}, but potentially faster. */ - public int getResponseCode() { - return responseCode; - } - - /** Sets the response status line (like "HTTP/1.0 200 OK"). */ - public Builder setStatusLine(String statusLine) throws IOException { - // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 - if (this.responseMessage != null) { - throw new IllegalStateException("statusLine is already set"); - } - // We allow empty message without leading white space since some servers - // do not send the white space when the message is empty. - boolean hasMessage = statusLine.length() > 13; - if (!statusLine.startsWith("HTTP/1.") - || statusLine.length() < 12 - || statusLine.charAt(8) != ' ' - || (hasMessage && statusLine.charAt(12) != ' ')) { - throw new ProtocolException("Unexpected status line: " + statusLine); - } - int httpMinorVersion = statusLine.charAt(7) - '0'; - if (httpMinorVersion < 0 || httpMinorVersion > 9) { - throw new ProtocolException("Unexpected status line: " + statusLine); - } - int responseCode; - try { - responseCode = Integer.parseInt(statusLine.substring(9, 12)); - } catch (NumberFormatException e) { - throw new ProtocolException("Unexpected status line: " + statusLine); - } - this.responseMessage = hasMessage ? statusLine.substring(13) : ""; - this.responseCode = responseCode; - this.statusLine = statusLine; - this.httpMinorVersion = httpMinorVersion; - return this; - } - /** * Add an HTTP header line containing a field name, a literal colon, and a * value. This works around empty header names and header names that start diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java new file mode 100644 index 000000000..c918df3c2 --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java @@ -0,0 +1,55 @@ +package com.squareup.okhttp.internal.http; + +import com.squareup.okhttp.Request; +import java.net.Proxy; +import java.net.URL; + +public final class RequestLine { + private RequestLine() { + } + + /** + * Returns the request status line, like "GET / HTTP/1.1". This is exposed + * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so + * it needs to be set even if the transport is SPDY. + */ + static String get(Request request, Proxy.Type proxyType, int httpMinorVersion) { + StringBuilder result = new StringBuilder(); + result.append(request.method()); + result.append(" "); + + if (includeAuthorityInRequestLine(request, proxyType)) { + result.append(request.url()); + } else { + result.append(requestPath(request.url())); + } + + result.append(" "); + result.append(version(httpMinorVersion)); + return result.toString(); + } + + /** + * Returns true if the request line should contain the full URL with host + * and port (like "GET http://android.com/foo HTTP/1.1") or only the path + * (like "GET /foo HTTP/1.1"). + */ + private static boolean includeAuthorityInRequestLine(Request request, Proxy.Type proxyType) { + return !request.isHttps() && proxyType == Proxy.Type.HTTP; + } + + /** + * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never empty, + * even if the request URL is. Includes the query component if it exists. + */ + public static String requestPath(URL url) { + String pathAndQuery = url.getFile(); + if (pathAndQuery == null) return "/"; + if (!pathAndQuery.startsWith("/")) return "/" + pathAndQuery; + return pathAndQuery; + } + + public static String version(int httpMinorVersion) { + return httpMinorVersion == 1 ? "HTTP/1.1" : "HTTP/1.0"; + } +} diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java index d44bcc2b8..07b6adc4d 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.CacheRequest; +import java.net.ProtocolException; import java.net.URL; import java.util.List; @@ -41,9 +42,9 @@ public final class SpdyTransport implements Transport { @Override public Request prepareRequest(Request request) { Request.Builder builder = request.newBuilder(); - String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0"; + String version = RequestLine.version(httpEngine.connection.getHttpMinorVersion()); URL url = request.url(); - builder.addSpdyRequestHeaders(request.method(), HttpEngine.requestPath(url), version, + builder.addSpdyRequestHeaders(request.method(), RequestLine.requestPath(url), version, HttpEngine.getOriginAddress(url), httpEngine.getRequest().url().getProtocol()); if (httpEngine.hasRequestBody()) { @@ -83,12 +84,50 @@ public final class SpdyTransport implements Transport { @Override public Response readResponseHeaders() throws IOException { List nameValueBlock = stream.getResponseHeaders(); - RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); - httpEngine.receiveHeaders(rawHeaders); - return new Response.Builder(httpEngine.getRequest(), rawHeaders.getResponseCode()) + Response response = readNameValueBlock(httpEngine.getRequest(), nameValueBlock) .handshake(httpEngine.connection.getHandshake()) - .rawHeaders(rawHeaders) .build(); + httpEngine.connection.setHttpMinorVersion(response.httpMinorVersion()); + httpEngine.receiveHeaders(response.rawHeaders()); + return response; + } + + /** Returns headers for a name value block containing a SPDY response. */ + public static Response.Builder readNameValueBlock(Request request, List nameValueBlock) + throws IOException { + if (nameValueBlock.size() % 2 != 0) { + throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock); + } + String status = null; + String version = null; + + RawHeaders.Builder headersBuilder = new RawHeaders.Builder(); + headersBuilder.set(Response.SELECTED_TRANSPORT, "spdy/3"); + for (int i = 0; i < nameValueBlock.size(); i += 2) { + String name = nameValueBlock.get(i); + String values = nameValueBlock.get(i + 1); + for (int start = 0; start < values.length(); ) { + int end = values.indexOf('\0', start); + if (end == -1) { + end = values.length(); + } + String value = values.substring(start, end); + if (":status".equals(name)) { + status = value; + } else if (":version".equals(name)) { + version = value; + } else { + headersBuilder.add(name, value); + } + start = end + 1; + } + } + if (status == null) throw new ProtocolException("Expected ':status' header not present"); + if (version == null) throw new ProtocolException("Expected ':version' header not present"); + + return new Response.Builder(request) + .statusLine(new StatusLine(version + " " + status)) + .rawHeaders(headersBuilder.build()); } @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java new file mode 100644 index 000000000..71f2212d7 --- /dev/null +++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java @@ -0,0 +1,64 @@ +package com.squareup.okhttp.internal.http; + +import java.io.IOException; +import java.net.ProtocolException; + +public final class StatusLine { + private final String statusLine; + private final int httpMinorVersion; + private final int responseCode; + private final String responseMessage; + + /** Sets the response status line (like "HTTP/1.0 200 OK"). */ + public StatusLine(String statusLine) throws IOException { + // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 + + // We allow empty message without leading white space since some servers + // do not send the white space when the message is empty. + boolean hasMessage = statusLine.length() > 13; + if (!statusLine.startsWith("HTTP/1.") + || statusLine.length() < 12 + || statusLine.charAt(8) != ' ' + || (hasMessage && statusLine.charAt(12) != ' ')) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + int httpMinorVersion = statusLine.charAt(7) - '0'; + if (httpMinorVersion < 0 || httpMinorVersion > 9) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + int responseCode; + try { + responseCode = Integer.parseInt(statusLine.substring(9, 12)); + } catch (NumberFormatException e) { + throw new ProtocolException("Unexpected status line: " + statusLine); + } + this.responseMessage = hasMessage ? statusLine.substring(13) : ""; + this.responseCode = responseCode; + this.statusLine = statusLine; + this.httpMinorVersion = httpMinorVersion; + } + + public String getStatusLine() { + return statusLine; + } + + /** + * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 + * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. + */ + public int httpMinorVersion() { + return httpMinorVersion != -1 ? httpMinorVersion : 1; + } + + /** Returns the HTTP status code or -1 if it is unknown. */ + public int code() { + return responseCode; + } + + /** Returns the HTTP status message or null if it is unknown. */ + public String message() { + return responseMessage; + } + +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java index 98f74c3cb..d109e5428 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java @@ -15,6 +15,7 @@ */ package com.squareup.okhttp.internal.http; +import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import java.io.IOException; import java.util.Arrays; @@ -31,9 +32,11 @@ public final class RawHeadersTest { "set-cookie", "Cookie1\u0000Cookie2", ":status", "200 OK", ":version", "HTTP/1.1"); - RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); + Request request = new Request.Builder("http://square.com/").build(); + Response response = SpdyTransport.readNameValueBlock(request, nameValueBlock).build(); + RawHeaders rawHeaders = response.rawHeaders(); assertEquals(4, rawHeaders.length()); - assertEquals("HTTP/1.1 200 OK", rawHeaders.getStatusLine()); + assertEquals("HTTP/1.1 200 OK", response.statusLine()); assertEquals("no-cache, no-store", rawHeaders.get("cache-control")); assertEquals("Cookie2", rawHeaders.get("set-cookie")); assertEquals("spdy/3", rawHeaders.get(Response.SELECTED_TRANSPORT)); @@ -55,7 +58,6 @@ public final class RawHeadersTest { builder.add("set-cookie", "Cookie1"); builder.add("set-cookie", "Cookie2"); builder.add(":status", "200 OK"); - // TODO: fromNameValueBlock should take the status line headers List nameValueBlock = builder.build().toNameValueBlock(); List expected = Arrays.asList( "cache-control", "no-cache, no-store", @@ -70,43 +72,4 @@ public final class RawHeadersTest { builder.add("Transfer-Encoding", "chunked"); assertEquals(Arrays.asList(), builder.build().toNameValueBlock()); } - - @Test public void statusMessage() throws IOException { - RawHeaders.Builder builder = new RawHeaders.Builder(); - String message = "Temporary Redirect"; - int version = 1; - int code = 200; - builder.setStatusLine("HTTP/1." + version + " " + code + " " + message); - RawHeaders rawHeaders = builder.build(); - assertEquals(message, rawHeaders.getResponseMessage()); - assertEquals(version, rawHeaders.getHttpMinorVersion()); - assertEquals(code, rawHeaders.getResponseCode()); - } - - @Test public void statusMessageWithEmptyMessage() throws IOException { - RawHeaders.Builder builder = new RawHeaders.Builder(); - int version = 1; - int code = 503; - builder.setStatusLine("HTTP/1." + version + " " + code + " "); - RawHeaders rawHeaders = builder.build(); - assertEquals("", rawHeaders.getResponseMessage()); - assertEquals(version, rawHeaders.getHttpMinorVersion()); - assertEquals(code, rawHeaders.getResponseCode()); - } - - /** - * This is not defined in the protocol but some servers won't add the leading - * empty space when the message is empty. - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 - */ - @Test public void statusMessageWithEmptyMessageAndNoLeadingSpace() throws IOException { - RawHeaders.Builder builder = new RawHeaders.Builder(); - int version = 1; - int code = 503; - builder.setStatusLine("HTTP/1." + version + " " + code); - RawHeaders rawHeaders = builder.build(); - assertEquals("", rawHeaders.getResponseMessage()); - assertEquals(version, rawHeaders.getHttpMinorVersion()); - assertEquals(code, rawHeaders.getResponseCode()); - } } diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java new file mode 100644 index 000000000..bc92397e6 --- /dev/null +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.okhttp.internal.http; + +import java.io.IOException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public final class StatusLineTest { + @Test public void parse() throws IOException { + String message = "Temporary Redirect"; + int version = 1; + int code = 200; + StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code + " " + message); + assertEquals(message, statusLine.message()); + assertEquals(version, statusLine.httpMinorVersion()); + assertEquals(code, statusLine.code()); + } + + @Test public void emptyMessage() throws IOException { + int version = 1; + int code = 503; + StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code + " "); + assertEquals("", statusLine.message()); + assertEquals(version, statusLine.httpMinorVersion()); + assertEquals(code, statusLine.code()); + } + + /** + * This is not defined in the protocol but some servers won't add the leading + * empty space when the message is empty. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 + */ + @Test public void emptyMessageAndNoLeadingSpace() throws IOException { + int version = 1; + int code = 503; + StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code); + assertEquals("", statusLine.message()); + assertEquals(version, statusLine.httpMinorVersion()); + assertEquals(code, statusLine.code()); + } +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java index 4494b1570..87cc0a372 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java @@ -18,7 +18,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.HttpResponseCache; import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Response; import com.squareup.okhttp.internal.RecordingAuthenticator; import com.squareup.okhttp.internal.RecordingHostnameVerifier; import com.squareup.okhttp.internal.RecordingOkAuthenticator; @@ -214,14 +213,12 @@ public final class URLConnectionTest { fail("Modified an unmodifiable view."); } catch (UnsupportedOperationException expected) { } - assertEquals(Response.SELECTED_TRANSPORT, urlConnection.getHeaderFieldKey(0)); - assertEquals("http/1.1", urlConnection.getHeaderField(0)); - assertEquals("A", urlConnection.getHeaderFieldKey(1)); - assertEquals("c", urlConnection.getHeaderField(1)); - assertEquals("B", urlConnection.getHeaderFieldKey(2)); - assertEquals("d", urlConnection.getHeaderField(2)); - assertEquals("A", urlConnection.getHeaderFieldKey(3)); - assertEquals("e", urlConnection.getHeaderField(3)); + assertEquals("A", urlConnection.getHeaderFieldKey(0)); + assertEquals("c", urlConnection.getHeaderField(0)); + assertEquals("B", urlConnection.getHeaderFieldKey(1)); + assertEquals("d", urlConnection.getHeaderField(1)); + assertEquals("A", urlConnection.getHeaderFieldKey(2)); + assertEquals("e", urlConnection.getHeaderField(2)); } @Test public void serverSendsInvalidResponseHeaders() throws Exception {