Skip to content

Commit

Permalink
WFLY-10084 Add support for the proxy protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Apr 12, 2018
1 parent 2b46fb3 commit ab48d04
Show file tree
Hide file tree
Showing 21 changed files with 224 additions and 83 deletions.
Expand Up @@ -51,8 +51,8 @@ public class UndertowConnectorTestCase {
public void getType() {
OptionMap options = OptionMap.builder().getMap();
assertSame(Connector.Type.AJP, new UndertowConnector(new AjpListenerService("", "", options, OptionMap.EMPTY)).getType());
assertSame(Connector.Type.HTTP, new UndertowConnector(new HttpListenerService("", "", options, OptionMap.EMPTY, false, false)).getType());
assertSame(Connector.Type.HTTPS, new UndertowConnector(new HttpsListenerService("", "", options, null, OptionMap.EMPTY)).getType());
assertSame(Connector.Type.HTTP, new UndertowConnector(new HttpListenerService("", "", options, OptionMap.EMPTY, false, false, false)).getType());
assertSame(Connector.Type.HTTPS, new UndertowConnector(new HttpsListenerService("", "", options, null, OptionMap.EMPTY, false)).getType());
}

@Test
Expand Down
Expand Up @@ -44,7 +44,7 @@ public class UndertowEngineTestCase {
private final String hostName = "default-host";
private final String route = "route";
private final Host host = new Host(this.hostName, Collections.emptyList(), "ROOT.war");
private final HttpsListenerService listener = new HttpsListenerService("default", "https", OptionMap.EMPTY, null, OptionMap.EMPTY);
private final HttpsListenerService listener = new HttpsListenerService("default", "https", OptionMap.EMPTY, null, OptionMap.EMPTY, false);
private final UndertowService service = new TestUndertowService("default-container", this.serverName, this.hostName, this.route, this.server);
private final Server server = new TestServer(this.serverName, this.hostName, this.service, this.host, this.listener);
private final Connector connector = mock(Connector.class);
Expand Down
Expand Up @@ -26,11 +26,14 @@
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATION_HEADERS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;
import static org.jboss.as.test.integration.management.util.ModelUtil.createOpNode;
import static org.jboss.as.test.integration.security.common.SSLTruststoreUtil.HTTPS_PORT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.net.Socket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -61,10 +64,13 @@
import org.jboss.dmr.ModelNode;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import io.undertow.util.FileUtils;

/**
* @author Dominik Pospisil <dpospisi@redhat.com>
*/
Expand All @@ -83,8 +89,8 @@ public class ListenerTestCase extends ContainerResourceMgmtTestBase {

@Deployment
public static Archive<?> getDeployment() {
JavaArchive ja = ShrinkWrap.create(JavaArchive.class, "dummy.jar");
ja.addClass(ListenerTestCase.class);
WebArchive ja = ShrinkWrap.create(WebArchive.class, "proxy.war");
ja.addClasses(ListenerTestCase.class, RemoteIpServlet.class);
return ja;
}

Expand Down Expand Up @@ -127,8 +133,6 @@ public void testHttpsConnector() throws Exception {
} finally {
removeListener(Listener.HTTPS);
}


}

@Test
Expand All @@ -141,26 +145,26 @@ public void testAjpConnector() throws Exception {
public void testAddAndRemoveRollbacks() throws Exception {

// execute and rollback add socket
ModelNode addSocketOp = getAddSocketBindingOp(Listener.HTTPJIO);
ModelNode addSocketOp = getAddSocketBindingOp(Listener.HTTP);
ModelNode ret = executeAndRollbackOperation(addSocketOp);
assertTrue("failed".equals(ret.get("outcome").asString()));

// add socket again
executeOperation(addSocketOp);

// execute and rollback add connector
ModelNode addConnectorOp = getAddListenerOp(Listener.HTTPJIO);
ModelNode addConnectorOp = getAddListenerOp(Listener.HTTP, false);
ret = executeAndRollbackOperation(addConnectorOp);
assertTrue("failed".equals(ret.get("outcome").asString()));

// add connector again
executeOperation(addConnectorOp);

// check it is listed
assertTrue(getListenerList().get("http").contains("test-" + Listener.HTTPJIO.getName() + "-listener"));
assertTrue(getListenerList().get("http").contains("test-" + Listener.HTTP.getName() + "-listener"));

// execute and rollback remove connector
ModelNode removeConnOp = getRemoveConnectorOp(Listener.HTTPJIO);
ModelNode removeConnOp = getRemoveConnectorOp(Listener.HTTP);
ret = executeAndRollbackOperation(removeConnOp);
assertEquals("failed", ret.get("outcome").asString());

Expand All @@ -174,22 +178,53 @@ public void testAddAndRemoveRollbacks() throws Exception {
assertFalse("Connector not removed.", WebUtil.testHttpURL(cURL));

// execute and rollback remove socket binding
ModelNode removeSocketOp = getRemoveSocketBindingOp(Listener.HTTPJIO);
ModelNode removeSocketOp = getRemoveSocketBindingOp(Listener.HTTP);
ret = executeAndRollbackOperation(removeSocketOp);
assertEquals("failed", ret.get("outcome").asString());

// execute remove socket again
executeOperation(removeSocketOp);
}

@Test
public void testProxyProtocolOverHTTP() throws Exception {
addListener(Listener.HTTP, true);
try (Socket s = new Socket(url.getHost(), 8181)) {
s.getOutputStream().write("PROXY TCP4 1.2.3.4 5.6.7.8 444 555\r\nGET /proxy/addr HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
String result = FileUtils.readFile(s.getInputStream());
Assert.assertTrue(result, result.contains("result:1.2.3.4:444 5.6.7.8:555"));
} finally {
removeListener(Listener.HTTP);
}
}


@Test
public void testProxyProtocolOverHTTPS() throws Exception {
addListener(Listener.HTTPS, true);
try (Socket s = new Socket(url.getHost(), 8181)) {
s.getOutputStream().write("PROXY TCP4 1.2.3.4 5.6.7.8 444 555\r\n".getBytes(StandardCharsets.US_ASCII));
Socket ssl = TestHttpClientUtils.getSslContext().getSocketFactory().createSocket(s, url.getHost(), HTTPS_PORT, true);
ssl.getOutputStream().write("GET /proxy/addr HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
String result = FileUtils.readFile(ssl.getInputStream());
Assert.assertTrue(result, result.contains("result:1.2.3.4:444 5.6.7.8:555"));
} finally {
removeListener(Listener.HTTPS);
}
}

private void addListener(Listener conn) throws Exception {
addListener(conn, false);
}

private void addListener(Listener conn, boolean proxyProtocol) throws Exception {

// add socket binding
ModelNode op = getAddSocketBindingOp(conn);
executeOperation(op);

// add connector
op = getAddListenerOp(conn);
op = getAddListenerOp(conn, proxyProtocol);
executeOperation(op);

// check it is listed
Expand All @@ -202,11 +237,14 @@ private ModelNode getAddSocketBindingOp(Listener conn) {
return op;
}

private ModelNode getAddListenerOp(Listener conn) {
private ModelNode getAddListenerOp(Listener conn, boolean proxyProtocol) {
final ModelNode composite = Util.getEmptyOperation(COMPOSITE, new ModelNode());
final ModelNode steps = composite.get(STEPS);
ModelNode op = createOpNode("subsystem=undertow/server=default-server/" + conn.getScheme() + "-listener=test-" + conn.getName() + "-listener", "add");
op.get("socket-binding").set("test-" + conn.getName() + socketBindingCount);
if(proxyProtocol) {
op.get("proxy-protocol").set(true);
}
if (conn.isSecure()) {
op.get("security-realm").set("ssl-realm");
}
Expand Down
@@ -0,0 +1,39 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2018, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.test.integration.management.api.web;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "/addr")
public class RemoteIpServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("result:" + req.getRemoteAddr() + ":" + req.getRemotePort() + " " + req.getLocalAddr() + ":" + req.getLocalPort());
}
}
Expand Up @@ -22,6 +22,8 @@

package org.jboss.as.test.http.util;

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -75,22 +77,7 @@ public class TestHttpClientUtils {
*/
public static CloseableHttpClient getHttpsClient(CredentialsProvider credentialsProvider) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {

public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);

ctx.init(null, new TrustManager[]{tm}, null);
SSLContext ctx = getSslContext();

SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(ctx, new NoopHostnameVerifier());

Expand All @@ -113,6 +100,26 @@ public X509Certificate[] getAcceptedIssuers() {
}
}

public static SSLContext getSslContext() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {

public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);

ctx.init(null, new TrustManager[]{tm}, null);
return ctx;
}

/**
* Creates a http client that sends cookies to every domain, not just the originating domain.
* As we don't actually have a load balancer for the clustering tests, we use this instead.
Expand Down
Expand Up @@ -8,24 +8,17 @@
*/
public enum Listener {

HTTP("http", "http", "HTTP/1.1", false),
HTTPS("http", "https", "HTTP/1.1", true),
AJP("ajp", "http", "AJP/1.3", false),
HTTPJIO("http", "http", "org.apache.coyote.http11.Http11Protocol", false),
HTTPSJIO("http", "https", "org.apache.coyote.http11.Http11Protocol", true),
AJPJIO("ajp", "http", "org.apache.coyote.ajp.AjpProtocol", false),
HTTPNATIVE("http", "http", "org.apache.coyote.http11.Http11AprProtocol", false),
HTTPSNATIVE("http","https", "org.apache.coyote.http11.Http11AprProtocol", true);
HTTP("http", "http", false),
HTTPS("http", "https", true),
AJP("ajp", "http", false);

private final String name;
private final String scheme;
private final String protocol;
private final boolean secure;

private Listener(String name, String scheme, String protocol, boolean secure) {
private Listener(String name, String scheme, boolean secure) {
this.name = name;
this.scheme = scheme;
this.protocol = protocol;
this.secure = secure;
}

Expand All @@ -37,10 +30,6 @@ public final String getScheme() {
return scheme;
}

public final String getProtocol() {
return protocol;
}

public final boolean isSecure() {
return secure;
}
Expand Down
Expand Up @@ -48,7 +48,7 @@ public class AjpListenerService extends ListenerService {
private final String scheme;

public AjpListenerService(String name, final String scheme, OptionMap listenerOptions, OptionMap socketOptions) {
super(name, listenerOptions, socketOptions);
super(name, listenerOptions, socketOptions, false);
this.scheme = scheme;
}

Expand Down
Expand Up @@ -254,4 +254,6 @@ public interface Constants {
String GET_SESSION_CREATION_TIME = "get-session-creation-time";
String GET_SESSION_CREATION_TIME_MILLIS = "get-session-creation-time-millis";
String DEFAULT_COOKIE_VERSION = "default-cookie-version";

String PROXY_PROTOCOL = "proxy-protocol";
}
Expand Up @@ -46,6 +46,7 @@ public class HttpListenerAdd extends ListenerAdd {

@Override
ListenerService createService(String name, final String serverName, final OperationContext context, ModelNode model, OptionMap listenerOptions, OptionMap socketOptions) throws OperationFailedException {
final boolean proxyProtocol = HttpListenerResourceDefinition.PROXY_PROTOCOL.resolveModelAttribute(context, model).asBoolean();
final boolean certificateForwarding = HttpListenerResourceDefinition.CERTIFICATE_FORWARDING.resolveModelAttribute(context, model).asBoolean();
final boolean proxyAddressForwarding = HttpListenerResourceDefinition.PROXY_ADDRESS_FORWARDING.resolveModelAttribute(context, model).asBoolean();
OptionMap.Builder listenerBuilder = OptionMap.builder().addAll(listenerOptions);
Expand All @@ -55,7 +56,7 @@ ListenerService createService(String name, final String serverName, final Operat

handleHttp2Options(context, model, listenerBuilder);

return new HttpListenerService(name, serverName, listenerBuilder.getMap(), socketOptions, certificateForwarding, proxyAddressForwarding);
return new HttpListenerService(name, serverName, listenerBuilder.getMap(), socketOptions, certificateForwarding, proxyAddressForwarding, proxyProtocol);
}

static void handleHttp2Options(OperationContext context, ModelNode model, OptionMap.Builder listenerBuilder) throws OperationFailedException {
Expand Down
Expand Up @@ -124,6 +124,13 @@ public class HttpListenerResourceDefinition extends ListenerResourceDefinition {
.setDefaultValue(new ModelNode(false))
.build();

protected static final SimpleAttributeDefinition PROXY_PROTOCOL = new SimpleAttributeDefinitionBuilder(Constants.PROXY_PROTOCOL, ModelType.BOOLEAN)
.setDefaultValue(new ModelNode(false))
.setRequired(false)
.setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES)
.setAllowExpression(true)
.build();

private HttpListenerResourceDefinition() {
super(UndertowExtension.HTTP_LISTENER_PATH);
}
Expand All @@ -146,6 +153,7 @@ public Collection<AttributeDefinition> getAttributes() {
attrs.add(HTTP2_MAX_HEADER_LIST_SIZE);
attrs.add(HTTP2_MAX_FRAME_SIZE);
attrs.add(REQUIRE_HOST_HTTP11);
attrs.add(PROXY_PROTOCOL);
return attrs;
}

Expand Down
Expand Up @@ -62,8 +62,8 @@ public class HttpListenerService extends ListenerService {

private final String serverName;

public HttpListenerService(String name, final String serverName, OptionMap listenerOptions, OptionMap socketOptions, boolean certificateForwarding, boolean proxyAddressForwarding) {
super(name, listenerOptions, socketOptions);
public HttpListenerService(String name, final String serverName, OptionMap listenerOptions, OptionMap socketOptions, boolean certificateForwarding, boolean proxyAddressForwarding, boolean proxyProtocol) {
super(name, listenerOptions, socketOptions, proxyProtocol);
this.serverName = serverName;
addWrapperHandler(handler -> {
httpUpgradeHandler.setNonUpgradeHandler(handler);
Expand Down
Expand Up @@ -52,6 +52,7 @@ ListenerService createService(String name, final String serverName, final Operat
OptionMap.Builder builder = OptionMap.builder().addAll(socketOptions);

ModelNode securityRealmModel = HttpsListenerResourceDefinition.SECURITY_REALM.resolveModelAttribute(context, model);
final boolean proxyProtocol = HttpListenerResourceDefinition.PROXY_PROTOCOL.resolveModelAttribute(context, model).asBoolean();
String cipherSuites = null;
if(securityRealmModel.isDefined()) {
//we only support setting these options for security realms
Expand All @@ -73,7 +74,7 @@ ListenerService createService(String name, final String serverName, final Operat

final boolean certificateForwarding = HttpListenerResourceDefinition.CERTIFICATE_FORWARDING.resolveModelAttribute(context, model).asBoolean();
final boolean proxyAddressForwarding = HttpListenerResourceDefinition.PROXY_ADDRESS_FORWARDING.resolveModelAttribute(context, model).asBoolean();
return new HttpsListenerService(name, serverName, listenerBuilder.getMap(), cipherSuites, builder.getMap(), certificateForwarding, proxyAddressForwarding);
return new HttpsListenerService(name, serverName, listenerBuilder.getMap(), cipherSuites, builder.getMap(), certificateForwarding, proxyAddressForwarding, proxyProtocol);
}

@Override
Expand Down

0 comments on commit ab48d04

Please sign in to comment.