/* * 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 */ package java.net.http; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetSocketAddress; import java.net.SocketPermission; import java.net.URI; import java.net.URISyntaxException; import java.net.URLPermission; 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; /** * 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) * */ class Exchange { final HttpRequestImpl request; final HttpClientImpl client; ExchangeImpl exchImpl; HttpResponseImpl response; final List permissions = new LinkedList<>(); AccessControlContext acc; boolean upgrading; // to HTTP/2 Exchange(HttpRequestImpl request) { this.request = request; this.upgrading = false; this.client = request.client(); } /* If different AccessControlContext to be used */ Exchange(HttpRequestImpl request, AccessControlContext acc) { this.request = request; this.acc = acc; this.upgrading = false; this.client = request.client(); } public HttpRequestImpl request() { return request; } public HttpResponseImpl response() throws IOException, InterruptedException { response = responseImpl(null); return response; } public void cancel() { if (exchImpl != null) exchImpl.cancel(); } public void h2Upgrade() { upgrading = true; request.setH2Upgrade(); } static final SocketPermission[] SOCKET_ARRAY = new SocketPermission[0]; HttpResponseImpl responseImpl(HttpConnection connection) throws IOException, InterruptedException { if (acc == null) { acc = request.getAccessControlContext(); } SecurityException e = securityCheck(acc); if (e != null) throw e; if (permissions.size() > 0) { try { return AccessController.doPrivileged( (PrivilegedExceptionAction)() -> 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); } } HttpResponseImpl responseImpl0(HttpConnection connection) throws IOException, InterruptedException { exchImpl = ExchangeImpl.get(this, connection); if (request.expectContinue()) { request.addSystemHeader("Expect", "100-Continue"); exchImpl.sendHeadersOnly(); HttpResponseImpl resp = exchImpl.getResponse(); Utils.logResponse(resp); if (resp.statusCode() != 100) { return resp; } exchImpl.sendBody(); return exchImpl.getResponse(); } else { exchImpl.sendRequest(); HttpResponseImpl resp = exchImpl.getResponse(); Utils.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 responseAsync(Void v) { return responseAsyncImpl(null); } CompletableFuture responseAsyncImpl(HttpConnection connection) { if (acc == null) { acc = request.getAccessControlContext(); } SecurityException e = securityCheck(acc); if (e != null) { return CompletableFuture.failedFuture(e); } if (permissions.size() > 0) { return AccessController.doPrivileged( (PrivilegedAction>)() -> responseAsyncImpl0(connection), null, permissions.toArray(SOCKET_ARRAY)); } else { return responseAsyncImpl0(connection); } } CompletableFuture responseAsyncImpl0(HttpConnection connection) { try { exchImpl = ExchangeImpl.get(this, connection); } catch (IOException | InterruptedException e) { return CompletableFuture.failedFuture(e); } if (request.expectContinue()) { request.addSystemHeader("Expect", "100-Continue"); return exchImpl.sendHeadersAsync() .thenCompose(exchImpl::getResponseAsync) .thenCompose((HttpResponseImpl r1) -> { int rcode = r1.statusCode(); CompletableFuture cf = checkForUpgradeAsync(r1, exchImpl); if (cf != null) return cf; if (rcode == 100) { return exchImpl.sendBodyAsync() .thenCompose(exchImpl::getResponseAsync) .thenApply((r) -> { Utils.logResponse(r); return r; }); } else { Exchange.this.response = r1; Utils.logResponse(r1); return CompletableFuture.completedFuture(r1); } }); } else { return exchImpl .sendRequestAsync() .thenCompose(exchImpl::getResponseAsync) .thenCompose((HttpResponseImpl r1) -> { int rcode = r1.statusCode(); CompletableFuture cf = checkForUpgradeAsync(r1, exchImpl); if (cf != null) { return cf; } else { Exchange.this.response = r1; Utils.logResponse(r1); return CompletableFuture.completedFuture(r1); } }) .thenApply((HttpResponseImpl response) -> { this.response = response; Utils.logResponse(response); return response; }); } } // 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 checkForUpgradeAsync(HttpResponseImpl resp, ExchangeImpl ex) { int rcode = resp.statusCode(); if (upgrading && (rcode == 101)) { Http1Exchange e = (Http1Exchange)ex; // check for 101 switching protocols return e.responseBodyAsync(HttpResponse.ignoreBody()) .thenCompose((Void v) -> Http2Connection.createAsync(e.connection(), client.client2(), this) .thenCompose((Http2Connection c) -> { c.putConnection(); Stream s = c.getStream(1); exchImpl = s; return s.getResponseAsync(null); }) ); } return CompletableFuture.completedFuture(resp); } private HttpResponseImpl checkForUpgrade(HttpResponseImpl resp, ExchangeImpl ex) throws IOException, InterruptedException { int rcode = resp.statusCode(); if (upgrading && (rcode == 101)) { Http1Exchange e = (Http1Exchange) ex; // must get connection from Http1Exchange e.responseBody(HttpResponse.ignoreBody(), false); Http2Connection h2con = new Http2Connection(e.connection(), client.client2(), this); h2con.putConnection(); Stream s = h2con.getStream(1); exchImpl = s; return s.getResponse(); } return resp; } T responseBody(HttpResponse.BodyProcessor processor) { try { return exchImpl.responseBody(processor); } catch (IOException e) { throw new UncheckedIOException(e); } } CompletableFuture responseBodyAsync(HttpResponse.BodyProcessor processor) { return exchImpl.responseBodyAsync(processor); } 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; } InetSocketAddress proxy = request.proxy(); if (proxy != null) { // 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; } 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; } }