[JAVA] The story of making a reverse proxy with ProxyServlet

This article is a relay article of 2018 Advent Calendar of Link Information Systems. .. Relayed by a group member of engineer.hanzomon. (For Facebook of the link information system, click here](https://ja-jp.facebook.com/lis.co.jp/))

Introduction

My name is @shinevillage and I am in charge of the 13th day of the Advent calendar. I am mainly in charge of developing web applications using Java. This time, when I was in charge of a certain job, I will talk about making a reverse proxy in Java as a test tool.

Why did you make it?

The term "reverse proxy" is more familiar to infrastructure people, but it can sometimes be wanted by web developers as well. (Click here for "What is a reverse proxy in the first place?")

In my case, when testing an app with the following configuration, a cross-domain problem broke out and I wanted it. qiita1.png

It seems that Apache and Nginx are the most popular for building reverse proxies, but sadly in my case ** "Free software exe installation is prohibited. Scripts I wrote are ok. I'm working in an environment like "I'll forgive you" **, and I couldn't use the above software, so I wrote a reverse proxy for testing in Java, the language in which the app is written.

What I used

--Java 1.8 (Reason for selection: Because it is used at work) -Smiley's HTTP Proxy Servlet 1.10 (Reason for selection: Because it is easy to use) -Spring Boot 1.5.14 (Reason for selection: Because the executable war can be created quickly)

Constitution

Smiley's HTTP Proxy Servlet provides a Servlet (ProxyServlet) that acts as a proxy server. You can use ** URITemplateProxyServlet ** to dynamically control the forwarding destination (host name, port number, context path) with the query string. The behavior of the reverse proxy is reproduced by setting the query string according to the requested URL with the Servlet filter. seq.png

Implementation

Build using Maven.

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>riverse-proxy</groupId>
  <artifactId>riverse-proxy</artifactId>
  <version>1.0</version>
  <packaging>war</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.14.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>Windows-31j</project.build.sourceEncoding>
        <project.reporting.outputEncoding>Windows-31j</project.reporting.outputEncoding>
        <maven.compile.source>1.8</maven.compile.source>
        <maven.compile.target>1.8</maven.compile.target>
        <java.version>1.8</java.version>
        <spring.version>4.3.18.RELEASE</spring.version>
    </properties>
    <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mitre.dsmiley.httpproxy</groupId>
            <artifactId>smiley-http-proxy-servlet</artifactId>
            <version>1.10</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Next is the main class. Register the Jakarta Servlet and ProxyServlet here.

Main.java



@SpringBootApplication
public class Main extends SpringBootServletInitializer implements WebApplicationInitializer {

  /**
   *Entry point when starting independently.
   */
  public static void main(String[] args) throws Throwable {
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Main.class);
    builder.run();
  }
  
  /**
   *Entry point when starting the Servlet container.
   */
  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(Main.class);
  }

  /**
   * {@see org.springframework.web.filter.HiddenHttpMethodFilter}Disable.
   * 
   *URITemplateProxyServlet is{@link ServletRequest#getInputStream}To use
   *When the request parameter is accessed on the filter side, the process Servlet side
   *Request parameters cannot be acquired.
   *Therefore, in this tool, the function provided by HiddenHttpMethodFilter is unnecessary, so the filter is disabled.
   */
  @Bean
  public FilterRegistrationBean hiddenHttpMethodFilterRegistration(HiddenHttpMethodFilter filter) {
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setFilter(filter);
    registration.setEnabled(false);
    return registration;
  }

  /**
   * {@see org.springframework.web.filter.HttpPutFormContentFilter}Disable.
   * 
   *URITemplateProxyServlet is{@link ServletRequest#getInputStream}To use
   *When the request parameter is accessed on the filter side, the proxy servlet side
   *Request parameters cannot be acquired.
   *Therefore, in this tool, the function provided by HttpPutFormContentFilter is unnecessary, so the filter is disabled.
   */
  @Bean
  public FilterRegistrationBean httpPutFormContentFilterRegistration(HttpPutFormContentFilter filter) {
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setFilter(filter);
    registration.setEnabled(false);
    return registration;
  }
  
  /**
   *Registration of Servlet filter for Web application A.
   */
  @Bean
  public FilterRegistrationBean applicationARiverseProxyFilterRegistration() {
    Filter filter = new AAppRiverseProxyFilter();
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setOrder(FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER);
    registration.setUrlPatterns(Arrays.asList("/webapp-a/*"));
    registration.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
    return registration;
  }

  /**
   *Registration of Servlet filter for Web application B.
   */
  @Bean
  public FilterRegistrationBean applicationBRiverseProxyFilterRegistration() {
    Filter filter = new BAppRiverseProxyFilter();
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setOrder(FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER);
    registration.setUrlPatterns(Arrays.asList("/webapp-b/*"));
    registration.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST));
    return registration;
  }
  
  /**
   *Proxy Servlet registration.
   * @see https://github.com/mitre/HTTP-Proxy-Servlet
   */
  @Bean
  public ServletRegistrationBean riverseProxyServletRegistration() {
    HttpServlet servlet = new URITemplateProxyServlet();
    ServletRegistrationBean registration = new ServletRegistrationBean(servlet);
    registration.addInitParameter("preserveHost", "true");
    registration.addInitParameter("preserveCookies", "true");
    registration.addInitParameter("http.protocol.handle-redirects", "true");
    registration.addInitParameter("http.socket.timeout", "300000");
    registration.addInitParameter("http.read.timeout", "300000");
    registration.addInitParameter("targetUri", "http://{__proxyHost}/{__proxyContextRoot}");
    registration.setUrlMappings(Arrays.asList("/webapp-a/*", "/webapp-b/*"));
    return registration;
  }
}

