ParameterUtil.java

package com.jsql.util;

import com.jsql.model.InjectionModel;
import com.jsql.model.bean.util.Interaction;
import com.jsql.model.bean.util.Request;
import com.jsql.model.exception.InjectionFailureException;
import com.jsql.model.injection.method.AbstractMethodInjection;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class ParameterUtil {

    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();

    /**
     * Query string built from the URL submitted by user.
     */
    private List<SimpleEntry<String, String>> listQueryString = new CopyOnWriteArrayList<>();

    /**
     * Request submitted by user.
     */
    private List<SimpleEntry<String, String>> listRequest = new CopyOnWriteArrayList<>();

    /**
     * Header submitted by user.
     */
    private List<SimpleEntry<String, String>> listHeader = new CopyOnWriteArrayList<>();

    private String rawRequest = StringUtils.EMPTY;
    private String rawHeader = StringUtils.EMPTY;
    private boolean isMultipartRequest = false;
    private static final String FORMAT_KEY_VALUE = "%s=%s";

    private final InjectionModel injectionModel;
    
    public ParameterUtil(InjectionModel injectionModel) {
        this.injectionModel = injectionModel;
    }
    
    /**
     * Send each parameters from the GUI to the model in order to
     * start the preparation of injection, the injection process is
     * started in a new thread via model function inputValidation().
     */
    public void controlInput(
        String urlQuery,
        String rawRequest,
        String rawHeader,
        AbstractMethodInjection methodInjection,
        String typeRequest,
        boolean isScanning
    ) {
        try {
            String urlQueryFixed = urlQuery;
               
            // Keep single check
            if (urlQueryFixed.isEmpty()) {
                throw new MalformedURLException("empty URL");
            } else if (!urlQueryFixed.matches("(?i)^https?://.*")) {
                if (!urlQueryFixed.matches("(?i)^\\w+://.*")) {
                    
                    LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Undefined URL protocol, forcing to [http://]");
                    urlQueryFixed = "http://"+ urlQueryFixed;
                    
                } else {
                    throw new MalformedURLException("unknown URL protocol");
                }
            }

            String authority = URI.create(urlQueryFixed).getAuthority();
            if (authority == null) {
                throw new MalformedURLException("incorrect domain authority");
            }
            String authorityPunycode = IDN.toASCII(authority);
            if (!authority.equals(authorityPunycode)) {
                LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Punycode domain detected, using [{}] instead of [{}]", authorityPunycode, authority);
                urlQueryFixed = urlQueryFixed.replace(authority, authorityPunycode);
            }

            this.initializeQueryString(urlQueryFixed);
            this.initializeHeader(rawHeader);
            this.initializeRequest(rawRequest);

            this.injectionModel.getMediatorUtils().getConnectionUtil().setMethodInjection(methodInjection);
            this.injectionModel.getMediatorUtils().getConnectionUtil().setTypeRequest(typeRequest);
            
            if (isScanning) {
                this.injectionModel.beginInjection();
            } else {
                // Start the model injection process in a thread
                new Thread(
                    this.injectionModel::beginInjection,
                    "ThreadBeginInjection"
                )
                .start();
            }
        } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
            
            LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Incorrect Url: {}", e.getMessage());
            
            // Incorrect URL, reset the start button
            var request = new Request();
            request.setMessage(Interaction.END_PREPARATION);
            this.injectionModel.sendToViews(request);
        }
    }

    /**
     * Check integrity of parameters defined by user.
     * @throws InjectionFailureException when params integrity is failure
     */
    public void checkParametersFormat() throws InjectionFailureException {
        
        this.checkOneOrLessStar();
        this.checkStarMatchMethod();
        this.checkMethodNotEmpty();
        this.checkMultipart();
    }

    private void checkMultipart() throws InjectionFailureException {

        isMultipartRequest = false;

        if (
            this.getListHeader()
            .stream()
            .filter(entry -> "Content-Type".equals(entry.getKey()))
            .anyMatch(entry ->
                entry.getValue() != null
                && entry.getValue().contains("multipart/form-data")
                && entry.getValue().contains("boundary=")
            )
        ) {
            LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Multipart boundary found in header");
            Matcher matcherBoundary = Pattern.compile("boundary=([^;]*)").matcher(this.getHeaderFromEntries());
            if (matcherBoundary.find()) {
                String boundary = matcherBoundary.group(1);
                if (!this.rawRequest.contains(boundary)) {
                    throw new InjectionFailureException(
                        String.format("Incorrect multipart data, boundary not found in body: %s", boundary)
                    );
                } else {
                    isMultipartRequest = true;
                }
            }
        }
    }

    private void checkOneOrLessStar() throws InjectionFailureException {
        
        var nbStarInParameter = 0;
        
        if (this.getQueryStringFromEntries().contains(InjectionModel.STAR)) {
            nbStarInParameter++;
        }
        if (this.getRequestFromEntries().contains(InjectionModel.STAR)) {
            nbStarInParameter++;
        }
        if (this.getHeaderFromEntries().contains(InjectionModel.STAR)) {
            nbStarInParameter++;
        }
        
        // Injection Point
        if (
            nbStarInParameter > 1
            || StringUtils.countMatches(this.getQueryStringFromEntries(), "*") > 1
            || StringUtils.countMatches(this.getRequestFromEntries(), "*") > 1
            || StringUtils.countMatches(this.getHeaderFromEntries(), "*") > 1
        ) {
            throw new InjectionFailureException("Character insertion [*] must be used once in Query String, Request or Header parameters");
        }
    }
    
    public void checkStarMatchMethod() throws InjectionFailureException {
        
        AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().getConnectionUtil().getMethodInjection();
        boolean isCheckingAllParam = this.injectionModel.getMediatorUtils().getPreferencesUtil().isCheckingAllParam();

        if (
            this.getQueryStringFromEntries().contains(InjectionModel.STAR)
            && methodInjection != this.injectionModel.getMediatorMethod().getQuery()
            && !isCheckingAllParam
        ) {
            throw new InjectionFailureException("Select method GET to use character [*] or remove [*] from GET parameters");
        } else if (
            this.getRequestFromEntries().contains(InjectionModel.STAR)
            && methodInjection != this.injectionModel.getMediatorMethod().getRequest()
            && !isCheckingAllParam
        ) {
            throw new InjectionFailureException("Select a Request method (like POST) to use [*], or remove [*] from Request parameters");
        } else if (
            this.getHeaderFromEntries().contains(InjectionModel.STAR)
            && methodInjection != this.injectionModel.getMediatorMethod().getHeader()
            && !isCheckingAllParam
        ) {
            throw new InjectionFailureException("Select method Header to use character [*] or remove [*] from Header parameters");
        }
    }
    
    public void checkMethodNotEmpty() throws InjectionFailureException {
        
        AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().getConnectionUtil().getMethodInjection();
        boolean isCheckingAllParam = this.injectionModel.getMediatorUtils().getPreferencesUtil().isCheckingAllParam();
        
        if (
            methodInjection == this.injectionModel.getMediatorMethod().getQuery()
            && !isCheckingAllParam
            && this.getListQueryString().isEmpty()
            && !this.injectionModel.getMediatorUtils().getConnectionUtil().getUrlBase().contains(InjectionModel.STAR)
        ) {
            throw new InjectionFailureException("No query string");
        } else if (
            methodInjection == this.injectionModel.getMediatorMethod().getRequest()
            && this.getListRequest().isEmpty()
        ) {
            throw new InjectionFailureException("Incorrect Request format");
        } else if (
            methodInjection == this.injectionModel.getMediatorMethod().getHeader()
            && this.getListHeader().isEmpty()
        ) {
            throw new InjectionFailureException("Incorrect Header format");
        }
    }
    
    public String initializeStar(SimpleEntry<String, String> parameterToInject) {
        
        String characterInsertionByUser;

        if (parameterToInject == null) {
            characterInsertionByUser = InjectionModel.STAR;
        } else {
            
            characterInsertionByUser = parameterToInject.getValue();
            parameterToInject.setValue(InjectionModel.STAR);
        }
        
        return characterInsertionByUser;
    }

    public void initializeQueryString(String urlQuery) throws MalformedURLException, URISyntaxException {

        // Format and get rid of anchor fragment using native URL
        var url = new URI(urlQuery).toURL();
        
        if (
            StringUtils.isEmpty(urlQuery)
            || StringUtils.isEmpty(url.getHost())
        ) {
            throw new MalformedURLException("empty URL");
        }
        
        this.injectionModel.getMediatorUtils().getConnectionUtil().setUrlByUser(urlQuery);
        this.injectionModel.getMediatorUtils().getConnectionUtil().setUrlBase(urlQuery);
        
        this.listQueryString.clear();
        
        // Parse url and GET query string
        var regexQueryString = Pattern.compile("(.*\\?)(.*)").matcher(urlQuery);
        
        if (!regexQueryString.find()) {
            return;
        }
        
        this.injectionModel.getMediatorUtils().getConnectionUtil().setUrlBase(regexQueryString.group(1));
        
        if (StringUtils.isNotEmpty(url.getQuery())) {
            this.listQueryString = Pattern.compile("&")
                .splitAsStream(url.getQuery())
                .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
                .map(keyValue -> new SimpleEntry<>(
                    keyValue[0],
                    keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
                ))
                .collect(Collectors.toList());
        }
    }

    public void initializeRequest(String rawRequest) {

        this.rawRequest = rawRequest;
        this.listRequest.clear();

        if (StringUtils.isNotEmpty(rawRequest)) {
            if (isMultipartRequest()) {
                // Pass request containing star * param without any parsing
                this.listRequest = new ArrayList<>(List.of(new SimpleEntry<>(
                    rawRequest,
                    ""
                )));
            } else {
                this.listRequest = Pattern.compile("&")
                    .splitAsStream(rawRequest)
                    .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
                    .map(keyValue -> new SimpleEntry<>(
                        keyValue[0],
                        keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
                    ))
                    .collect(Collectors.toList());
            }
        }
    }

    public void initializeHeader(String rawHeader) {
        
        this.rawHeader = rawHeader;
        this.listHeader.clear();

        if (StringUtils.isNotEmpty(rawHeader)) {
            this.listHeader = Pattern.compile("\\\\r\\\\n")
                .splitAsStream(rawHeader)
                .map(keyValue -> Arrays.copyOf(keyValue.split(":"), 2))
                .map(keyValue -> new SimpleEntry<>(
                    keyValue[0],
                    keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
                ))
                .collect(Collectors.toList());
        }
    }
    
    public String getQueryStringFromEntries() {
        return this.listQueryString.stream()
            .filter(Objects::nonNull)
            .map(entry -> {
                if (
                    this.injectionModel.getMediatorStrategy().getStrategy() == this.injectionModel.getMediatorStrategy().getMultibit()
                    && entry.getValue() != null
                    && entry.getValue().contains(InjectionModel.STAR)
                ) {
                    return String.format(FORMAT_KEY_VALUE, entry.getKey(), InjectionModel.STAR);
                } else {
                    return String.format(FORMAT_KEY_VALUE, entry.getKey(), entry.getValue());
                }
            })
            .collect(Collectors.joining("&"));
    }

    public String getRequestFromEntries() {
        return this.listRequest.stream()
            .filter(Objects::nonNull)
            .map(entry -> String.format(
                FORMAT_KEY_VALUE,
                entry.getKey(),
                StringUtils.isEmpty(entry.getValue()) ? "" : entry.getValue()
            ))
            .collect(Collectors.joining("&"));
    }
    
    public String getHeaderFromEntries() {
        return this.listHeader.stream()
            .filter(Objects::nonNull)
            .map(entry -> String.format(
                "%s:%s",
                entry.getKey(),
                entry.getValue()
            ))
            .collect(Collectors.joining("\\r\\n"));
    }

    public boolean isRequestSoap() {
        return this.rawRequest
            .trim()
            .matches("^(<soapenv:|<\\?xml).*");
    }

    
    // Getters / setters
    
    public String getRawRequest() {
        return this.rawRequest;
    }

    public String getRawHeader() {
        return this.rawHeader;
    }

    public List<SimpleEntry<String, String>> getListRequest() {
        return this.listRequest;
    }

    public void setListRequest(List<SimpleEntry<String, String>> listRequest) {
        this.listRequest = listRequest;
    }

    public List<SimpleEntry<String, String>> getListHeader() {
        return this.listHeader;
    }

    public void setListHeader(List<SimpleEntry<String, String>> listHeader) {
        this.listHeader = listHeader;
    }
    
    public List<SimpleEntry<String, String>> getListQueryString() {
        return this.listQueryString;
    }
    
    public void setListQueryString(List<SimpleEntry<String, String>> listQueryString) {
        this.listQueryString = listQueryString;
    }

    public boolean isMultipartRequest() {
        return isMultipartRequest;
    }
}