├── src ├── test │ ├── resources │ │ └── hosts │ └── java │ │ └── io │ │ └── vertx │ │ └── httpproxy │ │ ├── ProxyClientPipelinedTest.java │ │ ├── ProxyClientNotPersistentTest.java │ │ ├── DockerTest.java │ │ ├── CacheMaxAgeTest.java │ │ ├── ParseTest.java │ │ ├── ProxyTest.java │ │ ├── CacheConditionalGetTest.java │ │ ├── ProxyTestBase.java │ │ ├── CacheExpiresTest.java │ │ ├── CacheExpires2Test.java │ │ ├── ProxyClientKeepAliveTest.java │ │ └── ProxyRequestTest.java └── main │ └── java │ └── io │ └── vertx │ └── httpproxy │ ├── package-info.java │ ├── impl │ ├── CacheControl.java │ ├── HttpUtils.java │ ├── Resource.java │ ├── BufferingWriteStream.java │ ├── BufferingReadStream.java │ ├── BufferedReadStream.java │ ├── ParseUtils.java │ ├── ProxyRequestImpl.java │ ├── ProxyResponseImpl.java │ └── HttpProxyImpl.java │ ├── HttpProxy.java │ ├── Body.java │ ├── Main.java │ ├── ProxyResponse.java │ └── ProxyRequest.java ├── README.md ├── .editorconfig ├── .gitignore ├── dependency-reduced-pom.xml ├── pom.xml └── LICENSE-eplv10-aslv20.html /src/test/resources/hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 localhost 2 | 127.0.0.1 foo.com -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vert.x Http Proxy 2 | 3 | Project archived. 4 | 5 | Now part of Vert.x 4 (4.1) stack https://github.com/eclipse-vertx/vertx-http-proxy 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [**/examples/**.java] 12 | max_line_length = 80 13 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Julien Viet 3 | */ 4 | @ModuleGen(name = "vertx-reverse-proxy", groupPackage = "io.vertx") 5 | package io.vertx.httpproxy; 6 | 7 | import io.vertx.codegen.annotations.ModuleGen; -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ProxyClientPipelinedTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | /** 4 | * @author Julien Viet 5 | */ 6 | public class ProxyClientPipelinedTest extends ProxyClientKeepAliveTest { 7 | 8 | public ProxyClientPipelinedTest() { 9 | keepAlive = true; 10 | pipelining = true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vertx 2 | .DS_Store 3 | .gradle 4 | .idea 5 | .classpath 6 | .project 7 | .settings 8 | .yardoc 9 | .yardopts 10 | build 11 | target 12 | out 13 | *.iml 14 | *.ipr 15 | *.iws 16 | test-output 17 | Scratch.java 18 | ScratchTest.java 19 | test-results 20 | test-tmp 21 | *.class 22 | ScratchPad.java 23 | src/main/resources/ext-js/*.js 24 | src/main/java/io/vertx/java/**/*.java 25 | *.swp 26 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ProxyClientNotPersistentTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.ext.unit.TestContext; 4 | 5 | /** 6 | * @author Julien Viet 7 | */ 8 | public class ProxyClientNotPersistentTest extends ProxyClientKeepAliveTest { 9 | 10 | public ProxyClientNotPersistentTest() { 11 | keepAlive = false; 12 | pipelining = false; 13 | } 14 | 15 | public void testChunkedTransferEncodingRequest(TestContext ctx) { 16 | // super.testChunkedTransferEncodingRequest(ctx); 17 | // Does not pass for now - only when run in single 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/DockerTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.ext.unit.TestContext; 4 | import org.junit.Test; 5 | 6 | /** 7 | * @author Julien Viet 8 | */ 9 | public class DockerTest extends ProxyTestBase { 10 | 11 | @Test 12 | public void testDocker(TestContext ctx) throws Exception { 13 | 14 | /* 15 | Async async = ctx.async(); 16 | // async.awaitSuccess(); 17 | 18 | DockerBackend backend = new DockerBackend(vertx); 19 | backend.start(ctx.asyncAssertSuccess(v -> async.complete())); 20 | 21 | Async async2 = ctx.async(); 22 | 23 | HttpProxy proxy = HttpProxy.createProxy(vertx, options); 24 | proxy.addBackend(backend); 25 | proxy.listen(ctx.asyncAssertSuccess(v -> async2.complete())); 26 | 27 | 28 | Async async3 = ctx.async(); 29 | */ 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/CacheMaxAgeTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.core.MultiMap; 4 | import io.vertx.core.http.HttpHeaders; 5 | import io.vertx.ext.unit.TestContext; 6 | 7 | /** 8 | * @author Julien Viet 9 | */ 10 | public class CacheMaxAgeTest extends CacheExpiresTest { 11 | 12 | @Override 13 | protected void setCacheControl(MultiMap headers, long now, long delaySeconds) { 14 | headers.set(HttpHeaders.CACHE_CONTROL, "public, max-age=" + delaySeconds); 15 | } 16 | 17 | @Override 18 | public void testPublicGet(TestContext ctx) throws Exception { 19 | super.testPublicGet(ctx); 20 | } 21 | 22 | @Override 23 | public void testPublicHead(TestContext ctx) throws Exception { 24 | super.testPublicHead(ctx); 25 | } 26 | 27 | @Override 28 | public void testPublicExpiration(TestContext ctx) throws Exception { 29 | super.testPublicExpiration(ctx); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/CacheControl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | /** 4 | * @author Julien Viet 5 | */ 6 | public class CacheControl { 7 | 8 | private int maxAge; 9 | private boolean _public; 10 | 11 | public CacheControl parse(String header) { 12 | maxAge = -1; 13 | _public = false; 14 | String[] parts = header.split(","); // No regex 15 | for (String part : parts) { 16 | part = part.trim().toLowerCase(); 17 | switch (part) { 18 | case "public": 19 | _public = true; 20 | break; 21 | default: 22 | if (part.startsWith("max-age=")) { 23 | maxAge = Integer.parseInt(part.substring(8)); 24 | 25 | } 26 | break; 27 | } 28 | } 29 | return this; 30 | } 31 | 32 | public int maxAge() { 33 | return maxAge; 34 | } 35 | 36 | public boolean isPublic() { 37 | return _public; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/HttpProxy.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.codegen.annotations.Fluent; 4 | import io.vertx.codegen.annotations.VertxGen; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.http.HttpClient; 8 | import io.vertx.core.http.HttpServerRequest; 9 | import io.vertx.core.net.SocketAddress; 10 | 11 | import java.util.function.Function; 12 | 13 | /** 14 | * @author Julien Viet 15 | */ 16 | @VertxGen 17 | public interface HttpProxy extends Handler { 18 | 19 | static HttpProxy reverseProxy2(HttpClient client) { 20 | return new io.vertx.httpproxy.impl.HttpProxyImpl(client); 21 | } 22 | 23 | @Fluent 24 | default HttpProxy target(SocketAddress address) { 25 | return selector(req -> Future.succeededFuture(address)); 26 | } 27 | 28 | @Fluent 29 | default HttpProxy target(int port, String host) { 30 | return target(SocketAddress.inetSocketAddress(port, host)); 31 | } 32 | 33 | @Fluent 34 | HttpProxy selector(Function> selector); 35 | 36 | void handle(HttpServerRequest request); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/Body.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.codegen.annotations.VertxGen; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.streams.ReadStream; 6 | import io.vertx.httpproxy.impl.BufferedReadStream; 7 | 8 | @VertxGen 9 | public interface Body { 10 | 11 | static Body body(ReadStream stream, long len) { 12 | return new Body() { 13 | @Override 14 | public long length() { 15 | return len; 16 | } 17 | @Override 18 | public ReadStream stream() { 19 | return stream; 20 | } 21 | }; 22 | } 23 | 24 | static Body body(ReadStream stream) { 25 | return body(stream, -1L); 26 | } 27 | 28 | static Body body(Buffer buffer) { 29 | return new Body() { 30 | @Override 31 | public long length() { 32 | return buffer.length(); 33 | } 34 | @Override 35 | public ReadStream stream() { 36 | return new BufferedReadStream(buffer); 37 | } 38 | }; 39 | } 40 | 41 | /** 42 | * @return the body length or {@code -1} if that can't be determined 43 | */ 44 | long length(); 45 | 46 | /** 47 | * @return the body stream 48 | */ 49 | ReadStream stream(); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.MultiMap; 4 | import io.vertx.core.http.HttpHeaders; 5 | 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | class HttpUtils { 10 | 11 | static Boolean isChunked(MultiMap headers) { 12 | List te = headers.getAll("transfer-encoding"); 13 | if (te != null) { 14 | boolean chunked = false; 15 | for (String val : te) { 16 | if (val.equals("chunked")) { 17 | chunked = true; 18 | } else { 19 | return null; 20 | } 21 | } 22 | return chunked; 23 | } else { 24 | return false; 25 | } 26 | } 27 | 28 | static Date dateHeader(MultiMap headers) { 29 | String dateHeader = headers.get(HttpHeaders.DATE); 30 | if (dateHeader == null) { 31 | List warningHeaders = headers.getAll("warning"); 32 | if (warningHeaders.size() > 0) { 33 | for (String warningHeader : warningHeaders) { 34 | Date date = ParseUtils.parseWarningHeaderDate(warningHeader); 35 | if (date != null) { 36 | return date; 37 | } 38 | } 39 | } 40 | return null; 41 | } else { 42 | return ParseUtils.parseHeaderDate(dateHeader); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/Resource.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.MultiMap; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.http.HttpHeaders; 6 | import io.vertx.httpproxy.Body; 7 | import io.vertx.httpproxy.ProxyResponse; 8 | 9 | import java.util.Date; 10 | 11 | class Resource { 12 | 13 | final String absoluteUri; 14 | final int statusCode; 15 | final MultiMap headers; 16 | final long timestamp; 17 | final long maxAge; 18 | final Date lastModified; 19 | final String etag; 20 | final Buffer content = Buffer.buffer(); 21 | 22 | Resource(String absoluteUri, int statusCode, MultiMap headers, long timestamp, long maxAge) { 23 | String lastModifiedHeader = headers.get(HttpHeaders.LAST_MODIFIED); 24 | this.absoluteUri = absoluteUri; 25 | this.statusCode = statusCode; 26 | this.headers = headers; 27 | this.timestamp = timestamp; 28 | this.maxAge = maxAge; 29 | this.lastModified = lastModifiedHeader != null ? ParseUtils.parseHeaderDate(lastModifiedHeader) : null; 30 | this.etag = headers.get(HttpHeaders.ETAG); 31 | } 32 | 33 | void sendTo(ProxyResponse proxyResponse) { 34 | proxyResponse.setStatusCode(200); 35 | proxyResponse.headers().addAll(headers); 36 | proxyResponse.setBody(Body.body(content)); 37 | proxyResponse.send(ar -> { 38 | 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/BufferingWriteStream.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | import io.vertx.core.AsyncResult; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Handler; 7 | import io.vertx.core.buffer.Buffer; 8 | import io.vertx.core.streams.ReadStream; 9 | import io.vertx.core.streams.WriteStream; 10 | 11 | class BufferingWriteStream implements WriteStream { 12 | 13 | private final Buffer content; 14 | 15 | public BufferingWriteStream() { 16 | this.content = Buffer.buffer(); 17 | } 18 | 19 | public Buffer content() { 20 | return content; 21 | } 22 | 23 | @Override 24 | public WriteStream exceptionHandler(Handler handler) { 25 | return this; 26 | } 27 | 28 | @Override 29 | public Future write(Buffer data) { 30 | content.appendBuffer(data); 31 | return Future.succeededFuture(); 32 | } 33 | 34 | @Override 35 | public void write(Buffer data, Handler> handler) { 36 | content.appendBuffer(data); 37 | handler.handle(Future.succeededFuture()); 38 | } 39 | 40 | @Override 41 | public void end(Handler> handler) { 42 | handler.handle(Future.succeededFuture()); 43 | } 44 | 45 | @Override 46 | public WriteStream setWriteQueueMaxSize(int maxSize) { 47 | return this; 48 | } 49 | 50 | @Override 51 | public boolean writeQueueFull() { 52 | return false; 53 | } 54 | 55 | @Override 56 | public WriteStream drainHandler(@Nullable Handler handler) { 57 | return this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/BufferingReadStream.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.Handler; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.streams.ReadStream; 6 | 7 | class BufferingReadStream implements ReadStream { 8 | 9 | private final ReadStream stream; 10 | private final Buffer content; 11 | 12 | public BufferingReadStream(ReadStream stream, Buffer content) { 13 | this.stream = stream; 14 | this.content = content; 15 | } 16 | 17 | @Override 18 | public ReadStream exceptionHandler(Handler handler) { 19 | stream.exceptionHandler(handler); 20 | return this; 21 | } 22 | 23 | @Override 24 | public ReadStream handler(Handler handler) { 25 | if (handler != null) { 26 | stream.handler(buff -> { 27 | content.appendBuffer(buff); 28 | handler.handle(buff); 29 | }); 30 | } else { 31 | stream.handler(null); 32 | } 33 | return this; 34 | } 35 | 36 | @Override 37 | public ReadStream pause() { 38 | stream.pause(); 39 | return this; 40 | } 41 | 42 | @Override 43 | public ReadStream resume() { 44 | stream.resume(); 45 | return this; 46 | } 47 | 48 | @Override 49 | public ReadStream fetch(long amount) { 50 | stream.fetch(amount); 51 | return this; 52 | } 53 | 54 | @Override 55 | public ReadStream endHandler(Handler endHandler) { 56 | if (endHandler != null) { 57 | stream.endHandler(v -> { 58 | endHandler.handle(null); 59 | }); 60 | } else { 61 | stream.endHandler(null); 62 | } 63 | return this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/BufferedReadStream.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.Handler; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.streams.ReadStream; 6 | 7 | public class BufferedReadStream implements ReadStream { 8 | 9 | private long demand = 0L; 10 | private Handler endHandler; 11 | private Handler handler; 12 | private boolean ended = false; 13 | private final Buffer content; 14 | 15 | public BufferedReadStream() { 16 | this.content = Buffer.buffer(); 17 | } 18 | 19 | public BufferedReadStream(Buffer content) { 20 | this.content = content; 21 | } 22 | 23 | @Override 24 | public ReadStream exceptionHandler(Handler handler) { 25 | return this; 26 | } 27 | 28 | @Override 29 | public ReadStream handler(Handler handler) { 30 | this.handler = handler; 31 | return this; 32 | } 33 | 34 | @Override 35 | public ReadStream pause() { 36 | demand = 0L; 37 | return this; 38 | } 39 | 40 | @Override 41 | public ReadStream resume() { 42 | fetch(Long.MAX_VALUE); 43 | return this; 44 | } 45 | 46 | @Override 47 | public ReadStream fetch(long amount) { 48 | if (!ended && amount > 0) { 49 | ended = true; 50 | demand += amount; 51 | if (demand < 0L) { 52 | demand = Long.MAX_VALUE; 53 | } 54 | if (demand != Long.MAX_VALUE) { 55 | demand--; 56 | } 57 | if (handler != null && content.length() > 0) { 58 | handler.handle(content); 59 | } 60 | if (endHandler != null) { 61 | endHandler.handle(null); 62 | } 63 | } 64 | return this; 65 | } 66 | 67 | @Override 68 | public ReadStream endHandler(Handler endHandler) { 69 | this.endHandler = endHandler; 70 | return this; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ParseTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.httpproxy.impl.CacheControl; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | /** 8 | * @author Julien Viet 9 | */ 10 | public class ParseTest { 11 | 12 | @Test 13 | public void testParseCacheControlMaxAge() { 14 | CacheControl control = new CacheControl(); 15 | Assert.assertEquals(123, control.parse("max-age=123").maxAge()); 16 | Assert.assertEquals(-1, control.parse("").maxAge()); 17 | } 18 | 19 | @Test 20 | public void testParseCacheControlPublic() { 21 | CacheControl control = new CacheControl(); 22 | Assert.assertFalse(control.parse("max-age=123").isPublic()); 23 | Assert.assertTrue(control.parse("public").isPublic()); 24 | } 25 | 26 | /* 27 | @Test 28 | public void testCommaSplit() { 29 | assertCommaSplit("foo", "foo"); 30 | assertCommaSplit(" foo", "foo"); 31 | assertCommaSplit("foo ", "foo"); 32 | failCommaSplit("foo,", "foo"); 33 | failCommaSplit(",foo"); 34 | failCommaSplit("foo bar"); 35 | // assertCommaSplit("foo,bar", "foo", "bar"); 36 | // assertCommaSplit("foo ,bar", "foo", "bar"); 37 | // assertCommaSplit("foo, bar", "foo", "bar"); 38 | // assertCommaSplit("foo,bar ", "foo", "bar"); 39 | } 40 | 41 | private void assertCommaSplit(String header, String... expected) { 42 | LinkedList list = new LinkedList<>(); 43 | ParseUtils.commaSplit(header, list::add); 44 | assertEquals(Arrays.asList(expected), list); 45 | } 46 | 47 | private void failCommaSplit(String header, String... expected) { 48 | LinkedList list = new LinkedList<>(); 49 | try { 50 | ParseUtils.commaSplit(header, list::add); 51 | } catch (IllegalStateException e) { 52 | assertEquals(Arrays.asList(expected), list); 53 | return; 54 | } 55 | fail(); 56 | } 57 | */ 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/Main.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import com.beust.jcommander.JCommander; 4 | import com.beust.jcommander.Parameter; 5 | import io.netty.util.internal.logging.InternalLoggerFactory; 6 | import io.netty.util.internal.logging.Slf4JLoggerFactory; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.http.HttpClient; 9 | import io.vertx.core.http.HttpClientOptions; 10 | import io.vertx.core.http.HttpServer; 11 | import io.vertx.core.http.HttpServerOptions; 12 | 13 | /** 14 | * @author Julien Viet 15 | */ 16 | public class Main { 17 | 18 | @Parameter(names = "--port") 19 | public int port = 8080; 20 | 21 | @Parameter(names = "--address") 22 | public String address = "0.0.0.0"; 23 | 24 | public static void main(String[] args) { 25 | Main main = new Main(); 26 | JCommander jc = new JCommander(main); 27 | jc.parse(args); 28 | main.run(); 29 | } 30 | 31 | public void run() { 32 | InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE); 33 | Vertx vertx = Vertx.vertx(); 34 | HttpClient client = vertx.createHttpClient(new HttpClientOptions() 35 | .setMaxInitialLineLength(10000) 36 | .setLogActivity(true)); 37 | HttpProxy proxy = HttpProxy 38 | .reverseProxy2(client) 39 | .target(8081, "96.126.115.136"); 40 | HttpServer proxyServer = vertx.createHttpServer(new HttpServerOptions() 41 | .setPort(port) 42 | .setMaxInitialLineLength(10000) 43 | .setLogActivity(true)) 44 | .requestHandler(req -> { 45 | System.out.println("------------------------------------------"); 46 | System.out.println(req.path()); 47 | proxy.handle(req); 48 | }); 49 | proxyServer.listen(ar -> { 50 | if (ar.succeeded()) { 51 | System.out.println("Proxy server started on " + port); 52 | } else { 53 | ar.cause().printStackTrace(); 54 | } 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ProxyTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.http.HttpClient; 5 | import io.vertx.core.http.HttpClientResponse; 6 | import io.vertx.core.http.HttpMethod; 7 | import io.vertx.core.net.SocketAddress; 8 | import io.vertx.ext.unit.Async; 9 | import io.vertx.ext.unit.TestContext; 10 | import org.junit.Test; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * @author Julien Viet 19 | */ 20 | public class ProxyTest extends ProxyTestBase { 21 | 22 | @Test 23 | public void testRoundRobinSelector(TestContext ctx) { 24 | int numRequests = 10; 25 | SocketAddress[] backends = new SocketAddress[3]; 26 | for (int i = 0;i < backends.length;i++) { 27 | int value = i; 28 | backends[i] = startHttpBackend(ctx, 8081 + value, req -> req.response().end("" + value)); 29 | } 30 | AtomicInteger count = new AtomicInteger(); 31 | startProxy(req -> Future.succeededFuture(backends[count.getAndIncrement() % backends.length])); 32 | HttpClient client = vertx.createHttpClient(); 33 | Map result = Collections.synchronizedMap(new HashMap<>()); 34 | Async latch = ctx.async(); 35 | for (int i = 0;i < backends.length * numRequests;i++) { 36 | client 37 | .request(HttpMethod.GET, 8080, "localhost", "/") 38 | .compose(req -> req 39 | .send() 40 | .compose(HttpClientResponse::body) 41 | ).onSuccess(buff -> { 42 | result.computeIfAbsent(buff.toString(), k -> new AtomicInteger()).getAndIncrement(); 43 | synchronized (result) { 44 | int total = result.values().stream().reduce(0, (a, b) -> a + b.get(), (a, b) -> a + b); 45 | if (total == backends.length * numRequests) { 46 | for (int j = 0;j < backends.length;j++) { 47 | AtomicInteger val = result.remove("" + j); 48 | ctx.assertEquals(numRequests, val.get()); 49 | } 50 | ctx.assertEquals(result, Collections.emptyMap()); 51 | latch.complete(); 52 | } 53 | } 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/ProxyResponse.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.codegen.annotations.Fluent; 4 | import io.vertx.codegen.annotations.GenIgnore; 5 | import io.vertx.codegen.annotations.VertxGen; 6 | import io.vertx.core.AsyncResult; 7 | import io.vertx.core.Handler; 8 | import io.vertx.core.MultiMap; 9 | import io.vertx.core.buffer.Buffer; 10 | import io.vertx.core.http.HttpClientResponse; 11 | import io.vertx.core.streams.ReadStream; 12 | 13 | import java.util.function.Function; 14 | 15 | /** 16 | * @author Julien Viet 17 | */ 18 | @VertxGen 19 | public interface ProxyResponse { 20 | 21 | /** 22 | * @return the proxy request 23 | */ 24 | ProxyRequest request(); 25 | 26 | /** 27 | * @return the status code to be sent to the edge client 28 | */ 29 | int getStatusCode(); 30 | 31 | /** 32 | * Set the status code to be sent to the edge client. 33 | * 34 | *

