
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package userguide.springboot.security.webservices;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.UUID;

@PropertySource("classpath:application.properties")
public class RequestAndResponseValidatorFilter extends OncePerRequestFilter {
 
    private static Log logger = LogFactory.getLog(RequestAndResponseValidatorFilter.class);
    
    private static ThreadLocal<Long> requestBeginTime = new ThreadLocal<>();
    private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 16384;

    private int maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;

    @Value("${requestDebuggingActivated}")
    private int requestDebuggingActivated;

    public RequestAndResponseValidatorFilter() {
        super();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String uuid = UUID.randomUUID().toString();
        String logPrefix = "RequestAndResponseValidatorFilter.doFilterInternal , uuid: " + uuid + " , request.getRequestURI():" + request.getRequestURI() + " , ";
        
        logger.debug(logPrefix + "starting ... ");
        
        BadRequestMatcher bad = new BadRequestMatcher(requestDebuggingActivated);
        if (!bad.validate(request)) {
            throw new ServletException("request is invalid, it contains a potentially malicious String");
        }
        
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestToUse = request;

        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
        }

        HttpServletResponse responseToUse = response;
        if (!(response instanceof ContentCachingResponseWrapper)) {
            responseToUse = new ContentCachingResponseWrapper(response);
        }

        requestBeginTime.set(System.currentTimeMillis());

        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } finally {
            logRequest(createRequestMessage(requestToUse,uuid));
	    
            ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(responseToUse, ContentCachingResponseWrapper.class);
            if (responseWrapper != null) {
                if (isFirstRequest) {
                    try {
                        responseWrapper.copyBodyToResponse();
                    } catch (IOException e) {
                        logger.error("Fail to write response body back", e);
                    }
                }
            } 
        }
    }

    protected String createRequestMessage(HttpServletRequest request, String uuid) throws ServletException {
        
        StringBuilder msg = new StringBuilder();
        msg.append("HTTP request with uuid: " + uuid + " , ");
        msg.append(request.getMethod());
        msg.append(" uri=").append(request.getRequestURI());

        String queryString = request.getQueryString();
        if (queryString != null) {
            msg.append('?').append(queryString);
        }

        String user = request.getRemoteUser();
        if (user != null) {
            msg.append(";user=").append(user);
        }

        
        return msg.toString();
    }

    protected String createResponseMessage(HttpServletResponse resp, String uuid) throws ServletException{

        StringBuilder msg = new StringBuilder();
        msg.append("HTTP response with uuid: " + uuid + " , ");

        ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(resp, ContentCachingResponseWrapper.class);
        if (responseWrapper != null) {
            byte[] buf = responseWrapper.getContentAsByteArray();
            try {
                responseWrapper.copyBodyToResponse();
            } catch (IOException e) {
                logger.error("Fail to write response body back", e);
            }
            if (buf.length > 0) {
                String payload;
                try {
                    payload = new String(buf, 0, buf.length, responseWrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    payload = "[unknown]";
                }
                msg.append(";response=").append(payload);
            }
        }
        
        return msg.toString();
    }

    public static boolean validate(String msg) {
        // Input validation is inferior to output sanitation as its impossible to think of 
        // everything. See JsonHtmlXssSerializer for html encoding of the output
        
        if (msg != null && msg.toUpperCase().contains("DOCTYPE")) {
            logger.error("DOCTYPE keyword is disallowed");
            return false;
        }
	
        // reflected XSS
        if (msg != null && msg.toUpperCase().indexOf("SCRIPT") != -1) {
            logger.error("SCRIPT keyword is disallowed");
            return false;
        }
        // reflected XSS without script tag, sneaky ... <img onerror=alert(1)/>
        if (msg != null && msg.toUpperCase().indexOf("ALERT") != -1) {
            logger.error("ALERT keyword is disallowed");
            return false;
        }
                
        return true;
        
    }

    public int getMaxPayloadLength() {
        return maxPayloadLength;
    }

    protected void logRequest(String message) {

        String logPrefix = "RequestAndResponseValidatorFilter.logRequest() , ";
        long begin = requestBeginTime.get();
        long end = System.currentTimeMillis();
 
        long duration = end - begin;
        if (message != null && message.toString().toUpperCase().indexOf("CREDENTIALS") != -1) {
            logger.info(logPrefix + " , not logging credentials ... request time:" + duration);
        } else {
            logger.info(logPrefix + message + ", request time:" + duration);
        }
    }
}
