diff --git a/r2/example.ini b/r2/example.ini index d476a84de2..c6e1e19b17 100755 --- a/r2/example.ini +++ b/r2/example.ini @@ -91,6 +91,8 @@ shutdown_secret = 12345 https_endpoint = # name of the cookie to drop with login information login_cookie = reddit_session +# name of the admin cookie +admin_cookie = reddit_admin # the work factor for bcrypt, increment this every time computers double in # speed. don't worry, changing this won't break old passwords bcrypt_work_factor = 12 @@ -385,6 +387,8 @@ REPLY_AGE_LIMIT = 180 VOTE_AGE_LIMIT = 180 # minimum age, in days, of an account to be eligible to create a community min_membership_create_community = 30 +# how long the admin cookie should be valid for (in seconds) +ADMIN_COOKIE_TTL = 1800 # min amount of karma to edit WIKI_KARMA = 100 diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 09543577b1..7686f88e17 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -2462,5 +2462,5 @@ def POST_adminon(self, form, jquery, dest): if form.has_errors('password', errors.WRONG_PASSWORD): return - self.login(c.user, admin = True, rem = True) + self.enable_admin_mode(c.user) form.redirect(dest) diff --git a/r2/r2/controllers/buttons.py b/r2/r2/controllers/buttons.py index 3562349c99..cd51817ca0 100644 --- a/r2/r2/controllers/buttons.py +++ b/r2/r2/controllers/buttons.py @@ -36,10 +36,9 @@ def pre(self): MinimalController.pre(self) # override user loggedin behavior to ensure this page always # uses the page cache - (user, maybe_admin) = \ - valid_cookie(c.cookies[g.login_cookie].value - if g.login_cookie in c.cookies - else '') + user = valid_cookie(c.cookies[g.login_cookie].value + if g.login_cookie in c.cookies + else '') if user: self.user_is_loggedin = True diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 049a39293f..6f4b9e6d9c 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -1076,7 +1076,7 @@ def GET_adminoff(self, dest): """disable admin interaction with site.""" if not c.user.name in g.admins: return self.abort404() - self.login(c.user, admin = False, rem = True) + self.disable_admin_mode(c.user) return self.redirect(dest) def GET_validuser(self): diff --git a/r2/r2/controllers/reddit_base.py b/r2/r2/controllers/reddit_base.py index 0ad697f4e8..dd8d63756f 100644 --- a/r2/r2/controllers/reddit_base.py +++ b/r2/r2/controllers/reddit_base.py @@ -29,7 +29,7 @@ from r2.lib.utils import http_utils, is_subdomain, UniqueIterator, ip_and_slash16 from r2.lib.cache import LocalCache, make_key, MemcachedError import random as rand -from r2.models.account import valid_cookie, FakeAccount, valid_feed +from r2.models.account import valid_cookie, FakeAccount, valid_feed, valid_admin_cookie from r2.models.subreddit import Subreddit from r2.models import * from errors import ErrorSet @@ -40,7 +40,7 @@ from Cookie import CookieError from copy import copy from Cookie import CookieError -from datetime import datetime +from datetime import datetime, timedelta from hashlib import sha1, md5 from urllib import quote, unquote import simplejson @@ -746,14 +746,25 @@ def api_wrapper(self, kw): class RedditController(MinimalController): @staticmethod - def login(user, admin = False, rem = False): - c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(admin = admin), + def login(user, rem=False): + c.cookies[g.login_cookie] = Cookie(value = user.make_cookie(), expires = NEVER if rem else None) @staticmethod - def logout(admin = False): + def logout(): c.cookies[g.login_cookie] = Cookie(value='', expires=DELETE) + @staticmethod + def enable_admin_mode(user): + expiration_time = datetime.utcnow() + timedelta(seconds=g.ADMIN_COOKIE_TTL) + expiration = expiration_time.strftime('%a, %d %b %Y %H:%M:%S GMT') + c.cookies[g.admin_cookie] = Cookie(value=user.make_admin_cookie(), + expires=expiration) + + @staticmethod + def disable_admin_mode(user): + c.cookies[g.admin_cookie] = Cookie(value='', expires=DELETE) + def pre(self): c.response_wrappers = [] MinimalController.pre(self) @@ -781,12 +792,15 @@ def pre(self): # no logins for RSS feed unless valid_feed has already been called if not c.user: if c.extension != "rss": - (c.user, maybe_admin) = \ - valid_cookie(c.cookies[g.login_cookie].value - if g.login_cookie in c.cookies - else '') - if c.user: - c.user_is_loggedin = True + session_cookie = c.cookies.get(g.login_cookie) + if session_cookie: + c.user = valid_cookie(session_cookie.value) + if c.user: + c.user_is_loggedin = True + + admin_cookie = c.cookies.get(g.admin_cookie) + if c.user_is_loggedin and admin_cookie: + maybe_admin = valid_admin_cookie(admin_cookie.value) if not c.user: c.user = UnloggedUser(get_browser_langs()) diff --git a/r2/r2/lib/app_globals.py b/r2/r2/lib/app_globals.py index c667deb0b5..322667b8b9 100755 --- a/r2/r2/lib/app_globals.py +++ b/r2/r2/lib/app_globals.py @@ -116,6 +116,7 @@ class Globals(object): 'MODWINDOW', 'RATELIMIT', 'QUOTA_THRESHOLD', + 'ADMIN_COOKIE_TTL', 'num_comments', 'max_comments', 'max_comments_gold', diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index b029f9041c..07be0af9c0 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -753,11 +753,10 @@ def __init__(self, dest, *args, **kwargs): *args, **kwargs) def content(self): - return PasswordVerificationForm("adminon", dest=self.dest) + return PasswordVerificationForm(dest=self.dest) class PasswordVerificationForm(Templated): - def __init__(self, api, dest): - self.api = api + def __init__(self, dest): self.dest = dest Templated.__init__(self) diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index 2565b9ab0b..d3697d91fd 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -30,12 +30,15 @@ from r2.lib import filters from r2.lib.log import log_text -from pylons import g +from pylons import c, g, request from pylons.i18n import _ import time, sha from copy import copy from datetime import datetime, timedelta import bcrypt +import hmac +import hashlib + class AccountExists(Exception): pass @@ -202,16 +205,22 @@ def update_last_visit(self, current_time): (self.name, prev_visit, current_time)) set_last_visit(self) - def make_cookie(self, timestr = None, admin = False): + def make_cookie(self, timestr=None): if not self._loaded: self._load() timestr = timestr or time.strftime('%Y-%m-%dT%H:%M:%S') id_time = str(self._id) + ',' + timestr to_hash = ','.join((id_time, self.password, g.SECRET)) - if admin: - to_hash += 'admin' return id_time + ',' + sha.new(to_hash).hexdigest() + def make_admin_cookie(self, timestr=None): + if not self._loaded: + self._load() + timestr = timestr or datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') + hashable = ','.join((timestr, request.ip, request.user_agent, self.password)) + mac = hmac.new(g.SECRET, hashable, hashlib.sha1).hexdigest() + return ','.join((timestr, mac)) + def needs_captcha(self): return not g.disable_captcha and self.link_karma < 1 @@ -554,23 +563,46 @@ def valid_cookie(cookie): uid, timestr, hash = cookie.split(',') uid = int(uid) except: - return (False, False) + return False if g.read_only_mode: - return (False, False) + return False try: account = Account._byID(uid, True) if account._deleted: - return (False, False) + return False except NotFound: - return (False, False) + return False + + if constant_time_compare(cookie, account.make_cookie(timestr)): + return account + return False + + +def valid_admin_cookie(cookie): + if g.read_only_mode: + return False + + # parse the cookie + try: + timestr, hash = cookie.split(',') + except ValueError: + return False + + # make sure it's a recent cookie + try: + cookie_time = datetime.strptime(timestr, '%Y-%m-%dT%H:%M:%S') + except ValueError: + return False + + cookie_age = datetime.utcnow() - cookie_time + if cookie_age.total_seconds() > g.ADMIN_COOKIE_TTL: + return False + + # validate + return constant_time_compare(cookie, c.user.make_admin_cookie(timestr)) - if constant_time_compare(cookie, account.make_cookie(timestr, admin = False)): - return (account, False) - elif constant_time_compare(cookie, account.make_cookie(timestr, admin = True)): - return (account, True) - return (False, False) def valid_feed(name, feedhash, path): if name and feedhash and path: