/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* 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.extension.httpsessions;
import java.net.HttpCookie;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.httpclient.Cookie;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.network.HttpMessage;
import org.zaproxy.zap.session.CookieBasedSessionManagementHelper;
/**
* The Class SiteHttpSessions stores all the information regarding the sessions for a particular
* Site.
*/
public class HttpSessionsSite {
/** The Constant log. */
private static final Logger log = Logger.getLogger(HttpSessionsSite.class);
/** The last session id. */
private static int lastGeneratedSessionID = 0;
/** The extension. */
private ExtensionHttpSessions extension;
/** The site. */
private String site;
/** The sessions as a LinkedHashSet. */
private Set<HttpSession> sessions;
/** The active session. */
private HttpSession activeSession;
/** The model associated with this site. */
private HttpSessionsTableModel model;
/**
* Instantiates a new site http sessions object.
*
* @param extension the extension
* @param site the site
*/
public HttpSessionsSite(ExtensionHttpSessions extension, String site) {
super();
this.extension = extension;
this.site = site;
this.sessions = new LinkedHashSet<>();
this.model = new HttpSessionsTableModel(this);
this.activeSession = null;
}
/**
* Adds a new http session to this site.
*
* @param session the session
*/
public void addHttpSession(HttpSession session) {
synchronized (this.sessions) {
this.sessions.add(session);
}
this.model.addHttpSession(session);
}
/**
* Removes an existing session.
*
* @param session the session
*/
public void removeHttpSession(HttpSession session) {
if (session == activeSession) {
activeSession = null;
}
synchronized (this.sessions) {
this.sessions.remove(session);
}
this.model.removeHttpSession(session);
session.invalidate();
}
/**
* Gets the site.
*
* @return the site
*/
public String getSite() {
return site;
}
/**
* Sets the site.
*
* @param site the new site
*/
public void setSite(String site) {
this.site = site;
}
/**
* Gets the active session.
*
* @return the active session or <code>null</code>, if no session is set as active
* @see #setActiveSession(HttpSession)
*/
public HttpSession getActiveSession() {
return activeSession;
}
/**
* Sets the active session.
*
* @param activeSession the new active session.
* @see #getActiveSession()
* @see #unsetActiveSession()
* @throws IllegalArgumentException If the session provided as parameter is null.
*/
public void setActiveSession(HttpSession activeSession) {
if (log.isInfoEnabled()) {
log.info("Setting new active session for site '" + site + "': " + activeSession);
}
if (activeSession == null) {
throw new IllegalArgumentException(
"When settting an active session, a non-null session has to be provided.");
}
if (this.activeSession == activeSession) {
return;
}
if (this.activeSession != null) {
this.activeSession.setActive(false);
// If the active session was one with no tokens, delete it, as it will probably not
// match anything from this point forward
if (this.activeSession.getTokenValuesCount() == 0) {
this.removeHttpSession(this.activeSession);
} else {
// Notify the model that the session is updated
model.fireHttpSessionUpdated(this.activeSession);
}
}
this.activeSession = activeSession;
activeSession.setActive(true);
// Notify the model that the session is updated
model.fireHttpSessionUpdated(activeSession);
}
/**
* Unset any active session for this site.
*
* @see #setActiveSession(HttpSession)
*/
public void unsetActiveSession() {
if (log.isInfoEnabled()) {
log.info("Setting no active session for site '" + site + "'.");
}
if (this.activeSession != null) {
this.activeSession.setActive(false);
// If the active session was one with no tokens, delete it, at it will probably not
// match anything from this point forward
if (this.activeSession.getTokenValuesCount() == 0) {
this.removeHttpSession(this.activeSession);
} else {
// Notify the model that the session is updated
model.fireHttpSessionUpdated(this.activeSession);
}
this.activeSession = null;
}
}
/**
* Generates a unique session name.
* <p>
* The generated name is guaranteed to be unique compared to existing session names. If a
* generated name is already in use (happens if the user creates a session with a name that is
* equal to the ones generated) a new one will be generated until it's unique.
* <p>
* The generated session name is composed by the (internationalised) word "Session" appended
* with a space character and an (unique sequential) integer identifier. Each time the method is
* called the integer identifier is incremented, at least, by 1 unit.
* <p>
* Example session names generated:
* <p>
*
* <pre>
* Session 0
* Session 1
* Session 2
* </pre>
*
* @return the generated unique session name
* @see #lastGeneratedSessionID
*/
private String generateUniqueSessionName() {
String name;
do {
name = MessageFormat.format(Constant.messages.getString("httpsessions.session.defaultName"),
Integer.valueOf(lastGeneratedSessionID++));
} while (!isSessionNameUnique(name));
return name;
}
/**
* Tells whether the given session {@code name} is unique or not, compared to existing session
* names.
*
* @param name the session name that will be checked
* @return {@code true} if the session name is unique, {@code false} otherwise
* @see #sessions
*/
private boolean isSessionNameUnique(final String name) {
synchronized (this.sessions) {
for (HttpSession session : sessions) {
if (name.equals(session.getName())) {
return false;
}
}
}
return true;
}
/**
* Validates that the session {@code name} is not {@code null} or an empty string.
*
* @param name the session name to be validated
* @throws IllegalArgumentException if the {@code name} is {@code null} or an empty string
*/
private static void validateSessionName(final String name) {
if (name == null) {
throw new IllegalArgumentException("Session name must not be null.");
}
if (name.isEmpty()) {
throw new IllegalArgumentException("Session name must not be empty.");
}
}
/**
* Creates an empty session with the given {@code name} and sets it as the active session.
* <p>
* <strong>Note:</strong> It's responsibility of the caller to ensure that no session with the
* given {@code name} already exists.
*
* @param name the name of the session that will be created and set as the active session
* @throws IllegalArgumentException if the {@code name} is {@code null} or an empty string
* @see #addHttpSession(HttpSession)
* @see #setActiveSession(HttpSession)
* @see #isSessionNameUnique(String)
*/
private void createEmptySessionAndSetAsActive(final String name) {
validateSessionName(name);
final HttpSession session = new HttpSession(name, extension.getHttpSessionTokensSet(getSite()));
addHttpSession(session);
setActiveSession(session);
}
/**
* Creates an empty session with the given {@code name}.
* <p>
* The newly created session is set as the active session.
* <p>
* <strong>Note:</strong> If a session with the given {@code name} already exists no action is
* taken.
*
* @param name the name of the session
* @throws IllegalArgumentException if the {@code name} is {@code null} or an empty string
* @see #setActiveSession(HttpSession)
*/
public void createEmptySession(final String name) {
validateSessionName(name);
if (!isSessionNameUnique(name)) {
return;
}
createEmptySessionAndSetAsActive(name);
}
/**
* Creates a new empty session.
* <p>
* The newly created session is set as the active session.
*
* @see #setActiveSession(HttpSession)
*/
public void createEmptySession() {
createEmptySessionAndSetAsActive(generateUniqueSessionName());
}
/**
* Gets the model.
*
* @return the model
*/
public HttpSessionsTableModel getModel() {
return model;
}
/**
* Process the http request message before being sent.
*
* @param message the message
*/
public void processHttpRequestMessage(HttpMessage message) {
// Get the session tokens for this site
HttpSessionTokensSet siteTokensSet = extension.getHttpSessionTokensSet(getSite());
// No tokens for this site, so no processing
if (siteTokensSet == null) {
log.debug("No session tokens for: " + this.getSite());
return;
}
// Get the matching session, based on the request header
List<HttpCookie> requestCookies = message.getRequestHeader().getHttpCookies();
HttpSession session = getMatchingHttpSession(requestCookies, siteTokensSet);
if (log.isDebugEnabled()) {
log.debug("Matching session for request message (for site " + getSite() + "): " + session);
}
// If any session is active (forced), change the necessary cookies
if (activeSession != null && activeSession != session) {
CookieBasedSessionManagementHelper.processMessageToMatchSession(message, requestCookies,
activeSession);
} else {
if (activeSession == session) {
log.debug("Session of request message is the same as the active session, so no request changes needed.");
} else {
log.debug("No active session is selected.");
}
// Store the session in the HttpMessage for caching purpose
message.setHttpSession(session);
}
}
/**
* Process the http response message received after a request.
*
* @param message the message
*/
public void processHttpResponseMessage(HttpMessage message) {
// Get the session tokens for this site
HttpSessionTokensSet siteTokensSet = extension.getHttpSessionTokensSet(getSite());
// No tokens for this site, so no processing
if (siteTokensSet == null) {
log.debug("No session tokens for: " + this.getSite());
return;
}
// Create an auxiliary map of token values and insert keys for every token
Map<String, Cookie> tokenValues = new HashMap<>();
// Get new values that were set for tokens (e.g. using SET-COOKIE headers), if any
List<HttpCookie> cookiesToSet = message.getResponseHeader().getHttpCookies(message.getRequestHeader().getHostName());
for (HttpCookie cookie : cookiesToSet) {
String lcCookieName = cookie.getName();
if (siteTokensSet.isSessionToken(lcCookieName)) {
try {
// Use 0 if max-age less than -1, Cookie class does not accept negative (expired) max-age (-1 has special
// meaning).
long maxAge = cookie.getMaxAge() < -1 ? 0 : cookie.getMaxAge();
Cookie ck = new Cookie(cookie.getDomain(),lcCookieName,cookie.getValue(),cookie.getPath(),(int) maxAge,cookie.getSecure());
tokenValues.put(lcCookieName, ck);
} catch (IllegalArgumentException e) {
log.warn("Failed to create cookie [" + cookie + "] for site [" + getSite() + "]: " + e.getMessage());
}
}
}
// Get the cookies present in the request
List<HttpCookie> requestCookies = message.getRequestHeader().getHttpCookies();
// XXX When an empty HttpSession is set in the message and the response
// contains session cookies, the empty HttpSession is reused which
// causes the number of messages matched to be incorrect.
// Get the session, based on the request header
HttpSession session = message.getHttpSession();
if (session == null || !session.isValid()) {
session = getMatchingHttpSession(requestCookies, siteTokensSet);
if (log.isDebugEnabled()) {
log.debug("Matching session for response message (from site " + getSite() + "): " + session);
}
} else {
if (log.isDebugEnabled()) {
log.debug("Matching cached session for response message (from site " + getSite() + "): "
+ session);
}
}
boolean newSession = false;
// If the session didn't exist, create it now
if (session == null) {
session = new HttpSession(generateUniqueSessionName(),
extension.getHttpSessionTokensSet(getSite()));
this.addHttpSession(session);
// Add all the existing tokens from the request, if they don't replace one in the
// response
for (HttpCookie cookie : requestCookies) {
String cookieName = cookie.getName();
if (siteTokensSet.isSessionToken(cookieName)) {
if (!tokenValues.containsKey(cookieName)) {
// We must ensure that a cookie as always a valid domain and path in order to be able to reuse it.
// HttpClient will discard invalid cookies
String domain = cookie.getDomain();
if (domain == null) {
domain = message.getRequestHeader().getHostName();
}
String path = cookie.getPath();
if (path == null) {
path = "/"; // Default path
}
Cookie ck = new Cookie(domain, cookieName, cookie.getValue(), path, (int) cookie.getMaxAge(), cookie.getSecure());
tokenValues.put(cookieName,ck);
}
}
}
newSession = true;
}
// Update the session
if (!tokenValues.isEmpty()) {
for (Entry<String, Cookie> tv : tokenValues.entrySet()) {
session.setTokenValue(tv.getKey(), tv.getValue());
}
}
if (newSession && log.isDebugEnabled()) {
log.debug("Created a new session as no match was found: " + session);
}
// Update the count of messages matched
session.setMessagesMatched(session.getMessagesMatched() + 1);
this.model.fireHttpSessionUpdated(session);
// Store the session in the HttpMessage for caching purpose
message.setHttpSession(session);
}
/**
* Gets the matching http session for a particular message containing a list of cookies.
*
* @param siteTokens the tokens
* @param cookies the cookies present in the request header of the message
* @return the matching http session, if any, or null if no existing session was found to match
* all the tokens
*/
private HttpSession getMatchingHttpSession(List<HttpCookie> cookies, final HttpSessionTokensSet siteTokens) {
Collection<HttpSession> sessionsCopy;
synchronized (sessions) {
sessionsCopy = new ArrayList<>(sessions);
}
return CookieBasedSessionManagementHelper.getMatchingHttpSession(sessionsCopy, cookies, siteTokens);
}
@Override
public String toString() {
return "HttpSessionsSite [site=" + site + ", activeSession=" + activeSession + ", sessions="
+ sessions + "]";
}
/**
* Cleans up the sessions, eliminating the given session token.
*
* @param token the session token
*/
protected void cleanupSessionToken(String token) {
// Empty check
if (sessions.isEmpty()) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Removing duplicates and cleaning up sessions for site - token: " + site + " - "
+ token);
}
synchronized (this.sessions) {
// If there are no more session tokens, delete all sessions
HttpSessionTokensSet siteTokensSet = extension.getHttpSessionTokensSet(site);
if (siteTokensSet == null) {
log.info("No more session tokens. Removing all sessions...");
// Invalidate all sessions
for (HttpSession session : this.sessions) {
session.invalidate();
}
// Remove all sessions
this.sessions.clear();
this.activeSession = null;
this.model.removeAllElements();
return;
}
// Iterate through all the sessions, eliminate the given token and eliminate any duplicates
Map<String, HttpSession> uniqueSession = new HashMap<>(sessions.size());
List<HttpSession> toDelete = new LinkedList<>();
for (HttpSession session : this.sessions) {
// Eliminate the token
session.removeToken(token);
if (session.getTokenValuesCount() == 0 && !session.isActive()) {
toDelete.add(session);
continue;
} else {
model.fireHttpSessionUpdated(session);
}
// If there is already a session with these tokens, mark one of them for deletion
if (uniqueSession.containsKey(session.getTokenValuesString())) {
HttpSession prevSession = uniqueSession.get(session.getTokenValuesString());
// If the latter session is active, put it into the map and delete the other
if (session.isActive()) {
toDelete.add(prevSession);
session.setMessagesMatched(session.getMessagesMatched()
+ prevSession.getMessagesMatched());
} else {
toDelete.add(session);
prevSession.setMessagesMatched(session.getMessagesMatched()
+ prevSession.getMessagesMatched());
}
}
// If it's the first one with these token values, keep it
else {
uniqueSession.put(session.getTokenValuesString(), session);
}
}
// Delete the duplicate sessions
if (log.isInfoEnabled()) {
log.info("Removing duplicate or empty sessions: " + toDelete);
}
Iterator<HttpSession> it = toDelete.iterator();
while (it.hasNext()) {
HttpSession ses = it.next();
ses.invalidate();
sessions.remove(ses);
model.removeHttpSession(ses);
}
}
}
/**
* Gets an unmodifiable set of the http sessions. Attempts to modify the returned set, whether
* direct or via its iterator, result in an UnsupportedOperationException.
*
* @return the http sessions
*/
public Set<HttpSession> getHttpSessions() {
synchronized (this.sessions) {
return Collections.unmodifiableSet(sessions);
}
}
/**
* Gets the http session with a particular name, if any, or {@code null} otherwise.
*
* @param name the name
* @return the http session with a given name, or null, if no such session exists
*/
public HttpSession getHttpSession(String name) {
synchronized (this.sessions) {
for (HttpSession session : sessions) {
if (session.getName().equals(name)) {
return session;
}
}
}
return null;
}
/**
* Renames a http session, making sure the new name is unique for the site.
*
* @param oldName the old name
* @param newName the new name
* @return true, if successful
*/
public boolean renameHttpSession(String oldName, String newName) {
// Check new name validity
if (newName == null || newName.isEmpty()) {
log.warn("Trying to rename session from " + oldName + " illegal name: " + newName);
return false;
}
// Check existing old name
HttpSession session = getHttpSession(oldName);
if (session == null) {
return false;
}
// Check new name uniqueness
if (getHttpSession(newName) != null) {
log.warn("Trying to rename session from " + oldName + " to already existing: " + newName);
return false;
}
// Rename the session and notify model
session.setName(newName);
this.model.fireHttpSessionUpdated(session);
return true;
}
static void resetLastGeneratedSessionId() {
lastGeneratedSessionID = 0;
}
public static int getNextSessionId() {
return lastGeneratedSessionID++;
}
}