This is an automated email from the ASF dual-hosted git repository. zregvart pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/master by this push: new c33950c CAMEL-13521: Add reverse proxy option in camel-... c33950c is described below commit c33950c8a4627aacc9d7c22621847583c12b4014 Author: Zoran Regvart <zregv...@apache.org> AuthorDate: Mon May 13 12:29:22 2019 +0200 CAMEL-13521: Add reverse proxy option in camel-... ...netty4-http This adds support for reverse proxy functionality in `camel-netty4-http` component. --- components/camel-netty4-http/pom.xml | 5 ++ .../src/main/docs/netty4-http-component.adoc | 37 +++++++++- .../netty4/http/DefaultNettyHttpBinding.java | 56 +++++++++++---- .../component/netty4/http/NettyHttpComponent.java | 24 ++++--- .../netty4/http/NettyHttpConfiguration.java | 6 +- .../http/handlers/HttpServerChannelHandler.java | 11 ++- .../HttpServerMultiplexChannelHandler.java | 12 ++++ .../component/netty4/http/ProxyProtocolTest.java | 84 ++++++++++++++++++++++ .../src/main/java/org/apache/camel/Exchange.java | 5 +- .../NettyHttpComponentConfiguration.java | 3 +- 10 files changed, 215 insertions(+), 28 deletions(-) diff --git a/components/camel-netty4-http/pom.xml b/components/camel-netty4-http/pom.xml index dd08d5b..f381f0a 100644 --- a/components/camel-netty4-http/pom.xml +++ b/components/camel-netty4-http/pom.xml @@ -82,6 +82,11 @@ <artifactId>junit</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> <!-- for testing rest-dsl --> <dependency> <groupId>org.apache.camel</groupId> diff --git a/components/camel-netty4-http/src/main/docs/netty4-http-component.adoc b/components/camel-netty4-http/src/main/docs/netty4-http-component.adoc index 91790d7..e2de392 100644 --- a/components/camel-netty4-http/src/main/docs/netty4-http-component.adoc +++ b/components/camel-netty4-http/src/main/docs/netty4-http-component.adoc @@ -131,7 +131,7 @@ with the following path and query parameters: [width="100%",cols="2,5,^1,2",options="header"] |=== | Name | Description | Default | Type -| *protocol* | *Required* The protocol to use which is either http or https | | String +| *protocol* | *Required* The protocol to use which is either http, https or proxy - a consumer only option. | | String | *host* | *Required* The local hostname such as localhost, or 0.0.0.0 when being a consumer. The remote HTTP server hostname when using producer. | | String | *port* | The host port number | | int | *path* | Resource path | | String @@ -391,7 +391,7 @@ ProducerTemplate as shown below: And we get back "Bye World" as the output. -=== How do I let Netty match wildcards +==== How do I let Netty match wildcards By default Netty4 HTTP will only match on exact uri's. But you can instruct Netty to match prefixes. For example @@ -422,7 +422,7 @@ To match *any* endpoint you can do: from("netty4-http:http://0.0.0.0:8123?matchOnUriPrefix=true").to("mock:foo"); ----------------------------------------------------------------------------- -=== Using multiple routes with same port +==== Using multiple routes with same port In the same CamelContext you can have multiple routes from Netty4 HTTP that shares the same port (eg a @@ -517,6 +517,37 @@ And in the routes you refer to this option as shown below See the Netty HTTP Server Example for more details and example how to do that. +==== Implementing a reverse proxy + +Netty HTTP component can act as a reverse proxy, in that case +`Exchange.HTTP_SCHEME`, `Exchange.HTTP_HOST` and +`Exchange.HTTP_PORT` headers are populated from the absolute +URL received on the request line of the HTTP request. + +Here's an example of a HTTP proxy that simply transforms the response +from the origin server to uppercase. + +[source,java] +------------------------------------------------------------------------------------------ +from("netty-http:proxy://0.0.0.0:8080") + .toD("netty-http:" + + "${headers." + Exchange.HTTP_SCHEME + "}://" + + "${headers." + Exchange.HTTP_HOST + "}:" + + "${headers." + Exchange.HTTP_PORT + "}") + .process(this::processResponse); + +void processResponse(final Exchange exchange) { + final NettyHttpMessage message = exchange.getIn(NettyHttpMessage.class); + final FullHttpResponse response = message.getHttpResponse(); + + final ByteBuf buf = response.content(); + final String string = buf.toString(StandardCharsets.UTF_8); + + buf.resetWriterIndex(); + ByteBufUtil.writeUtf8(buf, string.toUpperCase(Locale.US)); +} +------------------------------------------------------------------------------------------ + === Using HTTP Basic Authentication The Netty HTTP consumer supports HTTP basic authentication by specifying diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/DefaultNettyHttpBinding.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/DefaultNettyHttpBinding.java index 630eae1..a626c0e 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/DefaultNettyHttpBinding.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/DefaultNettyHttpBinding.java @@ -41,9 +41,9 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; + import org.apache.camel.Exchange; import org.apache.camel.Message; -import org.apache.camel.NoTypeConversionAvailableException; import org.apache.camel.RuntimeCamelException; import org.apache.camel.TypeConverter; import org.apache.camel.component.netty4.NettyConstants; @@ -91,8 +91,9 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { populateCamelHeaders(request, answer.getHeaders(), exchange, configuration); } - if (configuration.isDisableStreamCache()) { + if (configuration.isHttpProxy() || configuration.isDisableStreamCache()) { // keep the body as is, and use type converters + // for proxy use case pass the request body buffer directly to the response to avoid additional processing answer.setBody(request.content()); } else { // turn the body into stream cached (on the client/consumer side we can facade the netty stream instead of converting to byte array) @@ -136,6 +137,10 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { headers.put(Exchange.HTTP_URI, uri.getPath()); headers.put(Exchange.HTTP_QUERY, uri.getQuery()); headers.put(Exchange.HTTP_RAW_QUERY, uri.getRawQuery()); + headers.put(Exchange.HTTP_SCHEME, uri.getScheme()); + headers.put(Exchange.HTTP_HOST, uri.getHost()); + final int port = uri.getPort(); + headers.put(Exchange.HTTP_PORT, port > 0 ? port : 80); // strip the starting endpoint path so the path is relative to the endpoint uri String path = uri.getRawPath(); @@ -270,7 +275,7 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { populateCamelHeaders(response, answer.getHeaders(), exchange, configuration); } - if (configuration.isDisableStreamCache()) { + if (configuration.isDisableStreamCache() || configuration.isHttpProxy()) { // keep the body as is, and use type converters answer.setBody(response.content()); } else { @@ -316,6 +321,15 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { public HttpResponse toNettyResponse(Message message, NettyHttpConfiguration configuration) throws Exception { LOG.trace("toNettyResponse: {}", message); + if (message instanceof NettyHttpMessage) { + final NettyHttpMessage nettyHttpMessage = (NettyHttpMessage) message; + final FullHttpResponse response = nettyHttpMessage.getHttpResponse(); + + if (response != null) { + return response.retain(); + } + } + // the message body may already be a Netty HTTP response if (message.getBody() instanceof HttpResponse) { return (HttpResponse) message.getBody(); @@ -459,8 +473,9 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { public HttpRequest toNettyRequest(Message message, String fullUri, NettyHttpConfiguration configuration) throws Exception { LOG.trace("toNettyRequest: {}", message); + Object body = message.getBody(); // the message body may already be a Netty HTTP response - if (message.getBody() instanceof HttpRequest) { + if (body instanceof HttpRequest) { return (HttpRequest) message.getBody(); } @@ -477,10 +492,24 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { } } - // just assume GET for now, we will later change that to the actual method to use - HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriForRequest); + final String headerProtocolVersion = message.getHeader(Exchange.HTTP_PROTOCOL_VERSION, String.class); + final HttpVersion protocol; + if (headerProtocolVersion == null) { + protocol = HttpVersion.HTTP_1_1; + } else { + protocol = HttpVersion.valueOf(headerProtocolVersion); + } - Object body = message.getBody(); + final String headerMethod = message.getHeader(Exchange.HTTP_METHOD, String.class); + + final HttpMethod httpMethod; + if (headerMethod == null) { + httpMethod = HttpMethod.GET; + } else { + httpMethod = HttpMethod.valueOf(headerMethod); + } + + FullHttpRequest request = new DefaultFullHttpRequest(protocol, httpMethod, uriForRequest); if (body != null) { // support bodies as native Netty ByteBuf buffer; @@ -492,18 +521,19 @@ public class DefaultNettyHttpBinding implements NettyHttpBinding, Cloneable { if (buffer == null) { // fallback to byte array as last resort byte[] data = message.getMandatoryBody(byte[].class); - buffer = NettyConverter.toByteBuffer(data); + + if (data.length > 0) { + buffer = NettyConverter.toByteBuffer(data); + } } } - if (buffer != null) { - request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uriForRequest, buffer); + + if (buffer != null && buffer.readableBytes() > 0) { + request = request.replace(buffer); int len = buffer.readableBytes(); // set content-length request.headers().set(HttpHeaderNames.CONTENT_LENGTH.toString(), len); LOG.trace("Content-Length: {}", len); - } else { - // we do not support this kind of body - throw new NoTypeConversionAvailableException(body, ByteBuf.class); } } diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpComponent.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpComponent.java index 8c9d023..4063478 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpComponent.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpComponent.java @@ -77,7 +77,7 @@ public class NettyHttpComponent extends NettyComponent implements HeaderFilterSt @Override protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { - NettyConfiguration config; + NettyHttpConfiguration config; if (getConfiguration() != null) { config = getConfiguration().copy(); } else { @@ -111,13 +111,14 @@ public class NettyHttpComponent extends NettyComponent implements HeaderFilterSt } // we must include the protocol in the remaining - boolean hasProtocol = remaining.startsWith("http://") || remaining.startsWith("http:") - || remaining.startsWith("https://") || remaining.startsWith("https:"); + boolean hasProtocol = remaining != null && (remaining.startsWith("http://") || remaining.startsWith("http:") + || remaining.startsWith("https://") || remaining.startsWith("https:") + || remaining.startsWith("proxy://") || remaining.startsWith("proxy:")); if (!hasProtocol) { // http is the default protocol remaining = "http://" + remaining; } - boolean hasSlash = remaining.startsWith("http://") || remaining.startsWith("https://"); + boolean hasSlash = remaining.startsWith("http://") || remaining.startsWith("https://") || remaining.startsWith("proxy://"); if (!hasSlash) { // must have double slash after protocol if (remaining.startsWith("http:")) { @@ -136,6 +137,8 @@ public class NettyHttpComponent extends NettyComponent implements HeaderFilterSt config.setPort(80); } else if (remaining.startsWith("https:")) { config.setPort(443); + } else if (remaining.startsWith("proxy:")) { + config.setPort(3128); // homage to Squid proxy } } if (config.getPort() == -1) { @@ -207,20 +210,25 @@ public class NettyHttpComponent extends NettyComponent implements HeaderFilterSt } @Override - protected NettyConfiguration parseConfiguration(NettyConfiguration configuration, String remaining, Map<String, Object> parameters) throws Exception { + protected NettyHttpConfiguration parseConfiguration(NettyConfiguration configuration, String remaining, Map<String, Object> parameters) throws Exception { // ensure uri is encoded to be valid String safe = UnsafeUriCharactersEncoder.encodeHttpURI(remaining); URI uri = new URI(safe); - configuration.parseURI(uri, parameters, this, "http", "https"); + configuration.parseURI(uri, parameters, this, "http", "https", "proxy"); // force using tcp as the underlying transport configuration.setProtocol("tcp"); configuration.setTextline(false); if (configuration instanceof NettyHttpConfiguration) { - ((NettyHttpConfiguration) configuration).setPath(uri.getPath()); + final NettyHttpConfiguration httpConfiguration = (NettyHttpConfiguration) configuration; + + httpConfiguration.setPath(uri.getPath()); + + return httpConfiguration; } - return configuration; + + throw new IllegalStateException("Received NettyConfiguration instead of expected NettyHttpConfiguration, this is not supported."); } public NettyHttpBinding getNettyHttpBinding() { diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpConfiguration.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpConfiguration.java index 11ec3f4..77be730 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpConfiguration.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/NettyHttpConfiguration.java @@ -97,7 +97,7 @@ public class NettyHttpConfiguration extends NettyConfiguration { } /** - * The protocol to use which is either http or https + * The protocol to use which is either http, https or proxy - a consumer only option. */ public void setProtocol(String protocol) { this.protocol = protocol; @@ -318,4 +318,8 @@ public class NettyHttpConfiguration extends NettyConfiguration { public boolean isUseRelativePath() { return this.useRelativePath; } + + public boolean isHttpProxy() { + return "proxy".equals(super.protocol); + } } diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerChannelHandler.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerChannelHandler.java index b852322..211d0c2 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerChannelHandler.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerChannelHandler.java @@ -35,10 +35,12 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpUtil; import org.apache.camel.Exchange; import org.apache.camel.LoggingLevel; +import org.apache.camel.Message; import org.apache.camel.component.netty4.NettyConverter; import org.apache.camel.component.netty4.NettyHelper; import org.apache.camel.component.netty4.handlers.ServerChannelHandler; import org.apache.camel.component.netty4.http.HttpPrincipal; +import org.apache.camel.component.netty4.http.NettyHttpConfiguration; import org.apache.camel.component.netty4.http.NettyHttpConsumer; import org.apache.camel.component.netty4.http.NettyHttpSecurityConfiguration; import org.apache.camel.component.netty4.http.SecurityAuthenticator; @@ -264,7 +266,9 @@ public class HttpServerChannelHandler extends ServerChannelHandler { @Override protected void beforeProcess(Exchange exchange, final ChannelHandlerContext ctx, final Object message) { - if (consumer.getConfiguration().isBridgeEndpoint()) { + final NettyHttpConfiguration configuration = consumer.getConfiguration(); + + if (configuration.isBridgeEndpoint()) { exchange.setProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.TRUE); exchange.setProperty(Exchange.SKIP_WWW_FORM_URLENCODED, Boolean.TRUE); } @@ -275,6 +279,11 @@ public class HttpServerChannelHandler extends ServerChannelHandler { // Just make sure we close the connection this time. exchange.setProperty(HttpHeaderNames.CONNECTION.toString(), HttpHeaderValues.CLOSE.toString()); } + + final Message in = exchange.getIn(); + if (configuration.isHttpProxy()) { + in.removeHeader("Proxy-Connection"); + } } @Override diff --git a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java index 0d48fee..0722fa8 100644 --- a/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java +++ b/components/camel-netty4-http/src/main/java/org/apache/camel/component/netty4/http/handlers/HttpServerMultiplexChannelHandler.java @@ -36,6 +36,7 @@ import io.netty.util.Attribute; import io.netty.util.AttributeKey; import org.apache.camel.Exchange; import org.apache.camel.component.netty4.http.HttpServerConsumerChannelFactory; +import org.apache.camel.component.netty4.http.NettyHttpConfiguration; import org.apache.camel.component.netty4.http.NettyHttpConsumer; import org.apache.camel.http.common.CamelServlet; import org.apache.camel.support.RestConsumerContextPathMatcher; @@ -193,6 +194,16 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelInboundHandl private HttpServerChannelHandler getHandler(HttpRequest request, String method) { HttpServerChannelHandler answer = null; + // quick path to find if there are handlers with HTTP proxy consumers + for (final HttpServerChannelHandler handler : consumers) { + NettyHttpConsumer consumer = handler.getConsumer(); + + final NettyHttpConfiguration configuration = consumer.getConfiguration(); + if (configuration.isHttpProxy()) { + return handler; + } + } + // need to strip out host and port etc, as we only need the context-path for matching if (method == null) { return null; @@ -222,6 +233,7 @@ public class HttpServerMultiplexChannelHandler extends SimpleChannelInboundHandl if (answer == null) { for (final HttpServerChannelHandler handler : consumers) { NettyHttpConsumer consumer = handler.getConsumer(); + String consumerPath = consumer.getConfiguration().getPath(); boolean matchOnUriPrefix = consumer.getEndpoint().getConfiguration().isMatchOnUriPrefix(); // Just make sure the we get the right consumer path first diff --git a/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/ProxyProtocolTest.java b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/ProxyProtocolTest.java new file mode 100644 index 0000000..a4caf17 --- /dev/null +++ b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/ProxyProtocolTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.netty4.http; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.test.AvailablePortFinder; +import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProxyProtocolTest { + + private final DefaultCamelContext context = new DefaultCamelContext(); + + private final int port = AvailablePortFinder.getNextAvailable(); + + public ProxyProtocolTest() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() throws Exception { + final int originPort = AvailablePortFinder.getNextAvailable(); + + // proxy from http://localhost:port to + // http://localhost:originPort/path + from("netty-http:proxy://localhost:" + port) + .to("netty-http:http://localhost:" + originPort); + + // origin service that serves `"origin server"` on + // http://localhost:originPort/path + from("netty-http:http://localhost:" + originPort + "/path").setBody() + .constant("origin server"); + } + }); + context.start(); + } + + @Test + public void shouldProvideProxyProtocolSupport() { + final NettyHttpEndpoint endpoint = context.getEndpoint("netty-http:proxy://localhost", NettyHttpEndpoint.class); + + assertThat(endpoint.getConfiguration().isHttpProxy()).isTrue(); + } + + @Test + public void shouldServeAsHttpProxy() throws Exception { + final Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", port)); + + // request for http://test/path will be proxied by http://localhost:port + // and diverted to http://localhost:originPort/path + final HttpURLConnection connection = (HttpURLConnection) new URL("http://test/path").openConnection(proxy); + + try (InputStream stream = connection.getInputStream()) { + assertThat(IOUtils.readLines(stream, StandardCharsets.UTF_8)).containsOnly("origin server"); + } + } + + @After + public void shutdownCamel() throws Exception { + context.stop(); + } +} diff --git a/core/camel-api/src/main/java/org/apache/camel/Exchange.java b/core/camel-api/src/main/java/org/apache/camel/Exchange.java index 0606959..ad38c87 100644 --- a/core/camel-api/src/main/java/org/apache/camel/Exchange.java +++ b/core/camel-api/src/main/java/org/apache/camel/Exchange.java @@ -141,7 +141,10 @@ public interface Exchange { String FILTER_NON_XML_CHARS = "CamelFilterNonXmlChars"; String GROUPED_EXCHANGE = "CamelGroupedExchange"; - + + String HTTP_SCHEME = "CamelHttpScheme"; + String HTTP_HOST = "CamelHttpHost"; + String HTTP_PORT = "CamelHttpPort"; String HTTP_BASE_URI = "CamelHttpBaseUri"; String HTTP_CHARACTER_ENCODING = "CamelHttpCharacterEncoding"; String HTTP_METHOD = "CamelHttpMethod"; diff --git a/platforms/spring-boot/components-starter/camel-netty4-http-starter/src/main/java/org/apache/camel/component/netty4/http/springboot/NettyHttpComponentConfiguration.java b/platforms/spring-boot/components-starter/camel-netty4-http-starter/src/main/java/org/apache/camel/component/netty4/http/springboot/NettyHttpComponentConfiguration.java index 0848e9f..d8e7fcd 100644 --- a/platforms/spring-boot/components-starter/camel-netty4-http-starter/src/main/java/org/apache/camel/component/netty4/http/springboot/NettyHttpComponentConfiguration.java +++ b/platforms/spring-boot/components-starter/camel-netty4-http-starter/src/main/java/org/apache/camel/component/netty4/http/springboot/NettyHttpComponentConfiguration.java @@ -165,7 +165,8 @@ public class NettyHttpComponentConfiguration public static class NettyHttpConfigurationNestedConfiguration { public static final Class CAMEL_NESTED_CLASS = org.apache.camel.component.netty4.http.NettyHttpConfiguration.class; /** - * The protocol to use which is either http or https + * The protocol to use which is either http, https or proxy - a consumer + * only option. */ private String protocol; /**