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.incubator.http.internal.websocket;
27
28 import jdk.incubator.http.internal.common.MinimalFuture;
29
30 import java.io.IOException;
31 import java.net.URI;
32 import java.net.URISyntaxException;
33 import jdk.incubator.http.HttpClient;
34 import jdk.incubator.http.HttpClient.Version;
35 import jdk.incubator.http.HttpHeaders;
36 import jdk.incubator.http.HttpRequest;
37 import jdk.incubator.http.HttpResponse;
38 import jdk.incubator.http.HttpResponse.BodyHandler;
39 import jdk.incubator.http.WebSocketHandshakeException;
40 import jdk.incubator.http.internal.common.Pair;
41
42 import java.nio.charset.StandardCharsets;
43 import java.security.MessageDigest;
44 import java.security.NoSuchAlgorithmException;
45 import java.security.SecureRandom;
46 import java.time.Duration;
47 import java.util.Base64;
48 import java.util.Collection;
49 import java.util.Collections;
50 import java.util.LinkedHashSet;
51 import java.util.List;
52 import java.util.Optional;
53 import java.util.Set;
54 import java.util.TreeSet;
55 import java.util.concurrent.CompletableFuture;
56 import java.util.stream.Collectors;
57
58 import static java.lang.String.format;
59 import static jdk.incubator.http.internal.common.Utils.isValidName;
60 import static jdk.incubator.http.internal.common.Utils.stringOf;
61
62 final class OpeningHandshake {
63
64 private static final String HEADER_CONNECTION = "Connection";
65 private static final String HEADER_UPGRADE = "Upgrade";
66 private static final String HEADER_ACCEPT = "Sec-WebSocket-Accept";
67 private static final String HEADER_EXTENSIONS = "Sec-WebSocket-Extensions";
68 private static final String HEADER_KEY = "Sec-WebSocket-Key";
69 private static final String HEADER_PROTOCOL = "Sec-WebSocket-Protocol";
70 private static final String HEADER_VERSION = "Sec-WebSocket-Version";
71
72 private static final Set<String> FORBIDDEN_HEADERS;
73
74 static {
75 FORBIDDEN_HEADERS = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
76 FORBIDDEN_HEADERS.addAll(List.of(HEADER_ACCEPT,
77 HEADER_EXTENSIONS,
78 HEADER_KEY,
79 HEADER_PROTOCOL,
80 HEADER_VERSION));
81 }
82
83 private static final SecureRandom srandom = new SecureRandom();
84
85 private final MessageDigest sha1;
86 private final HttpClient client;
87
88 {
89 try {
90 sha1 = MessageDigest.getInstance("SHA-1");
91 } catch (NoSuchAlgorithmException e) {
92 // Shouldn't happen: SHA-1 must be available in every Java platform
93 // implementation
94 throw new InternalError("Minimum requirements", e);
95 }
96 }
97
98 private final HttpRequest request;
99 private final Collection<String> subprotocols;
100 private final String nonce;
101
102 OpeningHandshake(BuilderImpl b) {
103 this.client = b.getClient();
104 URI httpURI = createRequestURI(b.getUri());
105 HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI);
106 Duration connectTimeout = b.getConnectTimeout();
107 if (connectTimeout != null) {
108 requestBuilder.timeout(connectTimeout);
109 }
110 for (Pair<String, String> p : b.getHeaders()) {
111 if (FORBIDDEN_HEADERS.contains(p.first)) {
112 throw illegal("Illegal header: " + p.first);
113 }
114 requestBuilder.header(p.first, p.second);
115 }
116 this.subprotocols = createRequestSubprotocols(b.getSubprotocols());
117 if (!this.subprotocols.isEmpty()) {
118 String p = this.subprotocols.stream().collect(Collectors.joining(", "));
119 requestBuilder.header(HEADER_PROTOCOL, p);
120 }
121 requestBuilder.header(HEADER_VERSION, "13"); // WebSocket's lucky number
122 this.nonce = createNonce();
123 requestBuilder.header(HEADER_KEY, this.nonce);
124 // Setting request version to HTTP/1.1 forcibly, since it's not possible
125 // to upgrade from HTTP/2 to WebSocket (as of August 2016):
126 //
127 // https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00
128 this.request = requestBuilder.version(Version.HTTP_1_1).GET().build();
129 WebSocketRequest r = (WebSocketRequest) this.request;
130 r.isWebSocket(true);
131 r.setSystemHeader(HEADER_UPGRADE, "websocket");
132 r.setSystemHeader(HEADER_CONNECTION, "Upgrade");
133 }
134
135 private static Collection<String> createRequestSubprotocols(
136 Collection<String> subprotocols)
137 {
138 LinkedHashSet<String> sp = new LinkedHashSet<>(subprotocols.size(), 1);
139 for (String s : subprotocols) {
140 if (s.trim().isEmpty() || !isValidName(s)) {
141 throw illegal("Bad subprotocol syntax: " + s);
142 }
143 if (!sp.add(s)) {
144 throw illegal("Duplicating subprotocol: " + s);
145 }
146 }
147 return Collections.unmodifiableCollection(sp);
148 }
149
150 /*
151 * Checks the given URI for being a WebSocket URI and translates it into a
152 * target HTTP URI for the Opening Handshake.
153 *
154 * https://tools.ietf.org/html/rfc6455#section-3
155 */
156 private static URI createRequestURI(URI uri) {
157 // TODO: check permission for WebSocket URI and translate it into
158 // http/https permission
159 String s = uri.getScheme(); // The scheme might be null (i.e. undefined)
160 if (!("ws".equalsIgnoreCase(s) || "wss".equalsIgnoreCase(s))
161 || uri.getFragment() != null)
162 {
163 throw illegal("Bad URI: " + uri);
164 }
165 String scheme = "ws".equalsIgnoreCase(s) ? "http" : "https";
166 try {
167 return new URI(scheme,
168 uri.getUserInfo(),
169 uri.getHost(),
170 uri.getPort(),
171 uri.getPath(),
172 uri.getQuery(),
173 null); // No fragment
174 } catch (URISyntaxException e) {
175 // Shouldn't happen: URI invariant
176 throw new InternalError(e);
177 }
178 }
179
180 CompletableFuture<Result> send() {
181 return client.sendAsync(this.request, BodyHandler.<Void>discard(null))
182 .thenCompose(this::resultFrom);
183 }
184
185 /*
186 * The result of the opening handshake.
187 */
188 static final class Result {
189
190 final String subprotocol;
191 final RawChannel channel;
192
193 private Result(String subprotocol, RawChannel channel) {
194 this.subprotocol = subprotocol;
195 this.channel = channel;
196 }
197 }
198
199 private CompletableFuture<Result> resultFrom(HttpResponse<?> response) {
200 // Do we need a special treatment for SSLHandshakeException?
201 // Namely, invoking
202 //
203 // Listener.onClose(StatusCodes.TLS_HANDSHAKE_FAILURE, "")
204 //
205 // See https://tools.ietf.org/html/rfc6455#section-7.4.1
206 Result result = null;
207 Exception exception = null;
208 try {
209 result = handleResponse(response);
210 } catch (IOException e) {
211 exception = e;
212 } catch (Exception e) {
213 exception = new WebSocketHandshakeException(response).initCause(e);
214 }
215 if (exception == null) {
234 HttpHeaders headers = response.headers();
235 String upgrade = requireSingle(headers, HEADER_UPGRADE);
236 if (!upgrade.equalsIgnoreCase("websocket")) {
237 throw checkFailed("Bad response field: " + HEADER_UPGRADE);
238 }
239 String connection = requireSingle(headers, HEADER_CONNECTION);
240 if (!connection.equalsIgnoreCase("Upgrade")) {
241 throw checkFailed("Bad response field: " + HEADER_CONNECTION);
242 }
243 requireAbsent(headers, HEADER_VERSION);
244 requireAbsent(headers, HEADER_EXTENSIONS);
245 String x = this.nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
246 this.sha1.update(x.getBytes(StandardCharsets.ISO_8859_1));
247 String expected = Base64.getEncoder().encodeToString(this.sha1.digest());
248 String actual = requireSingle(headers, HEADER_ACCEPT);
249 if (!actual.trim().equals(expected)) {
250 throw checkFailed("Bad " + HEADER_ACCEPT);
251 }
252 String subprotocol = checkAndReturnSubprotocol(headers);
253 RawChannel channel = ((RawChannel.Provider) response).rawChannel();
254 return new Result(subprotocol, channel);
255 }
256
257 private String checkAndReturnSubprotocol(HttpHeaders responseHeaders)
258 throws CheckFailedException
259 {
260 Optional<String> opt = responseHeaders.firstValue(HEADER_PROTOCOL);
261 if (!opt.isPresent()) {
262 // If there is no such header in the response, then the server
263 // doesn't want to use any subprotocol
264 return "";
265 }
266 String s = requireSingle(responseHeaders, HEADER_PROTOCOL);
267 // An empty string as a subprotocol's name is not allowed by the spec
268 // and the check below will detect such responses too
269 if (this.subprotocols.contains(s)) {
270 return s;
271 } else {
272 throw checkFailed("Unexpected subprotocol: " + s);
273 }
274 }
283 stringOf(values)));
284 }
285 }
286
287 private static String requireSingle(HttpHeaders responseHeaders,
288 String headerName)
289 {
290 List<String> values = responseHeaders.allValues(headerName);
291 if (values.isEmpty()) {
292 throw checkFailed("Response field missing: " + headerName);
293 } else if (values.size() > 1) {
294 throw checkFailed(format("Response field '%s' multivalued: %s",
295 headerName,
296 stringOf(values)));
297 }
298 return values.get(0);
299 }
300
301 private static String createNonce() {
302 byte[] bytes = new byte[16];
303 OpeningHandshake.srandom.nextBytes(bytes);
304 return Base64.getEncoder().encodeToString(bytes);
305 }
306
307 private static IllegalArgumentException illegal(String message) {
308 return new IllegalArgumentException(message);
309 }
310
311 private static CheckFailedException checkFailed(String message) {
312 throw new CheckFailedException(message);
313 }
314 }
|
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.incubator.http.internal.websocket;
27
28 import jdk.incubator.http.internal.common.MinimalFuture;
29
30 import java.io.IOException;
31 import java.net.InetSocketAddress;
32 import java.net.Proxy;
33 import java.net.ProxySelector;
34 import java.net.URI;
35 import java.net.URISyntaxException;
36 import jdk.incubator.http.HttpClient;
37 import jdk.incubator.http.HttpClient.Version;
38 import jdk.incubator.http.HttpHeaders;
39 import jdk.incubator.http.HttpRequest;
40 import jdk.incubator.http.HttpResponse;
41 import jdk.incubator.http.HttpResponse.BodyHandler;
42 import jdk.incubator.http.WebSocketHandshakeException;
43 import jdk.incubator.http.internal.common.Pair;
44 import jdk.incubator.http.internal.common.Utils;
45
46 import java.net.URLPermission;
47 import java.nio.charset.StandardCharsets;
48 import java.security.AccessController;
49 import java.security.MessageDigest;
50 import java.security.NoSuchAlgorithmException;
51 import java.security.PrivilegedAction;
52 import java.security.SecureRandom;
53 import java.time.Duration;
54 import java.util.Base64;
55 import java.util.Collection;
56 import java.util.Collections;
57 import java.util.LinkedHashSet;
58 import java.util.List;
59 import java.util.Optional;
60 import java.util.Set;
61 import java.util.TreeSet;
62 import java.util.concurrent.CompletableFuture;
63 import java.util.stream.Collectors;
64 import java.util.stream.Stream;
65
66 import static java.lang.String.format;
67 import static jdk.incubator.http.internal.common.Utils.isValidName;
68 import static jdk.incubator.http.internal.common.Utils.permissionForProxy;
69 import static jdk.incubator.http.internal.common.Utils.stringOf;
70
71 public class OpeningHandshake {
72
73 private static final String HEADER_CONNECTION = "Connection";
74 private static final String HEADER_UPGRADE = "Upgrade";
75 private static final String HEADER_ACCEPT = "Sec-WebSocket-Accept";
76 private static final String HEADER_EXTENSIONS = "Sec-WebSocket-Extensions";
77 private static final String HEADER_KEY = "Sec-WebSocket-Key";
78 private static final String HEADER_PROTOCOL = "Sec-WebSocket-Protocol";
79 private static final String HEADER_VERSION = "Sec-WebSocket-Version";
80
81 private static final Set<String> ILLEGAL_HEADERS;
82
83 static {
84 ILLEGAL_HEADERS = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
85 ILLEGAL_HEADERS.addAll(List.of(HEADER_ACCEPT,
86 HEADER_EXTENSIONS,
87 HEADER_KEY,
88 HEADER_PROTOCOL,
89 HEADER_VERSION));
90 }
91
92 private static final SecureRandom random = new SecureRandom();
93
94 private final MessageDigest sha1;
95 private final HttpClient client;
96
97 {
98 try {
99 sha1 = MessageDigest.getInstance("SHA-1");
100 } catch (NoSuchAlgorithmException e) {
101 // Shouldn't happen: SHA-1 must be available in every Java platform
102 // implementation
103 throw new InternalError("Minimum requirements", e);
104 }
105 }
106
107 private final HttpRequest request;
108 private final Collection<String> subprotocols;
109 private final String nonce;
110
111 public OpeningHandshake(BuilderImpl b) {
112 checkURI(b.getUri());
113 Proxy proxy = proxyFor(b.getProxySelector(), b.getUri());
114 checkPermissions(b, proxy);
115 this.client = b.getClient();
116 URI httpURI = createRequestURI(b.getUri());
117 HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(httpURI);
118 Duration connectTimeout = b.getConnectTimeout();
119 if (connectTimeout != null) {
120 requestBuilder.timeout(connectTimeout);
121 }
122 for (Pair<String, String> p : b.getHeaders()) {
123 if (ILLEGAL_HEADERS.contains(p.first)) {
124 throw illegal("Illegal header: " + p.first);
125 }
126 requestBuilder.header(p.first, p.second);
127 }
128 this.subprotocols = createRequestSubprotocols(b.getSubprotocols());
129 if (!this.subprotocols.isEmpty()) {
130 String p = this.subprotocols.stream().collect(Collectors.joining(", "));
131 requestBuilder.header(HEADER_PROTOCOL, p);
132 }
133 requestBuilder.header(HEADER_VERSION, "13"); // WebSocket's lucky number
134 this.nonce = createNonce();
135 requestBuilder.header(HEADER_KEY, this.nonce);
136 // Setting request version to HTTP/1.1 forcibly, since it's not possible
137 // to upgrade from HTTP/2 to WebSocket (as of August 2016):
138 //
139 // https://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-00
140 this.request = requestBuilder.version(Version.HTTP_1_1).GET().build();
141 WebSocketRequest r = (WebSocketRequest) this.request;
142 r.isWebSocket(true);
143 r.setSystemHeader(HEADER_UPGRADE, "websocket");
144 r.setSystemHeader(HEADER_CONNECTION, "Upgrade");
145 r.setProxy(proxy);
146 }
147
148 private static Collection<String> createRequestSubprotocols(
149 Collection<String> subprotocols)
150 {
151 LinkedHashSet<String> sp = new LinkedHashSet<>(subprotocols.size(), 1);
152 for (String s : subprotocols) {
153 if (s.trim().isEmpty() || !isValidName(s)) {
154 throw illegal("Bad subprotocol syntax: " + s);
155 }
156 if (!sp.add(s)) {
157 throw illegal("Duplicating subprotocol: " + s);
158 }
159 }
160 return Collections.unmodifiableCollection(sp);
161 }
162
163 /*
164 * Checks the given URI for being a WebSocket URI and translates it into a
165 * target HTTP URI for the Opening Handshake.
166 *
167 * https://tools.ietf.org/html/rfc6455#section-3
168 */
169 static URI createRequestURI(URI uri) {
170 String s = uri.getScheme();
171 assert "ws".equalsIgnoreCase(s) || "wss".equalsIgnoreCase(s);
172 String scheme = "ws".equalsIgnoreCase(s) ? "http" : "https";
173 try {
174 return new URI(scheme,
175 uri.getUserInfo(),
176 uri.getHost(),
177 uri.getPort(),
178 uri.getPath(),
179 uri.getQuery(),
180 null); // No fragment
181 } catch (URISyntaxException e) {
182 // Shouldn't happen: URI invariant
183 throw new InternalError(e);
184 }
185 }
186
187 public CompletableFuture<Result> send() {
188 PrivilegedAction<CompletableFuture<Result>> pa = () ->
189 client.sendAsync(this.request, BodyHandler.<Void>discard(null))
190 .thenCompose(this::resultFrom);
191 return AccessController.doPrivileged(pa);
192 }
193
194 /*
195 * The result of the opening handshake.
196 */
197 static final class Result {
198
199 final String subprotocol;
200 final TransportSupplier transport;
201
202 private Result(String subprotocol, TransportSupplier transport) {
203 this.subprotocol = subprotocol;
204 this.transport = transport;
205 }
206 }
207
208 private CompletableFuture<Result> resultFrom(HttpResponse<?> response) {
209 // Do we need a special treatment for SSLHandshakeException?
210 // Namely, invoking
211 //
212 // Listener.onClose(StatusCodes.TLS_HANDSHAKE_FAILURE, "")
213 //
214 // See https://tools.ietf.org/html/rfc6455#section-7.4.1
215 Result result = null;
216 Exception exception = null;
217 try {
218 result = handleResponse(response);
219 } catch (IOException e) {
220 exception = e;
221 } catch (Exception e) {
222 exception = new WebSocketHandshakeException(response).initCause(e);
223 }
224 if (exception == null) {
243 HttpHeaders headers = response.headers();
244 String upgrade = requireSingle(headers, HEADER_UPGRADE);
245 if (!upgrade.equalsIgnoreCase("websocket")) {
246 throw checkFailed("Bad response field: " + HEADER_UPGRADE);
247 }
248 String connection = requireSingle(headers, HEADER_CONNECTION);
249 if (!connection.equalsIgnoreCase("Upgrade")) {
250 throw checkFailed("Bad response field: " + HEADER_CONNECTION);
251 }
252 requireAbsent(headers, HEADER_VERSION);
253 requireAbsent(headers, HEADER_EXTENSIONS);
254 String x = this.nonce + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
255 this.sha1.update(x.getBytes(StandardCharsets.ISO_8859_1));
256 String expected = Base64.getEncoder().encodeToString(this.sha1.digest());
257 String actual = requireSingle(headers, HEADER_ACCEPT);
258 if (!actual.trim().equals(expected)) {
259 throw checkFailed("Bad " + HEADER_ACCEPT);
260 }
261 String subprotocol = checkAndReturnSubprotocol(headers);
262 RawChannel channel = ((RawChannel.Provider) response).rawChannel();
263 return new Result(subprotocol, new TransportSupplier(channel));
264 }
265
266 private String checkAndReturnSubprotocol(HttpHeaders responseHeaders)
267 throws CheckFailedException
268 {
269 Optional<String> opt = responseHeaders.firstValue(HEADER_PROTOCOL);
270 if (!opt.isPresent()) {
271 // If there is no such header in the response, then the server
272 // doesn't want to use any subprotocol
273 return "";
274 }
275 String s = requireSingle(responseHeaders, HEADER_PROTOCOL);
276 // An empty string as a subprotocol's name is not allowed by the spec
277 // and the check below will detect such responses too
278 if (this.subprotocols.contains(s)) {
279 return s;
280 } else {
281 throw checkFailed("Unexpected subprotocol: " + s);
282 }
283 }
292 stringOf(values)));
293 }
294 }
295
296 private static String requireSingle(HttpHeaders responseHeaders,
297 String headerName)
298 {
299 List<String> values = responseHeaders.allValues(headerName);
300 if (values.isEmpty()) {
301 throw checkFailed("Response field missing: " + headerName);
302 } else if (values.size() > 1) {
303 throw checkFailed(format("Response field '%s' multivalued: %s",
304 headerName,
305 stringOf(values)));
306 }
307 return values.get(0);
308 }
309
310 private static String createNonce() {
311 byte[] bytes = new byte[16];
312 OpeningHandshake.random.nextBytes(bytes);
313 return Base64.getEncoder().encodeToString(bytes);
314 }
315
316 private static CheckFailedException checkFailed(String message) {
317 throw new CheckFailedException(message);
318 }
319
320 private static URI checkURI(URI uri) {
321 String scheme = uri.getScheme();
322 if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme)))
323 throw illegal("invalid URI scheme: " + scheme);
324 if (uri.getHost() == null)
325 throw illegal("URI must contain a host: " + uri);
326 if (uri.getFragment() != null)
327 throw illegal("URI must not contain a fragment: " + uri);
328 return uri;
329 }
330
331 private static IllegalArgumentException illegal(String message) {
332 return new IllegalArgumentException(message);
333 }
334
335 /**
336 * Returns the proxy for the given URI when sent through the given client,
337 * or {@code null} if none is required or applicable.
338 */
339 private static Proxy proxyFor(Optional<ProxySelector> selector, URI uri) {
340 if (!selector.isPresent()) {
341 return null;
342 }
343 URI requestURI = createRequestURI(uri); // Based on the HTTP scheme
344 List<Proxy> pl = selector.get().select(requestURI);
345 if (pl.isEmpty()) {
346 return null;
347 }
348 Proxy proxy = pl.get(0);
349 if (proxy.type() != Proxy.Type.HTTP) {
350 return null;
351 }
352 return proxy;
353 }
354
355 /**
356 * Performs the necessary security permissions checks to connect ( possibly
357 * through a proxy ) to the builders WebSocket URI.
358 *
359 * @throws SecurityException if the security manager denies access
360 */
361 static void checkPermissions(BuilderImpl b, Proxy proxy) {
362 SecurityManager sm = System.getSecurityManager();
363 if (sm == null) {
364 return;
365 }
366 Stream<String> headers = b.getHeaders().stream().map(p -> p.first).distinct();
367 URLPermission perm1 = Utils.permissionForServer(b.getUri(), "", headers);
368 sm.checkPermission(perm1);
369 if (proxy == null) {
370 return;
371 }
372 URLPermission perm2 = permissionForProxy((InetSocketAddress) proxy.address());
373 if (perm2 != null) {
374 sm.checkPermission(perm2);
375 }
376 }
377 }
|