/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2013 The ZAP Development Team
 *
 * Licensed 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 org.zaproxy.zap.session;

import java.net.HttpCookie;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.extension.httpsessions.HttpSession;
import org.zaproxy.zap.extension.httpsessions.HttpSessionTokensSet;

/** Helper for Cookie-based session management. */
public class CookieBasedSessionManagementHelper {

    private static final Logger LOGGER =
            LogManager.getLogger(CookieBasedSessionManagementHelper.class);

    /**
     * Modifies a message so its Request Header/Body matches the web session provided.
     *
     * @param message the message
     * @param session the session
     */
    public static void processMessageToMatchSession(HttpMessage message, HttpSession session) {
        processMessageToMatchSession(message, message.getRequestHeader().getHttpCookies(), session);
    }

    /**
     * Modifies a message so its Request Header/Body matches the web session provided.
     *
     * @param message the message
     * @param requestCookies a pre-computed list with the request cookies (for optimization reasons)
     * @param session the session
     */
    public static void processMessageToMatchSession(
            HttpMessage message, List<HttpCookie> requestCookies, HttpSession session) {

        // Make a copy of the session tokens set, as they will be modified
        HttpSessionTokensSet tokensSet = session.getTokensNames();

        // If no tokens exists create dummy Object -> NPE
        if (tokensSet == null) {
            tokensSet = new HttpSessionTokensSet();
        }

        Set<String> unsetSiteTokens = new LinkedHashSet<>(tokensSet.getTokensSet());

        // Iterate through the cookies in the request
        Iterator<HttpCookie> it = requestCookies.iterator();
        while (it.hasNext()) {
            HttpCookie cookie = it.next();
            String cookieName = cookie.getName();

            // If the cookie is a token
            if (tokensSet.isSessionToken(cookieName)) {
                String tokenValue = session.getTokenValue(cookieName);
                LOGGER.debug("Changing value of token '{}' to: {}", cookieName, tokenValue);

                // Change it's value to the one in the active session, if any
                if (tokenValue != null) {
                    cookie.setValue(tokenValue);
                } // Or delete it, if the active session does not have a token value
                else {
                    it.remove();
                }

                // Remove the token from the token set so we know what tokens still have to be
                // added
                unsetSiteTokens.remove(cookieName);
            }
        }

        // Iterate through the tokens that are not present in the request and set the proper
        // value
        for (String token : unsetSiteTokens) {
            String tokenValue = session.getTokenValue(token);
            // Change it's value to the one in the active session, if any
            if (tokenValue != null) {
                LOGGER.debug("Adding token '{}' with value: {}", token, tokenValue);
                HttpCookie cookie = new HttpCookie(token, tokenValue);
                requestCookies.add(cookie);
            }
        }
        // Store the session in the HttpMessage for caching purpose
        message.setHttpSession(session);

        // Update the cookies in the message
        message.getRequestHeader().setCookies(requestCookies);
    }

    /**
     * Gets the matching http session, if any, for a particular message containing a list of
     * cookies, from a set of sessions.
     *
     * @param sessions the existing sessions
     * @param cookies the cookies present in the request header of the message
     * @param siteTokens the tokens
     * @return the matching http session, if any, or null if no existing session was found to match
     *     all the tokens
     */
    public static HttpSession getMatchingHttpSession(
            final Collection<HttpSession> sessions,
            List<HttpCookie> cookies,
            final HttpSessionTokensSet siteTokens) {

        // Pre-checks
        if (sessions.isEmpty()) {
            return null;
        }

        List<HttpSession> matchingSessions = new LinkedList<>(sessions);
        for (String token : siteTokens.getTokensSet()) {
            // Get the corresponding cookie from the cookies list
            HttpCookie matchingCookie = null;
            for (HttpCookie cookie : cookies) {
                if (cookie.getName().equals(token)) {
                    matchingCookie = cookie;
                    break;
                }
            }
            // Filter the sessions that do not match the cookie value
            Iterator<HttpSession> it = matchingSessions.iterator();
            while (it.hasNext()) {
                if (!it.next().matchesToken(token, matchingCookie)) {
                    it.remove();
                }
            }
        }

        // Return the matching session
        if (matchingSessions.size() >= 1) {
            if (matchingSessions.size() > 1) {
                LOGGER.warn(
                        "Multiple sessions matching the cookies from response. Using first one.");
            }
            return matchingSessions.get(0);
        }
        return null;
    }
}
