Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add Http Request and Response APIs #3

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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