Thursday, October 31, 2019

Building an HTTP Proxy Server with Jetty

web proxy server by jetty

Jetty9 makes it straightforward to build a Web Proxy Server. Using its ProxyServlet class, you can intercept incoming web requests for a specific host and modify or transform the responses as needed.

Below is an example that demonstrates how to leverage ProxyServlet to customize responses for specific incoming web requests.

Key Features

  1. Simple Integration with Jetty9: Use ProxyServlet to handle proxying logic seamlessly.
  2. Intercept and Modify Responses: Customize or transform server responses before passing them to the client.
  3. Dynamic Proxy Behavior: Add logic for filtering, caching, or monitoring requests.

Maven


    <dependency>
      <groupid>org.eclipse.jetty</groupid>
      <artifactid>jetty-server</artifactid>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupid>org.eclipse.jetty</groupid>
      <artifactid>jetty-proxy</artifactid>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupid>org.eclipse.jetty</groupid>
      <artifactid>jetty-servlet</artifactid>
      <version>${jetty.version}</version>
    </dependency>

Main

Server server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(8080);
server.addConnector(connector);

ConnectHandler proxy = new ConnectHandler();
server.setHandler(proxy);

CustomProxyServlet customProxyServlet = new CustomProxyServlet();
customProxyServlet.addHostFilter("maybe.somewhere.com", new DefaultFilterableHost()).referHostFilterByUrl("maybe.somewhere.com", "mightbe.somewhere.com");

// Setup proxy servlet
ServletContextHandler context = new ServletContextHandler(proxy, "/", ServletContextHandler.SESSIONS);
ServletHolder proxyServlet = new ServletHolder(customProxyServlet);
proxyServlet.setInitParameter("maxThreads", "10");
context.addServlet(proxyServlet, "/*");

server.start();
server.join();


Filterable Host Interface

public interface FilterableHost {
    boolean canHandle(URL url);
    void process(final HttpServletRequest request, final HttpServletResponse response);
}

Default Filterable Host

public class DefaultFilterableHost implements FilterableHost {


    @Override
    public boolean canHandle(URL url) {
        return url.getPath().endsWith("/somepath");
    }

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) {
        response.setStatus(HttpStatus.OK_200);
        response.addHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.KEEP_ALIVE.asString());
        response.addHeader(HttpHeader.TRANSFER_ENCODING.asString(), HttpHeaderValue.CHUNKED.asString());
        response.getOutputStream().write("some your revised response");
        return response;
    }

}

Proxy servlet

public class CustomProxyServlet extends ProxyServlet {

    private final Map filterMap;

    public CustomProxyServlet() {
        this.filterMap = new HashMap<>();
    }

    public CustomProxyServlet referHostFilterByUrl(String fromUrl, String toUrl) {
        if(filterMap.containsKey(fromUrl)) {
            filterMap.put(toUrl, filterMap.get(fromUrl));
            return this;
        } else {
            throw new IllegalStateException("can't find " + fromUrl + " in filterMap. you should put hostFilter for " + fromUrl + " first");
        }
    }

    public CustomProxyServlet addHostFilter(String url, FilterableHost filter) {
        if(filterMap.containsKey(url) == false) {
            filterMap.put(url, filter);
            return this;
        } else{
            throw new IllegalStateException("host url " + url + " already exists");
        }
    }

    @Override
    protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {

        URL url = new URL(request.getRequestURL().toString()); // Extract host from URL

        if(filterMap.containsKey(url.getHost())) {
            final FilterableHost filterableHost = filterMap.get(url.getHost());
            if(filterableHost.canHandle(url)) {
                final AsyncContext asyncContext = request.startAsync();
                // We do not timeout the continuation, but the proxy request
                asyncContext.setTimeout(0);
                filterableHost.process(request, response);
                asyncContext.complete();
            }
        } else {
            super.service(request, response);
        }
    }


    @Override
    protected Response.Listener newProxyResponseListener(HttpServletRequest request, HttpServletResponse response)
    {
        return new CustomProxyServletProxyResponseListener(request, response);
    }


    protected class CustomProxyServletProxyResponseListener extends Response.Listener.Adapter
    {
        private final HttpServletRequest request;
        private final HttpServletResponse response;

        protected CustomProxyServletProxyResponseListener(HttpServletRequest request, HttpServletResponse response)
        {
            this.request = request;
            this.response = response;
        }

        @Override
        public void onBegin(Response proxyResponse)
        {
            response.setStatus(proxyResponse.getStatus());
        }

        @Override
        public void onHeaders(Response proxyResponse)
        {
            onServerResponseHeaders(request, response, proxyResponse);
        }

        @Override
        public void onContent(final Response proxyResponse, ByteBuffer content, final Callback callback)
        {
            byte[] buffer;
            int offset;
            int length = content.remaining();
            if (content.hasArray())
            {
                buffer = content.array();
                offset = content.arrayOffset();
            }
            else
            {
                buffer = new byte[length];
                content.get(buffer);
                offset = 0;
            }

            onResponseContent(request, response, proxyResponse, buffer, offset, length, new Callback.Nested(callback)
            {
                @Override
                public void failed(Throwable x)
                {
                    super.failed(x);
                    proxyResponse.abort(x);
                }
            });
        }

        @Override
        public void onComplete(Result result)
        {
            if (result.isSucceeded())
                onProxyResponseSuccess(request, response, result.getResponse());
            else
                onProxyResponseFailure(request, response, result.getResponse(), result.getFailure());
        }
    }

}

Full source: https://gist.github.com/magicsih/cd9fa8b96e58fa7a8f977c82ad7a8d5f

Use Cases

  1. Web Proxy Server:
    • Proxy requests to a target server and apply transformations to the response.
  2. Content Filtering:
    • Block or modify specific content in the response.
  3. Performance Monitoring:
    • Log or analyze requests and responses for debugging or analytics.