1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17package libcore.net.http;
18
19import com.squareup.okhttp.OkHttpConnection;
20import com.squareup.okhttp.OkHttpsConnection;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.OutputStream;
24import java.net.CacheResponse;
25import java.net.ProtocolException;
26import java.net.Proxy;
27import java.net.SecureCacheResponse;
28import java.net.URL;
29import java.security.Permission;
30import java.security.Principal;
31import java.security.cert.Certificate;
32import java.security.cert.CertificateException;
33import java.util.List;
34import java.util.Map;
35import javax.net.ssl.SSLHandshakeException;
36import javax.net.ssl.SSLPeerUnverifiedException;
37import javax.net.ssl.SSLSocket;
38import javax.net.ssl.SSLSocketFactory;
39
40public final class HttpsURLConnectionImpl extends OkHttpsConnection {
41
42    /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
43    private final HttpUrlConnectionDelegate delegate;
44
45    public HttpsURLConnectionImpl(URL url, int port) {
46        super(url);
47        delegate = new HttpUrlConnectionDelegate(url, port);
48    }
49
50    public HttpsURLConnectionImpl(URL url, int port, Proxy proxy) {
51        super(url);
52        delegate = new HttpUrlConnectionDelegate(url, port, proxy);
53    }
54
55    private void checkConnected() {
56        if (delegate.getSSLSocket() == null) {
57            throw new IllegalStateException("Connection has not yet been established");
58        }
59    }
60
61    HttpEngine getHttpEngine() {
62        return delegate.getHttpEngine();
63    }
64
65    @Override
66    public String getCipherSuite() {
67        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
68        if (cacheResponse != null) {
69            return cacheResponse.getCipherSuite();
70        }
71        checkConnected();
72        return delegate.getSSLSocket().getSession().getCipherSuite();
73    }
74
75    @Override
76    public Certificate[] getLocalCertificates() {
77        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
78        if (cacheResponse != null) {
79            List<Certificate> result = cacheResponse.getLocalCertificateChain();
80            return result != null ? result.toArray(new Certificate[result.size()]) : null;
81        }
82        checkConnected();
83        return delegate.getSSLSocket().getSession().getLocalCertificates();
84    }
85
86    @Override
87    public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
88        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
89        if (cacheResponse != null) {
90            List<Certificate> result = cacheResponse.getServerCertificateChain();
91            return result != null ? result.toArray(new Certificate[result.size()]) : null;
92        }
93        checkConnected();
94        return delegate.getSSLSocket().getSession().getPeerCertificates();
95    }
96
97    @Override
98    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
99        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
100        if (cacheResponse != null) {
101            return cacheResponse.getPeerPrincipal();
102        }
103        checkConnected();
104        return delegate.getSSLSocket().getSession().getPeerPrincipal();
105    }
106
107    @Override
108    public Principal getLocalPrincipal() {
109        SecureCacheResponse cacheResponse = delegate.getCacheResponse();
110        if (cacheResponse != null) {
111            return cacheResponse.getLocalPrincipal();
112        }
113        checkConnected();
114        return delegate.getSSLSocket().getSession().getLocalPrincipal();
115    }
116
117    @Override
118    public void disconnect() {
119        delegate.disconnect();
120    }
121
122    @Override
123    public InputStream getErrorStream() {
124        return delegate.getErrorStream();
125    }
126
127    @Override
128    public String getRequestMethod() {
129        return delegate.getRequestMethod();
130    }
131
132    @Override
133    public int getResponseCode() throws IOException {
134        return delegate.getResponseCode();
135    }
136
137    @Override
138    public String getResponseMessage() throws IOException {
139        return delegate.getResponseMessage();
140    }
141
142    @Override
143    public void setRequestMethod(String method) throws ProtocolException {
144        delegate.setRequestMethod(method);
145    }
146
147    @Override
148    public boolean usingProxy() {
149        return delegate.usingProxy();
150    }
151
152    @Override
153    public boolean getInstanceFollowRedirects() {
154        return delegate.getInstanceFollowRedirects();
155    }
156
157    @Override
158    public void setInstanceFollowRedirects(boolean followRedirects) {
159        delegate.setInstanceFollowRedirects(followRedirects);
160    }
161
162    @Override
163    public void connect() throws IOException {
164        connected = true;
165        delegate.connect();
166    }
167
168    @Override
169    public boolean getAllowUserInteraction() {
170        return delegate.getAllowUserInteraction();
171    }
172
173    @Override
174    public Object getContent() throws IOException {
175        return delegate.getContent();
176    }
177
178    @SuppressWarnings("unchecked") // Spec does not generify
179    @Override
180    public Object getContent(Class[] types) throws IOException {
181        return delegate.getContent(types);
182    }
183
184    @Override
185    public String getContentEncoding() {
186        return delegate.getContentEncoding();
187    }
188
189    @Override
190    public int getContentLength() {
191        return delegate.getContentLength();
192    }
193
194    @Override
195    public String getContentType() {
196        return delegate.getContentType();
197    }
198
199    @Override
200    public long getDate() {
201        return delegate.getDate();
202    }
203
204    @Override
205    public boolean getDefaultUseCaches() {
206        return delegate.getDefaultUseCaches();
207    }
208
209    @Override
210    public boolean getDoInput() {
211        return delegate.getDoInput();
212    }
213
214    @Override
215    public boolean getDoOutput() {
216        return delegate.getDoOutput();
217    }
218
219    @Override
220    public long getExpiration() {
221        return delegate.getExpiration();
222    }
223
224    @Override
225    public String getHeaderField(int pos) {
226        return delegate.getHeaderField(pos);
227    }
228
229    @Override
230    public Map<String, List<String>> getHeaderFields() {
231        return delegate.getHeaderFields();
232    }
233
234    @Override
235    public Map<String, List<String>> getRequestProperties() {
236        return delegate.getRequestProperties();
237    }
238
239    @Override
240    public void addRequestProperty(String field, String newValue) {
241        delegate.addRequestProperty(field, newValue);
242    }
243
244    @Override
245    public String getHeaderField(String key) {
246        return delegate.getHeaderField(key);
247    }
248
249    @Override
250    public long getHeaderFieldDate(String field, long defaultValue) {
251        return delegate.getHeaderFieldDate(field, defaultValue);
252    }
253
254    @Override
255    public int getHeaderFieldInt(String field, int defaultValue) {
256        return delegate.getHeaderFieldInt(field, defaultValue);
257    }
258
259    @Override
260    public String getHeaderFieldKey(int posn) {
261        return delegate.getHeaderFieldKey(posn);
262    }
263
264    @Override
265    public long getIfModifiedSince() {
266        return delegate.getIfModifiedSince();
267    }
268
269    @Override
270    public InputStream getInputStream() throws IOException {
271        return delegate.getInputStream();
272    }
273
274    @Override
275    public long getLastModified() {
276        return delegate.getLastModified();
277    }
278
279    @Override
280    public OutputStream getOutputStream() throws IOException {
281        return delegate.getOutputStream();
282    }
283
284    @Override
285    public Permission getPermission() throws IOException {
286        return delegate.getPermission();
287    }
288
289    @Override
290    public String getRequestProperty(String field) {
291        return delegate.getRequestProperty(field);
292    }
293
294    @Override
295    public URL getURL() {
296        return delegate.getURL();
297    }
298
299    @Override
300    public boolean getUseCaches() {
301        return delegate.getUseCaches();
302    }
303
304    @Override
305    public void setAllowUserInteraction(boolean newValue) {
306        delegate.setAllowUserInteraction(newValue);
307    }
308
309    @Override
310    public void setDefaultUseCaches(boolean newValue) {
311        delegate.setDefaultUseCaches(newValue);
312    }
313
314    @Override
315    public void setDoInput(boolean newValue) {
316        delegate.setDoInput(newValue);
317    }
318
319    @Override
320    public void setDoOutput(boolean newValue) {
321        delegate.setDoOutput(newValue);
322    }
323
324    @Override
325    public void setIfModifiedSince(long newValue) {
326        delegate.setIfModifiedSince(newValue);
327    }
328
329    @Override
330    public void setRequestProperty(String field, String newValue) {
331        delegate.setRequestProperty(field, newValue);
332    }
333
334    @Override
335    public void setUseCaches(boolean newValue) {
336        delegate.setUseCaches(newValue);
337    }
338
339    @Override
340    public void setConnectTimeout(int timeoutMillis) {
341        delegate.setConnectTimeout(timeoutMillis);
342    }
343
344    @Override
345    public int getConnectTimeout() {
346        return delegate.getConnectTimeout();
347    }
348
349    @Override
350    public void setReadTimeout(int timeoutMillis) {
351        delegate.setReadTimeout(timeoutMillis);
352    }
353
354    @Override
355    public int getReadTimeout() {
356        return delegate.getReadTimeout();
357    }
358
359    @Override
360    public String toString() {
361        return delegate.toString();
362    }
363
364    @Override
365    public void setFixedLengthStreamingMode(int contentLength) {
366        delegate.setFixedLengthStreamingMode(contentLength);
367    }
368
369    @Override
370    public void setChunkedStreamingMode(int chunkLength) {
371        delegate.setChunkedStreamingMode(chunkLength);
372    }
373
374    private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
375        private HttpUrlConnectionDelegate(URL url, int port) {
376            super(url, port);
377        }
378
379        private HttpUrlConnectionDelegate(URL url, int port, Proxy proxy) {
380            super(url, port, proxy);
381        }
382
383        @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
384                HttpConnection connection, RetryableOutputStream requestBody) throws IOException {
385            return new HttpsEngine(this, method, requestHeaders, connection, requestBody,
386                    HttpsURLConnectionImpl.this);
387        }
388
389        public SecureCacheResponse getCacheResponse() {
390            HttpsEngine engine = (HttpsEngine) httpEngine;
391            return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null;
392        }
393
394        public SSLSocket getSSLSocket() {
395            HttpsEngine engine = (HttpsEngine) httpEngine;
396            return engine != null ? engine.sslSocket : null;
397        }
398    }
399
400    private static final class HttpsEngine extends HttpEngine {
401
402        /**
403         * Local stash of HttpsEngine.connection.sslSocket for answering
404         * queries such as getCipherSuite even after
405         * httpsEngine.Connection has been recycled. It's presence is also
406         * used to tell if the HttpsURLConnection is considered connected,
407         * as opposed to the connected field of URLConnection or the a
408         * non-null connect in HttpURLConnectionImpl
409         */
410        private SSLSocket sslSocket;
411
412        private final HttpsURLConnectionImpl enclosing;
413
414        /**
415         * @param policy the HttpURLConnectionImpl with connection configuration
416         * @param enclosing the HttpsURLConnection with HTTPS features
417         */
418        private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
419                HttpConnection connection, RetryableOutputStream requestBody,
420                HttpsURLConnectionImpl enclosing) throws IOException {
421            super(policy, method, requestHeaders, connection, requestBody);
422            this.sslSocket = connection != null ? connection.getSecureSocketIfConnected() : null;
423            this.enclosing = enclosing;
424        }
425
426        @Override protected void connect() throws IOException {
427            // First try an SSL connection with compression and various TLS
428            // extensions enabled, if it fails (and its not unheard of that it
429            // will) fallback to a barebones connection.
430            try {
431                makeSslConnection(true);
432            } catch (IOException e) {
433                // If the problem was a CertificateException from the X509TrustManager,
434                // do not retry, we didn't have an abrupt server initiated exception.
435                if (e instanceof SSLHandshakeException
436                        && e.getCause() instanceof CertificateException) {
437                    throw e;
438                }
439                release(false);
440                makeSslConnection(false);
441            }
442        }
443
444        /**
445         * Attempt to make an HTTPS connection.
446         *
447         * @param tlsTolerant If true, assume server can handle common
448         * TLS extensions and SSL deflate compression. If false, use
449         * an SSL3 only fallback mode without compression.
450         */
451        private void makeSslConnection(boolean tlsTolerant) throws IOException {
452            // make an SSL Tunnel on the first message pair of each SSL + proxy connection
453            if (connection == null) {
454                connection = openSocketConnection();
455                if (connection.getAddress().getProxy() != null) {
456                    makeTunnel(policy, connection, getRequestHeaders());
457                }
458            }
459
460            // if super.makeConnection returned a connection from the
461            // pool, sslSocket needs to be initialized here. If it is
462            // a new connection, it will be initialized by
463            // getSecureSocket below.
464            sslSocket = connection.getSecureSocketIfConnected();
465
466            // we already have an SSL connection,
467            if (sslSocket != null) {
468                return;
469            }
470
471            sslSocket = connection.setupSecureSocket(
472                    enclosing.getSSLSocketFactory(), enclosing.getHostnameVerifier(), tlsTolerant);
473        }
474
475        /**
476         * To make an HTTPS connection over an HTTP proxy, send an unencrypted
477         * CONNECT request to create the proxy connection. This may need to be
478         * retried if the proxy requires authorization.
479         */
480        private void makeTunnel(HttpURLConnectionImpl policy, HttpConnection connection,
481                RequestHeaders requestHeaders) throws IOException {
482            RawHeaders rawRequestHeaders = requestHeaders.getHeaders();
483            while (true) {
484                HttpEngine connect = new ProxyConnectEngine(policy, rawRequestHeaders, connection);
485                connect.sendRequest();
486                connect.readResponse();
487
488                int responseCode = connect.getResponseCode();
489                switch (connect.getResponseCode()) {
490                case HTTP_OK:
491                    return;
492                case HTTP_PROXY_AUTH:
493                    rawRequestHeaders = new RawHeaders(rawRequestHeaders);
494                    boolean credentialsFound = policy.processAuthHeader(HTTP_PROXY_AUTH,
495                            connect.getResponseHeaders(), rawRequestHeaders);
496                    if (credentialsFound) {
497                        continue;
498                    } else {
499                        throw new IOException("Failed to authenticate with proxy");
500                    }
501                default:
502                    throw new IOException("Unexpected response code for CONNECT: " + responseCode);
503                }
504            }
505        }
506
507        @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
508            return cacheResponse instanceof SecureCacheResponse;
509        }
510
511        @Override protected boolean includeAuthorityInRequestLine() {
512            // Even if there is a proxy, it isn't involved. Always request just the file.
513            return false;
514        }
515
516        @Override protected SSLSocketFactory getSslSocketFactory() {
517            return enclosing.getSSLSocketFactory();
518        }
519
520        @Override protected OkHttpConnection getHttpConnectionToCache() {
521            return enclosing;
522        }
523    }
524
525    private static class ProxyConnectEngine extends HttpEngine {
526        public ProxyConnectEngine(HttpURLConnectionImpl policy, RawHeaders requestHeaders,
527                HttpConnection connection) throws IOException {
528            super(policy, HttpEngine.CONNECT, requestHeaders, connection, null);
529        }
530
531        @Override protected boolean requiresTunnel() {
532            return true;
533        }
534    }
535}
536