/**
 * Copyright (c) 2008-2010 Yahoo! Inc.
 * All rights reserved. 
 * The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
 */
package hudson.security.csrf;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;

import hudson.Extension;
import hudson.model.PersistentDescriptor;
import jenkins.util.SystemProperties;
import hudson.Util;
import jenkins.model.Jenkins;
import hudson.model.ModelObject;

import javax.annotation.Nonnull;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import jenkins.security.HexStringConfidentialKey;

import net.sf.json.JSONObject;

import org.acegisecurity.Authentication;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

/**
 * A crumb issuing algorithm based on the request principal and the remote address.
 * 
 * @author dty
 */
public class DefaultCrumbIssuer extends CrumbIssuer {
    
    private transient MessageDigest md;
    private boolean excludeClientIPFromCrumb;

    @Restricted(NoExternalUse.class)
    public static /* non-final: Groovy Console */ boolean EXCLUDE_SESSION_ID = SystemProperties.getBoolean(DefaultCrumbIssuer.class.getName() + ".EXCLUDE_SESSION_ID");

    @DataBoundConstructor
    public DefaultCrumbIssuer(boolean excludeClientIPFromCrumb) {
        try {
            this.md = MessageDigest.getInstance("MD5");
            this.excludeClientIPFromCrumb = excludeClientIPFromCrumb;
        } catch (NoSuchAlgorithmException e) {
            this.md = null;
            this.excludeClientIPFromCrumb = false;
            LOGGER.log(Level.SEVERE, "Can't find MD5", e);
        }
    }

    public boolean isExcludeClientIPFromCrumb() {
        return this.excludeClientIPFromCrumb;
    }
    
    private Object readResolve() {
        try {
            this.md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            this.md = null;
            LOGGER.log(Level.SEVERE, "Can't find MD5", e);
        }
        
        return this;
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    protected synchronized String issueCrumb(ServletRequest request, String salt) {
        if (request instanceof HttpServletRequest) {
            if (md != null) {
                HttpServletRequest req = (HttpServletRequest) request;
                StringBuilder buffer = new StringBuilder();
                Authentication a = Jenkins.getAuthentication();
                buffer.append(a.getName());
                buffer.append(';');
                if (!isExcludeClientIPFromCrumb()) {
                    buffer.append(getClientIP(req));
                }
                if (!EXCLUDE_SESSION_ID) {
                    buffer.append(';');
                    buffer.append(getSessionId(req));
                }

                md.update(buffer.toString().getBytes());
                return Util.toHexString(md.digest(salt.getBytes()));
            }
        }
        return null;
    }

    private String getSessionId(@Nonnull HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "NO_SESSION";
        }
        return session.getId();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean validateCrumb(ServletRequest request, String salt, String crumb) {
        if (request instanceof HttpServletRequest) {
            String newCrumb = issueCrumb(request, salt);
            if ((newCrumb != null) && (crumb != null)) {
                // String.equals() is not constant-time, but this is
                return MessageDigest.isEqual(newCrumb.getBytes(StandardCharsets.US_ASCII),
                        crumb.getBytes(StandardCharsets.US_ASCII));
            }
        }
        return false;
    }

    private static final String X_FORWARDED_FOR = "X-Forwarded-For";

    private String getClientIP(HttpServletRequest req) {
        String defaultAddress = req.getRemoteAddr();
        String forwarded = req.getHeader(X_FORWARDED_FOR);
        if (forwarded != null) {
	        String[] hopList = forwarded.split(",");
            if (hopList.length >= 1) {
                return hopList[0];
            }
        }
        return defaultAddress;
    }
    
    @Extension @Symbol("standard")
    public static final class DescriptorImpl extends CrumbIssuerDescriptor<DefaultCrumbIssuer> implements ModelObject, PersistentDescriptor {

        private final static HexStringConfidentialKey CRUMB_SALT = new HexStringConfidentialKey(Jenkins.class,"crumbSalt",16);
        
        public DescriptorImpl() {
            super(CRUMB_SALT.get(), SystemProperties.getString("hudson.security.csrf.requestfield", CrumbIssuer.DEFAULT_CRUMB_NAME));
        }

        @Override
        public String getDisplayName() {
            return Messages.DefaultCrumbIssuer_DisplayName();
        }

        @Override
        public DefaultCrumbIssuer newInstance(StaplerRequest req, JSONObject formData) throws FormException {
            if (req == null) {
                // This state is prohibited according to the Javadoc of the super method.
                throw new FormException("DefaultCrumbIssuer new instance method is called for null Stapler request. "
                        + "Such call is prohibited.", "req");
            }
            return req.bindJSON(DefaultCrumbIssuer.class, formData);
        }
    }
    
    private static final Logger LOGGER = Logger.getLogger(DefaultCrumbIssuer.class.getName());
}

@@ -1673,333 +1673,99 @@
    t
-ry %7B%0A            this.md = MessageDigest.getInstance(%22MD5%22);%0A            this.excludeClientIPFromCrumb = excludeClientIPFromCrumb;%0A        %7D catch (NoSuchAlgorithmException e) %7B%0A            this.md = null;%0A            this.excludeClientIPFromCrumb = false;%0A            LOGGER.log(Level.SEVERE, %22Can't find MD5%22, e);%0A        %7D
+his.excludeClientIPFromCrumb = excludeClientIPFromCrumb;%0A        initializeMessageDigest();
 %0A   
@@ -1906,24 +1906,132 @@
 Resolve() %7B%0A
+        initializeMessageDigest();%0A        return this;%0A    %7D%0A%0A    private void initializeMessageDigest() %7B%0A
         try 
@@ -2040,29 +2040,24 @@
             
-this.
 md = Message
@@ -2076,19 +2076,23 @@
 stance(%22
-MD5
+SHA-256
 %22);%0A    
@@ -2146,21 +2146,16 @@
         
-this.
 md = nul
@@ -2197,70 +2197,81 @@
 ERE,
+ e, () -%3E
  %22Can
-'
+no
 t find 
-MD5%22, e);%0A        %7D%0A        %0A        return this;
+SHA-256 MessageDigest implementation.%22);%0A        %7D
 %0A   

Use SHA-256 for crumbs (#4134)