/*********************************************************************** * * $CVSHeader$ * * This file is part of WebScarab, an Open Web Application Security * Project utility. For details, please see http://www.owasp.org/ * * Copyright (c) 2002 - 2004 Rogan Dawes * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Getting Source * ============== * * Source for this application is maintained at Sourceforge.net, a * repository for free software projects. * * For details, please see http://www.sourceforge.net/projects/owasp * */ /* * SiteModel.java * * Created on July 13, 2004, 3:58 PM */ package org.owasp.webscarab.model; import EDU.oswego.cs.dl.util.concurrent.Sync; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.logging.Logger; import java.util.logging.Level; import javax.swing.event.EventListenerList; import org.owasp.webscarab.util.MRUCache; import org.owasp.webscarab.util.ReentrantReaderPreferenceReadWriteLock; import java.io.File; /** * Provides a model of the conversations that have been seen * @author rogan */ public class FrameworkModel { private ReentrantReaderPreferenceReadWriteLock _rwl = new ReentrantReaderPreferenceReadWriteLock(); private static final Cookie[] NO_COOKIES = new Cookie[0]; private EventListenerList _listenerList = new EventListenerList(); // keeps a fairly small cache of recently used HttpUrl objects private Map<ConversationID, HttpUrl> _urlCache = new MRUCache<ConversationID, HttpUrl>(200); private SiteModelStore _store = null; private FrameworkUrlModel _urlModel; private FrameworkConversationModel _conversationModel; private boolean _modified = false; private Logger _logger = Logger.getLogger(getClass().getName()); /** * Creates a new ConversationModel */ public FrameworkModel() { _logger.setLevel(Level.INFO); _conversationModel = new FrameworkConversationModel(this); _urlModel = new FrameworkUrlModel(); } public void setSession(String type, Object store, String session) throws StoreException { try { _rwl.writeLock().acquire(); if (type.equals("FileSystem") && store instanceof File) { try { _store = new FileSystemStore((File) store); } catch (Exception e) { throw new StoreException("Error initialising session : " + e.getMessage()); } } else { _rwl.writeLock().release(); throw new StoreException("Unknown store type " + type + " and store " + store); } _rwl.readLock().acquire(); // downgrade _rwl.writeLock().release(); _urlModel.fireUrlsChanged(); _conversationModel.fireConversationsChanged(); fireCookiesChanged(); _rwl.readLock().release(); } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } } public Sync readLock() { return _rwl.readLock(); } public UrlModel getUrlModel() { return _urlModel; } public ConversationModel getConversationModel() { return _conversationModel; } /** * instructs the SiteModel to flush any unwritten data in the underlying store to * disk, prior to exit. * @throws StoreException if there is any problem writing to the store */ public void flush() throws StoreException { if (_modified) { try { _rwl.readLock().acquire(); try { _store.flush(); _modified = false; } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } } } /** * indicates whether there have been modifications to the site model *@return true if the model has been modified since it was last flushed, false otherwise */ public boolean isModified() { return _modified; } /** * reserve a conversation ID for later use. This is mostly used by the Proxy plugin * to allow it to add conversations in the order that they are seen, not in the * order that they are completed. * @return a new ConversationID */ public ConversationID reserveConversationID() { return new ConversationID(); } /** * adds a request and a response to the model, also specifying which plugin caused * it. * @param id the previously reserved ConversationID that identifies this conversation * @param request the request * @param response the response from the server * @param origin the plugin that created this conversation */ public void addConversation(ConversationID id, Date when, Request request, Response response, String origin) { try { HttpUrl url = request.getURL(); addUrl(url); // fires appropriate events _rwl.writeLock().acquire(); int index = _store.addConversation(id, when, request, response); _store.setConversationProperty(id, "METHOD", request.getMethod()); _store.setConversationProperty(id, "URL", request.getURL().toString()); _store.setConversationProperty(id, "STATUS", response.getStatusLine()); _store.setConversationProperty(id, "WHEN", Long.toString(when.getTime())); _store.setConversationProperty(id, "ORIGIN", origin); byte[] content=response.getContent(); if (content != null && content.length > 0) _store.setConversationProperty(id, "RESPONSE_SIZE", Integer.toString(content.length)); _rwl.readLock().acquire(); _rwl.writeLock().release(); _conversationModel.fireConversationAdded(id, index); // FIXME _rwl.readLock().release(); addUrlProperty(url, "METHODS", request.getMethod()); addUrlProperty(url, "STATUS", response.getStatusLine()); } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } _modified = true; } public String getConversationOrigin(ConversationID id) { return getConversationProperty(id, "ORIGIN"); } public Date getConversationDate(ConversationID id) { try { _rwl.readLock().acquire(); try { String when = getConversationProperty(id, "WHEN"); if (when == null) return null; try { long time = Long.parseLong(when); return new Date(time); } catch (NumberFormatException nfe) { System.err.println("NumberFormatException parsing date for Conversation " + id + ": " + nfe); return null; } } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * returns the url of the conversation in question * @param conversation the conversation * @return the url */ public HttpUrl getRequestUrl(ConversationID conversation) { try { _rwl.readLock().acquire(); try { // this allows us to reuse HttpUrl objects if (_urlCache.containsKey(conversation)) return (HttpUrl) _urlCache.get(conversation); String url = getConversationProperty(conversation, "URL"); try { HttpUrl httpUrl = new HttpUrl(url); _urlCache.put(conversation, httpUrl); return httpUrl; } catch (MalformedURLException mue) { System.err.println("Malformed URL for Conversation " + conversation + ": " + mue); return null; } } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * sets the specified property of the conversation * @param conversation the conversation ID * @param property the name of the property to change * @param value the value to use */ public void setConversationProperty(ConversationID conversation, String property, String value) { try { _rwl.writeLock().acquire(); _store.setConversationProperty(conversation, property, value); _rwl.readLock().acquire(); // downgrade _rwl.writeLock().release(); _conversationModel.fireConversationChanged(conversation, 0); // FIXME fireConversationPropertyChanged(conversation, property); _rwl.readLock().release(); } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } _modified = true; } /** * adds the value to a list of existing values for the specified property and conversation * @param conversation the conversation * @param property the name of the property * @param value the value to add */ public boolean addConversationProperty(ConversationID conversation, String property, String value) { boolean change = false; try { _rwl.writeLock().acquire(); change = _store.addConversationProperty(conversation, property, value); _rwl.readLock().acquire(); // downgrade to read lock _rwl.writeLock().release(); if (change) { _conversationModel.fireConversationChanged(conversation, 0); // FIXME fireConversationPropertyChanged(conversation, property); } _rwl.readLock().release(); } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } _modified = _modified || change; return change; } /** * returns a String containing the value that has been identified for a particular conversation property * @param conversation the conversation id * @param property the name of the property * @return the property value, or null if none has been set */ public String getConversationProperty(ConversationID conversation, String property) { String[] values = getConversationProperties(conversation, property); if (values == null || values.length == 0) return null; if (values.length == 1) return values[0]; StringBuffer value = new StringBuffer(values[0]); for (int i=1; i<values.length; i++) value.append(", ").append(values[i]); return value.toString(); } public String getRequestMethod(ConversationID id) { return getConversationProperty(id, "METHOD"); } public String getResponseStatus(ConversationID id) { return getConversationProperty(id, "STATUS"); } /** * returns a String array containing the values that has been set for a particular conversation property * @param conversation the conversation id * @param property the name of the property * @return an array of strings representing the property values, possibly zero length */ public String[] getConversationProperties(ConversationID conversation, String property) { try { _rwl.readLock().acquire(); try { return _store.getConversationProperties(conversation, property); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } private void addUrl(HttpUrl url) { try { _rwl.readLock().acquire(); try { if (!_store.isKnownUrl(url)) { HttpUrl[] path = url.getUrlHierarchy(); for (int i=0; i<path.length; i++) { if (!_store.isKnownUrl(path[i])) { _rwl.readLock().release(); // must give it up before writing // XXX We could be vulnerable to a race condition here // we should check again to make sure that it does not exist // AFTER we get our writelock // FIXME There is something very strange going on here // sometimes we deadlock if we just do a straight acquire // but there does not seem to be anything competing for the lock. // This works, but it feels like a kluge! FIXME!!! // _rwl.writeLock().acquire(); while (!_rwl.writeLock().attempt(5000)) { _logger.severe("Timed out waiting for write lock, trying again"); _rwl.debug(); } if (!_store.isKnownUrl(path[i])) { _store.addUrl(path[i]); _rwl.readLock().acquire(); // downgrade without giving up lock _rwl.writeLock().release(); _urlModel.fireUrlAdded(path[i], 0); // FIXME _modified = true; } else { // modified by some other thread?! Go through the motions . . . _rwl.readLock().acquire(); _rwl.writeLock().release(); } } } } } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } } /** * sets the specified property of the url * @param url the url * @param property the name of the property to change * @param value the value to use */ public void setUrlProperty(HttpUrl url, String property, String value) { addUrl(url); try { _rwl.writeLock().acquire(); _store.setUrlProperty(url, property, value); _rwl.readLock().acquire(); // downgrade write to read _rwl.writeLock().release(); _urlModel.fireUrlChanged(url, 0); // FIXME fireUrlPropertyChanged(url, property); _rwl.readLock().release(); } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } _modified = true; } /** * adds the value to a list of existing values for the specified property and Url * @param url the url * @param property the name of the property * @param value the value to add */ public boolean addUrlProperty(HttpUrl url, String property, String value) { boolean change = false; addUrl(url); try { _rwl.writeLock().acquire(); change = _store.addUrlProperty(url, property, value); _rwl.readLock().acquire(); _rwl.writeLock().release(); if (change) { _urlModel.fireUrlChanged(url, 0); fireUrlPropertyChanged(url, property); } _rwl.readLock().release(); } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } _modified = _modified || change; return change; } /** * returns a String array containing the values that has been set for a particular url property * @param url the url * @param property the name of the property * @return an array of strings representing the property values, possibly zero length */ public String[] getUrlProperties(HttpUrl url, String property) { try { _rwl.readLock().acquire(); try { return _store.getUrlProperties(url, property); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * returns a String containing the value that has been identified for a particular url property * @param url the url * @param property the name of the property * @return the property value, or null if none has been set */ public String getUrlProperty(HttpUrl url, String property) { String[] values = getUrlProperties(url, property); if (values == null || values.length == 0) return null; if (values.length == 1) return values[0]; StringBuffer value = new StringBuffer(30); value.append(values[0]); for(int i=1; i< values.length; i++) value.append(", ").append(values[i]); return value.toString(); } /** * returns the request corresponding to the conversation ID * @param conversation the conversation ID * @return the request */ public Request getRequest(ConversationID conversation) { try { _rwl.readLock().acquire(); try { return _store.getRequest(conversation); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * returns the response corresponding to the conversation ID * @param conversation the conversation ID * @return the response */ public Response getResponse(ConversationID conversation) { try { _rwl.readLock().acquire(); try { return _store.getResponse(conversation); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * adds a listener to the model * @param listener the listener to add */ public void addModelListener(FrameworkListener listener) { synchronized(_listenerList) { _listenerList.add(FrameworkListener.class, listener); } } /** * removes a listener from the model * @param listener the listener to remove */ public void removeModelListener(FrameworkListener listener) { synchronized(_listenerList) { _listenerList.remove(FrameworkListener.class, listener); } } /** * returns the number of uniquely named cookies that have been added to the model. * This does not consider changes in value of cookies. * @return the number of cookies */ public int getCookieCount() { if (_store == null) return 0; try { _rwl.readLock().acquire(); try { return _store.getCookieCount(); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } /** * returns the number of unique values that have been observed for the specified cookie * @param key a key identifying the cookie * @return the number of values in the model */ public int getCookieCount(String key) { try { _rwl.readLock().acquire(); try { return _store.getCookieCount(key); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } /** * returns a key representing the cookie name at the position specified * @return a key which can be used to get values for this cookie * @param index which cookie in the list */ public String getCookieAt(int index) { try { _rwl.readLock().acquire(); try { return _store.getCookieAt(index); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * returns the actual Cookie corresponding to the key and position specified * @param key the cookie identifier * @param index the position in the list * @return the cookie */ public Cookie getCookieAt(String key, int index) { try { _rwl.readLock().acquire(); try { return _store.getCookieAt(key, index); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * returns the position of the cookie in its list. * (The key is extracted from the cookie itself) * @param cookie the cookie * @return the position in the list */ public int getIndexOfCookie(Cookie cookie) { try { _rwl.readLock().acquire(); try { return _store.getIndexOfCookie(cookie); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } /** * returns the position of the cookie in its list. * (The key is extracted from the cookie itself) * @param cookie the cookie * @return the position in the list */ public int getIndexOfCookie(String key, Cookie cookie) { try { _rwl.readLock().acquire(); try { return _store.getIndexOfCookie(key, cookie); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } public Cookie getCurrentCookie(String key) { try { _rwl.readLock().acquire(); try { int count = _store.getCookieCount(key); return _store.getCookieAt(key, count-1); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } /** * adds a cookie to the model * @param cookie the cookie to add */ public void addCookie(Cookie cookie) { try { _rwl.writeLock().acquire(); boolean added = _store.addCookie(cookie); if (! added) { // we already had the cookie _rwl.writeLock().release(); } else { _modified = true; _rwl.readLock().acquire(); _rwl.writeLock().release(); fireCookieAdded(cookie); _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } } /** * removes a cookie from the model * @param cookie the cookie to remove */ public void removeCookie(Cookie cookie) { try { _rwl.writeLock().acquire(); boolean deleted = _store.removeCookie(cookie); if (deleted) { _modified = true; _rwl.readLock().acquire(); _rwl.writeLock().release(); fireCookieRemoved(cookie); _rwl.readLock().release(); } else { _rwl.writeLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); } } /** * returns an array of cookies that would be applicable to a request sent to the url. * @param url the url * @return an array of cookies, or a zero length array if there are none applicable. */ public Cookie[] getCookiesForUrl(HttpUrl url) { try { _rwl.readLock().acquire(); try { List<Cookie> cookies = new ArrayList<Cookie>(); String host = url.getHost(); String path = url.getPath(); int size = getCookieCount(); for (int i=0; i<size; i++) { String key = getCookieAt(i); Cookie cookie = getCurrentCookie(key); String domain = cookie.getDomain(); if (host.equals(domain) || (domain.startsWith(".") && host.endsWith(domain))) { if (path.startsWith(cookie.getPath())) { cookies.add(cookie); } } } return cookies.toArray(NO_COOKIES); } finally { _rwl.readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return NO_COOKIES; } } /** * notifies listeners that a completely new cookie was added * @param cookie the cookie */ protected void fireCookieAdded(Cookie cookie) { // Guaranteed to return a non-null array Object[] listeners = _listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event FrameworkEvent evt = new FrameworkEvent(this, cookie); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==FrameworkListener.class) { try { ((FrameworkListener)listeners[i+1]).cookieAdded(evt); } catch (Exception e) { _logger.severe("Unhandled exception: " + e); } } } } /** * notifies listeners that all values for cookie have been removed. * @param cookie the last cookie that was removed */ protected void fireCookieRemoved(Cookie cookie) { // Guaranteed to return a non-null array Object[] listeners = _listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event FrameworkEvent evt = new FrameworkEvent(this, cookie); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==FrameworkListener.class) { try { ((FrameworkListener)listeners[i+1]).cookieRemoved(evt); } catch (Exception e) { _logger.severe("Unhandled exception: " + e); } } } } /** * notifies listeners that all cookies in the model have changed */ protected void fireCookiesChanged() { // Guaranteed to return a non-null array Object[] listeners = _listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==FrameworkListener.class) { try { ((FrameworkListener)listeners[i+1]).cookiesChanged(); } catch (Exception e) { _logger.severe("Unhandled exception: " + e); } } } } /** * notifies listeners that a conversation property changed * @param cookie the cookie */ protected void fireConversationPropertyChanged(ConversationID id, String property) { // Guaranteed to return a non-null array Object[] listeners = _listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event FrameworkEvent evt = new FrameworkEvent(this, id, property); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==FrameworkListener.class) { try { ((FrameworkListener)listeners[i+1]).conversationPropertyChanged(evt); } catch (Exception e) { _logger.severe("Unhandled exception: " + e); } } } } /** * notifies listeners that an URL property changed * @param cookie the cookie */ protected void fireUrlPropertyChanged(HttpUrl url, String property) { // Guaranteed to return a non-null array Object[] listeners = _listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event FrameworkEvent evt = new FrameworkEvent(this, url, property); for (int i = listeners.length-2; i>=0; i-=2) { if (listeners[i]==FrameworkListener.class) { try { ((FrameworkListener)listeners[i+1]).urlPropertyChanged(evt); } catch (Exception e) { _logger.severe("Unhandled exception: " + e); } } } } private class FrameworkUrlModel extends AbstractUrlModel { public Sync readLock() { return _rwl.readLock(); } public int getChildCount(HttpUrl parent) { if (_store == null) return 0; try { readLock().acquire(); try { return _store.getChildCount(parent); } finally { readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } public int getIndexOf(HttpUrl url) { try { readLock().acquire(); try { return _store.getIndexOf(url); } finally { readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return -1; } } public HttpUrl getChildAt(HttpUrl parent, int index) { try { readLock().acquire(); try { return _store.getChildAt(parent, index); } finally { readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } } private class FrameworkConversationModel extends AbstractConversationModel { public FrameworkConversationModel(FrameworkModel model) { super(model); } public Sync readLock() { return _rwl.readLock(); } public ConversationID getConversationAt(int index) { try { readLock().acquire(); try { return _store.getConversationAt(null, index); } finally { readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return null; } } public int getConversationCount() { if (_store == null) return 0; try { readLock().acquire(); try { return _store.getConversationCount(null); } finally { readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } public int getIndexOfConversation(ConversationID id) { try { readLock().acquire(); try { return _store.getIndexOfConversation(null, id); } finally { readLock().release(); } } catch (InterruptedException ie) { _logger.severe("Interrupted! " + ie); return 0; } } } }