/** * Encapsulates a client-side HTTP request. * * <p> * * <p>Instances of this class are created by an {@link HttpClient} instance, via one of the methods * corresponding to the specific HTTP methods, or the generic {@link HttpClient#request} method * * <p> * * <p>Once an instance of this class has been obtained, headers can be set on it, and data can be * written to its body, if required. Once you are ready to send the request, the {@link #end()} * method must called. * * <p> * * <p>Nothing is sent until the request has been internally assigned an HTTP connection. The {@link * HttpClient} instance will return an instance of this class immediately, even if there are no HTTP * connections available in the pool. Any requests sent before a connection is assigned will be * queued internally and actually sent when an HTTP connection becomes available from the pool. * * <p> * * <p>The headers of the request are actually sent either when the {@link #end()} method is called, * or, when the first part of the body is written, whichever occurs first. * * <p> * * <p>This class supports both chunked and non-chunked HTTP. * * <p> * * <p>This class can only be used from the event loop that created it. * * <p> * * <p>An example of using this class is as follows: * * <p> * * <pre> * * HttpClientRequest req = httpClient.post("/some-url", new EventHandler<HttpClientResponse>() { * public void onEvent(HttpClientResponse response) { * System.out.println("Got response: " + response.statusCode); * } * }); * * req.putHeader("some-header", "hello"); * req.putHeader("Content-Length", 5); * req.write(Buffer.create(new byte[]{1, 2, 3, 4, 5})); * req.write(Buffer.create(new byte[]{6, 7, 8, 9, 10})); * req.end(); * * </pre> * * @author <a href="http://tfox.org">Tim Fox</a> */ public class HttpClientRequest implements WriteStream { private static final Logger log = LoggerFactory.getLogger(HttpClient.class); HttpClientRequest( final HttpClient client, final String method, final String uri, final Handler<HttpClientResponse> respHandler, final Context context, final Thread th) { this(client, method, uri, respHandler, context, th, false); } // Raw request - used by websockets // Raw requests won't have any headers set automatically, like Content-Length and Connection HttpClientRequest( final HttpClient client, final String method, final String uri, final Handler<HttpClientResponse> respHandler, final Context context, final Thread th, final ClientConnection conn) { this(client, method, uri, respHandler, context, th, true); this.conn = conn; conn.setCurrentRequest(this); } private HttpClientRequest( final HttpClient client, final String method, final String uri, final Handler<HttpClientResponse> respHandler, final Context context, final Thread th, final boolean raw) { this.client = client; this.request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), uri); this.chunked = false; this.respHandler = respHandler; this.context = context; this.th = th; this.raw = raw; } private final HttpClient client; private final HttpRequest request; private final Handler<HttpClientResponse> respHandler; private Handler<Void> continueHandler; private final Context context; private final boolean raw; final Thread th; private boolean chunked; private ClientConnection conn; private Handler<Void> drainHandler; private Handler<Exception> exceptionHandler; private boolean headWritten; private boolean completed; private LinkedList<PendingChunk> pendingChunks; private int pendingMaxSize = -1; private boolean connecting; private boolean writeHead; private long written; private long contentLength = 0; /** * If {@code chunked} is {@code true}, this request will use HTTP chunked encoding, and each call * to write to the body will correspond to a new HTTP chunk sent on the wire. If chunked encoding * is used the HTTP header {@code Transfer-Encoding} with a value of {@code Chunked} will be * automatically inserted in the request. * * <p>If {@code chunked} is {@code false}, this request will not use HTTP chunked encoding, and * therefore if any data is written the body of the request, the total size of that data must be * set in the {@code Content-Length} header <b>before</b> any data is written to the request body. * If no data is written, then a {@code Content-Length} header with a value of {@code 0} will be * automatically inserted when the request is sent. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest setChunked(boolean chunked) { check(); if (written > 0) { throw new IllegalStateException("Cannot set chunked after data has been written on request"); } this.chunked = chunked; return this; } /** * Inserts a header into the request. The {@link Object#toString()} method will be called on * {@code value} to determine the String value to actually use for the header value. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest putHeader(String key, Object value) { check(); request.setHeader(key, value); checkContentLengthChunked(key, value); return this; } /** * Inserts all the specified headers into the request. The {@link Object#toString()} method will * be called on the header values {@code value} to determine the String value to actually use for * the header value. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest putAllHeaders(Map<String, ? extends Object> m) { check(); for (Map.Entry<String, ? extends Object> entry : m.entrySet()) { request.setHeader(entry.getKey(), entry.getValue().toString()); checkContentLengthChunked(entry.getKey(), entry.getValue()); } return this; } /** Write a {@link Buffer} to the request body. */ public void writeBuffer(Buffer chunk) { check(); write(chunk.getChannelBuffer(), null); } /** * Write a {@link Buffer} to the request body. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest write(Buffer chunk) { check(); return write(chunk.getChannelBuffer(), null); } /** * Write a {@link String} to the request body, encoded in UTF-8. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest write(String chunk) { check(); return write(Buffer.create(chunk).getChannelBuffer(), null); } /** * Write a {@link String} to the request body, encoded using the encoding {@code enc}. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest write(String chunk, String enc) { check(); return write(Buffer.create(chunk, enc).getChannelBuffer(), null); } /** * Write a {@link Buffer} to the request body. The {@code doneHandler} is called after the buffer * is actually written to the wire. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest write(Buffer chunk, Handler<Void> doneHandler) { check(); return write(chunk.getChannelBuffer(), doneHandler); } /** * Write a {@link String} to the request body, encoded in UTF-8. The {@code doneHandler} is called * after the buffer is actually written to the wire. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest write(String chunk, Handler<Void> doneHandler) { checkThread(); checkComplete(); return write(Buffer.create(chunk).getChannelBuffer(), doneHandler); } /** * Write a {@link String} to the request body, encoded with encoding {@code enc}. The {@code * doneHandler} is called after the buffer is actually written to the wire. * * <p> * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest write(String chunk, String enc, Handler<Void> doneHandler) { check(); return write(Buffer.create(chunk, enc).getChannelBuffer(), doneHandler); } /** * Data is queued until it is actually sent. To set the point at which the queue is considered * "full" call this method specifying the {@code maxSize} in bytes. * * <p>This method is used by the {@link org.vertx.java.core.streams.Pump} class to pump data * between different streams and perform flow control. */ public void setWriteQueueMaxSize(int maxSize) { check(); if (conn != null) { conn.setWriteQueueMaxSize(maxSize); } else { pendingMaxSize = maxSize; } } /** * If the amount of data that is currently queued is greater than the write queue max size see * {@link #setWriteQueueMaxSize(int)} then the request queue is considered full. * * <p>Data can still be written to the request even if the write queue is deemed full, however it * should be used as indicator to stop writing and push back on the source of the data, otherwise * you risk running out of available RAM. * * <p>This method is used by the {@link org.vertx.java.core.streams.Pump} class to pump data * between different streams and perform flow control. * * @return {@code true} if the write queue is full, {@code false} otherwise */ public boolean writeQueueFull() { check(); if (conn != null) { return conn.writeQueueFull(); } else { return false; } } /** * This method sets a drain handler {@code handler} on the request. The drain handler will be * called when write queue is no longer full and it is safe to write to it again. * * <p>The drain handler is actually called when the write queue size reaches <b>half</b> the write * queue max size to prevent thrashing. This method is used as part of a flow control strategy, * e.g. it is used by the {@link org.vertx.java.core.streams.Pump} class to pump data between * different streams. * * @param handler */ public void drainHandler(Handler<Void> handler) { check(); this.drainHandler = handler; if (conn != null) { conn .handleInterestedOpsChanged(); // If the channel is already drained, we want to call it // immediately } } /** * Set {@code handler} as an exception handler on the request. Any exceptions that occur, either * at connection setup time or later will be notified by calling the handler. If the request has * no handler than any exceptions occurring will be output to {@link System#err} */ public void exceptionHandler(Handler<Exception> handler) { check(); this.exceptionHandler = handler; } /** * If you send an HTTP request with the header {@code Expect} set to the value {@code * 100-continue} and the server responds with an interim HTTP response with a status code of * {@code 100} and a continue handler has been set using this method, then the {@code handler} * will be called. * * <p>You can then continue to write data to the request body and later end it. This is normally * used in conjunction with the {@link #sendHead()} method to force the request header to be * written before the request has ended. */ public void continueHandler(Handler<Void> handler) { check(); this.continueHandler = handler; } /** * Forces the head of the request to be written before {@link #end()} is called on the request. * This is normally used to implement HTTP 100-continue handling, see {@link * #continueHandler(org.vertx.java.core.Handler)} for more information. * * @return A reference to this, so multiple method calls can be chained. */ public HttpClientRequest sendHead() { check(); if (conn != null) { if (!headWritten) { writeHead(); headWritten = true; } } else { connect(); writeHead = true; } return this; } /** Same as {@link #end(Buffer)} but writes a String with the default encoding */ public void end(String chunk) { end(Buffer.create(chunk)); } /** Same as {@link #end(Buffer)} but writes a String with the specified encoding */ public void end(String chunk, String enc) { end(Buffer.create(chunk, enc)); } /** * Same as {@link #end()} but writes some data to the request body before ending. If the request * is not chunked and no other data has been written then the Content-Length header will be * automatically set */ public void end(Buffer chunk) { if (!chunked && contentLength == 0) { contentLength = chunk.length(); request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(contentLength)); } write(chunk); end(); } /** * Ends the request. If no data has been written to the request body, and {@link #sendHead()} has * not been called then the actual request won't get written until this method gets called. * * <p>Once the request has ended, it cannot be used any more, and if keep alive is true the * underlying connection will be returned to the {@link HttpClient} pool so it can be assigned to * another request. */ public void end() { check(); completed = true; if (conn != null) { if (!headWritten) { // No body writeHead(); } else if (chunked) { // Body written - we use HTTP chunking so must send an empty buffer writeEndChunk(); } conn.endRequest(); } else { connect(); } } void handleDrained() { checkThread(); if (drainHandler != null) { drainHandler.handle(null); } } void handleException(Exception e) { checkThread(); if (exceptionHandler != null) { exceptionHandler.handle(e); } else { log.error("Unhandled exception", e); } } void handleResponse(HttpClientResponse resp) { try { if (resp.statusCode == 100) { if (continueHandler != null) { continueHandler.handle(null); } } else { respHandler.handle(resp); } } catch (Throwable t) { if (t instanceof Exception) { handleException((Exception) t); } else { log.error("Unhandled exception", t); } } } private void checkContentLengthChunked(String key, Object value) { if (key.equals(HttpHeaders.Names.CONTENT_LENGTH)) { contentLength = Integer.parseInt(value.toString()); } else if (key.equals(HttpHeaders.Names.TRANSFER_ENCODING) && value.equals(HttpHeaders.Values.CHUNKED)) { chunked = true; } } private void connect() { if (!connecting) { // We defer actual connection until the first part of body is written or end is called // This gives the user an opportunity to set an exception handler before connecting so // they can capture any exceptions on connection client.getConnection( new Handler<ClientConnection>() { public void handle(ClientConnection conn) { connected(conn); } }, context); connecting = true; } } private void connected(ClientConnection conn) { checkThread(); conn.setCurrentRequest(this); this.conn = conn; // If anything was written or the request ended before we got the connection, then // we need to write it now if (pendingMaxSize != -1) { conn.setWriteQueueMaxSize(pendingMaxSize); } if (pendingChunks != null || writeHead || completed) { writeHead(); headWritten = true; } if (pendingChunks != null) { for (PendingChunk chunk : pendingChunks) { sendChunk(chunk.chunk, chunk.doneHandler); } } if (completed) { if (chunked) { writeEndChunk(); } conn.endRequest(); } } private void writeHead() { request.setChunked(chunked); if (!raw) { request.setHeader(HttpHeaders.Names.HOST, conn.hostHeader); if (chunked) { request.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); } else if (contentLength == 0) { // request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); } } conn.write(request); } private HttpClientRequest write(ChannelBuffer buff, Handler<Void> doneHandler) { written += buff.readableBytes(); if (!raw && !chunked && written > contentLength) { throw new IllegalStateException( "You must set the Content-Length header to be the total size of the message " + "payload BEFORE sending any data if you are not using HTTP chunked encoding. " + "Current written: " + written + " Current Content-Length: " + contentLength); } if (conn == null) { if (pendingChunks == null) { pendingChunks = new LinkedList<>(); } pendingChunks.add(new PendingChunk(buff, doneHandler)); connect(); } else { if (!headWritten) { writeHead(); headWritten = true; } sendChunk(buff, doneHandler); } return this; } private void sendChunk(ChannelBuffer buff, Handler<Void> doneHandler) { Object write = chunked ? new DefaultHttpChunk(buff) : buff; ChannelFuture writeFuture = conn.write(write); if (doneHandler != null) { conn.addFuture(doneHandler, writeFuture); } } private void writeEndChunk() { conn.write(new DefaultHttpChunk(ChannelBuffers.EMPTY_BUFFER)); } private void check() { checkThread(); checkComplete(); } private void checkComplete() { if (completed) { throw new IllegalStateException("Request already complete"); } } private void checkThread() { // All ops must always be invoked on same thread if (Thread.currentThread() != th) { throw new IllegalStateException( "Invoked with wrong thread, actual: " + Thread.currentThread() + " expected: " + th); } } private static class PendingChunk { final ChannelBuffer chunk; final Handler<Void> doneHandler; private PendingChunk(ChannelBuffer chunk, Handler<Void> doneHandler) { this.chunk = chunk; this.doneHandler = doneHandler; } } }
/** * Handler for ietf-08. * * @author Michael Dobozy * @author Bob McWhirter */ public class Handshake00 implements Handshake { private static Logger log = LoggerFactory.getLogger(Handshake08.class); private WebSocketChallenge00 challenge; protected String getWebSocketLocation(HttpRequest request) { return "ws://" + request.getHeader(HttpHeaders.Names.HOST) + request.getUri(); } public Handshake00() throws NoSuchAlgorithmException { this.challenge = new WebSocketChallenge00(); } public static boolean matches(HttpRequest request) { return (request.containsHeader("Sec-WebSocket-Key1") && request.containsHeader("Sec-WebSocket-Key2")); } public void fillInRequest(HttpClientRequest req, String hostHeader) throws Exception { req.putHeader(HttpHeaders.Names.CONNECTION, "Upgrade"); req.putHeader(HttpHeaders.Names.UPGRADE, "WebSocket"); req.putHeader(HttpHeaders.Names.HOST, hostHeader); req.putHeader(HttpHeaders.Names.SEC_WEBSOCKET_KEY1, this.challenge.getKey1String()); req.putHeader(HttpHeaders.Names.SEC_WEBSOCKET_KEY2, this.challenge.getKey2String()); Buffer buff = Buffer.create(6); buff.appendBytes(challenge.getKey3()); buff.appendByte((byte) '\r'); buff.appendByte((byte) '\n'); req.write(buff); } public HttpResponse generateResponse(HttpRequest request) throws Exception { HttpResponse response = new DefaultHttpResponse( HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake - IETF-00")); response.addHeader(HttpHeaders.Names.CONNECTION, "Upgrade"); response.addHeader(HttpHeaders.Names.UPGRADE, "WebSocket"); String origin = request.getHeader(Names.ORIGIN); if (origin != null) { response.addHeader(Names.SEC_WEBSOCKET_ORIGIN, request.getHeader(Names.ORIGIN)); } response.addHeader(Names.SEC_WEBSOCKET_LOCATION, getWebSocketLocation(request)); String protocol = request.getHeader(Names.SEC_WEBSOCKET_PROTOCOL); if (protocol != null) { response.addHeader(Names.SEC_WEBSOCKET_PROTOCOL, protocol); } // Calculate the answer of the challenge. String key1 = request.getHeader(Names.SEC_WEBSOCKET_KEY1); String key2 = request.getHeader(Names.SEC_WEBSOCKET_KEY2); byte[] key3 = new byte[8]; request.getContent().readBytes(key3); byte[] solution = WebSocketChallenge00.solve(key1, key2, key3); ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(solution.length + 2); buffer.writeBytes(solution); response.addHeader("Content-Length", buffer.readableBytes()); response.setContent(buffer); response.setChunked(false); return response; } public void onComplete(HttpClientResponse response, final CompletionHandler<Void> doneHandler) { final Buffer buff = Buffer.create(16); response.dataHandler( new Handler<Buffer>() { public void handle(Buffer data) { buff.appendBuffer(data); } }); response.endHandler( new SimpleHandler() { public void handle() { byte[] bytes = buff.getBytes(); SimpleFuture<Void> fut = new SimpleFuture<>(); try { if (challenge.verify(bytes)) { fut.setResult(null); } else { fut.setException(new Exception("Invalid websocket handshake response")); } } catch (Exception e) { fut.setException(e); } doneHandler.handle(fut); } }); } public ChannelHandler getEncoder(boolean server) { return new WebSocketFrameEncoder00(); } public ChannelHandler getDecoder() { return new WebSocketFrameDecoder00(); } }