The initial value is the origin response status code. 35 | * 36 | * @param sc the status code 37 | * @return a reference to this, so the API can be used fluently 38 | */ 39 | @Fluent 40 | ProxyResponse setStatusCode(int sc); 41 | 42 | /** 43 | * @return the headers that will be sent to the edge client, the returned headers can be modified. The headers 44 | * map is populated with the origin response headers 45 | */ 46 | MultiMap headers(); 47 | 48 | /** 49 | * Put an HTTP header 50 | * 51 | * @param name The header name 52 | * @param value The header value 53 | * @return a reference to this, so the API can be used fluently 54 | */ 55 | @GenIgnore 56 | @Fluent 57 | ProxyResponse putHeader(CharSequence name, CharSequence value); 58 | 59 | /** 60 | * @return the response body to be sent to the edge client 61 | */ 62 | Body getBody(); 63 | 64 | /** 65 | * Set the request body to be sent to the edge client. 66 | * 67 | *

The initial request body value is the origin response body. 68 | * 69 | * @param body the new body 70 | * @return a reference to this, so the API can be used fluently 71 | */ 72 | @Fluent 73 | ProxyResponse setBody(Body body); 74 | 75 | /** 76 | * Set a body filter. 77 | * 78 | *

The body filter can rewrite the response body sent to the edge client. 79 | * 80 | * @param filter the filter 81 | * @return a reference to this, so the API can be used fluently 82 | */ 83 | @Fluent 84 | ProxyResponse bodyFilter(Function, ReadStream> filter); 85 | 86 | boolean publicCacheControl(); 87 | 88 | long maxAge(); 89 | 90 | /** 91 | * @return the {@code etag} sent by the origin response 92 | */ 93 | String etag(); 94 | 95 | /** 96 | * Send the proxy response to the edge client. 97 | * 98 | * @param completionHandler the handler to be called when the response has been sent 99 | */ 100 | void send(Handler> completionHandler); 101 | 102 | /** 103 | * Release the proxy response. 104 | * 105 | *

The HTTP client response is resumed, no HTTP server response is sent. 106 | */ 107 | @Fluent 108 | ProxyResponse release(); 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/CacheConditionalGetTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 4 | import io.vertx.core.http.HttpClient; 5 | import io.vertx.core.http.HttpHeaders; 6 | import io.vertx.core.http.HttpMethod; 7 | import io.vertx.core.net.impl.SocketAddressImpl; 8 | import io.vertx.ext.unit.Async; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.httpproxy.impl.ParseUtils; 11 | import org.junit.Rule; 12 | import org.junit.Test; 13 | 14 | import java.util.Date; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 18 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 19 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 20 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 21 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 22 | import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; 23 | 24 | /** 25 | * @author Julien Viet 26 | */ 27 | public class CacheConditionalGetTest extends ProxyTestBase { 28 | 29 | private AtomicInteger hits = new AtomicInteger(); 30 | private HttpClient client; 31 | 32 | @Rule 33 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8081)); 34 | 35 | @Override 36 | public void setUp() { 37 | super.setUp(); 38 | hits.set(0); 39 | client = vertx.createHttpClient(); 40 | } 41 | 42 | @Test 43 | public void testIfModifiedSinceRespondsNotModified(TestContext ctx) throws Exception { 44 | long now = System.currentTimeMillis(); 45 | stubFor(get(urlEqualTo("/img.jpg")).inScenario("s").whenScenarioStateIs(STARTED) 46 | .willReturn( 47 | aResponse() 48 | .withStatus(200) 49 | .withHeader("Cache-Control", "public") 50 | .withHeader("ETag", "tag0") 51 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(now))) 52 | .withHeader("Last-Modified", ParseUtils.formatHttpDate(new Date(now - 5000))) 53 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(now + 5000))) 54 | .withBody("content"))); 55 | startProxy(new SocketAddressImpl(8081, "localhost")); 56 | Async latch = ctx.async(); 57 | client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg").compose(req1 -> 58 | req1.send().compose(resp1 -> { 59 | ctx.assertEquals(200, resp1.statusCode()); 60 | return resp1.body(); 61 | }) 62 | ).onComplete(ctx.asyncAssertSuccess(body1 -> { 63 | ctx.assertEquals("content", body1.toString()); 64 | vertx.setTimer(3000, id -> { 65 | client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") 66 | .compose(req2 -> req2 67 | .putHeader(HttpHeaders.IF_MODIFIED_SINCE, ParseUtils.formatHttpDate(new Date(now - 5000))) 68 | .send() 69 | .compose(resp2 -> { 70 | ctx.assertEquals(304, resp2.statusCode()); 71 | return resp2.body(); 72 | })).onComplete(ctx.asyncAssertSuccess(body2 -> { 73 | ctx.assertEquals("", body2.toString()); 74 | latch.complete(); 75 | })); 76 | }); 77 | })); 78 | latch.awaitSuccess(10000); 79 | /* 80 | ServeEvent event1 = getAllServeEvents().get(1); 81 | assertNull(event1.getRequest().getHeader("If-None-Match")); 82 | assertEquals(200, event1.getResponse().getStatus()); 83 | ServeEvent event0 = getAllServeEvents().get(0); 84 | assertEquals("tag0", event0.getRequest().getHeader("If-None-Match")); 85 | assertEquals(304, event0.getResponse().getStatus()); 86 | */ 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/ParseUtils.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.time.DayOfWeek; 5 | import java.util.Date; 6 | import java.util.Locale; 7 | import java.util.TimeZone; 8 | 9 | /** 10 | * @author Julien Viet 11 | */ 12 | public class ParseUtils { 13 | 14 | public static Date parseHeaderDate(String value) { 15 | try { 16 | return parseHttpDate(value); 17 | } catch (Exception e) { 18 | return null; 19 | } 20 | } 21 | 22 | public static Date parseWarningHeaderDate(String value) { 23 | // warn-code 24 | int index = value.indexOf(' '); 25 | if (index > 0) { 26 | // warn-agent 27 | index = value.indexOf(' ', index + 1); 28 | if (index > 0) { 29 | // warn-text 30 | index = value.indexOf(' ', index + 1); 31 | if (index > 0) { 32 | // warn-date 33 | int len = value.length(); 34 | if (index + 2 < len && value.charAt(index + 1) == '"' && value.charAt(len - 1) == '"') { 35 | // Space for 2 double quotes 36 | String date = value.substring(index + 2, len - 1); 37 | try { 38 | return parseHttpDate(date); 39 | } catch (Exception ignore) { 40 | } 41 | } 42 | } 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | private static SimpleDateFormat RFC_1123_DATE_TIME() { 49 | SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 50 | format.setTimeZone(TimeZone.getTimeZone("GMT")); 51 | return format; 52 | } 53 | 54 | private static SimpleDateFormat RFC_850_DATE_TIME() { 55 | SimpleDateFormat format = new SimpleDateFormat("EEEEEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US); 56 | format.setTimeZone(TimeZone.getTimeZone("GMT")); 57 | return format; 58 | } 59 | 60 | private static SimpleDateFormat ASC_TIME() { 61 | SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US); 62 | format.setTimeZone(TimeZone.getTimeZone("GMT")); 63 | return format; 64 | } 65 | 66 | public static String formatHttpDate(Date date) { 67 | return RFC_1123_DATE_TIME().format(date); 68 | } 69 | 70 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1 71 | public static Date parseHttpDate(String value) throws Exception { 72 | int sep = 0; 73 | while (true) { 74 | if (sep < value.length()) { 75 | char c = value.charAt(sep); 76 | if (c == ',') { 77 | String s = value.substring(0, sep); 78 | if (parseWkday(s) != null) { 79 | // rfc1123-date 80 | return RFC_1123_DATE_TIME().parse(value); 81 | } else if (parseWeekday(s) != null) { 82 | // rfc850-date 83 | return RFC_850_DATE_TIME().parse(value); 84 | } 85 | return null; 86 | } else if (c == ' ') { 87 | String s = value.substring(0, sep); 88 | if (parseWkday(s) != null) { 89 | // asctime-date 90 | return ASC_TIME().parse(value); 91 | } 92 | return null; 93 | } 94 | sep++; 95 | } else { 96 | return null; 97 | } 98 | } 99 | } 100 | 101 | private static DayOfWeek parseWkday(String value) { 102 | switch (value) { 103 | case "Mon": 104 | return DayOfWeek.MONDAY; 105 | case "Tue": 106 | return DayOfWeek.TUESDAY; 107 | case "Wed": 108 | return DayOfWeek.WEDNESDAY; 109 | case "Thu": 110 | return DayOfWeek.THURSDAY; 111 | case "Fri": 112 | return DayOfWeek.FRIDAY; 113 | case "Sat": 114 | return DayOfWeek.SATURDAY; 115 | case "Sun": 116 | return DayOfWeek.SUNDAY; 117 | default: 118 | return null; 119 | } 120 | } 121 | 122 | private static DayOfWeek parseWeekday(String value) { 123 | switch (value) { 124 | case "Monday": 125 | return DayOfWeek.MONDAY; 126 | case "Tuesday": 127 | return DayOfWeek.TUESDAY; 128 | case "Wednesday": 129 | return DayOfWeek.WEDNESDAY; 130 | case "Thursday": 131 | return DayOfWeek.THURSDAY; 132 | case "Friday": 133 | return DayOfWeek.FRIDAY; 134 | case "Saturday": 135 | return DayOfWeek.SATURDAY; 136 | case "Sunday": 137 | return DayOfWeek.SUNDAY; 138 | default: 139 | return null; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/ProxyRequestImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.MultiMap; 6 | import io.vertx.core.buffer.Buffer; 7 | import io.vertx.core.http.HttpClientRequest; 8 | import io.vertx.core.http.HttpHeaders; 9 | import io.vertx.core.http.HttpMethod; 10 | import io.vertx.core.http.HttpServerRequest; 11 | import io.vertx.core.http.HttpServerResponse; 12 | import io.vertx.core.http.HttpVersion; 13 | import io.vertx.core.streams.Pipe; 14 | import io.vertx.core.streams.ReadStream; 15 | import io.vertx.httpproxy.Body; 16 | import io.vertx.httpproxy.ProxyRequest; 17 | import io.vertx.httpproxy.ProxyResponse; 18 | 19 | import java.util.function.Function; 20 | 21 | public class ProxyRequestImpl implements ProxyRequest { 22 | 23 | private HttpMethod method; 24 | private HttpVersion version; 25 | private String uri; 26 | private String absoluteURI; 27 | private Body body; 28 | private MultiMap headers; 29 | HttpClientRequest edgeRequest; 30 | private HttpServerResponse edgeResponse; 31 | 32 | public ProxyRequestImpl(HttpServerRequest edgeRequest) { 33 | 34 | // Determine content length 35 | long contentLength = -1L; 36 | String contentLengthHeader = edgeRequest.getHeader(HttpHeaders.CONTENT_LENGTH); 37 | if (contentLengthHeader != null) { 38 | try { 39 | contentLength = Long.parseLong(contentLengthHeader); 40 | } catch (NumberFormatException e) { 41 | // Ignore ??? 42 | } 43 | } 44 | 45 | this.method = edgeRequest.method(); 46 | this.version = edgeRequest.version(); 47 | this.body = Body.body(edgeRequest, contentLength); 48 | this.uri = edgeRequest.uri(); 49 | this.headers = MultiMap.caseInsensitiveMultiMap().addAll(edgeRequest.headers()); 50 | this.absoluteURI = edgeRequest.absoluteURI(); 51 | this.edgeResponse = edgeRequest.response(); 52 | } 53 | 54 | @Override 55 | public HttpVersion version() { 56 | return version; 57 | } 58 | 59 | @Override 60 | public String getURI() { 61 | return uri; 62 | } 63 | 64 | @Override 65 | public ProxyRequest setURI(String uri) { 66 | this.uri = uri; 67 | return this; 68 | } 69 | 70 | @Override 71 | public Body getBody() { 72 | return body; 73 | } 74 | 75 | @Override 76 | public ProxyRequestImpl setBody(Body body) { 77 | this.body = body; 78 | return this; 79 | } 80 | 81 | @Override 82 | public String absoluteURI() { 83 | return absoluteURI; 84 | } 85 | 86 | @Override 87 | public HttpMethod getMethod() { 88 | return method; 89 | } 90 | 91 | @Override 92 | public ProxyRequest setMethod(HttpMethod method) { 93 | this.method = method; 94 | return this; 95 | } 96 | 97 | @Override 98 | public ProxyRequest release() { 99 | body.stream().resume(); 100 | headers.clear(); 101 | body = null; 102 | return this; 103 | } 104 | 105 | @Override 106 | public ProxyResponse response() { 107 | return new ProxyResponseImpl(this, edgeResponse); 108 | } 109 | 110 | void sendRequest(Handler> responseHandler) { 111 | 112 | edgeRequest.map(r -> { 113 | r.pause(); // Pause it 114 | return new ProxyResponseImpl(this, edgeResponse, r); 115 | }).onComplete(responseHandler); 116 | 117 | 118 | edgeRequest.setMethod(method); 119 | edgeRequest.setURI(uri); 120 | 121 | // Add all end-to-end headers 122 | headers.forEach(header -> { 123 | String name = header.getKey(); 124 | String value = header.getValue(); 125 | if (name.equalsIgnoreCase("host")) { 126 | // Skip 127 | } else { 128 | edgeRequest.headers().add(name, value); 129 | } 130 | }); 131 | 132 | long len = body.length(); 133 | if (len >= 0) { 134 | edgeRequest.putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(len)); 135 | } else { 136 | edgeRequest.setChunked(true); 137 | } 138 | 139 | Pipe pipe = body.stream().pipe(); 140 | pipe.endOnComplete(true); 141 | pipe.endOnFailure(false); 142 | pipe.to(edgeRequest, ar -> { 143 | if (ar.failed()) { 144 | edgeRequest.reset(); 145 | } 146 | }); 147 | } 148 | 149 | @Override 150 | public ProxyRequestImpl putHeader(CharSequence name, CharSequence value) { 151 | headers.set(name, value); 152 | return this; 153 | } 154 | 155 | @Override 156 | public MultiMap headers() { 157 | return headers; 158 | } 159 | 160 | @Override 161 | public ProxyRequest bodyFilter(Function, ReadStream> filter) { 162 | return this; 163 | } 164 | 165 | @Override 166 | public void send(HttpClientRequest request, Handler> completionHandler) { 167 | edgeRequest = request; 168 | sendRequest(completionHandler); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ProxyTestBase.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.core.AbstractVerticle; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Handler; 6 | import io.vertx.core.Promise; 7 | import io.vertx.core.Vertx; 8 | import io.vertx.core.http.HttpClient; 9 | import io.vertx.core.http.HttpClientOptions; 10 | import io.vertx.core.http.HttpServer; 11 | import io.vertx.core.http.HttpServerOptions; 12 | import io.vertx.core.http.HttpServerRequest; 13 | import io.vertx.core.net.NetServer; 14 | import io.vertx.core.net.NetSocket; 15 | import io.vertx.core.net.SocketAddress; 16 | import io.vertx.core.net.impl.SocketAddressImpl; 17 | import io.vertx.ext.unit.Async; 18 | import io.vertx.ext.unit.TestContext; 19 | import io.vertx.ext.unit.junit.VertxUnitRunner; 20 | import org.junit.After; 21 | import org.junit.Before; 22 | import org.junit.runner.RunWith; 23 | 24 | import java.io.Closeable; 25 | import java.util.concurrent.*; 26 | import java.util.function.Function; 27 | 28 | /** 29 | * @author Julien Viet 30 | */ 31 | @RunWith(VertxUnitRunner.class) 32 | public class ProxyTestBase { 33 | 34 | protected HttpServerOptions proxyOptions; 35 | protected HttpClientOptions clientOptions; 36 | 37 | 38 | protected Vertx vertx; 39 | 40 | @Before 41 | public void setUp() { 42 | proxyOptions = new HttpServerOptions().setPort(8080).setHost("localhost"); 43 | clientOptions = new HttpClientOptions(); 44 | vertx = Vertx.vertx(); 45 | } 46 | 47 | @After 48 | public void tearDown(TestContext context) { 49 | vertx.close(context.asyncAssertSuccess()); 50 | } 51 | 52 | protected Closeable startProxy(SocketAddress backend) { 53 | return startProxy(req -> Future.succeededFuture(backend)); 54 | } 55 | 56 | protected Closeable startProxy(Function> selector) { 57 | CompletableFuture res = new CompletableFuture<>(); 58 | vertx.deployVerticle(new AbstractVerticle() { 59 | @Override 60 | public void start(Promise startFuture) { 61 | HttpClient proxyClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 62 | HttpServer proxyServer = vertx.createHttpServer(new HttpServerOptions(proxyOptions)); 63 | HttpProxy proxy = HttpProxy.reverseProxy2(proxyClient); 64 | proxy.selector(selector); 65 | proxyServer.requestHandler(proxy); 66 | proxyServer.listen(ar -> startFuture.handle(ar.mapEmpty())); 67 | } 68 | }, ar -> { 69 | if (ar.succeeded()) { 70 | String id = ar.result(); 71 | res.complete(() -> { 72 | CountDownLatch latch = new CountDownLatch(1); 73 | vertx.undeploy(id, ar2 -> latch.countDown()); 74 | try { 75 | latch.await(10, TimeUnit.SECONDS); 76 | } catch (InterruptedException e) { 77 | Thread.currentThread().interrupt(); 78 | throw new AssertionError(e); 79 | } 80 | }); 81 | } else { 82 | res.completeExceptionally(ar.cause()); 83 | } 84 | }); 85 | try { 86 | return res.get(10, TimeUnit.SECONDS); 87 | } catch (InterruptedException e) { 88 | Thread.currentThread().interrupt(); 89 | throw new AssertionError(e); 90 | } catch (ExecutionException e) { 91 | throw new AssertionError(e.getMessage()); 92 | } catch (TimeoutException e) { 93 | throw new AssertionError(e); 94 | } 95 | } 96 | 97 | protected void startHttpServer(TestContext ctx, HttpServerOptions options, Handler handler) { 98 | HttpServer proxyServer = vertx.createHttpServer(options); 99 | proxyServer.requestHandler(handler); 100 | Async async1 = ctx.async(); 101 | proxyServer.listen(ctx.asyncAssertSuccess(p -> async1.complete())); 102 | async1.awaitSuccess(); 103 | } 104 | 105 | protected SocketAddress startHttpBackend(TestContext ctx, int port, Handler handler) { 106 | return startHttpBackend(ctx, new HttpServerOptions().setPort(port).setHost("localhost"), handler); 107 | } 108 | 109 | protected SocketAddress startHttpBackend(TestContext ctx, HttpServerOptions options, Handler handler) { 110 | HttpServer backendServer = vertx.createHttpServer(options); 111 | backendServer.requestHandler(handler); 112 | Async async = ctx.async(); 113 | backendServer.listen(ctx.asyncAssertSuccess(s -> async.complete())); 114 | async.awaitSuccess(); 115 | return new SocketAddressImpl(options.getPort(), "localhost"); 116 | } 117 | 118 | protected SocketAddress startNetBackend(TestContext ctx, int port, Handler handler) { 119 | NetServer backendServer = vertx.createNetServer(new HttpServerOptions().setPort(port).setHost("localhost")); 120 | backendServer.connectHandler(handler); 121 | Async async = ctx.async(); 122 | backendServer.listen(ctx.asyncAssertSuccess(s -> async.complete())); 123 | async.awaitSuccess(); 124 | return new SocketAddressImpl(port, "localhost"); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/ProxyRequest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.codegen.annotations.Fluent; 4 | import io.vertx.codegen.annotations.GenIgnore; 5 | import io.vertx.codegen.annotations.VertxGen; 6 | import io.vertx.core.AsyncResult; 7 | import io.vertx.core.Handler; 8 | import io.vertx.core.MultiMap; 9 | import io.vertx.core.buffer.Buffer; 10 | import io.vertx.core.http.HttpClientRequest; 11 | import io.vertx.core.http.HttpMethod; 12 | import io.vertx.core.http.HttpServerRequest; 13 | import io.vertx.core.http.HttpVersion; 14 | import io.vertx.core.streams.ReadStream; 15 | import io.vertx.httpproxy.impl.ProxyRequestImpl; 16 | 17 | import java.util.function.Function; 18 | 19 | /** 20 | * @author Julien Viet 21 | */ 22 | @VertxGen 23 | public interface ProxyRequest { 24 | 25 | static ProxyRequest reverseProxy(HttpServerRequest request) { 26 | request.pause(); 27 | ProxyRequestImpl proxyRequest = new ProxyRequestImpl(request); 28 | return proxyRequest; 29 | } 30 | 31 | /** 32 | * @return the HTTP version of the edge request 33 | */ 34 | HttpVersion version(); 35 | 36 | /** 37 | * @return the edge request absolute URI 38 | */ 39 | String absoluteURI(); 40 | 41 | /** 42 | * @return the HTTP method to be sent to the origin server 43 | */ 44 | HttpMethod getMethod(); 45 | 46 | /** 47 | * Set the HTTP method to be sent to the origin server. 48 | * 49 | *

The initial HTTP method value is the edge request HTTP method. 50 | * 51 | * @param method the new method 52 | * @return a reference to this, so the API can be used fluently 53 | */ 54 | @Fluent 55 | ProxyRequest setMethod(HttpMethod method); 56 | 57 | /** 58 | * @return the request URI to be sent to the origin server 59 | */ 60 | String getURI(); 61 | 62 | /** 63 | * Set the request URI to be sent to the origin server. 64 | * 65 | *

The initial request URI value is the edge request URI. 66 | * 67 | * @param uri the new URI 68 | * @return a reference to this, so the API can be used fluently 69 | */ 70 | @Fluent 71 | ProxyRequest setURI(String uri); 72 | 73 | /** 74 | * @return the request body to be sent to the origin server 75 | */ 76 | Body getBody(); 77 | 78 | /** 79 | * Set the request body to be sent to the origin server. 80 | * 81 | *

The initial request body value is the edge request body. 82 | * 83 | * @param body the new body 84 | * @return a reference to this, so the API can be used fluently 85 | */ 86 | @Fluent 87 | ProxyRequest setBody(Body body); 88 | 89 | /** 90 | * @return the headers that will be sent to the origin server, the returned headers can be modified. The headers 91 | * map is populated with the edge request headers 92 | */ 93 | MultiMap headers(); 94 | 95 | /** 96 | * Put an HTTP header 97 | * 98 | * @param name The header name 99 | * @param value The header value 100 | * @return a reference to this, so the API can be used fluently 101 | */ 102 | @GenIgnore 103 | @Fluent 104 | ProxyRequest putHeader(CharSequence name, CharSequence value); 105 | 106 | /** 107 | * Set a body filter. 108 | * 109 | *

The body filter can rewrite the request body sent to the origin server. 110 | * 111 | * @param filter the filter 112 | * @return a reference to this, so the API can be used fluently 113 | */ 114 | @Fluent 115 | ProxyRequest bodyFilter(Function, ReadStream> filter); 116 | 117 | /** 118 | * Proxy this request and response to the origin server using the specified request. 119 | * 120 | * @param request the request connected to the origin server 121 | * @param completionHandler the completion handler 122 | */ 123 | default void proxy(HttpClientRequest request, Handler> completionHandler) { 124 | send(request, ar -> { 125 | if (ar.succeeded()) { 126 | ProxyResponse resp = ar.result(); 127 | resp.send(completionHandler); 128 | } else { 129 | completionHandler.handle(ar.mapEmpty()); 130 | } 131 | }); 132 | } 133 | 134 | /** 135 | * Send this request to the origin server using the specified request. 136 | * 137 | *

The {@code completionHandler} will be called with the proxy response sent by the origin server. 138 | * 139 | * @param request the request connected to the origin server 140 | * @param completionHandler the completion handler 141 | */ 142 | void send(HttpClientRequest request, Handler> completionHandler); 143 | 144 | /** 145 | * Release the proxy request. 146 | * 147 | *

The HTTP server request is resumed, no HTTP server response is sent. 148 | * 149 | * @return a reference to this, so the API can be used fluently 150 | */ 151 | @Fluent 152 | ProxyRequest release(); 153 | 154 | /** 155 | * Create and return a default proxy response. 156 | * 157 | * @return a default proxy response 158 | */ 159 | ProxyResponse response(); 160 | 161 | } 162 | -------------------------------------------------------------------------------- /dependency-reduced-pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | vertx-ext-parent 5 | io.vertx 6 | 23 7 | ../pom.xml/pom.xml 8 | 9 | 4.0.0 10 | vertx-reverse-proxy 11 | Vert.x Gateway 12 | 3.4.0-SNAPSHOT 13 | 14 | 15 | 16 | 17 | org.codehaus.mojo 18 | exec-maven-plugin 19 | 1.3.2 20 | 21 | io.vertx.ext.shell.Main 22 | test 23 | 24 | 25 | 26 | 27 | 28 | 29 | maven-resources-plugin 30 | 31 | 32 | default-resources 33 | process-classes 34 | 35 | 36 | 37 | 38 | maven-surefire-plugin 39 | 40 | false 41 | 42 | ${project.build.directory} 43 | 60 44 | PARANOID 45 | 46 | -server -Xms128m -Xmx1024m -XX:NewRatio=2 47 | 48 | 49 | 50 | maven-jar-plugin 51 | 52 | 53 | default-jar 54 | process-classes 55 | 56 | 57 | 58 | service:io.vertx.ext.shell 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | maven-shade-plugin 67 | 2.3 68 | 69 | 70 | package 71 | 72 | shade 73 | 74 | 75 | 76 | 77 | *:* 78 | 79 | META-INF/*.SF 80 | META-INF/*.DSA 81 | META-INF/*.RSA 82 | 83 | 84 | 85 | fat 86 | 87 | 88 | io.vertx.httpproxy.Main 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | io.vertx 100 | vertx-docgen 101 | 3.4.0-SNAPSHOT 102 | provided 103 | 104 | 105 | io.vertx 106 | vertx-codetrans 107 | 3.4.0-SNAPSHOT 108 | provided 109 | 110 | 111 | io.vertx 112 | vertx-codegen 113 | 3.4.0-SNAPSHOT 114 | provided 115 | 116 | 117 | mvel2 118 | org.mvel 119 | 120 | 121 | 122 | 123 | junit 124 | junit 125 | 4.12 126 | test 127 | 128 | 129 | hamcrest-core 130 | org.hamcrest 131 | 132 | 133 | 134 | 135 | io.vertx 136 | vertx-unit 137 | 3.4.0-SNAPSHOT 138 | test 139 | 140 | 141 | 142 | 143 | 144 | io.vertx 145 | vertx-dependencies 146 | ${stack.version} 147 | pom 148 | import 149 | 150 | 151 | 152 | 153 | 3.4.0-SNAPSHOT 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 4.0.0 22 | 23 | 24 | io.vertx 25 | vertx-parent 26 | 12 27 | 28 | 29 | vertx-reverse-proxy 30 | 1.0.0-SNAPSHOT 31 | 32 | Vert.x Http Proxy 33 | 34 | 35 | 4.0.0.Beta3 36 | ${project.build.directory} 37 | 38 | 39 | 40 | 41 | 42 | io.vertx 43 | vertx-dependencies 44 | ${stack.version} 45 | pom 46 | import 47 | 48 | 49 | 50 | 51 | 52 | 53 | io.vertx 54 | vertx-core 55 | 56 | 57 | 58 | com.beust 59 | jcommander 60 | 1.48 61 | true 62 | 63 | 64 | 65 | 66 | 67 | org.slf4j 68 | slf4j-api 69 | 1.7.16 70 | true 71 | 72 | 73 | 74 | io.vertx 75 | vertx-docgen 76 | provided 77 | 78 | 79 | io.vertx 80 | vertx-codetrans 81 | provided 82 | 83 | 84 | io.vertx 85 | vertx-codegen 86 | provided 87 | 88 | 89 | 96 | 97 | 98 | junit 99 | junit 100 | 4.13.1 101 | test 102 | 103 | 104 | io.vertx 105 | vertx-unit 106 | test 107 | 108 | 109 | com.github.tomakehurst 110 | wiremock-standalone 111 | 2.6.0 112 | test 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | maven-resources-plugin 122 | 123 | 124 | default-resources 125 | process-classes 126 | 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-surefire-plugin 132 | 133 | false 134 | 135 | ${project.build.directory} 136 | 60 137 | PARANOID 138 | 139 | -server -Xms128m -Xmx1024m -XX:NewRatio=2 140 | 141 | 142 | 143 | maven-jar-plugin 144 | 145 | 146 | default-jar 147 | process-classes 148 | 149 | 150 | 151 | service:io.vertx.ext.shell 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-shade-plugin 162 | 2.3 163 | 164 | 165 | 166 | package 167 | 168 | shade 169 | 170 | 171 | 172 | 173 | *:* 174 | 175 | META-INF/*.SF 176 | META-INF/*.DSA 177 | META-INF/*.RSA 178 | 179 | 180 | 181 | ${fatjar.dir} 182 | ${project.artifactId}-${project.version}-fat 183 | 184 | 185 | 186 | io.vertx.httpproxy.Main 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | org.codehaus.mojo 201 | exec-maven-plugin 202 | 1.3.2 203 | 204 | io.vertx.ext.shell.Main 205 | test 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/ProxyResponseImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.MultiMap; 6 | import io.vertx.core.buffer.Buffer; 7 | import io.vertx.core.http.HttpClientResponse; 8 | import io.vertx.core.http.HttpHeaders; 9 | import io.vertx.core.http.HttpServerResponse; 10 | import io.vertx.core.streams.Pipe; 11 | import io.vertx.core.streams.ReadStream; 12 | import io.vertx.httpproxy.Body; 13 | import io.vertx.httpproxy.ProxyRequest; 14 | import io.vertx.httpproxy.ProxyResponse; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Date; 18 | import java.util.Iterator; 19 | import java.util.List; 20 | import java.util.function.Function; 21 | 22 | class ProxyResponseImpl implements ProxyResponse { 23 | 24 | private final ProxyRequestImpl request; 25 | private final HttpServerResponse edgeResponse; 26 | private int statusCode; 27 | private Body body; 28 | private MultiMap headers; 29 | private HttpClientResponse originResponse; 30 | private long maxAge; 31 | private String etag; 32 | private boolean publicCacheControl; 33 | private Function, ReadStream> bodyFilter = Function.identity(); 34 | 35 | ProxyResponseImpl(ProxyRequestImpl request, HttpServerResponse edgeResponse) { 36 | this.originResponse = null; 37 | this.statusCode = 200; 38 | this.headers = MultiMap.caseInsensitiveMultiMap(); 39 | this.request = request; 40 | this.edgeResponse = edgeResponse; 41 | } 42 | 43 | ProxyResponseImpl(ProxyRequestImpl request, HttpServerResponse edgeResponse, HttpClientResponse originResponse) { 44 | 45 | // Determine content length 46 | long contentLength = -1L; 47 | String contentLengthHeader = originResponse.getHeader(HttpHeaders.CONTENT_LENGTH); 48 | if (contentLengthHeader != null) { 49 | try { 50 | contentLength = Long.parseLong(contentLengthHeader); 51 | } catch (NumberFormatException e) { 52 | // Ignore ??? 53 | } 54 | } 55 | 56 | this.request = request; 57 | this.originResponse = originResponse; 58 | this.edgeResponse = edgeResponse; 59 | this.statusCode = originResponse.statusCode(); 60 | this.body = Body.body(originResponse, contentLength); 61 | 62 | long maxAge = -1; 63 | boolean publicCacheControl = false; 64 | String cacheControlHeader = originResponse.getHeader(HttpHeaders.CACHE_CONTROL); 65 | if (cacheControlHeader != null) { 66 | CacheControl cacheControl = new CacheControl().parse(cacheControlHeader); 67 | if (cacheControl.isPublic()) { 68 | publicCacheControl = true; 69 | if (cacheControl.maxAge() > 0) { 70 | maxAge = (long)cacheControl.maxAge() * 1000; 71 | } else { 72 | String dateHeader = originResponse.getHeader(HttpHeaders.DATE); 73 | String expiresHeader = originResponse.getHeader(HttpHeaders.EXPIRES); 74 | if (dateHeader != null && expiresHeader != null) { 75 | maxAge = ParseUtils.parseHeaderDate(expiresHeader).getTime() - ParseUtils.parseHeaderDate(dateHeader).getTime(); 76 | } 77 | } 78 | } 79 | } 80 | this.maxAge = maxAge; 81 | this.publicCacheControl = publicCacheControl; 82 | this.etag = originResponse.getHeader(HttpHeaders.ETAG); 83 | this.headers = MultiMap.caseInsensitiveMultiMap().addAll(originResponse.headers()); 84 | } 85 | 86 | @Override 87 | public ProxyRequest request() { 88 | return request; 89 | } 90 | 91 | @Override 92 | public int getStatusCode() { 93 | return statusCode; 94 | } 95 | 96 | @Override 97 | public ProxyResponseImpl setStatusCode(int sc) { 98 | statusCode = sc; 99 | return this; 100 | } 101 | 102 | @Override 103 | public Body getBody() { 104 | return body; 105 | } 106 | 107 | @Override 108 | public ProxyResponseImpl setBody(Body body) { 109 | this.body = body; 110 | return this; 111 | } 112 | 113 | @Override 114 | public boolean publicCacheControl() { 115 | return publicCacheControl; 116 | } 117 | 118 | @Override 119 | public long maxAge() { 120 | return maxAge; 121 | } 122 | 123 | @Override 124 | public String etag() { 125 | return etag; 126 | } 127 | 128 | @Override 129 | public MultiMap headers() { 130 | return headers; 131 | } 132 | 133 | @Override 134 | public ProxyResponse putHeader(CharSequence name, CharSequence value) { 135 | headers.set(name, value); 136 | return this; 137 | } 138 | 139 | @Override 140 | public ProxyResponse bodyFilter(Function, ReadStream> filter) { 141 | bodyFilter = filter; 142 | return this; 143 | } 144 | 145 | @Override 146 | public void send(Handler> completionHandler) { 147 | // Set stuff 148 | edgeResponse.setStatusCode(statusCode); 149 | 150 | // Date header 151 | Date date = HttpUtils.dateHeader(headers); 152 | if (date == null) { 153 | date = new Date(); 154 | } 155 | try { 156 | edgeResponse.putHeader("date", ParseUtils.formatHttpDate(date)); 157 | } catch (Exception e) { 158 | e.printStackTrace(); 159 | } 160 | 161 | // Warning header 162 | List warningHeaders = headers.getAll("warning"); 163 | if (warningHeaders.size() > 0) { 164 | warningHeaders = new ArrayList<>(warningHeaders); 165 | String dateHeader = headers.get("date"); 166 | Date dateInstant = dateHeader != null ? ParseUtils.parseHeaderDate(dateHeader) : null; 167 | Iterator i = warningHeaders.iterator(); 168 | // Suppress incorrect warning header 169 | while (i.hasNext()) { 170 | String warningHeader = i.next(); 171 | Date warningInstant = ParseUtils.parseWarningHeaderDate(warningHeader); 172 | if (warningInstant != null && dateInstant != null && !warningInstant.equals(dateInstant)) { 173 | i.remove(); 174 | } 175 | } 176 | } 177 | edgeResponse.putHeader("warning", warningHeaders); 178 | 179 | // Handle other headers 180 | headers.forEach(header -> { 181 | String name = header.getKey(); 182 | String value = header.getValue(); 183 | if (name.equalsIgnoreCase("date") || name.equalsIgnoreCase("warning") || name.equalsIgnoreCase("transfer-encoding")) { 184 | // Skip 185 | } else { 186 | edgeResponse.headers().add(name, value); 187 | } 188 | }); 189 | 190 | // 191 | if (body == null) { 192 | edgeResponse.end(); 193 | return; 194 | } 195 | 196 | long len = body.length(); 197 | if (len >= 0) { 198 | edgeResponse.putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(len)); 199 | } else { 200 | edgeResponse.setChunked(true); 201 | } 202 | ReadStream bodyStream = bodyFilter.apply(body.stream()); 203 | sendResponse(bodyStream, completionHandler); 204 | } 205 | 206 | @Override 207 | public ProxyResponseImpl release() { 208 | if (originResponse != null) { 209 | originResponse.resume(); 210 | originResponse = null; 211 | body = null; 212 | headers.clear(); 213 | } 214 | return this; 215 | } 216 | 217 | private void sendResponse(ReadStream body, Handler> completionHandler) { 218 | Pipe pipe = body.pipe(); 219 | pipe.endOnSuccess(true); 220 | pipe.endOnFailure(false); 221 | pipe.to(edgeResponse, ar -> { 222 | if (ar.failed()) { 223 | request.edgeRequest.reset(); 224 | edgeResponse.reset(); 225 | } 226 | completionHandler.handle(ar); 227 | }); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/CacheExpiresTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.core.Handler; 4 | import io.vertx.core.MultiMap; 5 | import io.vertx.core.http.HttpClient; 6 | import io.vertx.core.http.HttpHeaders; 7 | import io.vertx.core.http.HttpMethod; 8 | import io.vertx.core.net.SocketAddress; 9 | import io.vertx.ext.unit.Async; 10 | import io.vertx.ext.unit.TestContext; 11 | import io.vertx.httpproxy.impl.ParseUtils; 12 | import org.junit.Test; 13 | 14 | import java.util.Date; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * @author Julien Viet 19 | */ 20 | public class CacheExpiresTest extends ProxyTestBase { 21 | 22 | private AtomicInteger hits = new AtomicInteger(); 23 | private HttpClient client; 24 | 25 | @Override 26 | public void setUp() { 27 | super.setUp(); 28 | hits.set(0); 29 | client = vertx.createHttpClient(); 30 | } 31 | 32 | protected void setCacheControl(MultiMap headers, long now, long delaySeconds) { 33 | Date tomorrow = new Date(); 34 | tomorrow.setTime(now + delaySeconds * 1000); 35 | headers.set(HttpHeaders.CACHE_CONTROL, "public"); 36 | headers.set(HttpHeaders.EXPIRES, ParseUtils.formatHttpDate(tomorrow)); 37 | } 38 | 39 | @Test 40 | public void testPublicGet(TestContext ctx) throws Exception { 41 | testPublic(ctx, HttpMethod.GET); 42 | } 43 | 44 | @Test 45 | public void testPublicHead(TestContext ctx) throws Exception { 46 | testPublic(ctx, HttpMethod.HEAD); 47 | } 48 | 49 | private void testPublic(TestContext ctx, HttpMethod method) throws Exception { 50 | Async latch = ctx.async(); 51 | testPublic(ctx, responseHeaders -> { 52 | vertx.setTimer(2000, id -> { 53 | client.request(method, 8080, "localhost", "/") 54 | .compose(req2 -> req2.send().compose(resp2 -> { 55 | ctx.assertEquals(200, resp2.statusCode()); 56 | ctx.assertEquals(responseHeaders.get(HttpHeaders.DATE), resp2.getHeader(HttpHeaders.DATE)); 57 | ctx.assertEquals(1, hits.get()); 58 | return resp2.body(); 59 | })).onComplete(ctx.asyncAssertSuccess(body2 -> { 60 | if (method == HttpMethod.HEAD) { 61 | ctx.assertEquals("", body2.toString()); 62 | } else { 63 | ctx.assertEquals("content", body2.toString()); 64 | } 65 | latch.complete(); 66 | })); 67 | }); 68 | }); 69 | } 70 | 71 | @Test 72 | public void testPublicExpiration(TestContext ctx) throws Exception { 73 | Async latch = ctx.async(); 74 | testPublic(ctx, responseHeaders -> { 75 | vertx.setTimer(6000, id -> { 76 | client.request(HttpMethod.GET, 8080, "localhost", "/") 77 | .compose(req2 -> 78 | req2.send().compose(resp2 -> { 79 | ctx.assertEquals(200, resp2.statusCode()); 80 | ctx.assertEquals(2, hits.get()); 81 | ctx.assertNotEquals(responseHeaders.get(HttpHeaders.DATE), resp2.getHeader(HttpHeaders.DATE)); 82 | return resp2.body(); 83 | }) 84 | ).onComplete(ctx.asyncAssertSuccess(body2 -> { 85 | ctx.assertEquals("content", body2.toString()); 86 | latch.complete(); 87 | })); 88 | }); 89 | }); 90 | } 91 | 92 | @Test 93 | public void testPublicValidClientMaxAge(TestContext ctx) throws Exception { 94 | Async latch = ctx.async(); 95 | testPublic(ctx, responseHeaders -> { 96 | vertx.setTimer(1000, id -> { 97 | client.request(HttpMethod.GET, 8080, "localhost", "/").compose(req2 -> 98 | req2.putHeader(HttpHeaders.CACHE_CONTROL, "max-age=2") 99 | .send().compose(resp2 -> { 100 | ctx.assertEquals(200, resp2.statusCode()); 101 | ctx.assertEquals(1, hits.get()); 102 | ctx.assertEquals(responseHeaders.get(HttpHeaders.DATE), resp2.getHeader(HttpHeaders.DATE)); 103 | return resp2.body(); 104 | }) 105 | ).onComplete(ctx.asyncAssertSuccess(body2 -> { 106 | ctx.assertEquals("content", body2.toString()); 107 | latch.complete(); 108 | })); 109 | }); 110 | }); 111 | } 112 | 113 | @Test 114 | public void testPublicInvalidClientMaxAge(TestContext ctx) throws Exception { 115 | Async latch = ctx.async(); 116 | testPublic(ctx, responseHeaders -> { 117 | vertx.setTimer(1000, id -> { 118 | client.request(HttpMethod.GET, 8080, "localhost", "/").compose(req2 -> 119 | req2.putHeader(HttpHeaders.CACHE_CONTROL, "max-age=1") 120 | .send() 121 | .compose(resp2 -> { 122 | ctx.assertEquals(200, resp2.statusCode()); 123 | ctx.assertEquals(2, hits.get()); 124 | ctx.assertNotEquals(responseHeaders.get(HttpHeaders.DATE), resp2.getHeader(HttpHeaders.DATE)); 125 | return resp2.body(); 126 | }) 127 | ).onComplete(ctx.asyncAssertSuccess(body2 -> { 128 | ctx.assertEquals("content", body2.toString()); 129 | latch.complete(); 130 | })); 131 | }); 132 | }); 133 | } 134 | 135 | private void testPublic(TestContext ctx, Handler respHandler) throws Exception { 136 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 137 | hits.incrementAndGet(); 138 | ctx.assertEquals(HttpMethod.GET, req.method()); 139 | Date now = new Date(); 140 | setCacheControl(req.response().headers(), now.getTime(), 5); 141 | req.response() 142 | .putHeader(HttpHeaders.LAST_MODIFIED, ParseUtils.formatHttpDate(now)) 143 | .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(now)) 144 | .end("content"); 145 | }); 146 | startProxy(backend); 147 | client.request(HttpMethod.GET, 8080, "localhost", "/").compose(req -> 148 | req.send().compose(resp -> { 149 | ctx.assertEquals(200, resp.statusCode()); 150 | return resp.body().onSuccess(body -> respHandler.handle(resp.headers())); 151 | })).onComplete(ctx.asyncAssertSuccess(body -> { 152 | ctx.assertEquals("content", body.toString()); 153 | })); 154 | } 155 | 156 | @Test 157 | public void testPublicInvalidClientMaxAgeRevalidation(TestContext ctx) throws Exception { 158 | testPublicInvalidClientMaxAge(ctx, 5); 159 | } 160 | 161 | /* 162 | @Test 163 | public void testPublicInvalidClientMaxAgeOverwrite(TestContext ctx) throws Exception { 164 | testPublicInvalidClientMaxAge(ctx, 3); 165 | } 166 | */ 167 | 168 | private void testPublicInvalidClientMaxAge(TestContext ctx, long maxAge) throws Exception { 169 | Async latch = ctx.async(); 170 | long now = System.currentTimeMillis(); 171 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 172 | ctx.assertEquals(HttpMethod.GET, req.method()); 173 | setCacheControl(req.response().headers(), now, 5); 174 | switch (hits.getAndIncrement()) { 175 | case 0: 176 | ctx.assertEquals(null, req.getHeader(HttpHeaders.ETAG)); 177 | req.response() 178 | .putHeader(HttpHeaders.LAST_MODIFIED, ParseUtils.formatHttpDate(new Date(now))) 179 | .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(new Date(now))) 180 | .putHeader(HttpHeaders.ETAG, "" + now) 181 | .end("content"); 182 | break; 183 | case 1: 184 | ctx.assertEquals("" + now, req.getHeader(HttpHeaders.IF_NONE_MATCH)); 185 | if (System.currentTimeMillis() < now + maxAge * 1000) { 186 | req.response() 187 | .setStatusCode(304) 188 | .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 189 | .putHeader(HttpHeaders.ETAG, "" + now) 190 | .end(); 191 | } else { 192 | req.response() 193 | .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 194 | .putHeader(HttpHeaders.ETAG, "" + now + "2") 195 | .end("content2"); 196 | } 197 | break; 198 | default: 199 | ctx.fail(); 200 | } 201 | }); 202 | startProxy(backend); 203 | client.request(HttpMethod.GET, 8080, "localhost", "/").compose(req1 -> 204 | req1.send().compose(resp1 -> { 205 | ctx.assertEquals(200, resp1.statusCode()); 206 | return resp1.body(); 207 | })).onComplete(ctx.asyncAssertSuccess(body1 -> { 208 | ctx.assertEquals("content", body1.toString()); 209 | vertx.setTimer(3000, id -> { 210 | client.request(HttpMethod.GET, 8080, "localhost", "/").compose(req2 -> 211 | req2.putHeader(HttpHeaders.CACHE_CONTROL, "max-age=1") 212 | .send() 213 | .compose(resp2 -> { 214 | ctx.assertEquals(200, resp2.statusCode()); 215 | return resp2.body(); 216 | })).onComplete(ctx.asyncAssertSuccess(body2 -> { 217 | ctx.assertEquals("content", body2.toString()); 218 | ctx.assertEquals(2, hits.get()); 219 | // ctx.assertNotEquals(resp1.getHeader(HttpHeaders.DATE), resp2.getHeader(HttpHeaders.DATE)); 220 | latch.complete(); 221 | })); 222 | }); 223 | })); 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/httpproxy/impl/HttpProxyImpl.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy.impl; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Handler; 6 | import io.vertx.core.Promise; 7 | import io.vertx.core.buffer.Buffer; 8 | import io.vertx.core.http.HttpClient; 9 | import io.vertx.core.http.HttpClientRequest; 10 | import io.vertx.core.http.HttpHeaders; 11 | import io.vertx.core.http.HttpMethod; 12 | import io.vertx.core.http.HttpServerRequest; 13 | import io.vertx.core.http.HttpVersion; 14 | import io.vertx.core.http.RequestOptions; 15 | import io.vertx.core.net.SocketAddress; 16 | import io.vertx.httpproxy.Body; 17 | import io.vertx.httpproxy.HttpProxy; 18 | import io.vertx.httpproxy.ProxyRequest; 19 | import io.vertx.httpproxy.ProxyResponse; 20 | 21 | import java.util.Date; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.function.BiFunction; 25 | import java.util.function.Function; 26 | 27 | public class HttpProxyImpl implements HttpProxy { 28 | 29 | private static final BiFunction CACHE_GET_AND_VALIDATE = (key, resource) -> { 30 | long now = System.currentTimeMillis(); 31 | long val = resource.timestamp + resource.maxAge; 32 | return val < now ? null : resource; 33 | }; 34 | 35 | private final HttpClient client; 36 | private Function> selector = req -> Future.failedFuture("No target available"); 37 | private final Map cache = new HashMap<>(); 38 | 39 | public HttpProxyImpl(HttpClient client) { 40 | this.client = client; 41 | } 42 | 43 | @Override 44 | public HttpProxy selector(Function> selector) { 45 | this.selector = selector; 46 | return this; 47 | } 48 | 49 | @Override 50 | public void handle(HttpServerRequest frontRequest) { 51 | handleProxyRequest(frontRequest); 52 | } 53 | 54 | private Future resolveTarget(HttpServerRequest frontRequest) { 55 | return selector.apply(frontRequest).flatMap(server -> { 56 | RequestOptions requestOptions = new RequestOptions(); 57 | requestOptions.setServer(server); 58 | return client.request(requestOptions); 59 | }); 60 | } 61 | 62 | boolean revalidateResource(ProxyResponse response, Resource resource) { 63 | if (resource.etag != null && response.etag() != null) { 64 | return resource.etag.equals(response.etag()); 65 | } 66 | return true; 67 | } 68 | 69 | private void end(ProxyRequest proxyRequest, int sc) { 70 | proxyRequest 71 | .release() 72 | .response() 73 | .setStatusCode(sc) 74 | .putHeader(HttpHeaders.CONTENT_LENGTH, "0") 75 | .setBody(null) 76 | .send(ar -> {}); 77 | 78 | } 79 | 80 | private void handleProxyRequest(HttpServerRequest frontRequest) { 81 | ProxyRequest proxyRequest = ProxyRequest.reverseProxy(frontRequest); 82 | 83 | // Encoding check 84 | Boolean chunked = HttpUtils.isChunked(frontRequest.headers()); 85 | if (chunked == null) { 86 | end(proxyRequest, 400); 87 | return; 88 | } 89 | 90 | // Handle from cache 91 | HttpMethod method = frontRequest.method(); 92 | if (method == HttpMethod.GET || method == HttpMethod.HEAD) { 93 | String cacheKey = proxyRequest.absoluteURI(); 94 | Resource resource = cache.computeIfPresent(cacheKey, CACHE_GET_AND_VALIDATE); 95 | if (resource != null) { 96 | if (tryHandleProxyRequestFromCache(proxyRequest, frontRequest, resource)) { 97 | return; 98 | } 99 | } 100 | } 101 | handleProxyRequestAndProxyResponse(proxyRequest, frontRequest); 102 | } 103 | 104 | private void handleProxyRequestAndProxyResponse(ProxyRequest proxyRequest, HttpServerRequest frontRequest) { 105 | handleProxyRequest(proxyRequest, frontRequest, ar -> { 106 | if (ar.succeeded()) { 107 | handleProxyResponse(ar.result(), ar2 -> {}); 108 | } else { 109 | // TODO ??? 110 | } 111 | }); 112 | } 113 | 114 | private void handleProxyRequest(ProxyRequest proxyRequest, HttpServerRequest frontRequest, Handler> handler) { 115 | Future f = resolveTarget(frontRequest); 116 | f.onComplete(ar -> { 117 | if (ar.succeeded()) { 118 | handleProxyRequest(proxyRequest, frontRequest, ar.result(), handler); 119 | } else { 120 | frontRequest.resume(); 121 | Promise promise = Promise.promise(); 122 | frontRequest.exceptionHandler(promise::tryFail); 123 | frontRequest.endHandler(promise::tryComplete); 124 | promise.future().onComplete(ar2 -> { 125 | end(proxyRequest, 404); 126 | }); 127 | handler.handle(Future.failedFuture(ar.cause())); 128 | } 129 | }); 130 | } 131 | 132 | private void handleProxyRequest(ProxyRequest proxyRequest, HttpServerRequest frontRequest, HttpClientRequest backRequest, Handler> handler) { 133 | proxyRequest.send(backRequest, ar2 -> { 134 | if (ar2.succeeded()) { 135 | handler.handle(ar2); 136 | } else { 137 | frontRequest.response().setStatusCode(502).end(); 138 | handler.handle(Future.failedFuture(ar2.cause())); 139 | } 140 | }); 141 | } 142 | 143 | private void handleProxyResponse(ProxyResponse response, Handler> completionHandler) { 144 | 145 | // Check validity 146 | Boolean chunked = HttpUtils.isChunked(response.headers()); 147 | if (chunked == null) { 148 | // response.request().release(); // Is it needed ??? 149 | end(response.request(), 501); 150 | completionHandler.handle(Future.failedFuture("TODO")); 151 | return; 152 | } 153 | 154 | if (chunked && response.request().version() == HttpVersion.HTTP_1_0) { 155 | String contentLength = response.headers().get(HttpHeaders.CONTENT_LENGTH); 156 | if (contentLength == null) { 157 | // Special handling for HTTP 1.0 clients that cannot handle chunked encoding 158 | Body body = response.getBody(); 159 | response.release(); 160 | BufferingWriteStream buffer = new BufferingWriteStream(); 161 | body.stream().pipeTo(buffer, ar -> { 162 | if (ar.succeeded()) { 163 | Buffer content = buffer.content(); 164 | response.setBody(Body.body(content)); 165 | continueHandleResponse(response, completionHandler); 166 | } else { 167 | System.out.println("Not implemented"); 168 | } 169 | }); 170 | return; 171 | } 172 | } 173 | continueHandleResponse(response, completionHandler); 174 | } 175 | 176 | private void continueHandleResponse(ProxyResponse response, Handler> completionHandler) { 177 | ProxyRequest request = response.request(); 178 | Handler> handler; 179 | if (response.publicCacheControl() && response.maxAge() > 0) { 180 | if (request.getMethod() == HttpMethod.GET) { 181 | String absoluteUri = request.absoluteURI(); 182 | Resource res = new Resource( 183 | absoluteUri, 184 | response.getStatusCode(), 185 | response.headers(), 186 | System.currentTimeMillis(), 187 | response.maxAge()); 188 | response.bodyFilter(s -> new BufferingReadStream(s, res.content)); 189 | handler = ar3 -> { 190 | completionHandler.handle(ar3); 191 | if (ar3.succeeded()) { 192 | cache.put(absoluteUri, res); 193 | } 194 | }; 195 | } else { 196 | if (request.getMethod() == HttpMethod.HEAD) { 197 | Resource resource = cache.get(request.absoluteURI()); 198 | if (resource != null) { 199 | if (!revalidateResource(response, resource)) { 200 | // Invalidate cache 201 | cache.remove(request.absoluteURI()); 202 | } 203 | } 204 | } 205 | handler = completionHandler; 206 | } 207 | } else { 208 | handler = completionHandler; 209 | } 210 | 211 | response.send(handler); 212 | } 213 | 214 | private boolean tryHandleProxyRequestFromCache(ProxyRequest proxyRequest, HttpServerRequest frontRequest, Resource resource) { 215 | String cacheControlHeader = frontRequest.getHeader(HttpHeaders.CACHE_CONTROL); 216 | if (cacheControlHeader != null) { 217 | CacheControl cacheControl = new CacheControl().parse(cacheControlHeader); 218 | if (cacheControl.maxAge() >= 0) { 219 | long now = System.currentTimeMillis(); 220 | long currentAge = now - resource.timestamp; 221 | if (currentAge > cacheControl.maxAge() * 1000) { 222 | String etag = resource.headers.get(HttpHeaders.ETAG); 223 | if (etag != null) { 224 | proxyRequest.headers().set(HttpHeaders.IF_NONE_MATCH, resource.etag); 225 | handleProxyRequest(proxyRequest, frontRequest, ar -> { 226 | if (ar.succeeded()) { 227 | ProxyResponse proxyResp = ar.result(); 228 | int sc = proxyResp.getStatusCode(); 229 | switch (sc) { 230 | case 200: 231 | handleProxyResponse(proxyResp, ar2 -> {}); 232 | break; 233 | case 304: 234 | // Warning: this relies on the fact that HttpServerRequest will not send a body for HEAD 235 | proxyResp.release(); 236 | resource.sendTo(proxyRequest.response()); 237 | break; 238 | default: 239 | System.out.println("Not implemented"); 240 | break; 241 | } 242 | } else { 243 | System.out.println("Not implemented"); 244 | } 245 | }); 246 | return true; 247 | } else { 248 | return false; 249 | } 250 | } 251 | } 252 | } 253 | 254 | // 255 | String ifModifiedSinceHeader = frontRequest.getHeader(HttpHeaders.IF_MODIFIED_SINCE); 256 | if ((frontRequest.method() == HttpMethod.GET || frontRequest.method() == HttpMethod.HEAD) && ifModifiedSinceHeader != null && resource.lastModified != null) { 257 | Date ifModifiedSince = ParseUtils.parseHeaderDate(ifModifiedSinceHeader); 258 | if (resource.lastModified.getTime() <= ifModifiedSince.getTime()) { 259 | frontRequest.response().setStatusCode(304).end(); 260 | return true; 261 | } 262 | } 263 | 264 | resource.sendTo(proxyRequest.response()); 265 | return true; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/CacheExpires2Test.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 4 | import com.github.tomakehurst.wiremock.stubbing.ServeEvent; 5 | import io.vertx.core.MultiMap; 6 | import io.vertx.core.http.HttpClient; 7 | import io.vertx.core.http.HttpClientOptions; 8 | import io.vertx.core.http.HttpHeaders; 9 | import io.vertx.core.http.HttpMethod; 10 | import io.vertx.core.net.impl.SocketAddressImpl; 11 | import io.vertx.ext.unit.Async; 12 | import io.vertx.ext.unit.TestContext; 13 | import io.vertx.httpproxy.impl.ParseUtils; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | 17 | import java.util.Date; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | 20 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 21 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 22 | import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; 23 | import static junit.framework.TestCase.assertEquals; 24 | import static junit.framework.TestCase.assertNull; 25 | 26 | /** 27 | * @author Julien Viet 28 | */ 29 | public class CacheExpires2Test extends ProxyTestBase { 30 | 31 | private AtomicInteger hits = new AtomicInteger(); 32 | private HttpClient client; 33 | 34 | @Rule 35 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8081)); 36 | 37 | @Override 38 | public void setUp() { 39 | super.setUp(); 40 | hits.set(0); 41 | client = vertx.createHttpClient(new HttpClientOptions().setKeepAlive(true)); 42 | } 43 | 44 | protected void setCacheControl(MultiMap headers, long now, long delaySeconds) { 45 | Date tomorrow = new Date(); 46 | tomorrow.setTime(now + delaySeconds * 1000); 47 | headers.set(HttpHeaders.CACHE_CONTROL, "public"); 48 | headers.set(HttpHeaders.EXPIRES, ParseUtils.formatHttpDate(tomorrow)); 49 | } 50 | 51 | @Test 52 | public void testPublicInvalidClientMaxAgeRevalidation(TestContext ctx) throws Exception { 53 | stubFor(get(urlEqualTo("/img.jpg")).inScenario("s") 54 | .willReturn( 55 | aResponse() 56 | .withStatus(200) 57 | .withHeader("Cache-Control", "public") 58 | .withHeader("ETag", "tag0") 59 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 60 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) 61 | .withBody("content"))); 62 | stubFor(get(urlEqualTo("/img.jpg")).withHeader("If-None-Match", equalTo("tag0")).inScenario("s") 63 | .willReturn( 64 | aResponse() 65 | .withStatus(200) 66 | .withHeader("Cache-Control", "public") 67 | .withHeader("Etag", "tag1") 68 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 69 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) 70 | .withBody("content2"))); 71 | startProxy(new SocketAddressImpl(8081, "localhost")); 72 | Async latch = ctx.async(); 73 | client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") 74 | .compose(req -> req 75 | .send() 76 | .compose(resp1 -> { 77 | ctx.assertEquals(200, resp1.statusCode()); 78 | return resp1.body(); 79 | })) 80 | .onComplete(ctx.asyncAssertSuccess(body1 -> { 81 | ctx.assertEquals("content", body1.toString()); 82 | vertx.setTimer(3000, id -> { 83 | client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") 84 | .compose(req -> req 85 | .putHeader(HttpHeaders.CACHE_CONTROL, "max-age=1") 86 | .putHeader(HttpHeaders.CONTENT_ENCODING, "identity") 87 | .send().compose(resp2 -> { 88 | ctx.assertEquals(200, resp2.statusCode()); 89 | return resp2.body(); 90 | })).onComplete(ctx.asyncAssertSuccess(body2 -> { 91 | ctx.assertEquals("content2", body2.toString()); 92 | // ctx.assertNotEquals(resp1.getHeader(HttpHeaders.DATE), resp2.getHeader(HttpHeaders.DATE)); 93 | latch.complete(); 94 | })); 95 | }); 96 | })); 97 | 98 | latch.awaitSuccess(10000); 99 | /* 100 | ServeEvent event1 = getAllServeEvents().get(1); 101 | assertNull(event1.getRequest().getHeader("If-None-Match")); 102 | assertEquals(200, event1.getResponse().getStatus()); 103 | ServeEvent event0 = getAllServeEvents().get(0); 104 | assertEquals("tag0", event0.getRequest().getHeader("If-None-Match")); 105 | assertEquals(304, event0.getResponse().getStatus()); 106 | */ 107 | } 108 | 109 | @Test 110 | public void testUncacheableGetInvalidatesEntryOnOk(TestContext ctx) throws Exception { 111 | testUncacheableRequestInvalidatesEntryOnOk(ctx, HttpMethod.GET); 112 | } 113 | 114 | @Test 115 | public void testUncacheableHeadInvalidatesEntryOnOk(TestContext ctx) throws Exception { 116 | testUncacheableRequestInvalidatesEntryOnOk(ctx, HttpMethod.HEAD); 117 | } 118 | 119 | private void testUncacheableRequestInvalidatesEntryOnOk(TestContext ctx, HttpMethod method) throws Exception { 120 | stubFor(get(urlEqualTo("/img.jpg")).inScenario("s").whenScenarioStateIs(STARTED) 121 | .willReturn( 122 | aResponse() 123 | .withStatus(200) 124 | .withHeader("Cache-Control", "public") 125 | .withHeader("ETag", "tag0") 126 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 127 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) 128 | .withBody("content")) 129 | .willSetStateTo("abc")); 130 | stubFor(get(urlEqualTo("/img.jpg")).inScenario("s").whenScenarioStateIs("abc") 131 | .willReturn( 132 | aResponse() 133 | .withStatus(200) 134 | .withHeader("Cache-Control", "public") 135 | .withHeader("Etag", "tag1") 136 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 137 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) 138 | .withBody("content2"))); 139 | stubFor(head(urlEqualTo("/img.jpg")).inScenario("s").whenScenarioStateIs("abc") 140 | .willReturn( 141 | aResponse() 142 | .withStatus(200) 143 | .withHeader("Cache-Control", "public") 144 | .withHeader("Etag", "tag1") 145 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 146 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))))); 147 | startProxy(new SocketAddressImpl(8081, "localhost")); 148 | Async latch = ctx.async(); 149 | client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") 150 | .compose(req1 -> req1 151 | .send() 152 | .compose(resp1 -> { 153 | ctx.assertEquals(200, resp1.statusCode()); 154 | return resp1.body(); 155 | })) 156 | .onComplete(ctx.asyncAssertSuccess(body1 -> { 157 | ctx.assertEquals("content", body1.toString()); 158 | vertx.setTimer(3000, id -> { 159 | client.request(method, 8080, "localhost", "/img.jpg") 160 | .compose(req2 -> 161 | req2.putHeader(HttpHeaders.CACHE_CONTROL, "max-age=1") 162 | .send().compose(resp2 -> { 163 | ctx.assertEquals(200, resp2.statusCode()); 164 | return resp2.body(); 165 | })) 166 | .compose(body2 -> { 167 | ctx.assertEquals(method == HttpMethod.GET ? "content2" : "", body2.toString()); 168 | return client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg"); 169 | }) 170 | .compose(req3 -> req3 171 | .send() 172 | .compose(resp3 -> { 173 | ctx.assertEquals(200, resp3.statusCode()); 174 | return resp3.body(); 175 | })).onComplete(ctx.asyncAssertSuccess(body3 -> { 176 | ctx.assertEquals("content2", body3.toString()); 177 | latch.complete(); 178 | })); 179 | }); 180 | })); 181 | latch.awaitSuccess(10000); 182 | /* 183 | ServeEvent event1 = getAllServeEvents().get(1); 184 | assertNull(event1.getRequest().getHeader("If-None-Match")); 185 | assertEquals(200, event1.getResponse().getStatus()); 186 | ServeEvent event0 = getAllServeEvents().get(0); 187 | assertEquals("tag0", event0.getRequest().getHeader("If-None-Match")); 188 | assertEquals(304, event0.getResponse().getStatus()); 189 | */ 190 | } 191 | 192 | @Test 193 | public void testUncacheableHeadRevalidatesEntryOnOk(TestContext ctx) throws Exception { 194 | testUncacheableHeadRevalidatesEntry(ctx, 200); 195 | } 196 | 197 | @Test 198 | public void testUncacheableHeadRevalidatesEntryOnNotModified(TestContext ctx) throws Exception { 199 | testUncacheableHeadRevalidatesEntry(ctx, 304); 200 | } 201 | 202 | private void testUncacheableHeadRevalidatesEntry(TestContext ctx, int status) throws Exception { 203 | stubFor(get(urlEqualTo("/img.jpg")) 204 | .willReturn( 205 | aResponse() 206 | .withStatus(200) 207 | .withHeader("Cache-Control", "public") 208 | .withHeader("ETag", "tag0") 209 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 210 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) 211 | .withBody("content"))); 212 | stubFor(head(urlEqualTo("/img.jpg")) 213 | .willReturn( 214 | aResponse() 215 | .withStatus(status) 216 | .withHeader("Cache-Control", "public") 217 | .withHeader("ETag", "tag0") 218 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 219 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))))); 220 | startProxy(new SocketAddressImpl(8081, "localhost")); 221 | Async latch = ctx.async(); 222 | client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") 223 | .compose(req1 -> req1.send() 224 | .compose(resp1 -> { 225 | ctx.assertEquals(200, resp1.statusCode()); 226 | return resp1.body(); 227 | })) 228 | .onComplete(ctx.asyncAssertSuccess(body1 -> { 229 | ctx.assertEquals("content", body1.toString()); 230 | vertx.setTimer(3000, id -> { 231 | client.request(HttpMethod.HEAD, 8080, "localhost", "/img.jpg") 232 | .compose(req2 -> req2 233 | .putHeader(HttpHeaders.CACHE_CONTROL, "max-age=1") 234 | .send().compose(resp2 -> { 235 | ctx.assertEquals(200, resp2.statusCode()); 236 | return resp2.body(); 237 | })).onComplete(ctx.asyncAssertSuccess(body2 -> { 238 | ctx.assertEquals("", body2.toString()); 239 | latch.complete(); 240 | })); 241 | }); 242 | })); 243 | latch.awaitSuccess(10000); 244 | ServeEvent event1 = getAllServeEvents().get(1); 245 | assertNull(event1.getRequest().getHeader("If-None-Match")); 246 | assertEquals(200, event1.getResponse().getStatus()); 247 | ServeEvent event0 = getAllServeEvents().get(0); 248 | assertEquals("tag0", event0.getRequest().getHeader("If-None-Match")); 249 | assertEquals(status, event0.getResponse().getStatus()); 250 | } 251 | 252 | @Test 253 | public void testHeadDoesNotPopulateCache(TestContext ctx) throws Exception { 254 | stubFor(get(urlEqualTo("/img.jpg")) 255 | .willReturn( 256 | aResponse() 257 | .withStatus(200) 258 | .withHeader("Cache-Control", "public") 259 | .withHeader("ETag", "tag0") 260 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 261 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) 262 | .withBody("content"))); 263 | stubFor(head(urlEqualTo("/img.jpg")) 264 | .willReturn( 265 | aResponse() 266 | .withStatus(200) 267 | .withHeader("Cache-Control", "public") 268 | .withHeader("ETag", "tag0") 269 | .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) 270 | .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))))); 271 | startProxy(new SocketAddressImpl(8081, "localhost")); 272 | Async latch = ctx.async(); 273 | client.request(HttpMethod.HEAD, 8080, "localhost", "/img.jpg") 274 | .compose(req1 -> req1.send().compose(resp1 -> { 275 | ctx.assertEquals(200, resp1.statusCode()); 276 | return resp1.body(); 277 | })) 278 | .compose(body1 -> { 279 | ctx.assertEquals("", body1.toString()); 280 | return client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg"); 281 | }) 282 | .compose(req2 -> req2.send().compose(resp2 -> { 283 | ctx.assertEquals(200, resp2.statusCode()); 284 | return resp2.body(); 285 | })) 286 | .onComplete(ctx.asyncAssertSuccess(body2 -> { 287 | ctx.assertEquals("content", body2.toString()); 288 | latch.complete(); 289 | })); 290 | latch.awaitSuccess(10000); 291 | ServeEvent event1 = getAllServeEvents().get(1); 292 | assertNull(event1.getRequest().getHeader("If-None-Match")); 293 | assertEquals(200, event1.getResponse().getStatus()); 294 | ServeEvent event0 = getAllServeEvents().get(0); 295 | assertEquals(null, event0.getRequest().getHeader("If-None-Match")); 296 | assertEquals(200, event0.getResponse().getStatus()); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ProxyClientKeepAliveTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.core.Promise; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.http.*; 6 | import io.vertx.core.net.NetClient; 7 | import io.vertx.core.net.SocketAddress; 8 | import io.vertx.core.streams.WriteStream; 9 | import io.vertx.ext.unit.Async; 10 | import io.vertx.ext.unit.TestContext; 11 | import org.junit.Test; 12 | 13 | import java.io.Closeable; 14 | import java.util.Random; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | import java.util.concurrent.atomic.AtomicReference; 18 | 19 | import static io.vertx.core.http.HttpMethod.GET; 20 | import static io.vertx.core.http.HttpMethod.HEAD; 21 | 22 | /** 23 | * @author Julien Viet 24 | */ 25 | public class ProxyClientKeepAliveTest extends ProxyTestBase { 26 | 27 | protected boolean keepAlive = true; 28 | protected boolean pipelining = false; 29 | 30 | @Override 31 | public void setUp() { 32 | super.setUp(); 33 | clientOptions.setKeepAlive(keepAlive).setPipelining(pipelining); 34 | } 35 | 36 | /* 37 | @Test 38 | public void testNotfound(TestContext ctx) { 39 | startProxy(ctx); 40 | HttpClient client = vertx.createHttpClient(); 41 | Async async = ctx.async(); 42 | client.getNow(8080, "localhost", "/", resp -> { 43 | ctx.assertEquals(404, resp.statusCode()); 44 | async.complete(); 45 | }); 46 | } 47 | */ 48 | 49 | @Test 50 | public void testGet(TestContext ctx) { 51 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 52 | ctx.assertEquals("/somepath", req.uri()); 53 | ctx.assertEquals("localhost:8081", req.host()); 54 | req.response().end("Hello World"); 55 | }); 56 | startProxy(backend); 57 | HttpClient client = vertx.createHttpClient(); 58 | client.request(GET, 8080, "localhost", "/somepath") 59 | .compose(req -> req 60 | .send() 61 | .compose(resp -> { 62 | ctx.assertEquals(200, resp.statusCode()); 63 | return resp.body(); 64 | })) 65 | .onComplete(ctx.asyncAssertSuccess(body -> { 66 | ctx.assertEquals("Hello World", body.toString()); 67 | })); 68 | } 69 | 70 | @Test 71 | public void testPost(TestContext ctx) { 72 | byte[] body = new byte[1024]; 73 | Random random = new Random(); 74 | random.nextBytes(body); 75 | Async async = ctx.async(2); 76 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 77 | req.bodyHandler(buff -> { 78 | req.response().end(); 79 | ctx.assertEquals(Buffer.buffer(body), buff); 80 | async.countDown(); 81 | }); 82 | }); 83 | startProxy(backend); 84 | HttpClient client = vertx.createHttpClient(); 85 | client.request(GET, 8080, "localhost", "/") 86 | .compose(req -> req 87 | .send(Buffer.buffer(body)) 88 | .compose(resp -> { 89 | ctx.assertEquals(200, resp.statusCode()); 90 | return resp.body(); 91 | })) 92 | .onComplete(ctx.asyncAssertSuccess(buff -> async.countDown())); 93 | } 94 | 95 | @Test 96 | public void testBackendClosesDuringUpload(TestContext ctx) { 97 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 98 | AtomicInteger len = new AtomicInteger(); 99 | req.handler(buff -> { 100 | if (len.addAndGet(buff.length()) == 1024) { 101 | req.connection().close(); 102 | } 103 | }); 104 | }); 105 | startProxy(backend); 106 | HttpClient client = vertx.createHttpClient(); 107 | Async async = ctx.async(); 108 | client.request(HttpMethod.POST, 8080, "localhost", "/") 109 | .onComplete(ctx.asyncAssertSuccess(req -> { 110 | req.onComplete(ctx.asyncAssertSuccess(resp -> { 111 | ctx.assertEquals(502, resp.statusCode()); 112 | async.complete(); 113 | })); 114 | req.putHeader("Content-Length", "2048"); 115 | req.write(Buffer.buffer(new byte[1024])); 116 | })); 117 | } 118 | 119 | @Test 120 | public void testClientClosesDuringUpload(TestContext ctx) { 121 | Async async = ctx.async(); 122 | Promise closeLatch = Promise.promise(); 123 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 124 | req.response().closeHandler(v -> { 125 | async.complete(); 126 | }); 127 | req.handler(buff -> { 128 | closeLatch.tryComplete(); 129 | }); 130 | }); 131 | startProxy(backend); 132 | HttpClient client = vertx.createHttpClient(); 133 | client.request(HttpMethod.POST, 8080, "localhost", "/") 134 | .onComplete(ctx.asyncAssertSuccess(req -> { 135 | req.onComplete(ctx.asyncAssertFailure()); 136 | req.putHeader("Content-Length", "2048"); 137 | req.write(Buffer.buffer(new byte[1024])); 138 | closeLatch.future().onComplete(ar -> { 139 | req.connection().close(); 140 | }); 141 | })); 142 | } 143 | 144 | @Test 145 | public void testClientClosesAfterUpload(TestContext ctx) { 146 | Async async = ctx.async(); 147 | Promise closeLatch = Promise.promise(); 148 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 149 | req.endHandler(v -> { 150 | closeLatch.complete(); 151 | vertx.setTimer(200, id -> { 152 | req.response().setChunked(true).write("partial response"); 153 | }); 154 | }); 155 | req.response().closeHandler(v -> { 156 | async.complete(); 157 | }); 158 | }); 159 | startProxy(backend); 160 | HttpClient client = vertx.createHttpClient(); 161 | client.request(HttpMethod.POST, 8080, "localhost", "/") 162 | .onComplete(ctx.asyncAssertSuccess(req -> { 163 | closeLatch.future().onSuccess(v -> { 164 | HttpConnection conn = req.connection(); 165 | conn.close(); 166 | }); 167 | req.send(Buffer.buffer(new byte[1024]), ctx.asyncAssertFailure()); 168 | })); 169 | } 170 | 171 | @Test 172 | public void testBackendCloseResponseWithOnGoingRequest(TestContext ctx) { 173 | SocketAddress backend = startNetBackend(ctx, 8081, so -> { 174 | Buffer body = Buffer.buffer(); 175 | so.handler(buff -> { 176 | body.appendBuffer(buff); 177 | if (buff.toString().contains("\r\n\r\n")) { 178 | so.write( 179 | "HTTP/1.1 200 OK\r\n" + 180 | "content-length: 0\r\n" + 181 | "\r\n"); 182 | so.close(); 183 | } 184 | }); 185 | }); 186 | startProxy(backend); 187 | HttpClient client = vertx.createHttpClient(); 188 | client.request(HttpMethod.POST, 8080, "localhost", "/") 189 | .onComplete(ctx.asyncAssertSuccess(req -> { 190 | req.putHeader("Content-Length", "2048"); 191 | req.write(Buffer.buffer(new byte[1024])); 192 | req.onComplete(ctx.asyncAssertSuccess(resp -> { 193 | ctx.assertEquals(200, resp.statusCode()); 194 | })); 195 | })); 196 | } 197 | 198 | @Test 199 | public void testBackendCloseResponse(TestContext ctx) { 200 | testBackendCloseResponse(ctx, false); 201 | } 202 | 203 | @Test 204 | public void testBackendCloseChunkedResponse(TestContext ctx) { 205 | testBackendCloseResponse(ctx, true); 206 | } 207 | 208 | private void testBackendCloseResponse(TestContext ctx, boolean chunked) { 209 | CompletableFuture closeFuture = new CompletableFuture<>(); 210 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 211 | HttpServerResponse resp = req.response(); 212 | if (chunked) { 213 | resp.setChunked(true); 214 | } else { 215 | resp.putHeader("content-length", "10000"); 216 | } 217 | resp.write("part"); 218 | closeFuture.thenAccept(v -> { 219 | resp.close(); 220 | }); 221 | }); 222 | startProxy(backend); 223 | HttpClient client = vertx.createHttpClient(); 224 | Async async = ctx.async(); 225 | client.request(GET, 8080, "localhost", "/") 226 | .onComplete(ctx.asyncAssertSuccess(req -> { 227 | req.send(ctx.asyncAssertSuccess(resp -> { 228 | resp.handler(buff -> { 229 | closeFuture.complete(null); 230 | }); 231 | resp.exceptionHandler(err -> { 232 | async.complete(); 233 | }); 234 | })); 235 | })); 236 | } 237 | 238 | @Test 239 | public void testFrontendCloseResponse(TestContext ctx) { 240 | testFrontendCloseResponse(ctx, false); 241 | } 242 | 243 | @Test 244 | public void testFrontendCloseChunkedResponse(TestContext ctx) { 245 | testBackendCloseResponse(ctx, true); 246 | } 247 | 248 | private void testFrontendCloseResponse(TestContext ctx, boolean chunked) { 249 | Async async = ctx.async(); 250 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 251 | HttpServerResponse resp = req.response(); 252 | if (chunked) { 253 | resp.setChunked(true); 254 | } else { 255 | resp.putHeader("content-length", "10000"); 256 | } 257 | resp.write("part"); 258 | resp.exceptionHandler(err -> { 259 | async.complete(); 260 | }); 261 | }); 262 | startProxy(backend); 263 | HttpClient client = vertx.createHttpClient(); 264 | client.request(GET, 8081, "localhost", "/", ctx.asyncAssertSuccess(req -> { 265 | req.send(ctx.asyncAssertSuccess(resp -> { 266 | resp.handler(buff -> { 267 | resp.request().connection().close(); 268 | System.out.println("closing"); 269 | }); 270 | })); 271 | })); 272 | } 273 | 274 | @Test 275 | public void testBackendRepliesIncorrectHttpVersion(TestContext ctx) { 276 | SocketAddress backend = startNetBackend(ctx, 8081, so -> { 277 | so.write("HTTP/1.2 200 OK\r\n\r\n"); 278 | so.close(); 279 | }); 280 | startProxy(backend); 281 | HttpClient client = vertx.createHttpClient(); 282 | client.request(GET, 8080, "localhost", "/") 283 | .compose(req -> 284 | req.send().compose(resp -> { 285 | ctx.assertEquals(502, resp.statusCode()); 286 | return resp.body(); 287 | })).onComplete(ctx.asyncAssertSuccess(b -> { 288 | })); 289 | } 290 | 291 | @Test 292 | public void testSuppressIncorrectWarningHeaders(TestContext ctx) { 293 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 294 | req.response() 295 | .putHeader("date", "Tue, 15 Nov 1994 08:12:30 GMT") 296 | .putHeader("warning", "199 Miscellaneous warning \"Tue, 15 Nov 1994 08:12:31 GMT\"") 297 | .end(); 298 | }); 299 | startProxy(backend); 300 | HttpClient client = vertx.createHttpClient(); 301 | client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { 302 | req.send(ctx.asyncAssertSuccess(resp -> { 303 | ctx.assertNotNull(resp.getHeader("date")); 304 | ctx.assertNull(resp.getHeader("warning")); 305 | })); 306 | })); 307 | } 308 | 309 | @Test 310 | public void testAddMissingHeaderDate(TestContext ctx) { 311 | Async latch = ctx.async(); 312 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 313 | req.response() 314 | // .putHeader("date", "Tue, 15 Nov 1994 08:12:30 GMT") 315 | // .putHeader("warning", "199 Miscellaneous warning \"Tue, 15 Nov 1994 08:12:31 GMT\"") 316 | .end(); 317 | }); 318 | startProxy(backend); 319 | HttpClient client = vertx.createHttpClient(); 320 | client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { 321 | req.send(ctx.asyncAssertSuccess(resp -> { 322 | ctx.assertNotNull(resp.getHeader("date")); 323 | latch.complete(); 324 | })); 325 | })); 326 | } 327 | 328 | @Test 329 | public void testAddMissingHeaderDateFromWarning(TestContext ctx) { 330 | String expectedDate = "Tue, 15 Nov 1994 08:12:31 GMT"; 331 | String expectedWarning = "199 Miscellaneous warning \"" + expectedDate + "\""; 332 | Async latch = ctx.async(); 333 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 334 | req.response() 335 | .putHeader("warning", expectedWarning) 336 | .end(); 337 | }); 338 | startProxy(backend); 339 | HttpClient client = vertx.createHttpClient(); 340 | client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { 341 | req.send(ctx.asyncAssertSuccess(resp -> { 342 | ctx.assertEquals(expectedDate, resp.getHeader("date")); 343 | ctx.assertEquals(expectedWarning, resp.getHeader("warning")); 344 | latch.complete(); 345 | })); 346 | })); 347 | } 348 | 349 | @Test 350 | public void testChunkedResponseToHttp1_1Client(TestContext ctx) { 351 | checkChunkedResponse(ctx, HttpVersion.HTTP_1_1); 352 | } 353 | 354 | @Test 355 | public void testChunkedResponseToHttp1_0Client(TestContext ctx) { 356 | checkChunkedResponse(ctx, HttpVersion.HTTP_1_0); 357 | } 358 | 359 | private void checkChunkedResponse(TestContext ctx, HttpVersion version) { 360 | int num = 50; 361 | Async latch = ctx.async(); 362 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 363 | HttpServerResponse resp = req.response(); 364 | resp.setChunked(true); 365 | streamChunkedBody(resp, num); 366 | }); 367 | startProxy(backend); 368 | HttpClient client = vertx.createHttpClient(new HttpClientOptions().setProtocolVersion(version)); 369 | StringBuilder sb = new StringBuilder(); 370 | for (int i = 0;i < num;i++) { 371 | sb.append("chunk-").append(i); 372 | } 373 | client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { 374 | req.send(ctx.asyncAssertSuccess(resp -> { 375 | if (version == HttpVersion.HTTP_1_1) { 376 | ctx.assertEquals("chunked", resp.getHeader("transfer-encoding")); 377 | ctx.assertEquals(null, resp.getHeader("content-length")); 378 | } else { 379 | ctx.assertEquals(null, resp.getHeader("transfer-encoding")); 380 | ctx.assertEquals("" + sb.length(), resp.getHeader("content-length")); 381 | } 382 | resp.handler(buff -> { 383 | String part = buff.toString(); 384 | if (sb.indexOf(part) == 0) { 385 | sb.delete(0, part.length()); 386 | } else { 387 | ctx.fail(); 388 | } 389 | }); 390 | resp.endHandler(v -> { 391 | ctx.assertEquals("", sb.toString()); 392 | latch.complete(); 393 | }); 394 | })); 395 | })); 396 | } 397 | 398 | @Test 399 | public void testChunkedTransferEncodingRequest(TestContext ctx) { 400 | int num = 50; 401 | Async latch = ctx.async(); 402 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 403 | StringBuilder sb = new StringBuilder(); 404 | for (int i = 0;i < num;i++) { 405 | sb.append("chunk-").append(i); 406 | } 407 | ctx.assertEquals("chunked", req.getHeader("transfer-encoding")); 408 | req.handler(buff -> { 409 | String part = buff.toString(); 410 | if (sb.indexOf(part) == 0) { 411 | sb.delete(0, part.length()); 412 | } else { 413 | ctx.fail(); 414 | } 415 | }); 416 | req.endHandler(v -> { 417 | ctx.assertEquals("", sb.toString()); 418 | latch.complete(); 419 | }); 420 | req.response().end(); 421 | }); 422 | startProxy(backend); 423 | HttpClient client = vertx.createHttpClient(); 424 | client.request(GET, 8080, "localhost", "/") 425 | .onSuccess(req -> { 426 | req.setChunked(true); 427 | streamChunkedBody(req, num); 428 | }); 429 | } 430 | 431 | @Test 432 | public void testIllegalClientHttpVersion(TestContext ctx) { 433 | Async latch = ctx.async(); 434 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 435 | ctx.fail(); 436 | }); 437 | startProxy(backend); 438 | NetClient client = vertx.createNetClient(); 439 | client.connect(8080, "localhost", ctx.asyncAssertSuccess(so -> { 440 | Buffer resp = Buffer.buffer(); 441 | so.handler(resp::appendBuffer); 442 | so.closeHandler(v -> { 443 | ctx.assertTrue(resp.toString().startsWith("HTTP/1.1 501 Not Implemented\r\n")); 444 | latch.complete(); 445 | }); 446 | so.write("GET /somepath http/1.1\r\n\r\n"); 447 | })); 448 | } 449 | 450 | @Test 451 | public void testHandleLongInitialLength(TestContext ctx) { 452 | proxyOptions.setMaxInitialLineLength(10000); 453 | Async latch = ctx.async(); 454 | String uri = "/" + randomAlphaString(5999); 455 | SocketAddress backend = startHttpBackend(ctx, new HttpServerOptions().setPort(8081).setMaxInitialLineLength(10000), req -> { 456 | ctx.assertEquals(uri, req.uri()); 457 | req.response().end(); 458 | }); 459 | startProxy(backend); 460 | HttpClient client = vertx.createHttpClient(); 461 | client.request(GET, 8080, "localhost", "" + uri, ctx.asyncAssertSuccess(req -> { 462 | req.send(ctx.asyncAssertSuccess(resp -> { 463 | ctx.assertEquals(200, resp.statusCode()); 464 | latch.complete(); 465 | })); 466 | })); 467 | } 468 | 469 | @Test 470 | public void testLargeChunkExtValue(TestContext ctx) { 471 | String s = "" + randomAlphaString(4096); 472 | Async latch = ctx.async(); 473 | SocketAddress backend = startNetBackend(ctx, 8081, so -> { 474 | Buffer body = Buffer.buffer(); 475 | so.handler(buff -> { 476 | body.appendBuffer(buff); 477 | if (body.toString().endsWith("\r\n\r\n")) { 478 | so.write("" + 479 | "HTTP/1.1 200 OK\r\n" + 480 | "Transfer-Encoding: chunked\r\n" + 481 | "connection: close\r\n" + 482 | "\r\n" + 483 | "A; name=\"" + s + "\"\r\n" + 484 | "0123456789\r\n" + 485 | "0\r\n" + 486 | "\r\n" 487 | ); 488 | } 489 | }); 490 | }); 491 | clientOptions.setMaxInitialLineLength(5000); 492 | startProxy(backend); 493 | HttpClient client = vertx.createHttpClient(/*new HttpClientOptions().setProtocolVersion(HttpVersion.HTTP_1_0)*/); 494 | client.request(GET, 8080, "localhost", "/somepath").compose(req -> 495 | req.send().compose(resp -> { 496 | ctx.assertEquals(200, resp.statusCode()); 497 | return resp.body(); 498 | }) 499 | ).onComplete(ctx.asyncAssertSuccess(body -> { 500 | ctx.assertEquals("0123456789", body.toString()); 501 | latch.complete(); 502 | })); 503 | } 504 | 505 | @Test 506 | public void testRequestIllegalTransferEncoding1(TestContext ctx) throws Exception { 507 | checkBadRequest(ctx, 508 | "POST /somepath HTTP/1.1\r\n" + 509 | "transfer-encoding: identity\r\n" + 510 | "connection: close\r\n" + 511 | "\r\n", 512 | "POST /somepath HTTP/1.1\r\n" + 513 | "transfer-encoding: chunked, identity\r\n" + 514 | "connection: close\r\n" + 515 | "\r\n", 516 | "POST /somepath HTTP/1.1\r\n" + 517 | "transfer-encoding: identity, chunked\r\n" + 518 | "connection: close\r\n" + 519 | "\r\n", 520 | "POST /somepath HTTP/1.1\r\n" + 521 | "transfer-encoding: identity\r\n" + 522 | "transfer-encoding: chunked\r\n" + 523 | "connection: close\r\n" + 524 | "\r\n", 525 | "POST /somepath HTTP/1.1\r\n" + 526 | "transfer-encoding: chunked\r\n" + 527 | "transfer-encoding: identity\r\n" + 528 | "connection: close\r\n" + 529 | "\r\n", 530 | "POST /somepath HTTP/1.1\r\n" + 531 | "transfer-encoding: other, chunked\r\n" + 532 | "connection: close\r\n" + 533 | "\r\n", 534 | "POST /somepath HTTP/1.1\r\n" + 535 | "transfer-encoding: other\r\n" + 536 | "transfer-encoding: chunked\r\n" + 537 | "connection: close\r\n" + 538 | "\r\n"); 539 | } 540 | 541 | private void checkBadRequest(TestContext ctx, String... requests) throws Exception { 542 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 543 | ctx.fail(); 544 | }); 545 | startProxy(backend); 546 | for (String request : requests) { 547 | Async latch = ctx.async(); 548 | NetClient client = vertx.createNetClient(); 549 | try { 550 | client.connect(8080, "localhost", ctx.asyncAssertSuccess(so -> { 551 | Buffer resp = Buffer.buffer(); 552 | so.handler(buff -> { 553 | resp.appendBuffer(buff); 554 | if (resp.toString().startsWith("HTTP/1.1 400 Bad Request\r\n")) { 555 | latch.complete(); 556 | } 557 | }); 558 | so.write(request); 559 | })); 560 | latch.awaitSuccess(10000); 561 | } finally { 562 | client.close(); 563 | } 564 | } 565 | } 566 | 567 | @Test 568 | public void testResponseIllegalTransferEncoding(TestContext ctx) throws Exception { 569 | checkBadResponse(ctx, "" + 570 | "HTTP/1.1 200 OK\r\n" + 571 | "Transfer-Encoding: other, chunked\r\n" + 572 | "connection: close\r\n" + 573 | "\r\n" + 574 | "A\r\n" + 575 | "0123456789\r\n" + 576 | "0\r\n" + 577 | "\r\n", "" + 578 | "HTTP/1.1 200 OK\r\n" + 579 | "Transfer-Encoding: other\r\n" + 580 | "Transfer-Encoding: chunked\r\n" + 581 | "connection: close\r\n" + 582 | "\r\n" + 583 | "A\r\n" + 584 | "0123456789\r\n" + 585 | "0\r\n" + 586 | "\r\n"); 587 | } 588 | 589 | @Test 590 | public void testRawMethod(TestContext ctx) throws Exception { 591 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 592 | ctx.assertEquals("FOO", req.method().name()); 593 | req.response().end(); 594 | }); 595 | startProxy(backend); 596 | HttpClient client = vertx.createHttpClient(); 597 | client.request(HttpMethod.valueOf("FOO"), 8080, "localhost", "/") 598 | .compose(req -> req.send().compose(HttpClientResponse::body)) 599 | .onComplete(ctx.asyncAssertSuccess()); 600 | } 601 | 602 | @Test 603 | public void testHead(TestContext ctx) throws Exception { 604 | Async latch = ctx.async(); 605 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 606 | ctx.assertEquals(HEAD, req.method()); 607 | req.response().putHeader("content-length", "" + "content".length()).end(); 608 | }); 609 | startProxy(backend); 610 | HttpClient client = vertx.createHttpClient(); 611 | client.request(HEAD, 8080, "localhost", "/").compose(req -> 612 | req.send().compose(HttpClientResponse::body) 613 | ).onComplete(ctx.asyncAssertSuccess(body -> { 614 | ctx.assertEquals("", body.toString()); 615 | latch.complete(); 616 | })); 617 | } 618 | 619 | // TODO test we don't filter content... 620 | @Test 621 | public void testHeadWithNotSendBody(TestContext ctx) throws Exception { 622 | Async latch = ctx.async(); 623 | SocketAddress backend = startNetBackend(ctx, 8081, so -> { 624 | so.write( 625 | "HTTP/1.1 200 OK\r\n" + 626 | "content-length: 20\r\n" + 627 | "\r\n" + 628 | "0123456789" 629 | ); 630 | }); 631 | startProxy(backend); 632 | HttpClient client = vertx.createHttpClient(); 633 | client.request(HEAD, 8080, "localhost", "/").compose(req -> 634 | req.send().compose(HttpClientResponse::body) 635 | ).onComplete(ctx.asyncAssertSuccess(body -> { 636 | ctx.assertEquals("", body.toString()); 637 | latch.complete(); 638 | })); 639 | } 640 | 641 | private void checkBadResponse(TestContext ctx, String... responses) throws Exception { 642 | AtomicReference responseBody = new AtomicReference<>(); 643 | SocketAddress backend = startNetBackend(ctx, 8081, so -> { 644 | Buffer body = Buffer.buffer(); 645 | so.handler(buff -> { 646 | body.appendBuffer(buff); 647 | if (body.toString().endsWith("\r\n\r\n")) { 648 | System.out.println(body.toString()); 649 | so.write(responseBody.get()); 650 | } 651 | }); 652 | }); 653 | for (String response : responses) { 654 | responseBody.set(response); 655 | Async latch = ctx.async(); 656 | HttpClient client = vertx.createHttpClient(); 657 | try (Closeable proxy = startProxy(backend)) { 658 | client.request(GET, 8080, "localhost", "/somepath") 659 | .compose(req -> req.send().compose(resp -> { 660 | ctx.assertEquals(501, resp.statusCode()); 661 | return resp.body(); 662 | })).onComplete(ctx.asyncAssertSuccess(body -> { 663 | ctx.assertEquals("", body.toString()); 664 | latch.complete(); 665 | })); 666 | latch.awaitSuccess(10000); 667 | } finally { 668 | client.close(); 669 | } 670 | } 671 | } 672 | 673 | private void streamChunkedBody(WriteStream stream, int num) { 674 | AtomicInteger count = new AtomicInteger(0); 675 | vertx.setPeriodic(10, id -> { 676 | int val = count.getAndIncrement(); 677 | if (val < num) { 678 | stream.write(Buffer.buffer("chunk-" + val)); 679 | } else { 680 | vertx.cancelTimer(id); 681 | stream.end(); 682 | } 683 | }); 684 | } 685 | 686 | private StringBuilder randomAlphaString(int len) { 687 | Random random = new Random(); 688 | StringBuilder uri = new StringBuilder(); 689 | for (int i = 0;i < len;i++) { 690 | uri.append((char)('A' + random.nextInt(26))); 691 | } 692 | return uri; 693 | } 694 | 695 | @Test 696 | public void testPropagateHeaders(TestContext ctx) { 697 | SocketAddress backend = startHttpBackend(ctx, new HttpServerOptions().setPort(8081).setMaxInitialLineLength(10000), req -> { 698 | ctx.assertEquals("request_header_value", req.getHeader("request_header")); 699 | req.response().putHeader("response_header", "response_header_value").end(); 700 | }); 701 | startProxy(backend); 702 | HttpClient client = vertx.createHttpClient(); 703 | Async latch = ctx.async(); 704 | client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { 705 | req.putHeader("request_header", "request_header_value").send(ctx.asyncAssertSuccess(resp -> { 706 | ctx.assertEquals(200, resp.statusCode()); 707 | ctx.assertEquals("response_header_value", resp.getHeader("response_header")); 708 | latch.complete(); 709 | })); 710 | })); 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/httpproxy/ProxyRequestTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.httpproxy; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Handler; 5 | import io.vertx.core.MultiMap; 6 | import io.vertx.core.Promise; 7 | import io.vertx.core.buffer.Buffer; 8 | import io.vertx.core.http.HttpClient; 9 | import io.vertx.core.http.HttpClientOptions; 10 | import io.vertx.core.http.HttpClientResponse; 11 | import io.vertx.core.http.HttpHeaders; 12 | import io.vertx.core.http.HttpMethod; 13 | import io.vertx.core.http.HttpServerRequest; 14 | import io.vertx.core.http.HttpServerResponse; 15 | import io.vertx.core.http.HttpVersion; 16 | import io.vertx.core.http.RequestOptions; 17 | import io.vertx.core.net.NetClient; 18 | import io.vertx.core.net.NetSocket; 19 | import io.vertx.core.net.SocketAddress; 20 | import io.vertx.core.streams.ReadStream; 21 | import io.vertx.ext.unit.Async; 22 | import io.vertx.ext.unit.TestContext; 23 | import io.vertx.httpproxy.impl.BufferedReadStream; 24 | import org.junit.Ignore; 25 | import org.junit.Test; 26 | 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | import java.util.concurrent.atomic.AtomicInteger; 30 | 31 | /** 32 | * @author Julien Viet 33 | */ 34 | public class ProxyRequestTest extends ProxyTestBase { 35 | 36 | @Ignore 37 | @Test 38 | public void testProxyRequestIllegalHttpVersion(TestContext ctx) { 39 | runHttpTest(ctx, req -> req.response().end("Hello World"), ctx.asyncAssertFailure()); 40 | NetClient client = vertx.createNetClient(); 41 | client.connect(8080, "localhost", ctx.asyncAssertSuccess(so -> { 42 | so.write("GET /somepath http/1.1\r\n\r\n"); 43 | })); 44 | } 45 | 46 | @Test 47 | public void testBackendResponse(TestContext ctx) { 48 | runHttpTest(ctx, req -> req.response().end("Hello World"), ctx.asyncAssertSuccess()); 49 | HttpClient httpClient = vertx.createHttpClient(); 50 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath") 51 | .compose(req -> req.send().compose(HttpClientResponse::body)) 52 | .onComplete(ctx.asyncAssertSuccess()); 53 | } 54 | 55 | @Test 56 | public void testChunkedBackendResponse(TestContext ctx) { 57 | testChunkedBackendResponse(ctx, HttpVersion.HTTP_1_1); 58 | } 59 | 60 | @Ignore 61 | @Test 62 | public void testChunkedBackendResponseToHttp1_0Client(TestContext ctx) { 63 | testChunkedBackendResponse(ctx, HttpVersion.HTTP_1_0); 64 | } 65 | 66 | private void testChunkedBackendResponse(TestContext ctx, HttpVersion version) { 67 | runHttpTest(ctx, req -> req.response().setChunked(true).end("Hello World"), ctx.asyncAssertSuccess()); 68 | HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions().setProtocolVersion(version)); 69 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath") 70 | .compose(req -> req.send().compose(HttpClientResponse::body)) 71 | .onComplete(ctx.asyncAssertSuccess()); 72 | } 73 | 74 | @Ignore 75 | @Test 76 | public void testIllegalTransferEncodingBackendResponse(TestContext ctx) { 77 | runNetTest(ctx, req -> req.write("" + 78 | "HTTP/1.1 200 OK\r\n" + 79 | "transfer-encoding: identity\r\n" + 80 | "connection: close\r\n" + 81 | "\r\n"), ctx.asyncAssertSuccess()); 82 | HttpClient httpClient = vertx.createHttpClient(); 83 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath") 84 | .compose(req -> req.send().compose(HttpClientResponse::body)) 85 | .onComplete(ctx.asyncAssertSuccess()); 86 | } 87 | 88 | @Test 89 | public void testCloseBackendResponse(TestContext ctx) { 90 | testCloseBackendResponse(ctx, false); 91 | } 92 | 93 | @Test 94 | public void testCloseChunkedBackendResponse(TestContext ctx) { 95 | testCloseBackendResponse(ctx, true); 96 | } 97 | 98 | private void testCloseBackendResponse(TestContext ctx, boolean chunked) { 99 | CompletableFuture cont = new CompletableFuture<>(); 100 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 101 | HttpServerResponse resp = req.response(); 102 | if (chunked) { 103 | resp.setChunked(true); 104 | } else { 105 | resp.putHeader("content-length", "10000"); 106 | } 107 | resp.write("part"); 108 | cont.thenAccept(v -> { 109 | resp.close(); 110 | }); 111 | }); 112 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 113 | Async async = ctx.async(); 114 | startHttpServer(ctx, proxyOptions, req -> { 115 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 116 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 117 | proxyReq.proxy(clientReq, ctx.asyncAssertFailure(err -> async.complete())); 118 | })); 119 | }); 120 | HttpClient httpClient = vertx.createHttpClient(); 121 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> 122 | req.send(ctx.asyncAssertSuccess(resp -> { 123 | resp.handler(buff -> { 124 | cont.complete(null); 125 | }); 126 | })) 127 | )); 128 | } 129 | 130 | @Test 131 | public void testCloseFrontendResponse(TestContext ctx) { 132 | testCloseFrontendResponse(ctx, false); 133 | } 134 | 135 | @Test 136 | public void testCloseChunkedFrontendResponse(TestContext ctx) { 137 | testCloseFrontendResponse(ctx, true); 138 | } 139 | 140 | private void testCloseFrontendResponse(TestContext ctx, boolean chunked) { 141 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 142 | HttpServerResponse resp = req.response(); 143 | if (chunked) { 144 | resp.setChunked(true); 145 | } else { 146 | resp.putHeader("content-length", "10000"); 147 | } 148 | long id = vertx.setPeriodic(1, id_ -> { 149 | resp.write("part"); 150 | }); 151 | resp.closeHandler(v -> { 152 | vertx.cancelTimer(id); 153 | }); 154 | }); 155 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 156 | Async async = ctx.async(); 157 | startHttpServer(ctx, proxyOptions, req -> { 158 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 159 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 160 | proxyReq.proxy(clientReq, ctx.asyncAssertFailure(err -> async.complete())); 161 | })); 162 | }); 163 | HttpClient httpClient = vertx.createHttpClient(); 164 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> 165 | req.send(ctx.asyncAssertSuccess(resp -> { 166 | resp.handler(buff -> { 167 | resp.request().connection().close(); 168 | }); 169 | })) 170 | )); 171 | } 172 | 173 | @Test 174 | public void testCloseFrontendRequest(TestContext ctx) throws Exception { 175 | testCloseChunkedFrontendRequest(ctx, false); 176 | } 177 | 178 | @Test 179 | public void testCloseChunkedFrontendRequest(TestContext ctx) throws Exception { 180 | testCloseChunkedFrontendRequest(ctx, true); 181 | } 182 | 183 | private void testCloseChunkedFrontendRequest(TestContext ctx, boolean chunked) throws Exception { 184 | Promise latch = Promise.promise(); 185 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 186 | req.handler(buff -> { 187 | ctx.assertEquals("part", buff.toString()); 188 | latch.tryComplete(); 189 | }); 190 | }); 191 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 192 | Async async = ctx.async(); 193 | startHttpServer(ctx, proxyOptions, req -> { 194 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 195 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 196 | proxyReq.proxy(clientReq, ctx.asyncAssertFailure(err -> async.complete())); 197 | })); 198 | }); 199 | HttpClient httpClient = vertx.createHttpClient(); 200 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> { 201 | if (chunked) { 202 | req.setChunked(true); 203 | } else { 204 | req.putHeader("content-length", "10000"); 205 | } 206 | req.write("part"); 207 | latch.future().onSuccess(v -> { 208 | req.connection().close(); 209 | }); 210 | })); 211 | } 212 | 213 | @Test 214 | public void testCloseBackendRequest(TestContext ctx) throws Exception { 215 | testCloseBackendRequest(ctx, false); 216 | } 217 | 218 | @Test 219 | public void testCloseChunkedBackendRequest(TestContext ctx) throws Exception { 220 | testCloseBackendRequest(ctx, true); 221 | } 222 | 223 | private void testCloseBackendRequest(TestContext ctx, boolean chunked) throws Exception { 224 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 225 | req.handler(buff -> { 226 | ctx.assertEquals("part", buff.toString()); 227 | req.connection().close(); 228 | }); 229 | }); 230 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 231 | Async async = ctx.async(); 232 | startHttpServer(ctx, proxyOptions, req -> { 233 | req.pause(); 234 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 235 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(backReq -> { 236 | proxyReq.send(backReq, ctx.asyncAssertFailure(err -> { 237 | async.complete(); 238 | req.response().setStatusCode(502).end(); 239 | })); 240 | })); 241 | }); 242 | HttpClient httpClient = vertx.createHttpClient(); 243 | httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> { 244 | req.onComplete(ctx.asyncAssertSuccess(resp -> { 245 | ctx.assertEquals(502, resp.statusCode()); 246 | })); 247 | if (chunked) { 248 | req.setChunked(true); 249 | } else { 250 | req.putHeader("content-length", "10000"); 251 | } 252 | req.write("part"); 253 | })); 254 | } 255 | 256 | @Test 257 | public void testLatency(TestContext ctx) throws Exception { 258 | HttpClient client = vertx.createHttpClient(); 259 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 260 | HttpServerResponse resp = req.response(); 261 | req.bodyHandler(resp::end); 262 | }); 263 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 264 | Async async = ctx.async(); 265 | startHttpServer(ctx, proxyOptions, req -> { 266 | req.pause(); 267 | vertx.setTimer(500, id1 -> { 268 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 269 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(backReq -> { 270 | proxyReq.send(backReq, ctx.asyncAssertSuccess(resp -> { 271 | vertx.setTimer(500, id2 -> { 272 | resp.send(ctx.asyncAssertSuccess(v -> async.complete())); 273 | }); 274 | })); 275 | })); 276 | }); 277 | }); 278 | Buffer sent = Buffer.buffer("Hello world"); 279 | client.request(HttpMethod.POST, 8080, "localhost", "/somepath") 280 | .compose(req -> req.send(sent).compose(HttpClientResponse::body)) 281 | .onComplete(ctx.asyncAssertSuccess(received -> { 282 | ctx.assertEquals(sent, received); 283 | })); 284 | } 285 | 286 | @Test 287 | public void testRequestFilter(TestContext ctx) throws Exception { 288 | Filter filter = new Filter(); 289 | CompletableFuture onResume = new CompletableFuture<>(); 290 | HttpClient client = vertx.createHttpClient(); 291 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 292 | req.pause(); 293 | onResume.thenAccept(num -> { 294 | req.bodyHandler(body -> { 295 | ctx.assertEquals(filter.expected, body); 296 | req.response().end(); 297 | }); 298 | req.resume(); 299 | }); 300 | }); 301 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 302 | startHttpServer(ctx, proxyOptions, req -> { 303 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 304 | proxyReq.bodyFilter(filter::init); 305 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 306 | proxyReq.proxy(clientReq, ctx.asyncAssertSuccess()); 307 | })); 308 | }); 309 | client.request(HttpMethod.POST, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> { 310 | req.setChunked(true); 311 | AtomicInteger num = new AtomicInteger(); 312 | vertx.setPeriodic(1, id -> { 313 | if (filter.paused.get()) { 314 | vertx.cancelTimer(id); 315 | req.end(); 316 | onResume.complete(num.get()); 317 | } else { 318 | num.incrementAndGet(); 319 | req.write(CHUNK); 320 | } 321 | }); 322 | })); 323 | } 324 | 325 | @Test 326 | public void testResponseFilter(TestContext ctx) throws Exception { 327 | Filter filter = new Filter(); 328 | CompletableFuture onResume = new CompletableFuture<>(); 329 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 330 | HttpServerResponse resp = req.response().setChunked(true); 331 | AtomicInteger num = new AtomicInteger(); 332 | vertx.setPeriodic(1, id -> { 333 | if (!filter.paused.get()) { 334 | resp.write(CHUNK); 335 | num.getAndIncrement(); 336 | } else { 337 | vertx.cancelTimer(id); 338 | resp.end(); 339 | onResume.complete(num.get()); 340 | } 341 | }); 342 | }); 343 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 344 | startHttpServer(ctx, proxyOptions, req -> { 345 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 346 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 347 | proxyReq.send(clientReq, ctx.asyncAssertSuccess(proxyResp -> { 348 | proxyResp.bodyFilter(filter::init); 349 | proxyResp.send(ctx.asyncAssertSuccess()); 350 | })); 351 | })); 352 | }); 353 | Async async = ctx.async(); 354 | HttpClient client = vertx.createHttpClient(); 355 | client.request(HttpMethod.GET, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> { 356 | req.send().onComplete(ctx.asyncAssertSuccess(resp -> { 357 | resp.pause(); 358 | onResume.thenAccept(num -> { 359 | resp.resume(); 360 | }); 361 | resp.endHandler(v -> { 362 | async.complete(); 363 | }); 364 | })); 365 | })); 366 | } 367 | 368 | @Test 369 | public void testUpdateRequestHeaders(TestContext ctx) throws Exception { 370 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 371 | ctx.assertNull(req.getHeader("header")); 372 | ctx.assertEquals("proxy_header_value", req.getHeader("proxy_header")); 373 | req.response().putHeader("header", "header_value").end(); 374 | }); 375 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 376 | startHttpServer(ctx, proxyOptions, req -> { 377 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 378 | MultiMap clientHeaders = proxyReq.headers(); 379 | clientHeaders.add("proxy_header", "proxy_header_value"); 380 | ctx.assertEquals("header_value", clientHeaders.get("header")); 381 | clientHeaders.remove("header"); 382 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 383 | proxyReq.send(clientReq, ctx.asyncAssertSuccess(proxyResp -> { 384 | MultiMap targetHeaders = proxyResp.headers(); 385 | targetHeaders.add("proxy_header", "proxy_header_value"); 386 | ctx.assertEquals("header_value", targetHeaders.get("header")); 387 | targetHeaders.remove("header"); 388 | proxyResp.send(ctx.asyncAssertSuccess()); 389 | })); 390 | })); 391 | }); 392 | HttpClient client = vertx.createHttpClient(); 393 | client.request(HttpMethod.GET, 8080, "localhost", "/somepath") 394 | .compose(req -> 395 | req 396 | .putHeader("header", "header_value") 397 | .send() 398 | .compose(resp -> { 399 | ctx.assertEquals("proxy_header_value", resp.getHeader("proxy_header")); 400 | ctx.assertNull(resp.getHeader("header")); 401 | return resp.body(); 402 | }) 403 | ).onComplete(ctx.asyncAssertSuccess()); 404 | } 405 | 406 | @Test 407 | public void testReleaseProxyResponse(TestContext ctx) { 408 | Async drainedLatch = ctx.async(); 409 | CompletableFuture full = new CompletableFuture<>(); 410 | Buffer chunk = Buffer.buffer(new byte[1024]); 411 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 412 | HttpServerResponse resp = req.response(); 413 | resp 414 | .setChunked(true) 415 | .putHeader("header", "header-value"); 416 | vertx.setPeriodic(1, id -> { 417 | if (resp.writeQueueFull()) { 418 | vertx.cancelTimer(id); 419 | resp.drainHandler(v -> { 420 | drainedLatch.complete(); 421 | }); 422 | full.complete(null); 423 | } else { 424 | resp.write(chunk); 425 | } 426 | }); 427 | }); 428 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 429 | startHttpServer(ctx, proxyOptions, req -> { 430 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 431 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 432 | proxyReq.send(clientReq, ctx.asyncAssertSuccess(proxyResp -> { 433 | full.whenComplete((v, err) -> { 434 | proxyResp.release(); 435 | req.response().end("another-response"); 436 | }); 437 | })); 438 | })); 439 | }); 440 | HttpClient client = vertx.createHttpClient(); 441 | client.request(HttpMethod.GET, 8080, "localhost", "/somepath") 442 | .compose(req -> req.send().compose(resp -> { 443 | ctx.assertNull(resp.getHeader("header")); 444 | return resp.body(); 445 | })) 446 | .onComplete(ctx.asyncAssertSuccess(body -> { 447 | ctx.assertEquals("another-response", body.toString()); 448 | })); 449 | } 450 | 451 | @Test 452 | public void testReleaseProxyRequest(TestContext ctx) { 453 | CompletableFuture full = new CompletableFuture<>(); 454 | SocketAddress backend = startHttpBackend(ctx, 8081, req -> { 455 | ctx.assertEquals(null, req.getHeader("header")); 456 | req.body(ctx.asyncAssertSuccess(body -> { 457 | req.response().end(body); 458 | })); 459 | }); 460 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 461 | startHttpServer(ctx, proxyOptions, req -> { 462 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 463 | full.whenComplete((v, err) -> { 464 | proxyReq.release(); 465 | proxyReq.setBody(Body.body(Buffer.buffer("another-request"))); 466 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 467 | proxyReq.send(clientReq, ctx.asyncAssertSuccess(proxyResp -> { 468 | proxyResp.send(ctx.asyncAssertSuccess()); 469 | })); 470 | })); 471 | }); 472 | }); 473 | HttpClient client = vertx.createHttpClient(); 474 | Async drainedLatch = ctx.async(); 475 | Buffer chunk = Buffer.buffer(new byte[1024]); 476 | client.request(HttpMethod.GET, 8080, "localhost", "/somepath", ctx.asyncAssertSuccess(req -> { 477 | req.setChunked(true); 478 | req.putHeader("header", "header-value"); 479 | vertx.setPeriodic(1, id -> { 480 | if (req.writeQueueFull()) { 481 | req.drainHandler(v1 -> { 482 | req.end().onSuccess(v2 -> { 483 | drainedLatch.complete(); 484 | }); 485 | }); 486 | full.complete(null); 487 | } else { 488 | req.write(chunk); 489 | } 490 | }); 491 | req.onComplete(ctx.asyncAssertSuccess(resp -> { 492 | resp.body(ctx.asyncAssertSuccess(body -> { 493 | ctx.assertEquals("another-request", body.toString()); 494 | })); 495 | })); 496 | })); 497 | } 498 | 499 | @Test 500 | public void testSendDefaultProxyResponse(TestContext ctx) { 501 | startHttpServer(ctx, proxyOptions, req -> { 502 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 503 | proxyReq.response().send(ar -> { 504 | 505 | }); 506 | }); 507 | HttpClient client = vertx.createHttpClient(); 508 | Async async = ctx.async(); 509 | client.request(HttpMethod.GET, 8080, "localhost", "/somepath") 510 | .compose(req -> req.send().compose(resp -> { 511 | ctx.assertEquals(200, resp.statusCode()); 512 | ctx.assertNull(resp.getHeader(HttpHeaders.TRANSFER_ENCODING)); 513 | return resp.body(); 514 | })) 515 | .onComplete(ctx.asyncAssertSuccess(body -> { 516 | ctx.assertEquals("", body.toString()); 517 | async.complete(); 518 | })); 519 | } 520 | 521 | @Test 522 | public void testSendProxyResponse(TestContext ctx) { 523 | startHttpServer(ctx, proxyOptions, req -> { 524 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 525 | proxyReq.response() 526 | .setStatusCode(302) 527 | .putHeader("some-header", "some-header-value") 528 | .setBody(Body.body(Buffer.buffer("hello world"))) 529 | .send(ar -> { 530 | 531 | }); 532 | }); 533 | HttpClient client = vertx.createHttpClient(); 534 | Async async = ctx.async(); 535 | client.request(HttpMethod.GET, 8080, "localhost", "/somepath") 536 | .compose(req -> req.send().compose(resp -> { 537 | ctx.assertEquals(302, resp.statusCode()); 538 | ctx.assertEquals("some-header-value", resp.getHeader("some-header")); 539 | ctx.assertNull(resp.getHeader(HttpHeaders.TRANSFER_ENCODING)); 540 | return resp.body(); 541 | })) 542 | .onComplete(ctx.asyncAssertSuccess(body -> { 543 | ctx.assertEquals("hello world", body.toString()); 544 | async.complete(); 545 | })); 546 | } 547 | 548 | private static Buffer CHUNK; 549 | 550 | static { 551 | byte[] bytes = new byte[1024]; 552 | for (int i = 0;i < 1024;i++) { 553 | bytes[i] = (byte)('A' + (i % 26)); 554 | } 555 | CHUNK = Buffer.buffer(bytes); 556 | } 557 | 558 | static class Filter implements ReadStream { 559 | 560 | private final AtomicBoolean paused = new AtomicBoolean(); 561 | private ReadStream stream; 562 | private Buffer expected = Buffer.buffer(); 563 | private Handler dataHandler; 564 | private Handler exceptionHandler; 565 | private Handler endHandler; 566 | 567 | ReadStream init(ReadStream s) { 568 | stream = s; 569 | stream.handler(buff -> { 570 | if (dataHandler != null) { 571 | byte[] bytes = new byte[buff.length()]; 572 | for (int i = 0;i < bytes.length;i++) { 573 | bytes[i] = (byte)(('a' - 'A') + buff.getByte(i)); 574 | } 575 | expected.appendBytes(bytes); 576 | dataHandler.handle(Buffer.buffer(bytes)); 577 | } 578 | }); 579 | stream.exceptionHandler(err -> { 580 | if (exceptionHandler != null) { 581 | exceptionHandler.handle(err); 582 | } 583 | }); 584 | stream.endHandler(v -> { 585 | if (endHandler != null) { 586 | endHandler.handle(v); 587 | } 588 | }); 589 | return this; 590 | } 591 | 592 | @Override 593 | public ReadStream pause() { 594 | paused.set(true); 595 | stream.pause(); 596 | return this; 597 | } 598 | 599 | @Override 600 | public ReadStream resume() { 601 | stream.resume(); 602 | return this; 603 | } 604 | 605 | @Override 606 | public ReadStream fetch(long amount) { 607 | stream.fetch(amount); 608 | return this; 609 | } 610 | 611 | @Override 612 | public ReadStream exceptionHandler(Handler handler) { 613 | exceptionHandler = handler; 614 | return this; 615 | } 616 | 617 | @Override 618 | public ReadStream handler(Handler handler) { 619 | dataHandler = handler; 620 | return this; 621 | } 622 | 623 | @Override 624 | public ReadStream endHandler(Handler handler) { 625 | endHandler = handler; 626 | return this; 627 | } 628 | } 629 | 630 | private void runHttpTest(TestContext ctx, 631 | Handler backendHandler, 632 | Handler> expect) { 633 | Async async = ctx.async(); 634 | SocketAddress backend = startHttpBackend(ctx, 8081, backendHandler); 635 | HttpClient client = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 636 | startHttpServer(ctx, proxyOptions, req -> { 637 | req.pause(); // Should it be necessary ? 638 | ProxyRequest proxyRequest = ProxyRequest.reverseProxy(req); 639 | client.request(new RequestOptions().setServer(backend), ar -> { 640 | if (ar.succeeded()) { 641 | proxyRequest.send(ar.result(), ar2 -> { 642 | if (ar2.succeeded()) { 643 | ProxyResponse proxyResponse = ar2.result(); 644 | proxyResponse.send(ar3 -> { 645 | expect.handle(ar3); 646 | async.complete(); 647 | }); 648 | } else { 649 | req.resume().response().setStatusCode(502).end(); 650 | } 651 | }); 652 | } else { 653 | req.resume().response().setStatusCode(404).end(); 654 | } 655 | }); 656 | }); 657 | } 658 | 659 | private void runNetTest(TestContext ctx, 660 | Handler backendHandler, 661 | Handler> expect) { 662 | Async async = ctx.async(); 663 | SocketAddress backend = startNetBackend(ctx, 8081, backendHandler); 664 | HttpClient backendClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); 665 | startHttpServer(ctx, proxyOptions, req -> { 666 | ProxyRequest proxyReq = ProxyRequest.reverseProxy(req); 667 | backendClient.request(new RequestOptions().setServer(backend), ctx.asyncAssertSuccess(clientReq -> { 668 | proxyReq.proxy(clientReq, ar -> { 669 | expect.handle(ar); 670 | async.complete(); 671 | }); 672 | })); 673 | }); 674 | } 675 | } 676 | -------------------------------------------------------------------------------- /LICENSE-eplv10-aslv20.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eclipse Public License - Version 1.0 / Apache License - Version 2.0 6 | 7 | 8 | 9 |

10 | 11 |

Eclipse Public License - v 1.0 12 |

13 | 14 |

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER 15 | THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, 16 | REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE 17 | OF THIS AGREEMENT.

18 | 19 |

1. DEFINITIONS

20 | 21 |

"Contribution" means:

22 | 23 |

a) 24 | in the case of the initial Contributor, the initial code and documentation 25 | distributed under this Agreement, and
26 | b) in the case of each subsequent Contributor:

27 | 28 |

i) 29 | changes to the Program, and

30 | 31 |

ii) 32 | additions to the Program;

33 | 34 |

where 35 | such changes and/or additions to the Program originate from and are distributed 36 | by that particular Contributor. A Contribution 'originates' from a Contributor 37 | if it was added to the Program by such Contributor itself or anyone acting on 38 | such Contributor's behalf. Contributions do not include additions to the 39 | Program which: (i) are separate modules of software distributed in conjunction 40 | with the Program under their own license agreement, and (ii) are not derivative 41 | works of the Program.

42 | 43 |

"Contributor" means any person or 44 | entity that distributes the Program.

45 | 46 |

"Licensed Patents " mean patent 47 | claims licensable by a Contributor which are necessarily infringed by the use 48 | or sale of its Contribution alone or when combined with the Program.

49 | 50 |

"Program" means the Contributions 51 | distributed in accordance with this Agreement.

52 | 53 |

"Recipient" means anyone who 54 | receives the Program under this Agreement, including all Contributors.

55 | 56 |

2. GRANT OF RIGHTS

57 | 58 |

a) 59 | Subject to the terms of this Agreement, each Contributor hereby grants Recipient 60 | a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly 61 | display, publicly perform, distribute and sublicense the Contribution of such 62 | Contributor, if any, and such derivative works, in source code and object code 63 | form.

64 | 65 |

b) 66 | Subject to the terms of this Agreement, each Contributor hereby grants 67 | Recipient a non-exclusive, worldwide, royalty-free 68 | patent license under Licensed Patents to make, use, sell, offer to sell, import 69 | and otherwise transfer the Contribution of such Contributor, if any, in source 70 | code and object code form. This patent license shall apply to the combination 71 | of the Contribution and the Program if, at the time the Contribution is added 72 | by the Contributor, such addition of the Contribution causes such combination 73 | to be covered by the Licensed Patents. The patent license shall not apply to 74 | any other combinations which include the Contribution. No hardware per se is 75 | licensed hereunder.

76 | 77 |

c) 78 | Recipient understands that although each Contributor grants the licenses to its 79 | Contributions set forth herein, no assurances are provided by any Contributor 80 | that the Program does not infringe the patent or other intellectual property 81 | rights of any other entity. Each Contributor disclaims any liability to Recipient 82 | for claims brought by any other entity based on infringement of intellectual 83 | property rights or otherwise. As a condition to exercising the rights and 84 | licenses granted hereunder, each Recipient hereby assumes sole responsibility 85 | to secure any other intellectual property rights needed, if any. For example, 86 | if a third party patent license is required to allow Recipient to distribute 87 | the Program, it is Recipient's responsibility to acquire that license before 88 | distributing the Program.

89 | 90 |

d) 91 | Each Contributor represents that to its knowledge it has sufficient copyright 92 | rights in its Contribution, if any, to grant the copyright license set forth in 93 | this Agreement.