Next is the Servlet filter. The main behavior is defined as an abstract class, and the processing that depends on the transfer destination on the subclass side is described.

AbstractRiverseProxyFilter.java


/**
 *Reverse proxy filter.
 * {@see org.mitre.dsmiley.httpproxy.URITemplateProxyServlet}To
 *Customize the request and response to operate as a reverse proxy.
 */
abstract public class AbstractRiverseProxyFilter implements Filter {
  
  /**
   *Get the transfer destination host
   */
  abstract protected String getTransfarHost();

  /**
   *Get forwarding context root
   */
  abstract protected String getTransfarContextRoot();
  
  @Override
  public void doFilter(ServletRequest _request, ServletResponse _response,
            FilterChain filterChain) throws IOException, ServletException {

    HttpServletRequest  request  = (HttpServletRequest)  _request;

    // {@see org.mitre.dsmiley.httpproxy.URITemplateProxyServlet}To
    //Add the parameter to specify the transfer destination to the query string
    StringBuilder routingQuery = new StringBuilder();
    String query = request.getQueryString();
    if (!StringUtils.isEmpty(query)) {
      routingQuery.append(query);
      routingQuery.append("&");
    }
    routingQuery.append(String.format("__proxyHost=%s&__proxyContextRoot=%s", 
        this.getTransfarHost(), this.getTransfarContextRoot()));

    //Prevents the default encoding of the Servlet container from being set
    _response.setCharacterEncoding(null);

    //Cover the request object with a wrapper and pass it to the proxy servlet
    RequestRewriteWrapper wrapRequest 
       = new RequestRewriteWrapper(request, routingQuery.toString());
    filterChain.doFilter(wrapRequest, _response);
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}

  @Override
  public void destroy() {}

  /**
   *Request wrapper for rewriting the query string
   */
  private static class RequestRewriteWrapper extends HttpServletRequestWrapper {

    private final String queryString;

    public RequestRewriteWrapper(HttpServletRequest request, String query) {
      super(request);
      this.queryString = query;
    }

    /**
     * {@link HttpServletRequest#getQueryString()}Rapper.
     *Returns the query string with the forwarding destination added.
     */
    @Override
    public String getQueryString() {
      return this.queryString;
    }
  }
}

AAppRiverseProxyFilter.java


/**
 *Reverse proxy filter for web app A.
 */
public class AAppRiverseProxyFilter extends AbstractRiverseProxyFilter {

  @Override
  protected String getTransfarHost() {
    return "xxx.xxx.xxx.1";
  }

  @Override
  protected String getTransfarContextRoot() {
    return "webapp-a";
  }
}

BAppRiverseProxyFilter.java


/**
 *Reverse proxy filter for web app B.
 */
