Skip to content

Commit

Permalink
Adding a new endpoint property to the HttpCommonEndpoint which allows…
Browse files Browse the repository at this point in the history
… preserving the Host header in reverse proxy applications, this class is ued by the Http, Http4, and Jetty producers.

Updated the HttpProducer (Jetty/HTTP4) to set the Host header when this flag is enabled.  The older HTTP component does not readily let us override the Host header, this component will not support this parameter
Updated the Integration Test to validate the behavior for both when the new parameter is set, and unset.
  • Loading branch information
slim-bean authored and oscerd committed Apr 8, 2016
1 parent 8ebe5d6 commit 1749f36
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 10 deletions.
Expand Up @@ -46,6 +46,11 @@ public abstract class HttpCommonEndpoint extends DefaultEndpoint implements Head
description = "If the option is true, HttpProducer will ignore the Exchange.HTTP_URI header, and use the endpoint's URI for request."
+ " You may also set the option throwExceptionOnFailure to be false to let the HttpProducer send all the fault response back.")
boolean bridgeEndpoint;
@UriParam(label = "producer",
description = "If the option is true, HttpProducer will set the Host header to the value contained in the current exchange Host header, " +
"useful in reverse proxy applications where you want the Host header received by the downstream server to reflect the URL called by the upstream client, " +
"this allows applications which use the Host header to generate accurate URL's for a proxied service")
boolean preserveHostHeader;
@UriParam(label = "consumer",
description = "Whether or not the consumer should try to find a target consumer by matching the URI prefix if no exact match is found.")
boolean matchOnUriPrefix;
Expand Down Expand Up @@ -246,6 +251,19 @@ public void setBridgeEndpoint(boolean bridge) {
this.bridgeEndpoint = bridge;
}

public boolean isPreserveHostHeader() {
return preserveHostHeader;
}

/**
* If the option is true, HttpProducer will set the Host header to the value contained in the current exchange Host header,
* useful in reverse proxy applications where you want the Host header received by the downstream server to reflect the URL called by the upstream client,
* this allows applications which use the Host header to generate accurate URL's for a proxied service
*/
public void setPreserveHostHeader(boolean preserveHostHeader) {
this.preserveHostHeader = preserveHostHeader;
}

public boolean isMatchOnUriPrefix() {
return matchOnUriPrefix;
}
Expand Down
Expand Up @@ -106,8 +106,6 @@ public void process(Exchange exchange) throws Exception {
if (queryString != null) {
skipRequestHeaders = URISupport.parseQuery(queryString, false, true);
}
// Need to remove the Host key as it should be not used
exchange.getIn().getHeaders().remove("host");
}
HttpRequestBase httpRequest = createMethod(exchange);
Message in = exchange.getIn();
Expand Down Expand Up @@ -156,6 +154,16 @@ public void process(Exchange exchange) throws Exception {
}
}

//In reverse proxy applications it can be desirable for the downstream service to see the original Host header
//if this option is set, and the exchange Host header is not null, we will set it's current value on the httpRequest
if (getEndpoint().isPreserveHostHeader()) {
String hostHeader = exchange.getIn().getHeader("Host", String.class);
if (hostHeader != null) {
//HttpClient 4 will check to see if the Host header is present, and use it if it is, see org.apache.http.protocol.RequestTargetHost in httpcore
httpRequest.setHeader("Host", hostHeader);
}
}

// lets store the result in the output message.
HttpResponse httpResponse = null;
try {
Expand Down
Expand Up @@ -185,8 +185,6 @@ private void processInternal(Exchange exchange, AsyncCallback callback) throws E
if (queryString != null) {
skipRequestHeaders = URISupport.parseQuery(queryString, false, true);
}
// Need to remove the Host key as it should be not used
exchange.getIn().getHeaders().remove("host");
}

// propagate headers as HTTP headers
Expand Down Expand Up @@ -228,6 +226,16 @@ private void processInternal(Exchange exchange, AsyncCallback callback) throws E
}
}

//In reverse proxy applications it can be desirable for the downstream service to see the original Host header
//if this option is set, and the exchange Host header is not null, we will set it's current value on the httpExchange
if (getEndpoint().isPreserveHostHeader()) {
String hostHeader = exchange.getIn().getHeader("Host", String.class);
if (hostHeader != null) {
//HttpClient 4 will check to see if the Host header is present, and use it if it is, see org.apache.http.protocol.RequestTargetHost in httpcore
httpExchange.addRequestHeader("Host", hostHeader);
}
}

