Restrict next url parameter
[astakos] / snf-astakos-app / astakos / im / util.py
index b266765..eb9d759 100644 (file)
@@ -36,19 +36,22 @@ import datetime
 import time
 
 from urllib import quote
-from urlparse import urlsplit, urlunsplit
-from functools import wraps
+from urlparse import urlsplit, urlunsplit, urlparse
 
 from datetime import tzinfo, timedelta
-from django.http import HttpResponse, urlencode
+from django.http import HttpResponse, HttpResponseBadRequest, urlencode
 from django.template import RequestContext
-from django.contrib.sites.models import Site
 from django.utils.translation import ugettext as _
-from django.contrib.auth import login, authenticate
+from django.contrib.auth import authenticate
 from django.core.urlresolvers import reverse
+from django.core.exceptions import ValidationError
 
 from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
-from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE
+from astakos.im.settings import (
+    INVITATIONS_PER_LEVEL, COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE,
+    FORCE_PROFILE_UPDATE, LOGGING_LEVEL
+)
+from astakos.im.functions import login
 
 logger = logging.getLogger(__name__)
 
@@ -70,28 +73,6 @@ def isoformat(d):
 def epoch(datetime):
     return int(time.mktime(datetime.timetuple())*1000)
 
-def get_or_create_user(email, realname='', first_name='', last_name='', affiliation='', level=0, provider='local', password=''):
-    """Find or register a user into the internal database
-       and issue a token for subsequent requests.
-    """
-    user, created = AstakosUser.objects.get_or_create(email=email,
-        defaults={
-            'password':password,
-            'affiliation':affiliation,
-            'level':level,
-            'invitations':INVITATIONS_PER_LEVEL.get(level, 0),
-            'provider':provider,
-            'realname':realname,
-            'first_name':first_name,
-            'last_name':last_name
-        })
-    if created:
-        user.renew_token()
-        user.save()
-        logger.info('Created user %s', user)
-    
-    return user
-
 def get_context(request, extra_context={}, **kwargs):
     if not extra_context:
         extra_context = {}
@@ -117,6 +98,51 @@ def get_invitation(request):
         raise ValueError(_('Email: %s is reserved' % invitation.username))
     return invitation
 
+def restrict_next(url, domain=None, allowed_schemes=()):
+    """
+    Return url if having the supplied ``domain`` (if present) or one of the ``allowed_schemes``.
+    Otherwise return None.
+    
+    >>> print restrict_next('/im/feedback', '.okeanos.grnet.gr')
+    /im/feedback
+    >>> print restrict_next('pithos.okeanos.grnet.gr/im/feedback', '.okeanos.grnet.gr')
+    pithos.okeanos.grnet.gr/im/feedback
+    >>> print restrict_next('https://pithos.okeanos.grnet.gr/im/feedback', '.okeanos.grnet.gr')
+    https://pithos.okeanos.grnet.gr/im/feedback
+    >>> print restrict_next('pithos://127.0.0,1', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('pithos://127.0.0,1', '.okeanos.grnet.gr', allowed_schemes=('pithos'))
+    pithos://127.0.0,1
+    >>> print restrict_next('node1.example.com', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('//node1.example.com', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('https://node1.example.com', '.okeanos.grnet.gr')
+    None
+    >>> print restrict_next('https://node1.example.com')
+    https://node1.example.com
+    >>> print restrict_next('//node1.example.com')
+    //node1.example.com
+    >>> print restrict_next('node1.example.com')
+    node1.example.com
+    """
+    if not url:
+        return
+    parts = urlparse(url, scheme='http')
+    if not parts.netloc:
+        # fix url if does not conforms RFC 1808
+        url = '//%s' % url
+        parts = urlparse(url, scheme='http')
+    # TODO more scientific checks?
+    if not parts.netloc:    # internal url
+        return url
+    elif not domain:
+        return url
+    elif parts.netloc.endswith(domain):
+        return url
+    elif parts.scheme in allowed_schemes:
+        return url
+
 def prepare_response(request, user, next='', renew=False):
     """Return the unique username and the token
        as 'X-Auth-User' and 'X-Auth-Token' headers,
@@ -131,7 +157,12 @@ def prepare_response(request, user, next='', renew=False):
     renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now())
     if renew:
         user.renew_token()
-        user.save()
+        try:
+            user.save()
+        except ValidationError, e:
+            return HttpResponseBadRequest(e) 
+    
+    next = restrict_next(next, domain=COOKIE_DOMAIN)
     
     if FORCE_PROFILE_UPDATE and not user.is_verified and not user.is_superuser:
         params = ''
@@ -145,6 +176,7 @@ def prepare_response(request, user, next='', renew=False):
     user = authenticate(email=user.email, auth_token=user.auth_token)
     login(request, user)
     set_cookie(response, user)
+    request.session.set_expiry(user.auth_token_expires)
     
     if not next:
         next = reverse('astakos.im.views.index')
@@ -159,6 +191,8 @@ def set_cookie(response, user):
     response.set_cookie(COOKIE_NAME, value=cookie_value,
                         expires=expire_fmt, path='/',
                         domain=COOKIE_DOMAIN, secure=COOKIE_SECURE)
+    msg = 'Cookie [expiring %s] set for %s' % (user.auth_token_expires, user.email)
+    logger._log(LOGGING_LEVEL, msg, [])
 
 class lazy_string(object):
     def __init__(self, function, *args, **kwargs):
@@ -178,4 +212,7 @@ def reserved_email(email):
     return AstakosUser.objects.filter(email = email).count() != 0
 
 def get_query(request):
-    return request.__getattribute__(request.method)
\ No newline at end of file
+    try:
+        return request.__getattribute__(request.method)
+    except AttributeError:
+        return {}
\ No newline at end of file