Skip to content

Commit

Permalink
#30 building out the request forwarding netcode
Browse files Browse the repository at this point in the history
  • Loading branch information
hlafaille committed Aug 14, 2024
1 parent 97ec0ac commit 909a1b0
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 17 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ ATC; an API gateway designed from the ground up to be vendor independent.
* **Speedy:** Speed is a must when there may be hundreds of hops between microservices within your stack before the user finally gets a response. We can handle that, and more.
* **Free and Open Source, forever:** Every cloud vendor has their own proprietary solution to API management. Here, we aim to be independent of the big guys.

## Features

## Getting Started

We recommend you use an OCI Container (Docker, Podman) to deploy ATC. Follow these basic steps (and please file any issues you come across, we're still in early development)!
Expand Down
19 changes: 19 additions & 0 deletions integrationTests/spam.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

# URL to be requested
url="https://localhost:8443"
atcIdentityToken="N2UgODMgZTggMTMgZDYgZGIgYmQgYzIgMzMgOTMgMzYgMGUgZjMgMDIgNzEgNzI="
hostHeader="api.weather.gov"

# unlimit file descriptors
ulimit -n 10000

# Loop to call curl 10000 times
for i in {1..10000}
do
curl -s --insecure "$url" -H "X-ATC-IdentityToken: ${atcIdentityToken}" -H "Host: ${hostHeader}" &
done

wait

echo "Completed 10,000 curl requests."
81 changes: 81 additions & 0 deletions src/main/java/io/kerosenelabs/atc/client/SimpleHttpClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.kerosenelabs.atc.client;

import io.kerosenelabs.kindling.HttpRequest;
import io.kerosenelabs.kindling.HttpResponse;
import io.kerosenelabs.kindling.constant.HttpStatus;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SimpleHttpClient {
public static HttpResponse convertStdHttpResponseToKindlingHttpResponse(java.net.http.HttpResponse<String> stdHttpResponse) {
// convert std httpresponse to a kindling httpresponse
HashMap<String, String> headers = new HashMap<>();
for (Map.Entry<String, List<String>> entry : stdHttpResponse.headers().map().entrySet()) {
if (entry.getKey().contains(":status")) {
continue;
}
headers.put(entry.getKey(), entry.getValue().getFirst());
}
return new HttpResponse.Builder()
.status(HttpStatus.OK) // <-- TODO: change this to a method that gets an enum member by number value
.headers(headers)
.content(stdHttpResponse.body())
.build();
}

/**
* Convert the Kindling HttpRequest to a stdlib HttpRequest
* @param httpRequest
* @return
* @throws URISyntaxException
*/
public static java.net.http.HttpRequest convertKindlingHttpRequestToStdHttpRequest(HttpRequest httpRequest) throws URISyntaxException {
// create the request
URI hostUri = new URI("https://" + httpRequest.getHeaders().get("Host") + httpRequest.getResource());
java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder().uri(hostUri);

// do content
java.net.http.HttpRequest.BodyPublisher bodyPublisher;
if (httpRequest.getContent() == null) {
bodyPublisher = java.net.http.HttpRequest.BodyPublishers.noBody();
} else {
bodyPublisher = java.net.http.HttpRequest.BodyPublishers.ofString(httpRequest.getContent());
}

// set method
switch (httpRequest.getHttpMethod()) {
case GET:
requestBuilder.GET();
break;
case POST:
requestBuilder.POST(bodyPublisher);
break;
case DELETE:
requestBuilder.DELETE();
break;
case PUT:
requestBuilder.PUT(bodyPublisher);
break;
}
return requestBuilder.build();
}

/**
* Get the response from the provider
* @param request The request from the consumer
* @return
*/
public static HttpResponse doCall(HttpRequest request) throws URISyntaxException, IOException, InterruptedException {
try (HttpClient httpClient = HttpClient.newHttpClient()){
java.net.http.HttpRequest stdHttpRequest = convertKindlingHttpRequestToStdHttpRequest(request);
java.net.http.HttpResponse<String> providerResponse = httpClient.send(stdHttpRequest, java.net.http.HttpResponse.BodyHandlers.ofString());
return convertStdHttpResponseToKindlingHttpResponse(providerResponse);
}
}
}
56 changes: 39 additions & 17 deletions src/main/java/io/kerosenelabs/atc/server/CentralRequestHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.kerosenelabs.atc.client.SimpleHttpClient;
import io.kerosenelabs.atc.configuration.ConfigurationHandler;
import io.kerosenelabs.atc.configuration.model.Configuration;
import io.kerosenelabs.atc.model.ExceptionErrorResponse;
Expand All @@ -14,6 +15,9 @@
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
Expand All @@ -33,6 +37,9 @@ public CentralRequestHandler() throws IOException {

@Override
public HttpResponse handle(HttpRequest httpRequest) throws KindlingException {
// record when we started working this request
Instant before = Instant.now();

// get our identity token
String identityToken = httpRequest.getHeaders().get("X-ATC-IdentityToken");
if (identityToken == null) {
Expand Down Expand Up @@ -64,33 +71,48 @@ public HttpResponse handle(HttpRequest httpRequest) throws KindlingException {
throw new KindlingException("Disallowed request, consumer does not declare the provider as consumed");
}

// iterate over services, ensure that we have a service providing this endpoint
Configuration.Service.ProvidingEndpoint providedEndpoint = null;
// get a service by our host
Configuration.Service providingService = null;
for (Configuration.Service service : services) {
for (Configuration.Service.ProvidingEndpoint provides : service.getProvides()) {
if (provides.getEndpoint().equals(httpRequest.getResource()) && provides.getMethods().contains(httpRequest.getHttpMethod())) {
providedEndpoint = provides;
providingService = service;
break;
}
if (service.getHosts().contains(httpRequest.getHeaders().get("Host"))) {
providingService = service;
break;
}
}
if (providingService == null) {
throw new KindlingException("A provider with the specified host could not be found");
}

// iterate over services, ensure that we have a service providing this endpoint
Configuration.Service.ProvidingEndpoint providedEndpoint = null;
for (Configuration.Service.ProvidingEndpoint provides : providingService.getProvides()) {
if (provides.getEndpoint().equals(httpRequest.getResource()) && provides.getMethods().contains(httpRequest.getHttpMethod())) {
providedEndpoint = provides;
break;
}
}
if (providedEndpoint == null) {
throw new KindlingException("Disallowed request, provider does not provide this endpoint");
}

// get our host header, ensure it's part of the providing service's hosts
String host = httpRequest.getHeaders().get("Host");
if (host == null) {
throw new KindlingException("Host header is missing");
}
if (!providingService.getHosts().contains(host)) {
throw new KindlingException(String.format("Disallowed request, provider does not declare '%s' as a host", host));
// forward the request, get our response
HttpResponse httpResponse;
try {
httpResponse = SimpleHttpClient.doCall(httpRequest);
} catch (URISyntaxException | IOException | InterruptedException e) {
throw new RuntimeException(e);
}

// forward the request
return new HttpResponse.Builder().status(HttpStatus.NO_CONTENT).build();
// log our request time in the appropriate format
Instant after = Instant.now();
long duration = Duration.between(before, after).toMillis();
String durationUnit = "ms";
if (duration == 0) {
duration = Duration.between(before, after).toNanos() / 1_000;
durationUnit = "us";
}
log.debug("Request took: {}{}", duration, durationUnit);
return httpResponse;
}

@Override
Expand Down

0 comments on commit 909a1b0

Please sign in to comment.