Skip to content

Commit

Permalink
prepared 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-junkietech committed Feb 8, 2021
1 parent 0dfc00e commit 6c7fbd8
Show file tree
Hide file tree
Showing 7 changed files with 791 additions and 23 deletions.
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM openjdk:11-jre

WORKDIR /app

ADD target/${project.build.finalName}-jar-with-dependencies.jar app.jar
# ADD target/yaproxy-1.0-SNAPSHOT-jar-with-dependencies.jar app.jar
# ADD config.yml .

ENTRYPOINT ["java", "-jar", "app.jar", "config.yml"]
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# yaproxy - Yet Another Proxy

## What it be

yaproxy is an experimental proxy implementation for HTTP/1.1 written in Java.
yaproxy is designed to be used alternatively as __reverse proxy__ or as traditional __forward proxy__.

Used as __reverse proxy__ yaproxy sits between multiple clients and one or several instances of a downstream service. It supports multiple downstream services with multiple instances, downstream services are identified using the `Host` HTTP header.
yaproxy listens to HTTP requests and forwards them to one of the instances of a downstream service that will process the requests.
Requests are load-balanced and multiple load-balancing strategies are supported.
After processing the request, the downstream service sends the HTTP response back to yaproxy, that forwards the response to the client making the initial request.

Used as __forward proxy__ yaproxy works exactly the same: the difference is only its configuration.

## Requirements
* Java 11
* Maven 3.3 (only for build)

## Usage
yaproxy reads its configuration from a file at start.
A standard `config.yml` looks like:
```
proxy:
listen:
address: "127.0.0.1"
port: 8080
services:
- name: my-service
domain: my-service.my-company.com
hosts:
- address: "127.0.0.1"
port: 8888
- address: "127.0.0.2"
port: 8888
#downstreamPoxy: localhost:8888
```
A full fledged configuration may provide additional parameters like `downstreamPoxy` and `loadBalancerStrategy`:
```
proxy:
listen:
address: "127.0.0.1"
port: 8080
services:
- name: my-service
domain: my-service.my-company.com
#downstreamPoxy: localhost:8888
hosts:
- address: "127.0.0.1"
port: 8888
- address: "127.0.0.2"
port: 8888
#downstreamPoxy: localhost:8888
#downstreamPoxy: localhost:8888
loadBalancerStrategy: ROUND_ROBIN | RANDOM
```
yaproxy can be executed using the command:
`java -jar yaproxy-1.0-SNAPSHOT-jar-with-dependencies.jar config.yml`

