/* * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.incubator.http; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.SocketPermission; import java.net.URI; import java.net.URISyntaxException; import java.net.URLPermission; import java.nio.ByteBuffer; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import jdk.incubator.http.internal.common.MinimalFuture; import jdk.incubator.http.internal.common.Utils; import jdk.incubator.http.internal.common.Log; /** * One request/response exchange (handles 100/101 intermediate response also). * depth field used to track number of times a new request is being sent * for a given API request. If limit exceeded exception is thrown. * * Security check is performed here: * - uses AccessControlContext captured at API level * - checks for appropriate URLPermission for request * - if permission allowed, grants equivalent SocketPermission to call * - in case of direct HTTP proxy, checks additionally for access to proxy * (CONNECT proxying uses its own Exchange, so check done there) * */ final class Exchange<T> { final HttpRequestImpl request; final HttpClientImpl client; volatile ExchangeImpl<T> exchImpl; final List<SocketPermission> permissions = new LinkedList<>(); final AccessControlContext acc; final MultiExchange<?,T> multi; final Executor parentExecutor; final HttpRequest.BodyProcessor requestProcessor; boolean upgrading; // to HTTP/2 volatile Executor responseExecutor; final PushGroup<?,T> pushGroup; // buffer for receiving response headers private volatile ByteBuffer rxBuffer; Exchange(HttpRequestImpl request, MultiExchange<?,T> multi) { this.request = request; this.upgrading = false; this.client = multi.client(); this.multi = multi; this.acc = multi.acc; this.parentExecutor = multi.executor; this.requestProcessor = request.requestProcessor; this.pushGroup = multi.pushGroup; } /* If different AccessControlContext to be used */ Exchange(HttpRequestImpl request, MultiExchange<?,T> multi, AccessControlContext acc) { this.request = request; this.acc = acc; this.upgrading = false; this.client = multi.client(); this.multi = multi; this.parentExecutor = multi.executor; this.requestProcessor = request.requestProcessor; this.pushGroup = multi.pushGroup; } PushGroup<?,T> getPushGroup() { return pushGroup; } Executor executor() { return parentExecutor; } public HttpRequestImpl request() { return request; } HttpClientImpl client() { return client; } ByteBuffer getBuffer() { if(rxBuffer == null) { synchronized (this) { if(rxBuffer == null) { rxBuffer = Utils.getExchangeBuffer(); } } } return rxBuffer; } public Response response() throws IOException, InterruptedException { return responseImpl(null); } public T readBody(HttpResponse.BodyHandler<T> responseHandler) throws IOException { return exchImpl.readBody(responseHandler, true); } public CompletableFuture<T> readBodyAsync(HttpResponse.BodyHandler<T> handler) { return exchImpl.readBodyAsync(handler, true, responseExecutor); } public void cancel() { if (exchImpl != null) { exchImpl.cancel(); } } public void cancel(IOException cause) { if (exchImpl != null) { exchImpl.cancel(cause); } } public void h2Upgrade() { upgrading = true; request.setH2Upgrade(client.client2()); } static final SocketPermission[] SOCKET_ARRAY = new SocketPermission[0]; Response responseImpl(HttpConnection connection) throws IOException, InterruptedException { SecurityException e = securityCheck(acc); if (e != null) { throw e; } if (permissions.size() > 0) { try { return AccessController.doPrivileged( (PrivilegedExceptionAction<Response>)() -> responseImpl0(connection), null, permissions.toArray(SOCKET_ARRAY)); } catch (Throwable ee) { if (ee instanceof PrivilegedActionException) { ee = ee.getCause(); } if (ee instanceof IOException) { throw (IOException) ee; } else { throw new RuntimeException(ee); // TODO: fix } } } else { return responseImpl0(connection); } } private Response responseImpl0(HttpConnection connection) throws IOException, InterruptedException { exchImpl = ExchangeImpl.get(this, connection); exchImpl.setClientForRequest(requestProcessor); if (request.expectContinue()) { Log.logTrace("Sending Expect: 100-Continue"); request.addSystemHeader("Expect", "100-Continue"); exchImpl.sendHeadersOnly(); Log.logTrace("Waiting for 407-Expectation-Failed or 100-Continue"); Response resp = exchImpl.getResponse(); HttpResponseImpl.logResponse(resp); int rcode = resp.statusCode(); if (rcode != 100) { Log.logTrace("Expectation failed: Received {0}", rcode); if (upgrading && rcode == 101) { throw new IOException( "Unable to handle 101 while waiting for 100-Continue"); } return resp; } Log.logTrace("Received 100-Continue: sending body"); exchImpl.sendBody(); Log.logTrace("Body sent: waiting for response"); resp = exchImpl.getResponse(); HttpResponseImpl.logResponse(resp); return checkForUpgrade(resp, exchImpl); } else { exchImpl.sendRequest(); Response resp = exchImpl.getResponse(); HttpResponseImpl.logResponse(resp); return checkForUpgrade(resp, exchImpl); } } // Completed HttpResponse will be null if response succeeded // will be a non null responseAsync if expect continue returns an error public CompletableFuture<Response> responseAsync() { // take one thread from supplied executor to handle response headers and body responseExecutor = Utils.singleThreadExecutor(parentExecutor); return responseAsyncImpl(null); } CompletableFuture<Response> responseAsyncImpl(HttpConnection connection) { SecurityException e = securityCheck(acc); if (e != null) { return MinimalFuture.failedFuture(e); } if (permissions.size() > 0) { return AccessController.doPrivileged( (PrivilegedAction<CompletableFuture<Response>>)() -> responseAsyncImpl0(connection), null, permissions.toArray(SOCKET_ARRAY)); } else { return responseAsyncImpl0(connection); } } CompletableFuture<Response> responseAsyncImpl0(HttpConnection connection) { try { exchImpl = ExchangeImpl.get(this, connection); } catch (IOException | InterruptedException e) { return MinimalFuture.failedFuture(e); } if (request.expectContinue()) { request.addSystemHeader("Expect", "100-Continue"); Log.logTrace("Sending Expect: 100-Continue"); return exchImpl .sendHeadersAsync() .thenCompose((v) -> exchImpl.getResponseAsync(responseExecutor)) .thenCompose((Response r1) -> { HttpResponseImpl.logResponse(r1); int rcode = r1.statusCode(); if (rcode == 100) { Log.logTrace("Received 100-Continue: sending body"); return exchImpl.sendBodyAsync(parentExecutor) .thenCompose((v) -> exchImpl.getResponseAsync(responseExecutor)) .thenCompose((Response r2) -> { return checkForUpgradeAsync(r2, exchImpl); }).thenApply((Response r) -> { HttpResponseImpl.logResponse(r); return r; }); } else { Log.logTrace("Expectation failed: Received {0}", rcode); if (upgrading && rcode == 101) { IOException failed = new IOException( "Unable to handle 101 while waiting for 100"); return MinimalFuture.failedFuture(failed); } return exchImpl.readBodyAsync(this::ignoreBody, false, responseExecutor) .thenApply((v) -> { return r1; }); } }); } else { return exchImpl .sendRequestAsync(parentExecutor) .thenCompose((v) -> exchImpl.getResponseAsync(responseExecutor)) .thenCompose((Response r1) -> { return checkForUpgradeAsync(r1, exchImpl); }) .thenApply((Response response) -> { HttpResponseImpl.logResponse(response); return response; }); } } HttpResponse.BodyProcessor<T> ignoreBody(int status, HttpHeaders hdrs) { return HttpResponse.BodyProcessor.discard((T)null); } // if this response was received in reply to an upgrade // then create the Http2Connection from the HttpConnection // initialize it and wait for the real response on a newly created Stream private CompletableFuture<Response> checkForUpgradeAsync(Response resp, ExchangeImpl<T> ex) { int rcode = resp.statusCode(); if (upgrading && (rcode == 101)) { Http1Exchange<T> e = (Http1Exchange<T>)ex; // check for 101 switching protocols // 101 responses are not supposed to contain a body. // => should we fail if there is one? return e.readBodyAsync(this::ignoreBody, false, parentExecutor) .thenCompose((T v) -> // v is null Http2Connection.createAsync(e.connection(), client.client2(), this, e.getBuffer()) .thenCompose((Http2Connection c) -> { c.putConnection(); Stream<T> s = c.getStream(1); exchImpl = s; return s.getResponseAsync(null); }) ); } return MinimalFuture.completedFuture(resp); } private Response checkForUpgrade(Response resp, ExchangeImpl<T> ex) throws IOException, InterruptedException { int rcode = resp.statusCode(); if (upgrading && (rcode == 101)) { Http1Exchange<T> e = (Http1Exchange<T>) ex; // 101 responses are not supposed to contain a body. // => should we fail if there is one? // => readBody called here by analogy with // checkForUpgradeAsync above e.readBody(this::ignoreBody, false); // must get connection from Http1Exchange Http2Connection h2con = new Http2Connection(e.connection(), client.client2(), this, e.getBuffer()); h2con.putConnection(); Stream<T> s = h2con.getStream(1); exchImpl = s; Response xx = s.getResponse(); HttpResponseImpl.logResponse(xx); return xx; } return resp; } private URI getURIForSecurityCheck() { URI u; String method = request.method(); InetSocketAddress authority = request.authority(); URI uri = request.uri(); // CONNECT should be restricted at API level if (method.equalsIgnoreCase("CONNECT")) { try { u = new URI("socket", null, authority.getHostString(), authority.getPort(), null, null, null); } catch (URISyntaxException e) { throw new InternalError(e); // shouldn't happen } } else { u = uri; } return u; } /** * Do the security check and return any exception. * Return null if no check needed or passes. * * Also adds any generated permissions to the "permissions" list. */ private SecurityException securityCheck(AccessControlContext acc) { SecurityManager sm = System.getSecurityManager(); if (sm == null) { return null; } String method = request.method(); HttpHeaders userHeaders = request.getUserHeaders(); URI u = getURIForSecurityCheck(); URLPermission p = Utils.getPermission(u, method, userHeaders.map()); try { assert acc != null; sm.checkPermission(p, acc); permissions.add(getSocketPermissionFor(u)); } catch (SecurityException e) { return e; } ProxySelector ps = client.proxy().orElse(null); if (ps != null) { InetSocketAddress proxy = (InetSocketAddress) ps.select(u).get(0).address(); // TODO: check this // may need additional check if (!method.equals("CONNECT")) { // a direct http proxy. Need to check access to proxy try { u = new URI("socket", null, proxy.getHostString(), proxy.getPort(), null, null, null); } catch (URISyntaxException e) { throw new InternalError(e); // shouldn't happen } p = new URLPermission(u.toString(), "CONNECT"); try { sm.checkPermission(p, acc); } catch (SecurityException e) { permissions.clear(); return e; } String sockperm = proxy.getHostString() + ":" + Integer.toString(proxy.getPort()); permissions.add(new SocketPermission(sockperm, "connect,resolve")); } } return null; } HttpClient.Redirect followRedirects() { return client.followRedirects(); } HttpClient.Version version() { return client.version(); } private static SocketPermission getSocketPermissionFor(URI url) { if (System.getSecurityManager() == null) { return null; } StringBuilder sb = new StringBuilder(); String host = url.getHost(); sb.append(host); int port = url.getPort(); if (port == -1) { String scheme = url.getScheme(); if ("http".equals(scheme)) { sb.append(":80"); } else { // scheme must be https sb.append(":443"); } } else { sb.append(':') .append(Integer.toString(port)); } String target = sb.toString(); return new SocketPermission(target, "connect"); } AccessControlContext getAccessControlContext() { return acc; } }