jdk/src/java.httpclient/share/classes/java/net/http/Exchange.java
author michaelm
Thu, 25 Feb 2016 23:14:22 +0000
changeset 36131 379db4b2f95d
child 36433 dcbc230cfa4f
permissions -rw-r--r--
8087112: HTTP API and HTTP/1.1 implementation Reviewed-by: alanb, chegar, coffeys, psandoz, rriggs

/*
 * 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<SocketPermission> 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<HttpResponseImpl>)() ->
                             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();
            logResponse(resp);
            if (resp.statusCode() != 100) {
                return resp;
            }
            exchImpl.sendBody();
            return exchImpl.getResponse();
        } else {
            exchImpl.sendRequest();
            HttpResponseImpl resp = exchImpl.getResponse();
            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<HttpResponseImpl> responseAsync(Void v) {
        return responseAsyncImpl(null);
    }

    CompletableFuture<HttpResponseImpl> responseAsyncImpl(HttpConnection connection) {
        if (acc == null) {
            acc = request.getAccessControlContext();
        }
        SecurityException e = securityCheck(acc);
        if (e != null) {
            CompletableFuture<HttpResponseImpl> cf = new CompletableFuture<>();
            cf.completeExceptionally(e);
            return cf;
        }
        if (permissions.size() > 0) {
            return AccessController.doPrivileged(
                    (PrivilegedAction<CompletableFuture<HttpResponseImpl>>)() ->
                        responseAsyncImpl0(connection),
                    null,
                    permissions.toArray(SOCKET_ARRAY));
        } else {
            return responseAsyncImpl0(connection);
        }
    }

    CompletableFuture<HttpResponseImpl> responseAsyncImpl0(HttpConnection connection) {
        try {
            exchImpl = ExchangeImpl.get(this, connection);
        } catch (IOException | InterruptedException e) {
            CompletableFuture<HttpResponseImpl> cf = new CompletableFuture<>();
            cf.completeExceptionally(e);
            return cf;
        }
        if (request.expectContinue()) {
            request.addSystemHeader("Expect", "100-Continue");
            return exchImpl.sendHeadersAsync()
                    .thenCompose(exchImpl::getResponseAsync)
                    .thenCompose((HttpResponseImpl r1) -> {
                        int rcode = r1.statusCode();
                        CompletableFuture<HttpResponseImpl> cf =
                                checkForUpgradeAsync(r1, exchImpl);
                        if (cf != null)
                            return cf;
                        if (rcode == 100) {
                            return exchImpl.sendBodyAsync()
                                .thenCompose(exchImpl::getResponseAsync)
                                .thenApply((r) -> {
                                    logResponse(r);
                                    return r;
                                });
                        } else {
                            Exchange.this.response = r1;
                            logResponse(r1);
                            return CompletableFuture.completedFuture(r1);
                        }
                    });
        } else {
            return exchImpl
                .sendHeadersAsync()
                .thenCompose((Void v) -> {
                    // send body and get response at same time
                    exchImpl.sendBodyAsync();
                    return exchImpl.getResponseAsync(null);
                })
                    .thenCompose((HttpResponseImpl r1) -> {
                        int rcode = r1.statusCode();
                        CompletableFuture<HttpResponseImpl> cf =
                                checkForUpgradeAsync(r1, exchImpl);
                        if (cf != null) {
                            return cf;
                        } else {
                            Exchange.this.response = r1;
                            logResponse(r1);
                            return CompletableFuture.completedFuture(r1);
                        }
                    })
                .thenApply((HttpResponseImpl response) -> {
                    this.response = response;
                    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<HttpResponseImpl>
    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) -> {
                            Stream s = c.getStream(1);
                            exchImpl = s;
                            c.putConnection();
                            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> T responseBody(HttpResponse.BodyProcessor<T> processor) {
        try {
            return exchImpl.responseBody(processor);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }


    private void logResponse(HttpResponseImpl r) {
        if (!Log.requests())
            return;
        StringBuilder sb = new StringBuilder();
        String method = r.request().method();
        URI uri = r.uri();
        String uristring = uri == null ? "" : uri.toString();
        sb.append('(')
          .append(method)
          .append(" ")
          .append(uristring)
          .append(") ")
          .append(Integer.toString(r.statusCode()));
        Log.logResponse(sb.toString());
    }

    <T> CompletableFuture<T> responseBodyAsync(HttpResponse.BodyProcessor<T> 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();
        HttpHeadersImpl userHeaders = request.getUserHeaders();
        URI u = getURIForSecurityCheck();
        URLPermission p = Utils.getPermission(u, method, userHeaders.directMap());

        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;
    }
}