[TurboGears] Patch backported from upstream to add support for marking session cookie httponly
Toshio くらとみ
toshio at fedoraproject.org
Sat Nov 19 05:12:03 UTC 2011
commit ebba2cee53e9d183745751e80284a4c897cd5d86
Author: Toshio Kuratomi <toshio at fedoraproject.org>
Date: Fri Nov 18 21:11:53 2011 -0800
Patch backported from upstream to add support for marking session cookie
httponly
TurboGears.spec | 9 ++-
turbogears-httponly.patch | 250 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 258 insertions(+), 1 deletions(-)
---
diff --git a/TurboGears.spec b/TurboGears.spec
index eaac1e2..f70edb3 100644
--- a/TurboGears.spec
+++ b/TurboGears.spec
@@ -4,7 +4,7 @@
Name: TurboGears
Version: 1.1.3
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Back-to-front web development in Python
Group: Development/Languages
@@ -16,6 +16,8 @@ Patch0: %{name}-1.0.8-cherrypyreq.patch
# Reported upstream http://trac.turbogears.org/ticket/2419
Patch1: turbogears-sqlcreate.patch
Patch2: turbogears-feed.patch
+# Backport upstream commit r7389
+Patch3: turbogears-httponly.patch
# Patch to allow turbogears to work with old turbokid until/unless RHEL6
# updates
Patch100: TurboGears-old-turbokid.patch
@@ -111,6 +113,7 @@ TurboGears is easy to use for a wide range of web applications.
%patch0 -b .cherrypyreq
%patch1 -p1 -b .sqlcreate
%patch2 -p1 -b .feed
+%patch3 -p3 -b .httponly
%if 0%{?rhel} && 0%{?rhel} >= 6
%patch100 -p1 -b .deps
%endif
@@ -148,6 +151,10 @@ rm -rf %{buildroot}
%{python_sitelib}/turbogears/
%changelog
+* Fri Jul 15 2011 Toshio Kuratomi <toshio at feoraproject.org> - 1.1.3-2
+- Patch backported from upstream to add support for marking session cookie
+ httponly
+
* Fri Jul 15 2011 Toshio Kuratomi <toshio at feoraproject.org> - 1.1.3-1
- Update to 1.1.3
diff --git a/turbogears-httponly.patch b/turbogears-httponly.patch
new file mode 100644
index 0000000..1d58c99
--- /dev/null
+++ b/turbogears-httponly.patch
@@ -0,0 +1,250 @@
+--- a/branches/1.1/turbogears/visit/api.py
++++ b/branches/1.1/turbogears/visit/api.py
+@@ -21,230 +21,244 @@
+ from sha import new as sha1
+ import threading
+ import time
+-
++from Cookie import Morsel
+ from random import random
+ from datetime import timedelta, datetime
+
+ import cherrypy
+ import pkg_resources
+
+ from cherrypy.filters.basefilter import BaseFilter
+ from turbogears import config
+ from turbogears.util import load_class
+
+ log = logging.getLogger("turbogears.visit")
+
+ # Global VisitManager
+ _manager = None
+
+ # Global list of plugins for the Visit Tracking framework
+ _plugins = list()
+
+ # Accessor functions for getting and setting the current visit information.
+ def current():
+ """Retrieve the current visit record from the cherrypy request."""
+ return getattr(cherrypy.request, "tg_visit", None)
+
+ def set_current(visit):
+ """Set the current visit record on the cherrypy request being processed."""
+ cherrypy.request.tg_visit = visit
+
+ def _create_visit_manager(timeout):
+ """Create a VisitManager based on the plugin specified in the config file."""
+ plugin_name = config.get("visit.manager", "sqlalchemy")
+ plugins = pkg_resources.iter_entry_points(
+ "turbogears.visit.manager", plugin_name)
+ log.debug("Loading visit manager from plugin: %s", plugin_name)
+ provider_class = None
+ for entrypoint in plugins:
+ try:
+ provider_class = entrypoint.load()
+ break
+ except ImportError, e:
+ log.error("Error loading visit plugin '%s': %s", entrypoint, e)
+
+ if not provider_class and '.' in plugin_name:
+ try:
+ provider_class = load_class(plugin_name)
+ except ImportError, e:
+ log.error("Error loading visit class '%s': %s", plugin_name, e)
+ if not provider_class:
+ raise RuntimeError("VisitManager plugin missing: %s" % plugin_name)
+ return provider_class(timeout)
+
+
+ # Interface for the TurboGears extension
+
+ def start_extension():
+ global _manager
+
+ # Bail out if the application hasn't enabled this extension
+ if not config.get("visit.on", False):
+ return
+
+ # Bail out if this extension is already running
+ if _manager:
+ log.warning("Visit manager already running.")
+ return
+
+ # How long may the visit be idle before a new visit ID is assigned?
+ # The default is 20 minutes.
+ timeout = timedelta(minutes=config.get("visit.timeout", 20))
+ log.info("Visit Tracking starting (timeout = %i sec).", timeout.seconds)
+ # Create the thread that manages updating the visits
+ _manager = _create_visit_manager(timeout)
+
+ visit_filter = VisitFilter()
+ # Install Filter into the root filter chain
+ if not hasattr(cherrypy.root, "_cp_filters"):
+ cherrypy.root._cp_filters = list()
+ if not visit_filter in cherrypy.root._cp_filters:
+ cherrypy.root._cp_filters.append(visit_filter)
+
+ def shutdown_extension():
+ # Bail out if this extension is not running.
+ global _manager
+ if not _manager:
+ return
+ log.info("Visit Tracking shutting down.")
+ _manager.shutdown()
+ _manager = None
+
+ def create_extension_model():
+ """Create the data model of the VisitManager if one exists."""
+ if _manager:
+ _manager.create_model()
+
+ def enable_visit_plugin(plugin):
+ """Register a visit tracking plugin.
+
+ These plugins will be called for each request.
+
+ """
+ _plugins.append(plugin)
+
+
+ class Visit(object):
+ """Basic container for visit related data."""
+
+ def __init__(self, key, is_new):
+ self.key = key
+ self.is_new = is_new
+
+
+ class VisitFilter(BaseFilter):
+ """A filter that automatically tracks visitors."""
+
+ def __init__(self):
+ get = config.get
+ # Where to look for the session key in the request and in which order
+ self.source = [s.strip().lower() for s in
+ get("visit.source", "cookie").split(',')]
+ if set(self.source).difference(('cookie', 'form')):
+- log.warning("Unsupported 'visit.source' '%s' in configuration.")
++ log.error("Unsupported visit.source in configuration.")
+ # Get the name to use for the identity cookie.
+ self.cookie_name = get("visit.cookie.name", "tg-visit")
++ if Morsel().isReservedKey(self.cookie_name):
++ log.error("Reserved name chosen as visit.cookie.name.")
+ # and the name of the request param. MUST NOT contain dashes or dots,
+ # otherwise the NestedVariablesFilter will choke on it.
+ self.visit_key_param = get("visit.form.name", "tg_visit")
+ # TODO: The path should probably default to whatever
+ # the root is masquerading as in the event of a
+ # virtual path filter.
+ self.cookie_path = get("visit.cookie.path", "/")
+ # The secure bit should be set for HTTPS only sites
+ self.cookie_secure = get("visit.cookie.secure", False)
+ # By default, I don't specify the cookie domain.
+ self.cookie_domain = get("visit.cookie.domain", None)
+- assert self.cookie_domain != "localhost", "localhost" \
+- " is not a valid value for visit.cookie.domain. Try None instead."
++ if self.cookie_domain == "localhost":
++ log.error("Invalid value 'localhost' for visit.cookie.domain."
++ " Try None instead.")
+ # Use max age only if the cookie shall explicitly be permanent
+ self.cookie_max_age = get("visit.cookie.permanent",
+ False) and int(get("visit.timeout", "20")) * 60 or None
++ # Use httponly to specify that the cookie shall only be transfered
++ # in HTTP requests, and shall not be accessible through JavaScript.
++ # This is intended to mitigate some forms of cross-site scripting.
++ self.cookie_httponly = get("visit.cookie.httponly", False)
++ if self.cookie_httponly and not Morsel().isReservedKey('httponly'):
++ # Python versions < 2.6 do not support the httponly key
++ log.error("The visit.cookie.httponly setting"
++ " is not supported by this Python version.")
++ self.cookie_httponly = False
+ log.info("Visit filter initialized")
+
+ def before_main(self):
+ """Check whether submitted request belongs to an existing visit."""
+ if not config.get("visit.on", True):
+ set_current(None)
+ return
+ visit = current()
+ if not visit:
+ visit_key = None
+ for source in self.source:
+ if source == 'cookie':
+ visit_key = cherrypy.request.simple_cookie.get(
+ self.cookie_name)
+ if visit_key:
+ visit_key = visit_key.value
+ log.debug("Retrieved visit key '%s' from cookie '%s'.",
+ visit_key, self.cookie_name)
+ elif source == 'form':
+ visit_key = cherrypy.request.params.pop(
+ self.visit_key_param, None)
+ log.debug(
+ "Retrieved visit key '%s' from request param '%s'.",
+ visit_key, self.visit_key_param)
+ if visit_key:
+ visit = _manager.visit_for_key(visit_key)
+ break
+ if visit:
+ log.debug("Using visit from request with key: %s", visit_key)
+ else:
+ visit_key = self._generate_key()
+ visit = _manager.new_visit_with_key(visit_key)
+ log.debug("Created new visit with key: %s", visit_key)
+ self.send_cookie(visit_key)
+ set_current(visit)
+ # Inform all the plugins that a request has been made for the current
+ # visit. This gives plugins the opportunity to track click-path or
+ # retrieve the visitor's identity.
+ try:
+ for plugin in _plugins:
+ plugin.record_request(visit)
+ except cherrypy.InternalRedirect, e:
+ # Can't allow an InternalRedirect here because CherryPy is dumb,
+ # instead change cherrypy.request.object_path to the url desired.
+ cherrypy.request.object_path = e.path
+
+ @staticmethod
+ def _generate_key():
+ """Return a (pseudo)random hash based on seed."""
+ # Adding remoteHost and remotePort doesn't make this any more secure,
+ # but it makes people feel secure... It's not like I check to make
+ # certain you're actually making requests from that host and port. So
+ # it's basically more noise.
+ key_string = '%s%s%s%s' % (random(), datetime.now(),
+ cherrypy.request.remote_host, cherrypy.request.remote_port)
+ return sha1(key_string).hexdigest()
+
+ def clear_cookie(self):
+ """Clear any existing visit ID cookie."""
+ cookies = cherrypy.response.simple_cookie
+ # clear the cookie
+ log.debug("Clearing visit ID cookie")
+ cookies[self.cookie_name] = ''
+ cookies[self.cookie_name]['path'] = self.cookie_path
+ cookies[self.cookie_name]['expires'] = ''
+ cookies[self.cookie_name]['max-age'] = 0
+
+ def send_cookie(self, visit_key):
+ """Send an visit ID cookie back to the browser."""
+ cookies = cherrypy.response.simple_cookie
+ cookies[self.cookie_name] = visit_key
+ cookies[self.cookie_name]['path'] = self.cookie_path
+ if self.cookie_secure:
+ cookies[self.cookie_name]['secure'] = True
+ if self.cookie_domain:
+ cookies[self.cookie_name]['domain'] = self.cookie_domain
+ max_age = self.cookie_max_age
+ if max_age:
+ # use 'expires' because MSIE ignores 'max-age'
+ cookies[self.cookie_name]['expires'] = '"%s"' % time.strftime(
+ "%a, %d-%b-%Y %H:%M:%S GMT",
+ time.gmtime(time.time() + max_age))
+ # 'max-age' takes precedence on standard conformant browsers
+ # (this is better because it has no time sync issues)
+ cookies[self.cookie_name]['max-age'] = max_age
++ if self.cookie_httponly:
++ cookies[self.cookie_name]['httponly'] = True
+ log.debug("Sending visit ID cookie: %s",
+ cookies[self.cookie_name].output())
More information about the scm-commits
mailing list