Search code examples
amazon-elastic-beanstalktomcat8

How to resolve HTTP 404 error with Elastic Beanstalk (Apache+Tomcat) when double-slash in URL path, e.g. https://uat.myserver//rest/something


I have a Java web application that's hosted in Amazon Web Services and it's running on the retired Tomcat 8 with Amazon Linux.

I've attempted to create a new environment to test running the app on the latest Tomcat 8.5 with Corretto 11 running on Amazon Linux 2.

During this process I hit a big problem, within my app it's using a base URL, e.g. https://myserver.com and it's doing some string concatenation to add the path e.g. /rest/myendpoint which of course should be resolved as

https://myserver.com/rest/myendpoint

But unfortunately unknowingly it's being passed through C#'s new URL() which is appending a trailing slash before the concatenation.

So

https://myserver.com/rest/myendpoint

gets sent to the server as

https://myserver.com/rest//myendpoint

(Note the rest// should be rest/)

After deploying the Java war file to the new environment some requests from the app fail with a 404. I've analysed these network logs and of course it's all the URLs that have a double-escape in the path.

As a test in Postman writing the request to have a single slash worked fine, double-quotes failed with a HTTP 404, just like the app.

The problem now is that the app is already used in the wild, we can't change the app because this will involve a long delay (getting it built, app store approval process, then waiting for everyone to upgrade the app), most likely months.

I can't find anything in the AWS portal to configure this.

I did try swapping to Nginx but I experienced the same error.

I did see some people mentioning using .eb-extensions, but the whole apache re-write / mod rules are very foreign to me. In effect I want to re-write any double-slashes after http:// or https:// to single-slashes. Can you please advise on how this could be done?

I'm using the Elastic Beanstalk with Tomcat (currently backed with Apache) with the latest Tomcat Platform version - Tomcat 8.5 with Corretto 11 running on 64bit Amazon Linux 2.

Thank you.


Solution

  • Because I didn't really want to faff around with the .ebextensions, I extended an existing servlet filter (the earliest in the chain) to check the path. I was then able to forward using the dispatcher and it works perfectly.

    Whilst I'm sure that I can do something cleaner with reg-ex this done the trick for me and I could test it locally with Postman first.

    package com.devology.servlet.filters;
    
    import java.io.IOException;
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    public class AuthenticationFilter implements Filter {
    
        public AuthenticationFilter() {
        }
    
        private boolean doBeforeProcessing(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
    
            HttpServletRequest hsr = (HttpServletRequest) request;
            final String path = hsr.getServletPath();
            String fullUrl = hsr.getRequestURL().toString();
    
            String remoteAddress = hsr.getRemoteAddr();
    
            // Example URL from bad app...
            //
            // http://localhost:8084/MyApp//rest/login
            //
            // path        =                                    /rest
            // fullUrl     = http://localhost:8084/MyApp//rest/login
            // indexOfPath =                                    ^ = 34
            // 
            // If the character before the start of the path is also a slash then
            // we need to internally forward
            int indexOfPath = fullUrl.indexOf(path + "/");
            boolean needsUrlRewrite = false;
            if (indexOfPath != -1) {
                String previousCharacter = fullUrl.substring(indexOfPath - 1, indexOfPath);
                needsUrlRewrite = "/".equals(previousCharacter);
            }
            if (needsUrlRewrite) {
    
                // Just take the path from the point of indexOfPath, .e.g /rest/login
                String rewrittenPath = fullUrl.substring(indexOfPath);
                RequestDispatcher dispatcher = request.getServletContext().getRequestDispatcher(rewrittenPath);
                dispatcher.forward(request, response);
                return false;
            }
    
            // Any other checks like authorisation, or return true to let all through
            return true;
        }
    
        public void doFilter(ServletRequest request, ServletResponse response,
                FilterChain chain)
                throws IOException, ServletException {
    
            if (doBeforeProcessing(request, response)) {
                chain.doFilter(request, response);
            }
        }
    
        @Override
        public void init(FilterConfig fc) throws ServletException {
            // Your Init
        }
    
        @Override
        public void destroy() {
            // Your cleanup
        }
    
    }
    

    This assumes that you have it configured in web.xml like this...

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
        <filter>
            <filter-name>AuthenticationFilter</filter-name>
            <filter-class>com.devology.servlet.filters.AuthenticationFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>AuthenticationFilter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
        <servlet-mapping>
            <servlet-name>jsp</servlet-name>
            <url-pattern>*.jsp</url-pattern>
            <url-pattern>*.jspf</url-pattern>
            <url-pattern>*.html</url-pattern>
        </servlet-mapping>
        <session-config>
            <session-timeout>10</session-timeout>
        </session-config>
        <error-page>
            <error-code>500</error-code>
            <location>/error-500.html</location>
        </error-page>
        <error-page>
            <error-code>404</error-code>
            <location>/error-404.html</location>
        </error-page>
    </web-app>
    

    I didn't fix the issue with the .ebextensions and Apache configuration because I didn't really know what I was doing and the above servelet filter done exactly what I needed.