1 /*
2  * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package jdk.internal.net.http;
27 
28 import java.io.IOException;
29 import java.io.UncheckedIOException;
30 import java.net.ConnectException;
31 import java.net.InetSocketAddress;
32 import java.net.URI;
33 import java.util.Base64;
34 import java.util.Collections;
35 import java.util.HashSet;
36 import java.util.Map;
37 import java.util.Set;
38 import java.util.concurrent.ConcurrentHashMap;
39 import java.util.concurrent.CompletableFuture;
40 import jdk.internal.net.http.common.Log;
41 import jdk.internal.net.http.common.Logger;
42 import jdk.internal.net.http.common.MinimalFuture;
43 import jdk.internal.net.http.common.Utils;
44 import jdk.internal.net.http.frame.SettingsFrame;
45 import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
46 import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
47 import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
48 import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
49 import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE;
50 
51 /**
52  *  Http2 specific aspects of HttpClientImpl
53  */
54 class Http2ClientImpl {
55 
56     final static Logger debug =
57             Utils.getDebugLogger("Http2ClientImpl"::toString, Utils.DEBUG);
58 
59     private final HttpClientImpl client;
60 
Http2ClientImpl(HttpClientImpl client)61     Http2ClientImpl(HttpClientImpl client) {
62         this.client = client;
63     }
64 
65     /* Map key is "scheme:host:port" */
66     private final Map<String,Http2Connection> connections = new ConcurrentHashMap<>();
67 
68     private final Set<String> failures = Collections.synchronizedSet(new HashSet<>());
69 
70     /**
71      * When HTTP/2 requested only. The following describes the aggregate behavior including the
72      * calling code. In all cases, the HTTP2 connection cache
73      * is checked first for a suitable connection and that is returned if available.
74      * If not, a new connection is opened, except in https case when a previous negotiate failed.
75      * In that case, we want to continue using http/1.1. When a connection is to be opened and
76      * if multiple requests are sent in parallel then each will open a new connection.
77      *
78      * If negotiation/upgrade succeeds then
79      * one connection will be put in the cache and the others will be closed
80      * after the initial request completes (not strictly necessary for h2, only for h2c)
81      *
82      * If negotiate/upgrade fails, then any opened connections remain open (as http/1.1)
83      * and will be used and cached in the http/1 cache. Note, this method handles the
84      * https failure case only (by completing the CF with an ALPN exception, handled externally)
85      * The h2c upgrade is handled externally also.
86      *
87      * Specific CF behavior of this method.
88      * 1. completes with ALPN exception: h2 negotiate failed for first time. failure recorded.
89      * 2. completes with other exception: failure not recorded. Caller must handle
90      * 3. completes normally with null: no connection in cache for h2c or h2 failed previously
91      * 4. completes normally with connection: h2 or h2c connection in cache. Use it.
92      */
getConnectionFor(HttpRequestImpl req, Exchange<?> exchange)93     CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
94                                                         Exchange<?> exchange) {
95         URI uri = req.uri();
96         InetSocketAddress proxy = req.proxy();
97         String key = Http2Connection.keyFor(uri, proxy);
98 
99         synchronized (this) {
100             Http2Connection connection = connections.get(key);
101             if (connection != null) {
102                 try {
103                     if (connection.closed || !connection.reserveStream(true)) {
104                         if (debug.on())
105                             debug.log("removing found closed or closing connection: %s", connection);
106                         deleteConnection(connection);
107                     } else {
108                         // fast path if connection already exists
109                         if (debug.on())
110                             debug.log("found connection in the pool: %s", connection);
111                         return MinimalFuture.completedFuture(connection);
112                     }
113                 } catch (IOException e) {
114                     // thrown by connection.reserveStream()
115                     return MinimalFuture.failedFuture(e);
116                 }
117             }
118 
119             if (!req.secure() || failures.contains(key)) {
120                 // secure: negotiate failed before. Use http/1.1
121                 // !secure: no connection available in cache. Attempt upgrade
122                 if (debug.on()) debug.log("not found in connection pool");
123                 return MinimalFuture.completedFuture(null);
124             }
125         }
126         return Http2Connection
127                 .createAsync(req, this, exchange)
128                 .whenComplete((conn, t) -> {
129                     synchronized (Http2ClientImpl.this) {
130                         if (conn != null) {
131                             try {
132                                 conn.reserveStream(true);
133                             } catch (IOException e) {
134                                 throw new UncheckedIOException(e); // shouldn't happen
135                             }
136                             offerConnection(conn);
137                         } else {
138                             Throwable cause = Utils.getCompletionCause(t);
139                             if (cause instanceof Http2Connection.ALPNException)
140                                 failures.add(key);
141                         }
142                     }
143                 });
144     }
145 
146     /*
147      * Cache the given connection, if no connection to the same
148      * destination exists. If one exists, then we let the initial stream
149      * complete but allow it to close itself upon completion.
150      * This situation should not arise with https because the request
151      * has not been sent as part of the initial alpn negotiation
152      */
153     boolean offerConnection(Http2Connection c) {
154         if (debug.on()) debug.log("offering to the connection pool: %s", c);
155         if (c.closed || c.finalStream()) {
156             if (debug.on())
157                 debug.log("skipping offered closed or closing connection: %s", c);
158             return false;
159         }
160 
161         String key = c.key();
162         synchronized(this) {
163             Http2Connection c1 = connections.putIfAbsent(key, c);
164             if (c1 != null) {
165                 c.setFinalStream();
166                 if (debug.on())
167                     debug.log("existing entry in connection pool for %s", key);
168                 return false;
169             }
170             if (debug.on())
171                 debug.log("put in the connection pool: %s", c);
172             return true;
173         }
174     }
175 
176     void deleteConnection(Http2Connection c) {
177         if (debug.on())
178             debug.log("removing from the connection pool: %s", c);
179         synchronized (this) {
180             Http2Connection c1 = connections.get(c.key());
181             if (c1 != null && c1.equals(c)) {
182                 connections.remove(c.key());
183                 if (debug.on())
184                     debug.log("removed from the connection pool: %s", c);
185             }
186         }
187     }
188 
189     void stop() {
190         if (debug.on()) debug.log("stopping");
191         connections.values().forEach(this::close);
192         connections.clear();
193     }
194 
195     private void close(Http2Connection h2c) {
196         try { h2c.close(); } catch (Throwable t) {}
197     }
198 
199     HttpClientImpl client() {
200         return client;
201     }
202 
203     /** Returns the client settings as a base64 (url) encoded string */
204     String getSettingsString() {
205         SettingsFrame sf = getClientSettings();
206         byte[] settings = sf.toByteArray(); // without the header
207         Base64.Encoder encoder = Base64.getUrlEncoder()
208                                        .withoutPadding();
209         return encoder.encodeToString(settings);
210     }
211 
212     private static final int K = 1024;
213 
214     private static int getParameter(String property, int min, int max, int defaultValue) {
215         int value =  Utils.getIntegerNetProperty(property, defaultValue);
216         // use default value if misconfigured
217         if (value < min || value > max) {
218             Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
219                     "using default={4}", property, value, min, max, defaultValue);
220             value = defaultValue;
221         }
222         return value;
223     }
224 
225     // used for the connection window, to have a connection window size
226     // bigger than the initial stream window size.
227     int getConnectionWindowSize(SettingsFrame clientSettings) {
228         // Maximum size is 2^31-1. Don't allow window size to be less
229         // than the stream window size. HTTP/2 specify a default of 64 * K -1,
230         // but we use 2^26 by default for better performance.
231         int streamWindow = clientSettings.getParameter(INITIAL_WINDOW_SIZE);
232 
233         // The default is the max between the stream window size
234         // and the connection window size.
235         int defaultValue = Math.min(Integer.MAX_VALUE,
236                 Math.max(streamWindow, K*K*32));
237 
238         return getParameter(
239                 "jdk.httpclient.connectionWindowSize",
240                 streamWindow, Integer.MAX_VALUE, defaultValue);
241     }
242 
243     SettingsFrame getClientSettings() {
244         SettingsFrame frame = new SettingsFrame();
245         // default defined for HTTP/2 is 4 K, we use 16 K.
246         frame.setParameter(HEADER_TABLE_SIZE, getParameter(
247                 "jdk.httpclient.hpack.maxheadertablesize",
248                 0, Integer.MAX_VALUE, 16 * K));
249         // O: does not accept push streams. 1: accepts push streams.
250         frame.setParameter(ENABLE_PUSH, getParameter(
251                 "jdk.httpclient.enablepush",
252                 0, 1, 1));
253         // HTTP/2 recommends to set the number of concurrent streams
254         // no lower than 100. We use 100. 0 means no stream would be
255         // accepted. That would render the client to be non functional,
256         // so we won't let 0 be configured for our Http2ClientImpl.
257         frame.setParameter(MAX_CONCURRENT_STREAMS, getParameter(
258                 "jdk.httpclient.maxstreams",
259                 1, Integer.MAX_VALUE, 100));
260         // Maximum size is 2^31-1. Don't allow window size to be less
261         // than the minimum frame size as this is likely to be a
262         // configuration error. HTTP/2 specify a default of 64 * K -1,
263         // but we use 16 M  for better performance.
264         frame.setParameter(INITIAL_WINDOW_SIZE, getParameter(
265                 "jdk.httpclient.windowsize",
266                 16 * K, Integer.MAX_VALUE, 16*K*K));
267         // HTTP/2 specify a minimum size of 16 K, a maximum size of 2^24-1,
268         // and a default of 16 K. We use 16 K as default.
269         frame.setParameter(MAX_FRAME_SIZE, getParameter(
270                 "jdk.httpclient.maxframesize",
271                 16 * K, 16 * K * K -1, 16 * K));
272         return frame;
273     }
274 }
275