Esempio n. 1
0
/**
 * 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;
    }
  }
}
Esempio n. 2
0
/**
 * 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();
  }
}