## Features
* multiple load balancer strategies
* `RANDOM` (default)
* `ROUND_ROBIN`
* in-memory cache, compliant with HTTP/1.1 header `Cache-Control`
* matching regex [*(public, *)?(max-age|s-maxage)=(\d+)](https://regex101.com/r/1Gpcn6/1)
* automatic cache invalidation (every 1') whenever cache entries expired
* support for downstream proxy (though without authentication mechanism)

## Known limitations
* No support for HTTPS
* No support for chunked response entities nor for compressed response content
* Headers from cached responses are forwarded as they were originally received (neither filtered nor adjusted)

## Third-party libraries
* org.yaml
* junit
* org.apache.logging.log4j
* org.apache.httpcomponents

## References
* [RFC 2616 about HTTP/1.1](https://tools.ietf.org/html/rfc2616)
* [Java Concurrency with Load Balancer Simulation](https://turkogluc.com/java-concurrency-with-load-balancer-simulation/)

## Contributing

Contributions are welcome.

## Copyright / License

Copyright 2021 Yaproxy

This software is licensed under the terms of the GNU General Public License v3.0. See the [LICENSE](./LICENSE) file.
9 changes: 2 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

<groupId>com.github.greg.junkietech.yaproxy</groupId>
<artifactId>yaproxy</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.0</version>
<packaging>jar</packaging>

<name>yaproxy</name>
<url>http://maven.apache.org</url>
<url>https://github.com/greg-junkietech/yaproxy</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down Expand Up @@ -50,11 +50,6 @@
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<!-- <dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.5</version>
</dependency> -->
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,26 @@ public void handle(HttpExchange exchange) {
exchange.getRequestMethod(), exchange.getRequestURI().toString(),
exchange.getRemoteAddress().toString(), reqHost);
try {
ALoadBalancer<ServiceRoute> lb = serviceMap.get(reqHost);
/* String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
if (contentType == null || !contentType.startsWith("application/json")) {
HttpIOUtil.writeResponse(exchange, "{'error':'Content-Type application/json is required'}",
HttpStatus.SC_BAD_REQUEST);
} else */ if (!serviceMap.containsKey(reqHost)) {
} else */ if (lb == null) {
HttpIOUtil.writeResponse(exchange, "{'error':'no service domain defined for host " + reqHost + "'}",
HttpStatus.SC_BAD_REQUEST);
} else {
ALoadBalancer<ServiceRoute> lb = serviceMap.get(reqHost);
handleClientRequest(exchange, lb);
}
} catch (IOException e) {
LOGGER.warn("IOException occurred while serving {} {} {} from {} with header Host '{}': {}",
exchange.getProtocol(), exchange.getRequestMethod(), exchange.getRequestURI().toString(),
exchange.getRemoteAddress().toString(), reqHost, e.getMessage());
} catch (RuntimeException e) {
LOGGER.warn("RuntimeException occurred while serving {} {} {} from {} with header Host '{}': {}",
exchange.getProtocol(), exchange.getRequestMethod(), exchange.getRequestURI().toString(),
exchange.getRemoteAddress().toString(), reqHost, e.getMessage());
LOGGER.warn("RuntimeException details", e);
}
}

Expand Down Expand Up @@ -119,7 +124,7 @@ private static void handleClientRequestFromDownstream(HttpExchange exchange, ALo
private static ProxyRequest createProxyRequest(HttpExchange exchange, ServiceRoute svcRoute) {
HttpRequest httpRequest;

// HTTTP/1.1 RFC 2616 section 4.3: https://tools.ietf.org/html/rfc2616
// HTTP/1.1 RFC 2616 section 4.3: https://tools.ietf.org/html/rfc2616
// The presence of a message-body in a request is signaled by the
// inclusion of a Content-Length or Transfer-Encoding header field in
// the request's message-headers.
Expand Down Expand Up @@ -147,14 +152,13 @@ private static void handleServiceResponse(HttpResponse svcResponse, HttpExchange
copyResponseHeaders(svcResponse.getAllHeaders(), exchange.getResponseHeaders());

if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
// HTTTP/1.1 RFC 2616 section 4.3: https://tools.ietf.org/html/rfc2616
// HTTP/1.1 RFC 2616 section 4.3: https://tools.ietf.org/html/rfc2616
// All 1xx (informational), 204 (no content), and 304 (not modified) responses
// MUST NOT include a message-body. All other responses do include a
// message-body, although it MAY be of zero length.
exchange.sendResponseHeaders(statusCode, 0);
} else {
long contentLength = RequestHandler
.getContentLength(svcResponse.getFirstHeader("Content-Length").getValue());
long contentLength = RequestHandler.getContentLength(svcResponse.getFirstHeader("Content-Length"));
exchange.sendResponseHeaders(statusCode, contentLength);
HttpEntity entity = svcResponse.getEntity();
if (entity.isChunked()) {
Expand All @@ -170,7 +174,7 @@ private static void handleServiceResponseBody(HttpResponse svcResponse, HttpEnti
OutputStream os = exchange.getResponseBody();
int statusCode = svcResponse.getStatusLine().getStatusCode();

// see HTTTP/1.1 RFC 2616 section 14.9: https://tools.ietf.org/html/rfc2616#section-14.9
// see HTTP/1.1 RFC 2616 section 14.9: https://tools.ietf.org/html/rfc2616#section-14.9
Header ccHeader = svcResponse.getFirstHeader(HEADER_CACHE_CONTROL);
Matcher ccHeaderPatternMatcher;
final String cachingLoginfo;
Expand All @@ -182,7 +186,6 @@ private static void handleServiceResponseBody(HttpResponse svcResponse, HttpEnti
entity.writeTo(os);
} else if ((ccHeaderPatternMatcher = Pattern.compile(" *(public, *)?(max-age|s-maxage)=(\\d+)")
.matcher(ccHeader.getValue())).matches()) {
// TODO cope with large content where content length exceeds int capacity
byte[] content = handleCachingResponseBody(entity, lb, proxyRequest, contentLength, ccHeaderPatternMatcher,
svcResponse.getAllHeaders());
cachingLoginfo = String.format("cached as supported %s", ccHeader);
Expand All @@ -203,6 +206,7 @@ private static void handleServiceResponseBody(HttpResponse svcResponse, HttpEnti
private static byte[] handleCachingResponseBody(HttpEntity entity, ALoadBalancer<ServiceRoute> lb,
ProxyRequest proxyRequest, long contentLength, Matcher ccHeaderPatternMatcher, Header[] responseHeaders)
throws IOException {
// TODO cope with large content where content length exceeds int capacity (2'048 MB)
ByteArrayOutputStream baos = new ByteArrayOutputStream((int)contentLength);
entity.writeTo(baos);
byte[] responseBody = baos.toByteArray();
Expand All @@ -214,9 +218,16 @@ private static byte[] handleCachingResponseBody(HttpEntity entity, ALoadBalancer
return responseBody;
}

private static long getContentLength(String contentLengthHeader) {
private static long getContentLength(Header contentLengthHeader) {
if (contentLengthHeader != null) {
return Long.parseLong(contentLengthHeader);
return getContentLength(contentLengthHeader.getValue());
}
return -1L;
}

private static long getContentLength(String contentLengthHeaderValue) {
if (contentLengthHeaderValue != null) {
return Long.parseLong(contentLengthHeaderValue);
}
return -1L;
}
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/com/github/greg/junkietech/yaproxy/YAProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public YAProxy(Configuration config) throws IOException {
this.config = config;
this.clearCacheService = Executors.newScheduledThreadPool(config.getProxy().getServices().size());
this.serviceMap = initServiceMap();
this.server = initServer(serviceMap);
this.server = initServer();
initShutdownHook();
}

Expand All @@ -59,9 +59,9 @@ private void initShutdownHook() {
}

public void start() {
server.start();
LOGGER.info("server listening on {}:{}...", config.getProxy().getListen().getAddress(),
config.getProxy().getListen().getPort());
server.start();
}

private Map<String, ALoadBalancer<ServiceRoute>> initServiceMap() {
Expand Down Expand Up @@ -98,6 +98,7 @@ private Map<String, ALoadBalancer<ServiceRoute>> initServiceMap() {
private static HttpHost setupDownstreamProxy(HttpClientBuilder builder, EndPoint host, Service service,
Proxy proxy) {
String proxyHost = null;
HttpHost result = null;
if (host.getDownstreamPoxy() != null) {
proxyHost = host.getDownstreamPoxy();
} else if (service.getDownstreamPoxy() != null) {
Expand All @@ -106,11 +107,10 @@ private static HttpHost setupDownstreamProxy(HttpClientBuilder builder, EndPoint
proxyHost = proxy.getDownstreamPoxy();
}
if (proxyHost != null) {
HttpHost result = HttpHost.create(proxyHost);
result = HttpHost.create(proxyHost);
builder.setProxy(result);
return result;
}
return null;
return result;
}

private ALoadBalancer<ServiceRoute> createLoadBalancer(List<ServiceRoute> targetList, String serviceName) {
Expand All @@ -131,7 +131,7 @@ private ALoadBalancer<ServiceRoute> createLoadBalancer(List<ServiceRoute> target
return result;
}

private HttpServer initServer(final Map<String, ALoadBalancer<ServiceRoute>> serviceMap) throws IOException {
private HttpServer initServer() throws IOException {
HttpServer result = HttpServer.create(new InetSocketAddress(config.getProxy().getListen().getAddress(),
config.getProxy().getListen().getPort()), 0);
LOGGER.info("initializing server listening on {}:{}", config.getProxy().getListen().getAddress(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.sun.net.httpserver.HttpExchange;

@SuppressWarnings("restriction")
public class HttpIOUtil {
public static void writeResponse(HttpExchange exchange, String response, int code) throws IOException {
writeResponse(exchange, response.getBytes(), code);
Expand Down

0 comments on commit 6c7fbd8

Please sign in to comment.