5

For development purposes, not everyone can install nginx on their machines (like our developers on Windows environments), but we want to be able to do a reverse proxy that behaves like nginx.

Here's our very specific case:

We would like to serve both services from http://0.0.0.0:8080

So we would like to map it like this:

That way it works like nginx with url rewrite reverse proxying.

I checked out the Undertow source code and examples, and even this specific example: Reverse Proxy Example, but this is a load balancer example, I haven't found any example that covers what I need.

Also, I know Undertow is capable of this, because we know we can configure WildFly to cover this specific case without issues through the Undertow component configuration, but we would like to implement it ourselves as a lightweight solution for local development.

Does anyone know of an example to do this? or any documentation that has enough info to implement this? because I've also read Undertow's documentation on reverse proxying and it's not helpful at all.

Thanks

Raul G
  • 473
  • 1
  • 6
  • 18
  • 1
    Why not simply use a Zuul proxy to do that. Also Windows users are perfectly capably of [running nginx](http://nginx.org/en/docs/windows.html). Used that on several occasions (for testing purposes) and worked like a charm. – M. Deinum May 17 '16 at 14:15
  • Sounds ok, but we'd like to take this as a learning experience, and also, if we add this reverse proxy as a module inside our Gradle project, then developers don't have to configure anything on their machines, as the proxy would be contained there. – Raul G May 17 '16 at 14:18
  • 1
    They still would have to start it, and you can always create a gradle taks which downloads, unzip, configures the nginx proxy. Or use a virtual machine and/or docker. But as you are already using Spring Boot adding a Zuul proxy should be pretty easy to do. – M. Deinum May 17 '16 at 14:25
  • Yes, I'm actually looking into Zuul Spring Boot component instead of Undertow, so thanks for that suggestion, that way we can add Zuul inside spring boot through [this tutorial](https://spring.io/guides/gs/routing-and-filtering/) easier than trying to figure out if Undertow is capable, also Zuul is specifically designed for proxying, so it's a more robust solution for this specific case than Undertow, which we use for the other services. Add this comment as a solution so I can accept it as an answer, a +1 to you my good man :) – Raul G May 17 '16 at 14:31

2 Answers2

5

This should do the job.

It's Java8 so some parts may not work on your setup.

You can start it in a similar way as the example you've mentioned in your question.

package com.company

import com.google.common.collect.ImmutableMap;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.UndertowClient;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.ServerConnection;
import io.undertow.server.handlers.proxy.ProxyCallback;
import io.undertow.server.handlers.proxy.ProxyClient;
import io.undertow.server.handlers.proxy.ProxyConnection;
import org.xnio.IoUtils;
import org.xnio.OptionMap;

import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Start the ReverseProxy with an ImmutableMap of matching endpoints and a default
 * 
 * Example:
 * mapping: ImmutableMap("api" -> "http://some-domain.com")
 * default: "http://default-domain.com"
 * 
 * Request 1: localhost:8080/foo -> http://default-domain.com/foo
 * Request 2: localhost:8080/api/bar -> http://some-domain.com/bar
 */

public class ReverseProxyClient implements ProxyClient {
    private static final ProxyTarget TARGET = new ProxyTarget() {};

    private final UndertowClient client;
    private final ImmutableMap<String, URI> mapping;
    private final URI defaultTarget;

    public ReverseProxyClient(ImmutableMap<String, URI> mapping, URI defaultTarget) {
        this.client = UndertowClient.getInstance();
        this.mapping = mapping;
        this.defaultTarget = defaultTarget;
    }

    @Override
    public ProxyTarget findTarget(HttpServerExchange exchange) {
        return TARGET;
    }

    @Override
    public void getConnection(ProxyTarget target, HttpServerExchange exchange, ProxyCallback<ProxyConnection> callback, long timeout, TimeUnit timeUnit) {
        URI targetUri = defaultTarget;

        Matcher matcher = Pattern.compile("^/(\\w+)(/.*)").matcher(exchange.getRequestURI());
        if (matcher.find()) {
            String firstUriSegment = matcher.group(1);
            String remaininguri = matcher.group(2);
            if (mapping.containsKey(firstUriSegment)) {
                // If the first uri segment is in the mapping, update the targetUri
                targetUri = mapping.get(firstUriSegment);
                // Strip the request uri from the part that is used to map upon.
                exchange.setRequestURI(remaininguri);
            }
        }

        client.connect(
            new ConnectNotifier(callback, exchange),
            targetUri,
            exchange.getIoThread(),
            exchange.getConnection().getByteBufferPool(),
            OptionMap.EMPTY);
    }

    private final class ConnectNotifier implements ClientCallback<ClientConnection> {
        private final ProxyCallback<ProxyConnection> callback;
        private final HttpServerExchange exchange;

        private ConnectNotifier(ProxyCallback<ProxyConnection> callback, HttpServerExchange exchange) {
            this.callback = callback;
            this.exchange = exchange;
        }

        @Override
        public void completed(final ClientConnection connection) {
            final ServerConnection serverConnection = exchange.getConnection();
            serverConnection.addCloseListener(serverConnection1 -> IoUtils.safeClose(connection));
            callback.completed(exchange, new ProxyConnection(connection, "/"));
        }

        @Override
        public void failed(IOException e) {
            callback.failed(exchange);
        }
    }
}
Peter
  • 620
  • 6
  • 13
2

As per M. Deinum's comment suggestion, I'll use Zuul Spring Boot component instead of trying to do this with Undertow, as it's more fit for this task.

Here's a link on a tutorial to do this:

https://spring.io/guides/gs/routing-and-filtering/

Hope this helps anyone else, as this is a pretty common case, and I didn't know about Zuul on Spring Boot.

Raul G
  • 473
  • 1
  • 6
  • 18