94 | 95 |

3. REQUIREMENTS

96 | 97 |

A Contributor may choose to distribute the 98 | Program in object code form under its own license agreement, provided that: 99 |

100 | 101 |

a) 102 | it complies with the terms and conditions of this Agreement; and

103 | 104 |

b) 105 | its license agreement:

106 | 107 |

i) 108 | effectively disclaims on behalf of all Contributors all warranties and 109 | conditions, express and implied, including warranties or conditions of title 110 | and non-infringement, and implied warranties or conditions of merchantability 111 | and fitness for a particular purpose;

112 | 113 |

ii) 114 | effectively excludes on behalf of all Contributors all liability for damages, 115 | including direct, indirect, special, incidental and consequential damages, such 116 | as lost profits;

117 | 118 |

iii) 119 | states that any provisions which differ from this Agreement are offered by that 120 | Contributor alone and not by any other party; and

121 | 122 |

iv) 123 | states that source code for the Program is available from such Contributor, and 124 | informs licensees how to obtain it in a reasonable manner on or through a 125 | medium customarily used for software exchange.

126 | 127 |

When the Program is made available in source 128 | code form:

129 | 130 |

a) 131 | it must be made available under this Agreement; and

132 | 133 |

b) a 134 | copy of this Agreement must be included with each copy of the Program.

