Skip to content

Commit

Permalink
Add Http Request and Response APIs (#57)
Browse files Browse the repository at this point in the history
* Add Http Request and Response APIs
  • Loading branch information
alexw91 authored and Justin Boswell committed Jun 5, 2019
1 parent 50254a9 commit 9dfeeb4
Show file tree
Hide file tree
Showing 19 changed files with 1,428 additions and 31 deletions.
2 changes: 2 additions & 0 deletions codebuild/common-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ ENDPOINT=$(aws secretsmanager get-secret-value --secret-id "unit-test/endpoint"

# build java package
cd $CODEBUILD_SRC_DIR

ulimit -c unlimited
mvn -B test -DredirectTestOutputToFile=true -DreuseForks=false -Dendpoint=$ENDPOINT -Dcertificate=/tmp/certificate.pem -Dprivatekey=/tmp/privatekey.pem -Drootca=/tmp/AmazonRootCA1.pem
1 change: 1 addition & 0 deletions codebuild/linux-clang3-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
1 change: 1 addition & 0 deletions codebuild/linux-clang6-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
1 change: 1 addition & 0 deletions codebuild/linux-gcc-4x-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
1 change: 1 addition & 0 deletions codebuild/linux-gcc-4x-x86.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
1 change: 1 addition & 0 deletions codebuild/linux-gcc-5x-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
1 change: 1 addition & 0 deletions codebuild/linux-gcc-6x-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
1 change: 1 addition & 0 deletions codebuild/linux-gcc-7x-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ artifacts:
files:
- 'target/surefire-reports/**'
- 'hs_err_pid*'
- 'core*'
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.crt.http;

import java.nio.ByteBuffer;

/**
* Interface that Native code knows how to call when handling Http Request/Responses
*
* Maps 1-1 to the Native Http API here: https://github.com/awslabs/aws-c-http/blob/master/include/aws/http/request_response.h
*/
public interface CrtHttpStreamHandler {

/**
* Called from Native when new Http Headers have been received.
* Note that this function may be called multiple times as HTTP headers are received.
*
* @param stream The HttpStream object
* @param responseStatusCode The HTTP Response Status Code
* @param nextHeaders The headers received in the latest IO event.
*/
void onResponseHeaders(HttpStream stream, int responseStatusCode, HttpHeader[] nextHeaders);

/**
* Called from Native once all HTTP Headers are processed. Will not be called if there are no Http Headers in the
* response. Guaranteed to be called exactly once if there is at least 1 Header.
*
* @param stream The HttpStream object
* @param hasBody True if the HTTP Response had a Body, false otherwise.
*/
default void onResponseHeadersDone(HttpStream stream, boolean hasBody) {
/* Optional Callback, do nothing by default */
}

/**
* Called when new Body bytes have been received.
* Note that this function may be called multiple times as bodyBytes are received.
*
* Do NOT keep a reference to this ByteBuffer past the lifetime of this function call. The CommonRuntime reserves
* the right to use DirectByteBuffers pointing to memory that only lives as long as the function call.
*
* Sliding Window:
* The Native HttpConnection EventLoop will keep sending data until the end of the sliding Window is reached.
* The user application is responsible for setting the initial Window size appropriately when creating the
* HttpConnection, and for incrementing the sliding window appropriately throughout the lifetime of the HttpStream.
*
* For more info, see:
* - https://en.wikipedia.org/wiki/Sliding_window_protocol
*
* @param bodyBytesIn The HTTP Body Bytes received in the last IO Event. The user MUST either copy all bytes from
* this Buffer, since there will not be another chance to read this data.
* @return The number of bytes to move the sliding window by. Repeatedly returning zero will eventually cause the
* sliding window to fill up and data to stop flowing until the user slides the window back open.
*/
default int onResponseBody(HttpStream stream, ByteBuffer bodyBytesIn) {
/* Optional Callback, ignore incoming response body by default unless user wants to capture it. */
return bodyBytesIn.remaining();
}

/**
* Called from Native when the Response has completed.
* @param stream
* @param errorCode
*/
void onResponseComplete(HttpStream stream, int errorCode);

/**
* Called from Native when the Http Request has a Body (Eg PUT/POST requests).
* Note that this function may be called many times as Native sends the Request Body.
*
* Do NOT keep a reference to this ByteBuffer past the lifetime of this function call. The CommonRuntime reserves
* the right to use DirectByteBuffers pointing to memory that only lives as long as the function call.
*
* @param stream The HttpStream for this Request/Response Pair
* @param bodyBytesOut The Buffer to write the Request Body Bytes to.
* @return True if Request body is complete, false otherwise.
*/
default boolean sendRequestBody(HttpStream stream, ByteBuffer bodyBytesOut) {
/* Optional Callback, return empty request body by default unless user wants to return one. */
return true;
}

}
106 changes: 103 additions & 3 deletions src/main/java/software/amazon/awssdk/crt/http/HttpConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

package software.amazon.awssdk.crt.http;

import software.amazon.awssdk.crt.AsyncCallback;
import software.amazon.awssdk.crt.CrtResource;
import software.amazon.awssdk.crt.CrtRuntimeException;
import software.amazon.awssdk.crt.io.ClientBootstrap;
Expand All @@ -24,6 +23,7 @@

import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import static software.amazon.awssdk.crt.CRT.AWS_CRT_SUCCESS;

Expand All @@ -40,10 +40,12 @@ public class HttpConnection extends CrtResource {
private static final String HTTPS = "https";
private static final int DEFAULT_HTTP_PORT = 80;
private static final int DEFAULT_HTTPS_PORT = 443;
private static final int DEFAULT_MAX_WINDOW_SIZE = Integer.MAX_VALUE;

private final ClientBootstrap clientBootstrap;
private final SocketOptions socketOptions;
private final TlsContext tlsContext;
private final int windowSize;
private final URI uri;
private final int port;
private final boolean useTls;
Expand All @@ -62,7 +64,25 @@ public class HttpConnection extends CrtResource {
*/
public static CompletableFuture<HttpConnection> createConnection(URI uri, ClientBootstrap bootstrap,
SocketOptions socketOptions, TlsContext tlsContext) throws CrtRuntimeException {
HttpConnection conn = new HttpConnection(uri, bootstrap, socketOptions, tlsContext);
HttpConnection conn = new HttpConnection(uri, bootstrap, socketOptions, tlsContext, DEFAULT_MAX_WINDOW_SIZE);
return conn.connect();
}

/**
* Creates a new CompletableFuture for a new HttpConnection.
* @param uri Must be non-null and contain a hostname
* @param bootstrap The ClientBootstrap to use for the Connection
* @param socketOptions The SocketOptions to use for the Connection
* @param tlsContext The TlsContext to use for the Connection
* @param windowSize The Initial Window size for requests made on this connection
* @return CompletableFuture indicating when the connection has completed
* @throws CrtRuntimeException if Native threw a CrtRuntimeException
*/
public static CompletableFuture<HttpConnection> createConnection(URI uri, ClientBootstrap bootstrap,
SocketOptions socketOptions,
TlsContext tlsContext,
int windowSize) throws CrtRuntimeException {
HttpConnection conn = new HttpConnection(uri, bootstrap, socketOptions, tlsContext, windowSize);
return conn.connect();
}

Expand All @@ -73,14 +93,15 @@ public static CompletableFuture<HttpConnection> createConnection(URI uri, Client
* @param socketOptions The SocketOptions to use for the Connection
* @param tlsContext The TlsContext to use for the Connection
*/
private HttpConnection(URI uri, ClientBootstrap bootstrap, SocketOptions socketOptions, TlsContext tlsContext) {
private HttpConnection(URI uri, ClientBootstrap bootstrap, SocketOptions socketOptions, TlsContext tlsContext, int windowSize) {
if (uri == null) { throw new IllegalArgumentException("URI must not be null"); }
if (uri.getScheme() == null) { throw new IllegalArgumentException("URI does not have a Scheme"); }
if (!HTTP.equals(uri.getScheme()) && !HTTPS.equals(uri.getScheme())) { throw new IllegalArgumentException("URI has unknown Scheme"); }
if (uri.getHost() == null) { throw new IllegalArgumentException("URI does not have a Host name"); }
if (bootstrap == null || bootstrap.isNull()) { throw new IllegalArgumentException("ClientBootstrap must not be null"); }
if (socketOptions == null || socketOptions.isNull()) { throw new IllegalArgumentException("SocketOptions must not be null"); }
if (HTTPS.equals(uri.getScheme()) && tlsContext == null) { throw new IllegalArgumentException("TlsContext must not be null if https is used"); }
if (windowSize <= 0) { throw new IllegalArgumentException("Window Size must be greater than zero.");}

int port = uri.getPort();

Expand All @@ -100,6 +121,7 @@ private HttpConnection(URI uri, ClientBootstrap bootstrap, SocketOptions socketO
this.clientBootstrap = bootstrap;
this.socketOptions = socketOptions;
this.tlsContext = tlsContext;
this.windowSize = windowSize;
this.connectedFuture = new CompletableFuture<>();
this.shutdownFuture = new CompletableFuture<>();
}
Expand All @@ -117,18 +139,81 @@ private CompletableFuture<HttpConnection> connect() throws CrtRuntimeException {
clientBootstrap.native_ptr(),
socketOptions.native_ptr(),
useTls ? tlsContext.native_ptr() : 0,
windowSize,
uri.getHost(),
port));

return connectedFuture;
}

/**
* Schedules an HttpRequest on the Native EventLoop for this HttpConnection.
*
* @param request The Request to make to the Server.
* @param streamHandler The Stream Handler to be called from the Native EventLoop
* @throws CrtRuntimeException
* @return The HttpStream that represents this Request/Response Pair. It can be closed at any time during the
* request/response, but must be closed by the user thread making this request when it's done.
*/
public HttpStream makeRequest(HttpRequest request, CrtHttpStreamHandler streamHandler) throws CrtRuntimeException {
if (isShutdownComplete() || isNull()) {
throw new IllegalStateException("HttpConnection has been shut down, can't make requests on it.");
}

HttpStream stream = httpConnectionMakeRequest(native_ptr(),
request.getMethod(),
request.getEncodedPath(),
request.getHeaders(),
streamHandler);

if (stream == null || stream.isNull()) {
throw new IllegalStateException("HttpStream is null");
}

return stream;
}

/**
* Closes and frees this HttpConnection and any native sub-resources associated with this connection
*/
@Override
public void close() {
if (didConnectSuccessfully() && !isShutdownComplete()) {
/**
* We have to wait for the connection to finish shutting down to avoid race conditions between
* shutdown tasks and memory release tasks.
*
* The httpConnectionShutdown() call schedules shutdown tasks on the Native EventLoop that may send
* HTTP/TLS/TCP shutdown messages to peers if necessary and will eventually cause internal connection
* memory to stop being accessed.
*
* The httpConnectionRelease() call will begin releasing internal connection memory. If the shutdown isn't
* complete before httpConnectionRelease(), it can lead to the shutdown tasks accessing memory that's been
* released, resulting in Segfaults.
*/
try {
// Give Shutdown 10 seconds to complete, otherwise throw a Timeout Exception
this.shutdown().get(10, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

if (!isNull()) {
try {
/**
* FIXME: The above shutdown().get() should be enough to avoid race conditions, but aws-c-http has a
* bug in the way it orders it's shutdown callbacks. Add an artificial sleep here to avoid Race
* Condition with the EventLoop when shutting down the TLS Connection.
*
* Tracking Issue: https://github.com/awslabs/aws-c-http/issues/66
* TraceLog: https://gist.github.com/alexw91/e6205fd38ecc530a55b956c98ca189dc
*/
Thread.sleep(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}

httpConnectionRelease(release());
}

Expand Down Expand Up @@ -164,6 +249,15 @@ public CompletableFuture<Void> getShutdownFuture() {
return shutdownFuture;
}

private boolean didConnectSuccessfully() {
return connectedFuture.isDone() && !connectedFuture.isCompletedExceptionally();
}


private boolean isShutdownComplete() {
return shutdownFuture.isDone();
}

/**
* Schedules a task on the Native EventLoop to shut down the current connection
* @return When this future completes, the shutdown is complete
Expand All @@ -188,10 +282,16 @@ private static native long httpConnectionNew(HttpConnection thisObj,
long client_bootstrap,
long socketOptions,
long tlsContext,
int windowSize,
String endpoint,
int port) throws CrtRuntimeException;

private static native void httpConnectionShutdown(long connection) throws CrtRuntimeException;
private static native void httpConnectionRelease(long connection);

private static native HttpStream httpConnectionMakeRequest(long connection,
String method,
String uri,
HttpHeader[] headers,
CrtHttpStreamHandler crtHttpStreamHandler) throws CrtRuntimeException;
}
53 changes: 53 additions & 0 deletions src/main/java/software/amazon/awssdk/crt/http/HttpHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.crt.http;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class HttpHeader {
private final static Charset UTF8 = StandardCharsets.UTF_8;
private byte[] name; /* Not final, Native will manually set name after calling empty Constructor. */
private byte[] value; /* Not final, Native will manually set value after calling empty Constructor. */

/** Called by Native to create a new HttpHeader. This is so that Native doesn't have to worry about UTF8
* encoding/decoding issues. The user thread will deal with them when they call getName() or getValue() **/
private HttpHeader() {}

public HttpHeader(String name, String value){
this.name = name.getBytes(UTF8);
this.value = value.getBytes(UTF8);
}

public String getName() {
if (name == null) {
return "";
}
return new String(name, UTF8);
}

public String getValue() {
if (value == null) {
return "";
}
return new String(value, UTF8);
}

@Override
public String toString() {
return getName() + ":" + getValue();
}
}
Loading

0 comments on commit 9dfeeb4

Please sign in to comment.