blob: 597b4c0b6a1cb0265f908cebec56ddcaabc8f80f [file] [log] [blame]
/*
* 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;
}
}