/* * Copyright (c) 2011-2013 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.vertx.core.http.impl; import io.netty.buffer.ByteBuf; import io.netty.channel.*; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.websocketx.*; import io.netty.util.ReferenceCountUtil; import io.vertx.codegen.annotations.Nullable; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.VertxException; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.*; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpVersion; import io.vertx.core.http.impl.ws.WebSocketFrameInternal; import io.vertx.core.impl.ContextImpl; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.core.net.NetSocket; import io.vertx.core.net.impl.ConnectionBase; import io.vertx.core.net.impl.NetSocketImpl; import io.vertx.core.net.impl.VertxNetHandler; import io.vertx.core.spi.metrics.HttpClientMetrics; import java.net.URI; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.Queue; import static io.vertx.core.http.HttpHeaders.ACCEPT_ENCODING; import static io.vertx.core.http.HttpHeaders.CLOSE; import static io.vertx.core.http.HttpHeaders.CONNECTION; import static io.vertx.core.http.HttpHeaders.DEFLATE_GZIP; import static io.vertx.core.http.HttpHeaders.HOST; import static io.vertx.core.http.HttpHeaders.KEEP_ALIVE; import static io.vertx.core.http.HttpHeaders.TRANSFER_ENCODING; /** * * This class is optimised for performance when used on the same event loop. However it can be used safely from other threads. * * The internal state is protected using the synchronized keyword. If always used on the same event loop, then * we benefit from biased locking which makes the overhead of synchronized near zero. * * @author <a href="http://tfox.org">Tim Fox</a> */ class ClientConnection extends ConnectionBase implements HttpClientConnection, HttpClientStream { private static final Logger log = LoggerFactory.getLogger(ClientConnection.class); private final HttpClientImpl client; private final boolean ssl; private final String host; private final int port; private final Http1xPool pool; private final Object endpointMetric; // Requests can be pipelined so we need a queue to keep track of requests private final Deque<HttpClientRequestImpl> requests = new ArrayDeque<>(); private final HttpClientMetrics metrics; private final HttpVersion version; private WebSocketClientHandshaker handshaker; private HttpClientRequestImpl currentRequest; private HttpClientResponseImpl currentResponse; private HttpClientRequestImpl requestForResponse; private WebSocketImpl ws; private boolean reset; private boolean paused; private Buffer pausedChunk; ClientConnection(HttpVersion version, HttpClientImpl client, Object endpointMetric, Channel channel, boolean ssl, String host, int port, ContextImpl context, Http1xPool pool, HttpClientMetrics metrics) { super(client.getVertx(), channel, context); this.client = client; this.ssl = ssl; this.host = host; this.port = port; this.pool = pool; this.metrics = metrics; this.version = version; this.endpointMetric = endpointMetric; } public HttpClientMetrics metrics() { return metrics; } synchronized HttpClientRequestImpl getCurrentRequest() { return currentRequest; } synchronized void toWebSocket(String requestURI, MultiMap headers, WebsocketVersion vers, String subProtocols, int maxWebSocketFrameSize, Handler<WebSocket> wsConnect) { if (ws != null) { throw new IllegalStateException("Already websocket"); } try { URI wsuri = new URI(requestURI); if (!wsuri.isAbsolute()) { // Netty requires an absolute url wsuri = new URI((ssl ? "https:" : "http:") + "//" + host + ":" + port + requestURI); } WebSocketVersion version = WebSocketVersion.valueOf((vers == null ? WebSocketVersion.V13 : vers).toString()); HttpHeaders nettyHeaders; if (headers != null) { nettyHeaders = new DefaultHttpHeaders(); for (Map.Entry<String, String> entry: headers) { nettyHeaders.add(entry.getKey(), entry.getValue()); } } else { nettyHeaders = null; } handshaker = WebSocketClientHandshakerFactory.newHandshaker(wsuri, version, subProtocols, false, nettyHeaders, maxWebSocketFrameSize,!client.getOptions().isSendUnmaskedFrames(),false); ChannelPipeline p = channel.pipeline(); p.addBefore("handler", "handshakeCompleter", new HandshakeInboundHandler(wsConnect, version != WebSocketVersion.V00)); handshaker.handshake(channel).addListener(future -> { Handler<Throwable> handler = exceptionHandler(); if (!future.isSuccess() && handler != null) { handler.handle(future.cause()); } }); } catch (Exception e) { handleException(e); } } private final class HandshakeInboundHandler extends ChannelInboundHandlerAdapter { private final boolean supportsContinuation; private final Handler<WebSocket> wsConnect; private final ContextImpl context; private final Queue<Object> buffered = new ArrayDeque<>(); private FullHttpResponse response; private boolean handshaking = true; public HandshakeInboundHandler(Handler<WebSocket> wsConnect, boolean supportsContinuation) { this.supportsContinuation = supportsContinuation; this.wsConnect = wsConnect; this.context = vertx.getContext(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); // if still handshaking this means we not got any response back from the server and so need to notify the client // about it as otherwise the client would never been notified. if (handshaking) { handleException(new WebSocketHandshakeException("Connection closed while handshake in process")); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (handshaker != null && handshaking) { if (msg instanceof HttpResponse) { HttpResponse resp = (HttpResponse) msg; if (resp.getStatus().code() != 101) { handshaker = null; close(); handleException(new WebSocketHandshakeException("Websocket connection attempt returned HTTP status code " + resp.getStatus().code())); return; } response = new DefaultFullHttpResponse(resp.getProtocolVersion(), resp.getStatus()); response.headers().add(resp.headers()); } if (msg instanceof HttpContent) { if (response != null) { response.content().writeBytes(((HttpContent) msg).content()); if (msg instanceof LastHttpContent) { response.trailingHeaders().add(((LastHttpContent) msg).trailingHeaders()); try { handshakeComplete(ctx, response); channel.pipeline().remove(HandshakeInboundHandler.this); for (; ; ) { Object m = buffered.poll(); if (m == null) { break; } ctx.fireChannelRead(m); } } catch (WebSocketHandshakeException e) { close(); handleException(e); } } } } } else { buffered.add(msg); } } private void handleException(WebSocketHandshakeException e) { handshaking = false; buffered.clear(); Handler<Throwable> handler = exceptionHandler(); if (handler != null) { context.executeFromIO(() -> { handler.handle(e); }); } else { log.error("Error in websocket handshake", e); } } private void handshakeComplete(ChannelHandlerContext ctx, FullHttpResponse response) { handshaking = false; ChannelHandler handler = ctx.pipeline().get(HttpContentDecompressor.class); if (handler != null) { // remove decompressor as its not needed anymore once connection was upgraded to websockets ctx.pipeline().remove(handler); } // Need to set context before constructor is called as writehandler registration needs this ContextImpl.setContext(context); WebSocketImpl webSocket = new WebSocketImpl(vertx, ClientConnection.this, supportsContinuation, client.getOptions().getMaxWebsocketFrameSize(), client.getOptions().getMaxWebsocketMessageSize()); ws = webSocket; handshaker.finishHandshake(channel, response); context.executeFromIO(() -> { log.debug("WebSocket handshake complete"); webSocket.setMetric(metrics().connected(endpointMetric, metric(), webSocket)); wsConnect.handle(webSocket); }); } } public ClientConnection closeHandler(Handler<Void> handler) { return (ClientConnection) super.closeHandler(handler); } public boolean isValid() { return channel.isOpen(); } int getOutstandingRequestCount() { return requests.size(); } @Override public void checkDrained() { handleInterestedOpsChanged(); } @Override public synchronized void handleInterestedOpsChanged() { if (!isNotWritable()) { if (currentRequest != null) { currentRequest.handleDrained(); } else if (ws != null) { ws.writable(); } } } void handleResponse(HttpResponse resp) { if (resp.status().code() == 100) { //If we get a 100 continue it will be followed by the real response later, so we don't remove it yet requestForResponse = requests.peek(); } else { requestForResponse = requests.poll(); } if (requestForResponse == null) { throw new IllegalStateException("No response handler"); } io.netty.handler.codec.http.HttpVersion nettyVersion = resp.protocolVersion(); HttpVersion vertxVersion; if (nettyVersion == io.netty.handler.codec.http.HttpVersion.HTTP_1_0) { vertxVersion = HttpVersion.HTTP_1_0; } else if (nettyVersion == io.netty.handler.codec.http.HttpVersion.HTTP_1_1) { vertxVersion = HttpVersion.HTTP_1_1; } else { vertxVersion = null; } HttpClientResponseImpl nResp = new HttpClientResponseImpl(requestForResponse, vertxVersion, this, resp.status().code(), resp.status().reasonPhrase(), new HeadersAdaptor(resp.headers())); currentResponse = nResp; if (metrics.isEnabled()) { metrics.responseBegin(requestForResponse.metric(), nResp); } if (vertxVersion != null) { requestForResponse.handleResponse(nResp); } else { requestForResponse.handleException(new IllegalStateException("Unsupported HTTP version: " + nettyVersion)); } } public void doPause() { super.doPause(); paused = true; } public void doResume() { super.doResume(); paused = false; if (pausedChunk != null) { vertx.runOnContext(v -> { if (pausedChunk != null) { Buffer chunk = pausedChunk; pausedChunk = null; currentResponse.handleChunk(chunk); } }); } } void handleResponseChunk(Buffer buff) { if (paused) { if (pausedChunk == null) { pausedChunk = buff.copy(); } else { pausedChunk.appendBuffer(buff); } } else { if (pausedChunk != null) { buff = pausedChunk.appendBuffer(buff); pausedChunk = null; } currentResponse.handleChunk(buff); } } void handleResponseEnd(LastHttpContent trailer) { if (metrics.isEnabled()) { HttpClientRequestBase req = currentResponse.request(); Object reqMetric = req.metric(); if (req.exceptionOccurred != null) { metrics.requestReset(reqMetric); } else { metrics.responseEnd(reqMetric, currentResponse); } } Buffer last = pausedChunk; pausedChunk = null; currentResponse.handleEnd(last, new HeadersAdaptor(trailer.trailingHeaders())); // We don't signal response end for a 100-continue response as a real response will follow // Also we keep the connection open for an HTTP CONNECT if (currentResponse.statusCode() != 100 && requestForResponse.method() != io.vertx.core.http.HttpMethod.CONNECT) { boolean close = false; // See https://tools.ietf.org/html/rfc7230#section-6.3 String responseConnectionHeader = currentResponse.getHeader(HttpHeaders.Names.CONNECTION); io.vertx.core.http.HttpVersion protocolVersion = client.getOptions().getProtocolVersion(); String requestConnectionHeader = requestForResponse.headers().get(HttpHeaders.Names.CONNECTION); // We don't need to protect against concurrent changes on forceClose as it only goes from false -> true if (HttpHeaders.Values.CLOSE.equalsIgnoreCase(responseConnectionHeader) || HttpHeaders.Values.CLOSE.equalsIgnoreCase(requestConnectionHeader)) { // In all cases, if we have a close connection option then we SHOULD NOT treat the connection as persistent close = true; } else if (protocolVersion == io.vertx.core.http.HttpVersion.HTTP_1_0 && !HttpHeaders.Values.KEEP_ALIVE.equalsIgnoreCase(responseConnectionHeader)) { // In the HTTP/1.0 case both request/response need a keep-alive connection header the connection to be persistent // currently Vertx forces the Connection header if keepalive is enabled for 1.0 close = true; } if (close) { pool.responseEnded(this, true); } else { if (reset) { if (requests.isEmpty()) { pool.responseEnded(this, true); } } else { pool.responseEnded(this, false); } } } currentResponse = null; } synchronized void handleWsFrame(WebSocketFrameInternal frame) { if (ws != null) { ws.handleFrame(frame); } } protected synchronized void handleClosed() { super.handleClosed(); if (ws != null) { ws.handleClosed(); } Exception e = new VertxException("Connection was closed"); // Signal requests failed if (metrics.isEnabled()) { for (HttpClientRequestImpl req: requests) { metrics.requestReset(req.metric()); } if (currentResponse != null) { metrics.requestReset(currentResponse.request().metric()); } } // Connection was closed - call exception handlers for any requests in the pipeline or one being currently written for (HttpClientRequestImpl req: requests) { req.handleException(e); } if (currentRequest != null) { currentRequest.handleException(e); } else if (currentResponse != null) { currentResponse.handleException(e); } // The connection has been closed - tell the pool about it, this allows the pool to create more // connections. Note the pool doesn't actually remove the connection, when the next person to get a connection // gets the closed on, they will check if it's closed and if so get another one. pool.connectionClosed(this); } public ContextImpl getContext() { return super.getContext(); } public void resetRequest(long code) { if (!reset) { reset = true; currentRequest = null; requests.removeLast(); if (requests.size() == 0) { pool.responseEnded(this, true); } } } @Override public void resetResponse(long code) { reset = true; pool.responseEnded(this, true); } private HttpRequest createRequest(HttpVersion version, HttpMethod method, String rawMethod, String uri, MultiMap headers) { DefaultHttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(version), HttpUtils.toNettyHttpMethod(method, rawMethod), uri, false); if (headers != null) { for (Map.Entry<String, String> header : headers) { // Todo : multi valued headers request.headers().add(header.getKey(), header.getValue()); } } return request; } private void prepareHeaders(HttpRequest request, String hostHeader, boolean chunked) { HttpHeaders headers = request.headers(); headers.remove(TRANSFER_ENCODING); if (!headers.contains(HOST)) { request.headers().set(HOST, hostHeader); } if (chunked) { HttpHeaders.setTransferEncodingChunked(request); } if (client.getOptions().isTryUseCompression() && request.headers().get(ACCEPT_ENCODING) == null) { // if compression should be used but nothing is specified by the user support deflate and gzip. request.headers().set(ACCEPT_ENCODING, DEFLATE_GZIP); } if (!client.getOptions().isKeepAlive() && client.getOptions().getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_1) { request.headers().set(CONNECTION, CLOSE); } else if (client.getOptions().isKeepAlive() && client.getOptions().getProtocolVersion() == io.vertx.core.http.HttpVersion.HTTP_1_0) { request.headers().set(CONNECTION, KEEP_ALIVE); } } public void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked) { HttpRequest request = createRequest(version, method, rawMethod, uri, headers); prepareHeaders(request, hostHeader, chunked); writeToChannel(request); } public void writeHeadWithContent(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf buf, boolean end) { HttpRequest request = createRequest(version, method, rawMethod, uri, headers); prepareHeaders(request, hostHeader, chunked); if (end) { if (buf != null) { writeToChannel(new AssembledFullHttpRequest(request, buf)); } else { writeToChannel(new AssembledFullHttpRequest(request)); } } else { writeToChannel(new AssembledHttpRequest(request, buf)); } } @Override public void writeBuffer(ByteBuf buff, boolean end) { if (end) { if (buff != null && buff.isReadable()) { writeToChannel(new DefaultLastHttpContent(buff, false)); } else { writeToChannel(LastHttpContent.EMPTY_LAST_CONTENT); } } else if (buff != null) { writeToChannel(new DefaultHttpContent(buff)); } } @Override public void writeFrame(int type, int flags, ByteBuf payload) { throw new IllegalStateException("Cannot write an HTTP/2 frame over an HTTP/1.x connection"); } @Override protected synchronized void handleException(Throwable e) { super.handleException(e); if (currentRequest != null) { currentRequest.handleException(e); } else { HttpClientRequestImpl req = requests.poll(); if (req != null) { req.handleException(e); } else if (currentResponse != null) { currentResponse.handleException(e); } } } public synchronized void beginRequest(HttpClientRequestImpl req) { if (currentRequest != null) { throw new IllegalStateException("Connection is already writing a request"); } if (metrics.isEnabled()) { Object reqMetric = client.httpClientMetrics().requestBegin(endpointMetric, metric(), localAddress(), remoteAddress(), req); req.metric(reqMetric); } this.currentRequest = req; this.requests.add(req); } public synchronized void endRequest() { if (currentRequest == null) { throw new IllegalStateException("No write in progress"); } if (metrics.isEnabled()) { metrics.requestEnd(currentRequest.metric()); } currentRequest = null; pool.requestEnded(this); } @Override public synchronized void close() { if (handshaker == null) { super.close(); } else { // make sure everything is flushed out on close endReadAndFlush(); // close the websocket connection by sending a close frame. handshaker.close(channel, new CloseWebSocketFrame(1000, null)); } } public NetSocket createNetSocket() { // connection was upgraded to raw TCP socket NetSocketImpl socket = new NetSocketImpl(vertx, channel, context, client.getSslHelper(), metrics); socket.metric(metric()); Map<Channel, NetSocketImpl> connectionMap = new HashMap<>(1); connectionMap.put(channel, socket); // Flush out all pending data endReadAndFlush(); // remove old http handlers and replace the old handler with one that handle plain sockets ChannelPipeline pipeline = channel.pipeline(); ChannelHandler inflater = pipeline.get(HttpContentDecompressor.class); if (inflater != null) { pipeline.remove(inflater); } pipeline.remove("codec"); pipeline.replace("handler", "handler", new VertxNetHandler<NetSocketImpl>(channel, socket, connectionMap) { @Override public void exceptionCaught(ChannelHandlerContext chctx, Throwable t) throws Exception { // remove from the real mapping pool.removeChannel(channel); super.exceptionCaught(chctx, t); } @Override public void channelInactive(ChannelHandlerContext chctx) throws Exception { // remove from the real mapping pool.removeChannel(channel); super.channelInactive(chctx); } @Override public void channelRead(ChannelHandlerContext chctx, Object msg) throws Exception { if (msg instanceof HttpContent) { if (msg instanceof LastHttpContent) { handleResponseEnd((LastHttpContent) msg); } ReferenceCountUtil.release(msg); return; } super.channelRead(chctx, msg); } @Override protected void handleMsgReceived(NetSocketImpl conn, Object msg) { ByteBuf buf = (ByteBuf) msg; conn.handleDataReceived(Buffer.buffer(buf)); } }); return socket; } @Override public HttpClientConnection connection() { return this; } @Override public HttpVersion version() { // Used to determine the http version in the HttpClientRequest#sendHead handler , for HTTP/1.1 it will // not yet know but it will for HTTP/2 return null; } @Override public int id() { return -1; } // @Override public HttpConnection goAway(long errorCode, int lastStreamId, Buffer debugData) { throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY"); } @Override public HttpConnection goAwayHandler(@Nullable Handler<GoAway> handler) { throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY"); } @Override public HttpConnection shutdownHandler(@Nullable Handler<Void> handler) { throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY"); } @Override public HttpConnection shutdown() { throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY"); } @Override public HttpConnection shutdown(long timeoutMs) { throw new UnsupportedOperationException("HTTP/1.x connections don't support GOAWAY"); } @Override public Http2Settings settings() { throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS"); } @Override public HttpConnection updateSettings(Http2Settings settings) { throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS"); } @Override public HttpConnection updateSettings(Http2Settings settings, Handler<AsyncResult<Void>> completionHandler) { throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS"); } @Override public Http2Settings remoteSettings() { throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS"); } @Override public HttpConnection remoteSettingsHandler(Handler<Http2Settings> handler) { throw new UnsupportedOperationException("HTTP/1.x connections don't support SETTINGS"); } @Override public HttpConnection ping(Buffer data, Handler<AsyncResult<Buffer>> pongHandler) { throw new UnsupportedOperationException("HTTP/1.x connections don't support PING"); } @Override public HttpConnection pingHandler(@Nullable Handler<Buffer> handler) { throw new UnsupportedOperationException("HTTP/1.x connections don't support PING"); } @Override public ClientConnection exceptionHandler(Handler<Throwable> handler) { return (ClientConnection) super.exceptionHandler(handler); } }