| /* |
| * Copyright (C) 2013 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; |
| |
| 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.HttpURLConnection; |
| import java.net.ProtocolException; |
| import java.net.Proxy; |
| import java.net.URL; |
| |
| import static com.squareup.okhttp.internal.Util.getEffectivePort; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_PERM; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_TEMP; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MULT_CHOICE; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_PROXY_AUTH; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_SEE_OTHER; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_TEMP_REDIRECT; |
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_UNAUTHORIZED; |
| |
| final class Job implements Runnable, Policy { |
| private final Dispatcher dispatcher; |
| private final OkHttpClient client; |
| private final Response.Receiver responseReceiver; |
| |
| /** The request; possibly a consequence of redirects or auth headers. */ |
| private Request request; |
| |
| public Job(Dispatcher dispatcher, OkHttpClient client, Request request, |
| Response.Receiver responseReceiver) { |
| this.dispatcher = dispatcher; |
| this.client = client; |
| this.request = request; |
| this.responseReceiver = responseReceiver; |
| } |
| |
| @Override public int getChunkLength() { |
| return request.body().contentLength() == -1 ? HttpTransport.DEFAULT_CHUNK_LENGTH : -1; |
| } |
| |
| @Override public long getFixedContentLength() { |
| return request.body().contentLength(); |
| } |
| |
| @Override public boolean getUseCaches() { |
| return false; // TODO. |
| } |
| |
| @Override public HttpURLConnection getHttpConnectionToCache() { |
| return null; |
| } |
| |
| @Override public URL getURL() { |
| return request.url(); |
| } |
| |
| @Override public long getIfModifiedSince() { |
| return 0; // For HttpURLConnection only. We let the cache drive this. |
| } |
| |
| @Override public boolean usingProxy() { |
| return false; // We let the connection decide this. |
| } |
| |
| @Override public void setSelectedProxy(Proxy proxy) { |
| // Do nothing. |
| } |
| |
| Object tag() { |
| return request.tag(); |
| } |
| |
| @Override public void run() { |
| try { |
| Response response = execute(); |
| responseReceiver.onResponse(response); |
| } catch (IOException e) { |
| responseReceiver.onFailure(new Failure.Builder() |
| .request(request) |
| .exception(e) |
| .build()); |
| } finally { |
| // TODO: close the response body |
| // TODO: release the HTTP engine (potentially multiple!) |
| dispatcher.finished(this); |
| } |
| } |
| |
| private Response execute() throws IOException { |
| Connection connection = null; |
| Response redirectedBy = null; |
| |
| while (true) { |
| HttpEngine engine = newEngine(connection); |
| |
| Request.Body body = request.body(); |
| if (body != null) { |
| MediaType contentType = body.contentType(); |
| if (contentType == null) throw new IllegalStateException("contentType == null"); |
| if (engine.getRequestHeaders().getContentType() == null) { |
| engine.getRequestHeaders().setContentType(contentType.toString()); |
| } |
| } |
| |
| engine.sendRequest(); |
| |
| if (body != null) { |
| body.writeTo(engine.getRequestBody()); |
| } |
| |
| engine.readResponse(); |
| |
| int responseCode = engine.getResponseCode(); |
| Dispatcher.RealResponseBody responseBody = new Dispatcher.RealResponseBody( |
| engine.getResponseHeaders(), engine.getResponseBody()); |
| |
| Response response = new Response.Builder(request, responseCode) |
| .rawHeaders(engine.getResponseHeaders().getHeaders()) |
| .body(responseBody) |
| .redirectedBy(redirectedBy) |
| .build(); |
| |
| Request redirect = processResponse(engine, response); |
| |
| if (redirect == null) { |
| engine.automaticallyReleaseConnectionToPool(); |
| return response; |
| } |
| |
| // TODO: fail if too many redirects |
| // TODO: fail if not following redirects |
| // TODO: release engine |
| |
| connection = sameConnection(request, redirect) ? engine.getConnection() : null; |
| redirectedBy = response; |
| request = redirect; |
| } |
| } |
| |
| 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(); |
| } |
| } |
| |
| /** |
| * Figures out the HTTP request to make in response to receiving {@code |
| * response}. This will either add authentication headers or follow |
| * redirects. If a follow-up is either unnecessary or not applicable, this |
| * returns null. |
| */ |
| private Request processResponse(HttpEngine engine, Response response) throws IOException { |
| Request request = response.request(); |
| Proxy selectedProxy = engine.getConnection() != null |
| ? engine.getConnection().getRoute().getProxy() |
| : client.getProxy(); |
| int responseCode = response.code(); |
| |
| switch (responseCode) { |
| case HTTP_PROXY_AUTH: |
| if (selectedProxy.type() != Proxy.Type.HTTP) { |
| throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); |
| } |
| // fall-through |
| case HTTP_UNAUTHORIZED: |
| RawHeaders successorRequestHeaders = request.rawHeaders(); |
| boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(), |
| response.code(), response.rawHeaders(), successorRequestHeaders, selectedProxy, |
| this.request.url()); |
| return credentialsFound |
| ? request.newBuilder().rawHeaders(successorRequestHeaders).build() |
| : null; |
| |
| case HTTP_MULT_CHOICE: |
| case HTTP_MOVED_PERM: |
| case HTTP_MOVED_TEMP: |
| case HTTP_SEE_OTHER: |
| case HTTP_TEMP_REDIRECT: |
| String method = request.method(); |
| if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) { |
| // "If the 307 status code is received in response to a request other than GET or HEAD, |
| // the user agent MUST NOT automatically redirect the request" |
| return null; |
| } |
| |
| String location = response.header("Location"); |
| if (location == null) { |
| return null; |
| } |
| |
| URL url = new URL(request.url(), location); |
| if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) { |
| return null; // Don't follow redirects to unsupported protocols. |
| } |
| |
| return this.request.newBuilder().url(url).build(); |
| |
| default: |
| return null; |
| } |
| } |
| |
| private boolean sameConnection(Request a, Request b) { |
| return a.url().getHost().equals(b.url().getHost()) |
| && getEffectivePort(a.url()) == getEffectivePort(b.url()) |
| && a.url().getProtocol().equals(b.url().getProtocol()); |
| } |
| } |