/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.jdk.connector;
import java.io.IOException;
import java.net.CookieManager;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.glassfish.jersey.SslConfigurator;
/**
* @author Petr Janouch (petr.janouch at oracle.com)
*/
class HttpConnection {
/**
* Input buffer that is used by {@link TransportFilter} when SSL is turned on.
* The size cannot be smaller than a maximal size of a SSL packet, which is 16kB for payload + header, because
* {@link SslFilter} does not have its own buffer for buffering incoming
* data and therefore the entire SSL packet must fit into {@link SslFilter}
* input buffer.
* <p/>
*/
private static final int SSL_INPUT_BUFFER_SIZE = 17_000;
/**
* Input buffer that is used by {@link TransportFilter} when SSL is not turned on.
*/
private static final int INPUT_BUFFER_SIZE = 2048;
private static final Logger LOGGER = Logger.getLogger(HttpConnection.class.getName());
private final Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> filterChain;
private final CookieManager cookieManager;
// we are interested only in host-port pair, but URI is a convenient holder for it
private final URI uri;
private final StateChangeListener stateListener;
private final ScheduledExecutorService scheduler;
private final ConnectorConfiguration configuration;
private HttpRequest httpRequest;
private HttpResponse httResponse;
private Throwable error;
private State state = State.CREATED;
// by default we treat all connection as persistent
// this flag will change to false if we receive "Connection: Close" header
private boolean persistentConnection = true;
private Future<?> responseTimeout;
private Future<?> idleTimeout;
private Future<?> connectTimeout;
HttpConnection(URI uri,
CookieManager cookieManager,
ConnectorConfiguration configuration,
ScheduledExecutorService scheduler,
StateChangeListener stateListener) {
this.uri = uri;
this.cookieManager = cookieManager;
this.stateListener = stateListener;
this.configuration = configuration;
this.scheduler = scheduler;
filterChain = createFilterChain(uri, configuration);
}
synchronized void connect() {
if (state != State.CREATED) {
throw new IllegalStateException(
"Cannot try to establish connection if the connection is in other than CREATED state, current state: "
+ state);
}
changeState(State.CONNECTING);
scheduleConnectTimeout();
filterChain.connect(new InetSocketAddress(uri.getHost(), Utils.getPort(uri)), null);
}
synchronized void send(final HttpRequest httpRequest) {
if (state != State.IDLE) {
throw new IllegalStateException(
"Http request cannot be sent over a connection that is in other state than IDLE. Current state: " + state);
}
cancelIdleTimeout();
this.httpRequest = httpRequest;
// clean state left by previous request
httResponse = null;
error = null;
persistentConnection = true;
changeState(State.SENDING_REQUEST);
addRequestHeaders();
filterChain.write(httpRequest, new CompletionHandler<HttpRequest>() {
@Override
public void failed(Throwable throwable) {
handleError(throwable);
}
@Override
public void completed(HttpRequest result) {
handleHeaderSent();
}
});
}
synchronized void close() {
if (state == State.CLOSED) {
return;
}
cancelAllTimeouts();
filterChain.close();
changeState(State.CLOSED);
}
private synchronized void handleHeaderSent() {
if (state != State.SENDING_REQUEST) {
return;
}
scheduleResponseTimeout();
if (httpRequest.getBodyMode() == HttpRequest.BodyMode.NONE
|| httpRequest.getBodyMode() == HttpRequest.BodyMode.BUFFERED) {
changeState(State.RECEIVING_HEADER);
} else {
ChunkedBodyOutputStream bodyStream = (ChunkedBodyOutputStream) httpRequest.getBodyStream();
bodyStream.setCloseListener(() -> {
synchronized (HttpConnection.this) {
if (state != State.SENDING_REQUEST) {
return;
}
changeState(State.RECEIVING_HEADER);
}
});
}
}
private void addRequestHeaders() {
Map<String, List<String>> cookies;
try {
cookies = cookieManager.get(httpRequest.getUri(), httpRequest.getHeaders());
} catch (IOException e) {
handleError(e);
return;
}
// unfortunately CookieManager returns ""Cookie" -> empty list" pair if the cookie is not set
cookies.entrySet().stream().filter(cookieHeader -> cookieHeader.getValue() != null && !cookieHeader.getValue().isEmpty())
.forEach(cookieHeader -> httpRequest.getHeaders().put(cookieHeader.getKey(), cookieHeader.getValue()));
}
private void processResponseHeaders(HttpResponse response) throws IOException {
cookieManager.put(httpRequest.getUri(), httResponse.getHeaders());
List<String> connectionValues = response.getHeader(Constants.CONNECTION);
if (connectionValues != null) {
connectionValues.stream().filter(connectionValue -> connectionValue.equalsIgnoreCase(Constants.CONNECTION_CLOSE))
.forEach(connectionValue -> persistentConnection = false);
}
}
protected Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> createFilterChain(URI uri,
ConnectorConfiguration
configuration) {
boolean secure = Constants.HTTPS.equals(uri.getScheme());
Filter<ByteBuffer, ByteBuffer, ?, ?> socket;
if (secure) {
SSLContext sslContext = configuration.getSslContext();
TransportFilter transportFilter = new TransportFilter(SSL_INPUT_BUFFER_SIZE, configuration.getThreadPoolConfig(),
configuration.getContainerIdleTimeout());
if (sslContext == null) {
sslContext = SslConfigurator.getDefaultContext();
}
socket = new SslFilter(transportFilter, sslContext, uri.getHost(), configuration.getHostnameVerifier());
} else {
socket = new TransportFilter(INPUT_BUFFER_SIZE, configuration.getThreadPoolConfig(),
configuration.getContainerIdleTimeout());
}
int maxHeaderSize = configuration.getMaxHeaderSize();
HttpFilter httpFilter = new HttpFilter(socket, maxHeaderSize, maxHeaderSize + INPUT_BUFFER_SIZE);
ConnectorConfiguration.ProxyConfiguration proxyConfiguration = configuration.getProxyConfiguration();
if (proxyConfiguration.isConfigured()) {
ProxyFilter proxyFilter = new ProxyFilter(httpFilter, proxyConfiguration);
return new ConnectionFilter(proxyFilter);
}
return new ConnectionFilter(httpFilter);
}
private void changeState(State newState) {
State old = state;
state = newState;
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest(LocalizationMessages.CONNECTION_CHANGING_STATE(uri.getHost(), uri.getPort(), old, newState));
}
stateListener.onStateChanged(this, old, newState);
}
private void scheduleResponseTimeout() {
if (configuration.getResponseTimeout() == 0) {
return;
}
responseTimeout = scheduler.schedule(() -> {
synchronized (HttpConnection.this) {
if (state != State.RECEIVING_HEADER && state != State.RECEIVING_BODY) {
return;
}
responseTimeout = null;
changeState(State.RESPONSE_TIMEOUT);
close();
}
}, configuration.getResponseTimeout(), TimeUnit.MILLISECONDS);
}
private void cancelResponseTimeout() {
if (responseTimeout != null) {
responseTimeout.cancel(true);
responseTimeout = null;
}
}
private void scheduleConnectTimeout() {
if (configuration.getConnectTimeout() == 0) {
return;
}
connectTimeout = scheduler.schedule(() -> {
synchronized (HttpConnection.this) {
if (state != State.CONNECTING) {
return;
}
connectTimeout = null;
changeState(State.CONNECT_TIMEOUT);
close();
}
}, configuration.getConnectTimeout(), TimeUnit.MILLISECONDS);
}
private void cancelConnectTimeout() {
if (connectTimeout != null) {
connectTimeout.cancel(true);
connectTimeout = null;
}
}
private void scheduleIdleTimeout() {
if (configuration.getConnectionIdleTimeout() == 0) {
return;
}
idleTimeout = scheduler.schedule(() -> {
synchronized (HttpConnection.this) {
if (state != State.IDLE) {
return;
}
idleTimeout = null;
changeState(State.IDLE_TIMEOUT);
close();
}
}, configuration.getConnectionIdleTimeout(), TimeUnit.MILLISECONDS);
}
private void cancelIdleTimeout() {
if (idleTimeout != null) {
idleTimeout.cancel(true);
idleTimeout = null;
}
}
private void cancelAllTimeouts() {
cancelConnectTimeout();
cancelIdleTimeout();
cancelResponseTimeout();
}
private synchronized void handleError(Throwable t) {
cancelAllTimeouts();
error = t;
changeState(State.ERROR);
close();
}
private void changeStateToIdle() {
scheduleIdleTimeout();
changeState(State.IDLE);
}
Throwable getError() {
return error;
}
HttpResponse getHttResponse() {
return httResponse;
}
private synchronized void handleResponseRead() {
cancelResponseTimeout();
changeState(State.RECEIVED);
if (persistentConnection) {
changeStateToIdle();
} else {
changeState(State.CLOSED);
}
}
private class ConnectionFilter extends Filter<HttpRequest, HttpResponse, HttpRequest, HttpResponse> {
ConnectionFilter(Filter<HttpRequest, HttpResponse, ?, ?> downstreamFilter) {
super(downstreamFilter);
}
@Override
boolean processRead(HttpResponse response) {
synchronized (HttpConnection.this) {
if (state != State.RECEIVING_HEADER && state != State.SENDING_REQUEST) {
return false;
}
if (state == State.SENDING_REQUEST) {
// great we received response header so fast that we did not even switch into "receiving header" state,
// do it now to complete the formal lifecycle
// this happens when write completion listener is overtaken by "read event"
changeState(State.RECEIVING_HEADER);
}
httResponse = response;
try {
processResponseHeaders(response);
} catch (IOException e) {
handleError(e);
return false;
}
if (response.getHasContent()) {
AsynchronousBodyInputStream bodyStream = httResponse.getBodyStream();
changeState(State.RECEIVING_BODY);
bodyStream.setStateChangeLister(new AsynchronousBodyInputStream.StateChangeLister() {
@Override
public void onError(Throwable t) {
handleError(t);
}
@Override
public void onAllDataRead() {
handleResponseRead();
}
});
} else {
handleResponseRead();
}
return false;
}
}
@Override
void processConnect() {
synchronized (HttpConnection.this) {
if (state != State.CONNECTING) {
return;
}
downstreamFilter.startSsl();
}
}
@Override
void processSslHandshakeCompleted() {
synchronized (HttpConnection.this) {
if (state != State.CONNECTING) {
return;
}
cancelConnectTimeout();
changeStateToIdle();
}
}
@Override
void processConnectionClosed() {
synchronized (HttpConnection.this) {
cancelAllTimeouts();
changeState(State.CLOSED_BY_SERVER);
HttpConnection.this.close();
}
}
@Override
void processError(Throwable t) {
handleError(t);
}
@Override
void write(HttpRequest data, CompletionHandler<HttpRequest> completionHandler) {
downstreamFilter.write(data, completionHandler);
}
}
enum State {
CREATED,
CONNECTING,
CONNECT_TIMEOUT,
IDLE,
SENDING_REQUEST,
RECEIVING_HEADER,
RECEIVING_BODY,
RECEIVED,
RESPONSE_TIMEOUT,
CLOSED_BY_SERVER,
CLOSED,
ERROR,
IDLE_TIMEOUT
}
interface StateChangeListener {
void onStateChanged(HttpConnection connection, State oldState, State newState);
}
}