// set the callback, which will handle all the response logic
if (LOG.isDebugEnabled()) {
LOG.debug("Sending HTTP request to: {}", httpExchange.getUrl());
Expand Down
Expand Up @@ -28,13 +28,21 @@ public class JettyBridgeHostHeaderIssueTest extends CamelTestSupport {
private int port;
private int port2;
private int port3;
private int port4;
private int port5;
private String receivedHostHeaderEndpoint1;
private String receivedHostHeaderEndpoint2;
private String receivedHostHeaderEndpoint3;
private String receivedHostHeaderEndpoint4;

@Test
public void testHostHeader() throws Exception {
// TODO: the host header is removed in bridgeEndpoint in the http4 producer, that seems wrong
// as Camel as a reverse-proxy should update the host header accordingly

//The first two calls will test http4 producers

//The first call to our service will hit the first destination in the round robin load balancer
//this destination has the preserveProxyHeader parameter set to true, so we verify the Host header
//received by our downstream instance matches the address and port of the proxied service
Exchange reply = template.request("http4:localhost:" + port + "/myapp", new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
Expand All @@ -43,7 +51,12 @@ public void process(Exchange exchange) throws Exception {
});
assertNotNull(reply);
assertEquals("foo", reply.getOut().getBody(String.class));
//assert the received Host header is localhost:port (where port matches the /myapp port)
assertEquals("localhost:" + port, receivedHostHeaderEndpoint1);

//The second call to our service will hit the second destination in the round robin load balancer
//this destination does not have the preserveProxyHeader, so we expect the Host header received by the destination
//to match the url of the destination service itself
Exchange reply2 = template.request("http4:localhost:" + port + "/myapp", new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
Expand All @@ -52,25 +65,90 @@ public void process(Exchange exchange) throws Exception {
});
assertNotNull(reply2);
assertEquals("bar", reply2.getOut().getBody(String.class));
//assert the received Host header is localhost:port3 (where port3 matches the /bar destination server)
assertEquals("localhost:" + port3, receivedHostHeaderEndpoint2);


//The next two calls will use/test the jetty producers in the round robin load balancer

//The first has the preserveHostHeader option set to true, so we would expect to receive a Host header matching the /myapp proxied service
Exchange reply3 = template.request("http4:localhost:" + port + "/myapp", new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
exchange.getIn().setBody("Bye JWorld");
}
});
assertNotNull(reply3);
assertEquals("jbar", reply3.getOut().getBody(String.class));
//assert the received Host header is localhost:port (where port matches the /myapp destination server)
assertEquals("localhost:" + port, receivedHostHeaderEndpoint3);

//The second does not have a preserveHostHeader (preserveHostHeader=false), we would expect to see a Host header matching the destination service
Exchange reply4 = template.request("http4:localhost:" + port + "/myapp", new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
exchange.getIn().setBody("JAVA!!!!");
}
});
assertNotNull(reply4);
assertEquals("java???", reply4.getOut().getBody(String.class));
//assert the received Host header is localhost:port5 (where port3 matches the /jbarf destination server)
assertEquals("localhost:" + port5, receivedHostHeaderEndpoint4);
}

@Override
protected RouteBuilder createRouteBuilder() throws Exception {
port = AvailablePortFinder.getNextAvailable(12000);
port2 = AvailablePortFinder.getNextAvailable(12100);
port3 = AvailablePortFinder.getNextAvailable(12200);
port4 = AvailablePortFinder.getNextAvailable(12300);
port5 = AvailablePortFinder.getNextAvailable(12400);

return new RouteBuilder() {
@Override
public void configure() throws Exception {
from("jetty:http://localhost:" + port + "/myapp?matchOnUriPrefix=true")
.loadBalance().roundRobin()
.to("http4://localhost:" + port2 + "/foo?bridgeEndpoint=true&throwExceptionOnFailure=false")
.to("http4://localhost:" + port3 + "/bar?bridgeEndpoint=true&throwExceptionOnFailure=false");
.to("http4://localhost:" + port2 + "/foo?bridgeEndpoint=true&throwExceptionOnFailure=false&preserveHostHeader=true")
.to("http4://localhost:" + port3 + "/bar?bridgeEndpoint=true&throwExceptionOnFailure=false")
.to("jetty:http://localhost:" + port4 + "/jbar?bridgeEndpoint=true&throwExceptionOnFailure=false&preserveHostHeader=true")
.to("jetty:http://localhost:" + port5 + "/jbarf?bridgeEndpoint=true&throwExceptionOnFailure=false");

from("jetty:http://localhost:" + port2 + "/foo")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
receivedHostHeaderEndpoint1 = exchange.getIn().getHeader("Host", String.class);
}
})
.transform().constant("foo");

from("jetty:http://localhost:" + port3 + "/bar")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
receivedHostHeaderEndpoint2 = exchange.getIn().getHeader("Host", String.class);
}
})
.transform().constant("bar");

from("jetty:http://localhost:" + port2 + "/foo").transform().constant("foo");
from("jetty:http://localhost:" + port4 + "/jbar")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
receivedHostHeaderEndpoint3 = exchange.getIn().getHeader("Host", String.class);
}
})
.transform().constant("jbar");

from("jetty:http://localhost:" + port3 + "/bar").transform().constant("bar");
from("jetty:http://localhost:" + port5 + "/jbarf")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
receivedHostHeaderEndpoint4 = exchange.getIn().getHeader("Host", String.class);
}
})
.transform().constant("java???");
}
};
}
Expand Down

0 comments on commit 1749f36

Please sign in to comment.