
package de.ivu.elect.jee.faces;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpSecurityHeader {

  private static final Logger LOG = LoggerFactory.getLogger(HttpSecurityHeader.class);

  public static final AtomicBoolean CSP_ENABLED = new AtomicBoolean(true);

  public static final Map<String, String> HEADER_MAP = new ConcurrentHashMap<>();

  private static final Map<String, HttpHeaderContentSecurityPolicyContent> DYNAMIC_CONTENT_SECURITY_MAP = new ConcurrentHashMap<>();

  public static final String X_UA_COMPATIBLE = "X-UA-Compatible";

  public static final String X_UA_COMPATIBLE_DEFAULT = "IE=EDGE";

  public static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";

  public static final String X_CONTENT_TYPE_OPTIONS_DEFAULT = "nosniff";

  // WASDE-2279 veraltete Header entfernen
  //  public static final String X_FRAME_OPTIONS = "X-Frame-Options";
  //  public static final String X_FRAME_OPTIONS_DEFAULT = "SAMEORIGIN";

  // WASDE-2279 veraltete Header entfernen
  //  public static final String X_XSS_PROTECTION = "X-XSS-Protection";
  //  public static final String X_XSS_PROTECTION_DEFAULT = "1; mode=block";

  public static final String X_POWERED_BY = "X-Powered-By";
  public static final String X_POWERED_BY_DEFAULT = "elect iT GmbH";

  public static final String SERVER = "Server";
  public static final String SERVER_DEFAULT = "elect iT GmbH";

  public static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
  public static final String STRICT_TRANSPORT_SECURITY_DEFAULT = "max-age=16070400;includeSubDomains;preload"; // WASDE-1384, WASDE-2280

  public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
  public static final String CONTENT_SECURITY_POLICY_DEFAULT = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; frame-ancestors 'self'; block-all-mixed-content;"; // WASDE-1537, WASDE-1957

  /*
   * Erläuterungen zur verwendeten Content-Security-Policy:
   *
   *     mit Primefaces-CSP bis auf den Einsatz des CKEditors obsolet
   *     - script-src 'unsafe-inline' wird wegen Primefaces und unserer <script>-Tags in xhtml benötigt
   *     - script-src 'unsafe-eval' wird wegen Primefaces <p:confirm> benötigt und kann entfallen, wenn auf dessen Verwendung verzichtet wird
   *     - style-src 'unsafe-inline' wird wegen Primefaces und unserer style-Attribute in xhtml benötigt
   *
   *     weiterhin relevant
   *     - img-src data: wird benötigt, weil irgendwo Bilder base64-encodiert ausgegeben werden (Barcodes?)
   *     - frame-ancestors 'self' wird für die Hilfe in elect benötigt, die iframes verwendet
   *     - block-all-mixed-content damit bei verschlüsselten Verbindungen nichts aus unverschlüsselten Quellen nachgeladen wird
   *     - upgrade-insecure-requests NICHT verwenden, wenn block-all-mixed-content verwendet wird (siehe https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/upgrade-insecure-requests)
   *
   * Der Header "X-Content-Security-Policy" soll laut WASDE-1957 nicht mehr verwendet werden.
   * Begründung: "Der Header X-Content-Security-Policy ist veraltet und es wird empfohlen, diesen nur noch für den
   *             Internet Explorer zu benutzen. Dieser unterstützt jedoch nur die Direktive sandbox. Darüber
   *             hinausgehende Direktiven werden von diesem Header nicht oder nicht mehr unterstützt."
   */

  static {
    // defaults (Werden in ELECT in HttpHeaderKonfigurationInit überschrieben(!))
    HEADER_MAP.put(X_UA_COMPATIBLE, X_UA_COMPATIBLE_DEFAULT);
    HEADER_MAP.put(X_CONTENT_TYPE_OPTIONS, X_CONTENT_TYPE_OPTIONS_DEFAULT);
    //    HEADER_MAP.put(X_FRAME_OPTIONS, X_FRAME_OPTIONS_DEFAULT); // WASDE-2279 veraltete Header entfernen
    //    HEADER_MAP.put(X_XSS_PROTECTION, X_XSS_PROTECTION_DEFAULT); // WASDE-2279 veraltete Header entfernen
    HEADER_MAP.put(X_POWERED_BY, X_POWERED_BY_DEFAULT);
    HEADER_MAP.put(SERVER, SERVER_DEFAULT);
    HEADER_MAP.put(STRICT_TRANSPORT_SECURITY, STRICT_TRANSPORT_SECURITY_DEFAULT);
    if (CSP_ENABLED.get()) {
      HEADER_MAP.put(CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_DEFAULT);
    }
  }

  public static void apply(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
    for (Map.Entry<String, String> entry : HEADER_MAP.entrySet()) {
      if (StringUtils.isNotBlank(entry.getValue())) {
        servletResponse.setHeader(entry.getKey(), filter(entry.getKey(), entry.getValue(), servletRequest));
      }
    }
  }

  private static String filter(String key, String value, HttpServletRequest servletRequest) {
    if (CSP_ENABLED.get() && !DYNAMIC_CONTENT_SECURITY_MAP.isEmpty() && CONTENT_SECURITY_POLICY.equals(key)) {
      HttpHeaderContentSecurityPolicyContent viewPolicy = getHttpHeaderContentSecurityPolicy(servletRequest);
      if (viewPolicy != null) {
        String modifiedValue = value;
        for (Map.Entry<String, String> policyEntry : viewPolicy.getAdditionalPolicies().entrySet()) {
          modifiedValue = modifyCsp(modifiedValue, policyEntry.getKey(), policyEntry.getValue());
        }
        return modifiedValue;
      }
    }

    return value;
  }

  @Nullable
  private static HttpHeaderContentSecurityPolicyContent getHttpHeaderContentSecurityPolicy(HttpServletRequest servletRequest) {
    if (!CSP_ENABLED.get() || DYNAMIC_CONTENT_SECURITY_MAP.isEmpty()) {
      return null;
    }

    return getHttpHeaderContentSecurityPolicy(servletRequest.getRequestURI());
  }

  @Nullable
  static HttpHeaderContentSecurityPolicyContent getHttpHeaderContentSecurityPolicy(String requestURI) {
    return DYNAMIC_CONTENT_SECURITY_MAP.entrySet().stream().filter(entry -> requestURI.contains(entry.getKey())).findFirst().map(Map.Entry::getValue)
        .orElse(null);
  }

  static String modifyCsp(String csp, String policy, String add) {
    if (!CSP_ENABLED.get() || StringUtils.isBlank(csp) || StringUtils.isBlank(policy) || StringUtils.isBlank(add)) {
      return csp;
    }

    String cspTrimmed = csp.trim();

    if (cspTrimmed.contains(policy)) {
      // policy existiert und soll erweitert werden...
      StringBuilder result = new StringBuilder();
      for (String s : cspTrimmed.split(";")) {

        String trimmed = s.trim();
        if (trimmed.startsWith(policy)) {
          boolean found = false;
          for (String part : trimmed.split(" ")) {
            if (policy.equals(part)) {
              if (result.length() > 0) {
                result.append("; ");
              }
              result.append(policy);
            } else {
              if (add.equals(part)) {
                found = true;
              }
              result.append(" ");
              result.append(part);
            }
          }
          if (!found) {
            result.append(" ");
            result.append(add);
          }

        } else {
          if (result.length() > 0) {
            result.append(";");
          }
          result.append(s);
        }

      }
      if (cspTrimmed.endsWith(";")) {
        result.append(";");
      }

      return result.toString();

    } else {
      // policy existiert nicht und soll neu aufgenommen werden...
      StringBuilder result = new StringBuilder(cspTrimmed);
      if (!cspTrimmed.endsWith(";")) {
        result.append(";");
      }
      result.append(" ");
      result.append(policy);
      result.append(" ");
      result.append(add);
      result.append(";");
      return result.toString();
    }
  }

  public static void addCsp(String viewId, HttpHeaderContentSecurityPolicyContent httpHeaderContentSecurityPolicyContent) {
    if (CSP_ENABLED.get()) {
      LOG.info("CSP: " + viewId + ", " + httpHeaderContentSecurityPolicyContent);
      DYNAMIC_CONTENT_SECURITY_MAP.put(viewId, httpHeaderContentSecurityPolicyContent);
    } else {
      LOG.info("CSP disabled: " + viewId + ", " + httpHeaderContentSecurityPolicyContent);
    }
  }

  public static void disableCSP() {
    LOG.info("CSP disabled");
    CSP_ENABLED.set(false);
    DYNAMIC_CONTENT_SECURITY_MAP.clear();
    HEADER_MAP.remove(CONTENT_SECURITY_POLICY);
  }

}