public class BAppRiverseProxyFilter extends AbstractRiverseProxyFilter {

  @Override
  protected String getTransfarHost() {
    return "xxx.xxx.xxx.2";
  }

  @Override
  protected String getTransfarContextRoot() {
    return "webapp-b";
  }
}

Run

Start with the following command.

$ mvn spring-boot:run

After starting the tool, go to Web App A from the browser with "http : // localhost: 8080 / webapp-a /". You will be able to access Web App B with "http : // localhost: 8080 / webapp-b /".

Finally

The content of this article is an excerpt of only the basic part of the created tool. When actually making a tool, the transfer destination information is not written in the source code like the above sample, but @valueIt is a good idea to use a mechanism such as annotation to fetch it.

Even if I caught the net, there were not many articles writing reverse proxies in Java, so I wrote it this time. The reverse proxy in this article is just a ** "testing tool" **, so use a decent product for your production environment. <(_ _)>


Tomorrow is the 14th day. This is an article by @modest.

Recommended Posts

The story of making a reverse proxy with ProxyServlet
The story of making a game launcher with automatic loading function [Java]
The story of making dto, dao-like with java, sqlite
Story of making a task management application with swing, java
A story packed with the basics of Spring Boot (solved)
The basics of the process of making a call with an Android app
A story about hitting the League Of Legends API with JAVA
A story that struggled with the introduction of Web Apple Pay
The story of making a communication type Othello game using Scala.
The story of making the existing Dockerfile GPU compatible
The story of tuning android apps with libGDX
A story about making a Builder that inherits the Builder
The story of making it possible to build a project that was built by Maven with Ant
The story of making an electronic New Year's card app with Vue.js + Rails
The story of making Dr. Oakid using LINE BOT
A story about making catkin_make of rosjava compatible offline
Write a test by implementing the story of Mr. Nabeats in the world with ruby
A story stuck with NotSerializableException
Check the operation of two roles with a chat application
A story addicted to toString () of Interface proxied with JdkDynamicAopProxy
Explain the benefits of the State pattern with a movie rating
Find the number of days in a month with Kotlin
With the software I've been making for a long time ...
The story of pushing a Docker container to GitHub Package Registry and Docker Hub with GitHub Actions
The story of building a Java version of Minecraft server with GCP (and also set a whitelist)
[Java version] The story of serialization
Get a proxy instance of the component itself in Spring Boot
A story of connecting to a CentOS 8 server with an old Ansible
The story of @ViewScoped consuming memory
The story of toString () starting with passing an array to System.out.println
A memorandum of the FizzBuzz problem
A story about making a calculator to calculate the shell mound rate
Impressions of making BlackJack-cli with Ruby
[Illustration] Finding the sum of coins with a recursive function [Ruby]
A story that confirmed the profile of Yasuko Sawaguchi 36 years ago
A story that I was addicted to twice with the automatic startup setting of Tomcat 8 on CentOS 8
A story that I wanted to write a process equivalent to a while statement with the Stream API of Java8
How to operate IGV using socket communication, and the story of making a Ruby Gem using that method
How to quickly create a reverse proxy that supports HTTPS with Docker
The story of forgetting to close a file in Java and failing
Let's express the result of analyzing Java bytecode with a class diagram
Try to build a reverse proxy type configuration with Keycloak (Security Proxy edition)
(Note) Get a set of dependent library jars with the help of Gradle
The story of encountering Spring custom annotation
Create a jar file with the command
Check the contents of params with pry
The story of updating SonarQube's Docker Container
The story of RxJava suffering from NoSuchElementException
The story of AppClip support in Anyca
Run a DMN with the Camunda DMN Engine
Extract a part of a string with Ruby
A little addictive story with def initialize
About the treatment of BigDecimal (with reflection)
The story of writing Java in Emacs
Format the contents of LocalDate with DateTimeFormatter
Find the difference from a multiple of 10
The story of making an Android application that can adjust the sampling frequency of the accelerometer
Send a notification to slack with the free version of sentry (using lambda)
A story confirming the implementation of the SendGrid Java library when mail delivery fails
Validate the identity token of a user authenticated with AWS Cognito in Java
A quick note on using jshell with the official Docker image of the JDK