135 | 136 |

Contributors may not remove or alter any 137 | copyright notices contained within the Program.

138 | 139 |

Each Contributor must identify itself as the 140 | originator of its Contribution, if any, in a manner that reasonably allows 141 | subsequent Recipients to identify the originator of the Contribution.

142 | 143 |

4. COMMERCIAL DISTRIBUTION

144 | 145 |

Commercial distributors of software may 146 | accept certain responsibilities with respect to end users, business partners 147 | and the like. While this license is intended to facilitate the commercial use 148 | of the Program, the Contributor who includes the Program in a commercial 149 | product offering should do so in a manner which does not create potential 150 | liability for other Contributors. Therefore, if a Contributor includes the 151 | Program in a commercial product offering, such Contributor ("Commercial 152 | Contributor") hereby agrees to defend and indemnify every other 153 | Contributor ("Indemnified Contributor") against any losses, damages and 154 | costs (collectively "Losses") arising from claims, lawsuits and other 155 | legal actions brought by a third party against the Indemnified Contributor to 156 | the extent caused by the acts or omissions of such Commercial Contributor in 157 | connection with its distribution of the Program in a commercial product 158 | offering. The obligations in this section do not apply to any claims or Losses 159 | relating to any actual or alleged intellectual property infringement. In order 160 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 161 | Contributor in writing of such claim, and b) allow the Commercial Contributor 162 | to control, and cooperate with the Commercial Contributor in, the defense and 163 | any related settlement negotiations. The Indemnified Contributor may participate 164 | in any such claim at its own expense.

