Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement an Undertow reverse proxy that behaves like nginx

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 have a spring boot REST service running on http://0.0.0.0:8081
  • we have spring boot web application running on http://0.0.0.0:8082

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

So we would like to map it like this:

  • requests to http://0.0.0.0:8080/ get proxied to http://0.0.0.0:8082
  • requests to http://0.0.0.0:8080/api get proxied to http://0.0.0.0:8081

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

like image 731
Raul G Avatar asked May 17 '16 14:05

Raul G


1 Answers

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);
        }
    }
}
like image 54
Peter Avatar answered Nov 03 '22 05:11

Peter