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 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 */ 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 }