2019년 10월 31일 목요일

Jetty를 활용한 HTTP Proxy Server 구축

web proxy server by jetty

Jetty9을 활용하면 쉽게 Web Proxy Server를 구축할 수 있다.
아래의 소스는 ProxyServlet을 활용하여 특정 호스트 이름으로 들어오는 웹요청을 우리가 원하는 응답으로 바꾸거나 변조하는 예제를 담고 있다.

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