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 |
--------------------------------------------------------------------------------