165 | 166 |

For example, a Contributor might include the 167 | Program in a commercial product offering, Product X. That Contributor is then a 168 | Commercial Contributor. If that Commercial Contributor then makes performance 169 | claims, or offers warranties related to Product X, those performance claims and 170 | warranties are such Commercial Contributor's responsibility alone. Under this 171 | section, the Commercial Contributor would have to defend claims against the 172 | other Contributors related to those performance claims and warranties, and if a 173 | court requires any other Contributor to pay any damages as a result, the 174 | Commercial Contributor must pay those damages.

175 | 176 |

5. NO WARRANTY

177 | 178 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS 179 | AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT 180 | WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, 181 | WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, 182 | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 183 | responsible for determining the appropriateness of using and distributing the 184 | Program and assumes all risks associated with its exercise of rights under this 185 | Agreement , including but not limited to the risks and costs of program errors, 186 | compliance with applicable laws, damage to or loss of data, programs or 187 | equipment, and unavailability or interruption of operations.

188 | 189 |

6. DISCLAIMER OF LIABILITY

190 | 191 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS 192 | AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR 193 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 194 | (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY 195 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 196 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF 197 | THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF 198 | THE POSSIBILITY OF SUCH DAMAGES.

199 | 200 |

7. GENERAL

201 | 202 |

If any provision of this Agreement is invalid 203 | or unenforceable under applicable law, it shall not affect the validity or 204 | enforceability of the remainder of the terms of this Agreement, and without 205 | further action by the parties hereto, such provision shall be reformed to the 206 | minimum extent necessary to make such provision valid and enforceable.

207 | 208 |

If Recipient institutes patent litigation 209 | against any entity (including a cross-claim or counterclaim in a lawsuit) 210 | alleging that the Program itself (excluding combinations of the Program with 211 | other software or hardware) infringes such Recipient's patent(s), then such 212 | Recipient's rights granted under Section 2(b) shall terminate as of the date 213 | such litigation is filed.

214 | 215 |

All Recipient's rights under this Agreement 216 | shall terminate if it fails to comply with any of the material terms or 217 | conditions of this Agreement and does not cure such failure in a reasonable 218 | period of time after becoming aware of such noncompliance. If all Recipient's 219 | rights under this Agreement terminate, Recipient agrees to cease use and 220 | distribution of the Program as soon as reasonably practicable. However, 221 | Recipient's obligations under this Agreement and any licenses granted by 222 | Recipient relating to the Program shall continue and survive.

223 | 224 |

Everyone is permitted to copy and distribute 225 | copies of this Agreement, but in order to avoid inconsistency the Agreement is 226 | copyrighted and may only be modified in the following manner. The Agreement 227 | Steward reserves the right to publish new versions (including revisions) of 228 | this Agreement from time to time. No one other than the Agreement Steward has 229 | the right to modify this Agreement. The Eclipse Foundation is the initial 230 | Agreement Steward. The Eclipse Foundation may assign the responsibility to 231 | serve as the Agreement Steward to a suitable separate entity. Each new version 232 | of the Agreement will be given a distinguishing version number. The Program 233 | (including Contributions) may always be distributed subject to the version of 234 | the Agreement under which it was received. In addition, after a new version of 235 | the Agreement is published, Contributor may elect to distribute the Program 236 | (including its Contributions) under the new version. Except as expressly stated 237 | in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to 238 | the intellectual property of any Contributor under this Agreement, whether 239 | expressly, by implication, estoppel or otherwise. All rights in the Program not 240 | expressly granted under this Agreement are reserved.

241 | 242 |

This Agreement is governed by the laws of the 243 | State of New York and the intellectual property laws of the United States of 244 | America. No party to this Agreement will bring a legal action under this 245 | Agreement more than one year after the cause of action arose. Each party waives 246 | its rights to a jury trial in any resulting litigation.

247 | 248 |
249 | 250 |
251 | 252 |

Apache License
253 | Version 2.0, January 2004
254 | http://www.apache.org/licenses/
255 |

256 |

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

257 |

1. Definitions.

258 |

259 | "License" shall mean the terms and conditions for use, reproduction, 260 | and distribution as defined by Sections 1 through 9 of this document.

261 |

262 | "Licensor" shall mean the copyright owner or entity authorized by 263 | the copyright owner that is granting the License.

264 |

265 | "Legal Entity" shall mean the union of the acting entity and all 266 | other entities that control, are controlled by, or are under common 267 | control with that entity. For the purposes of this definition, 268 | "control" means (i) the power, direct or indirect, to cause the 269 | direction or management of such entity, whether by contract or 270 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 271 | outstanding shares, or (iii) beneficial ownership of such entity.

272 |

273 | "You" (or "Your") shall mean an individual or Legal Entity 274 | exercising permissions granted by this License.

275 |

276 | "Source" form shall mean the preferred form for making modifications, 277 | including but not limited to software source code, documentation 278 | source, and configuration files.

279 |

280 | "Object" form shall mean any form resulting from mechanical 281 | transformation or translation of a Source form, including but 282 | not limited to compiled object code, generated documentation, 283 | and conversions to other media types.

284 |

285 | "Work" shall mean the work of authorship, whether in Source or 286 | Object form, made available under the License, as indicated by a 287 | copyright notice that is included in or attached to the work 288 | (an example is provided in the Appendix below).

289 |

290 | "Derivative Works" shall mean any work, whether in Source or Object 291 | form, that is based on (or derived from) the Work and for which the 292 | editorial revisions, annotations, elaborations, or other modifications 293 | represent, as a whole, an original work of authorship. For the purposes 294 | of this License, Derivative Works shall not include works that remain 295 | separable from, or merely link (or bind by name) to the interfaces of, 296 | the Work and Derivative Works thereof.

297 |

298 | "Contribution" shall mean any work of authorship, including 299 | the original version of the Work and any modifications or additions 300 | to that Work or Derivative Works thereof, that is intentionally 301 | submitted to Licensor for inclusion in the Work by the copyright owner 302 | or by an individual or Legal Entity authorized to submit on behalf of 303 | the copyright owner. For the purposes of this definition, "submitted" 304 | means any form of electronic, verbal, or written communication sent 305 | to the Licensor or its representatives, including but not limited to 306 | communication on electronic mailing lists, source code control systems, 307 | and issue tracking systems that are managed by, or on behalf of, the 308 | Licensor for the purpose of discussing and improving the Work, but 309 | excluding communication that is conspicuously marked or otherwise 310 | designated in writing by the copyright owner as "Not a Contribution."

311 |

312 | "Contributor" shall mean Licensor and any individual or Legal Entity 313 | on behalf of whom a Contribution has been received by Licensor and 314 | subsequently incorporated within the Work.

315 | 316 |

2. Grant of Copyright License.

317 |

318 | Subject to the terms and conditions of 319 | this License, each Contributor hereby grants to You a perpetual, 320 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 321 | copyright license to reproduce, prepare Derivative Works of, 322 | publicly display, publicly perform, sublicense, and distribute the 323 | Work and such Derivative Works in Source or Object form.

324 | 325 | 326 |

3. Grant of Patent License.

327 | 328 |

329 | Subject to the terms and conditions of 330 | this License, each Contributor hereby grants to You a perpetual, 331 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 332 | (except as stated in this section) patent license to make, have made, 333 | use, offer to sell, sell, import, and otherwise transfer the Work, 334 | where such license applies only to those patent claims licensable 335 | by such Contributor that are necessarily infringed by their 336 | Contribution(s) alone or by combination of their Contribution(s) 337 | with the Work to which such Contribution(s) was submitted. If You 338 | institute patent litigation against any entity (including a 339 | cross-claim or counterclaim in a lawsuit) alleging that the Work 340 | or a Contribution incorporated within the Work constitutes direct 341 | or contributory patent infringement, then any patent licenses 342 | granted to You under this License for that Work shall terminate 343 | as of the date such litigation is filed. 344 |

345 |

346 | 4. Redistribution. 347 |

348 | 349 |

You may reproduce and distribute copies of the 350 | Work or Derivative Works thereof in any medium, with or without 351 | modifications, and in Source or Object form, provided that You 352 | meet the following conditions:

353 |
    354 |
  • 355 | (a) You must give any other recipients of the Work or 356 | Derivative Works a copy of this License; and 357 |
  • 358 |
  • 359 | (b) You must cause any modified files to carry prominent notices 360 | stating that You changed the files; and 361 |
  • 362 |
  • 363 | (c) You must retain, in the Source form of any Derivative Works 364 | that You distribute, all copyright, patent, trademark, and 365 | attribution notices from the Source form of the Work, 366 | excluding those notices that do not pertain to any part of 367 | the Derivative Works; and 368 |
  • 369 |
  • 370 | (d) If the Work includes a "NOTICE" text file as part of its 371 | distribution, then any Derivative Works that You distribute must 372 | include a readable copy of the attribution notices contained 373 | within such NOTICE file, excluding those notices that do not 374 | pertain to any part of the Derivative Works, in at least one 375 | of the following places: within a NOTICE text file distributed 376 | as part of the Derivative Works; within the Source form or 377 | documentation, if provided along with the Derivative Works; or, 378 | within a display generated by the Derivative Works, if and 379 | wherever such third-party notices normally appear. The contents 380 | of the NOTICE file are for informational purposes only and 381 | do not modify the License. You may add Your own attribution 382 | notices within Derivative Works that You distribute, alongside 383 | or as an addendum to the NOTICE text from the Work, provided 384 | that such additional attribution notices cannot be construed 385 | as modifying the License. 386 |
  • 387 |
388 | 389 |

390 | You may add Your own copyright statement to Your modifications and 391 | may provide additional or different license terms and conditions 392 | for use, reproduction, or distribution of Your modifications, or 393 | for any such Derivative Works as a whole, provided Your use, 394 | reproduction, and distribution of the Work otherwise complies with 395 | the conditions stated in this License. 396 |

397 |

398 | 5. Submission of Contributions. 399 |

400 | 401 |

Unless You explicitly state otherwise, 402 | any Contribution intentionally submitted for inclusion in the Work 403 | by You to the Licensor shall be under the terms and conditions of 404 | this License, without any additional terms or conditions. 405 | Notwithstanding the above, nothing herein shall supersede or modify 406 | the terms of any separate license agreement you may have executed 407 | with Licensor regarding such Contributions. 408 |

409 |

410 | 6. Trademarks. 411 |

412 | 413 |

This License does not grant permission to use the trade 414 | names, trademarks, service marks, or product names of the Licensor, 415 | except as required for reasonable and customary use in describing the 416 | origin of the Work and reproducing the content of the NOTICE file. 417 |

418 |

419 | 7. Disclaimer of Warranty. 420 |

421 | 422 |

Unless required by applicable law or 423 | agreed to in writing, Licensor provides the Work (and each 424 | Contributor provides its Contributions) on an "AS IS" BASIS, 425 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 426 | implied, including, without limitation, any warranties or conditions 427 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 428 | PARTICULAR PURPOSE. You are solely responsible for determining the 429 | appropriateness of using or redistributing the Work and assume any 430 | risks associated with Your exercise of permissions under this License. 431 |

432 |

433 | 8. Limitation of Liability. 434 |

435 | 436 |

In no event and under no legal theory, 437 | whether in tort (including negligence), contract, or otherwise, 438 | unless required by applicable law (such as deliberate and grossly 439 | negligent acts) or agreed to in writing, shall any Contributor be 440 | liable to You for damages, including any direct, indirect, special, 441 | incidental, or consequential damages of any character arising as a 442 | result of this License or out of the use or inability to use the 443 | Work (including but not limited to damages for loss of goodwill, 444 | work stoppage, computer failure or malfunction, or any and all 445 | other commercial damages or losses), even if such Contributor 446 | has been advised of the possibility of such damages. 447 |

448 |

449 | 9. Accepting Warranty or Additional Liability. 450 |

451 |

While redistributing 452 | the Work or Derivative Works thereof, You may choose to offer, 453 | and charge a fee for, acceptance of support, warranty, indemnity, 454 | or other liability obligations and/or rights consistent with this 455 | License. However, in accepting such obligations, You may act only 456 | on Your own behalf and on Your sole responsibility, not on behalf 457 | of any other Contributor, and only if You agree to indemnify, 458 | defend, and hold each Contributor harmless for any liability 459 | incurred by, or claims asserted against, such Contributor by reason 460 | of your accepting any such warranty or additional liability. 461 |

462 | 463 |

464 | END OF TERMS AND CONDITIONS 465 |

466 | 467 |

468 | APPENDIX: How to apply the Apache License to your work. 469 |

470 | 471 |

472 | To apply the Apache License to your work, attach the following 473 | boilerplate notice, with the fields enclosed by brackets "[]" 474 | replaced with your own identifying information. (Don't include 475 | the brackets!) The text should be enclosed in the appropriate 476 | comment syntax for the file format. We also recommend that a 477 | file or class name and description of purpose be included on the 478 | same "printed page" as the copyright notice for easier 479 | identification within third-party archives. 480 |

481 | 482 |

483 | Copyright [yyyy] [name of copyright owner] 484 |

485 | 486 |

487 | Licensed under the Apache License, Version 2.0 (the "License"); 488 | you may not use this file except in compliance with the License. 489 | You may obtain a copy of the License at 490 |

491 | 492 |

493 | http://www.apache.org/licenses/LICENSE-2.0 494 |

495 | 496 |

497 | Unless required by applicable law or agreed to in writing, software 498 | distributed under the License is distributed on an "AS IS" BASIS, 499 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 500 | See the License for the specific language governing permissions and 501 | limitations under the License. 502 |

503 |
504 | 505 | 506 | --------------------------------------------------------------------------------