Commits

David Bennett committed 36fb895

Do a simple xor crypt on the key before b64 to make it a little more obfuscated. Also, urlquote the object pk just in case it's a string with special characters in it (like "/").

  • Participants
  • Parent commits 21d0d41

Comments (0)

Files changed (6)

docs/settings.rst

 
     Whether or not to base64 encode part of the URL.
 
+.. attribute:: PRIVATE_MEDIA_ENCODE_KEY
+
+    :default: ``django.conf.settings.SECRET_KEY``
+
+    The key to use for URL encoding.
+
+    .. warning:: Changing your SECRET_KEY/ENCODE_KEY will change your file URLs!
+
 .. attribute:: PRIVATE_MEDIA_INSECURE
 
     :default: ``django.conf.settings.DEBUG``

private_media/conf.py

 class PrivateMediaConf(AppConf):
     # The URL prefix for Nginx internal location
     URL = '/_private/media/'
-    # Whether or not to base64 encode part of the URL
+    # Whether or not to xor+base64 encode part of the URL
     ENCODE_URL = True
+    # The key to use for URL encoding
+    ENCODE_KEY = settings.SECRET_KEY
     # Whether or not to serve files directly from Django
-    INSECURE = None
+    INSECURE = settings.DEBUG
     # Default handler to use when not serving directly from Django
     DEFAULT_HANDLER = 'private_media.handlers.nginx'
 
     class Meta:
         prefix = 'private_media'
-
-    def configure_insecure(self, value):
-        return settings.DEBUG if value is None else value

private_media/fields.py

-from base64 import urlsafe_b64encode
 from django.contrib.contenttypes.models import ContentType
 from django.core.urlresolvers import reverse
 from django.db.models.fields import files
 from private_media.conf import settings
+from private_media.utils import encode_key, urlquote
 
 
 class PrivateFieldFile(files.FieldFile):
 
     def _get_url(self):
         self._require_file()
-        content_type = ContentType.objects.get_for_model(self.instance)
+        key = (
+            ContentType.objects.get_for_model(self.instance).pk,
+            urlquote(self.instance.pk, safe=''),
+            self.field.name,
+        )
         if settings.PRIVATE_MEDIA_ENCODE_URL:
-            key = urlsafe_b64encode('%s/%s/%s' % (
-                content_type.pk, self.instance.pk, self.field.name)).strip('=')
-            return reverse('private_media-get_file_b64', args=[key, self.name])
+            key = encode_key('%s/%s/%s' % key)
+            return reverse('private_media-get_file_enc', args=(key, self.name))
         else:
-            return reverse('private_media-get_file', args=[
-                content_type.pk, self.instance.pk, self.field.name, self.name])
+            key += (self.name,)
+            return reverse('private_media-get_file', args=key)
     url = property(_get_url)
 
 

private_media/urls.py

 
 
 urlpatterns = patterns('private_media.views',
-    url(r'^(\d+)/(\d+)/([a-zA-Z_][0-9a-zA-Z_]*)/(.*)$', 'get_file',
+    url(r'^(\d+)/(.+)/([a-zA-Z_][0-9a-zA-Z_]*)/(.+)$', 'get_file',
         name='private_media-get_file'),
-    url(r'^([0-9a-zA-Z_-]+)/(.*)$', 'get_file_b64',
-        name='private_media-get_file_b64'),
+    url(r'^([0-9a-zA-Z_-]+)/(.+)$', 'get_file_enc',
+        name='private_media-get_file_enc'),
 )

private_media/utils.py

+from base64 import urlsafe_b64encode, urlsafe_b64decode
+from itertools import izip, cycle
+from urllib import unquote
+from django.utils.http import urlquote, force_unicode, smart_str, allow_lazy
+from private_media.conf import settings
+assert urlquote
+
+
+def xor_crypt_string(data, key=None):
+    if key is None:
+        key = settings.PRIVATE_MEDIA_ENCODE_KEY
+    return ''.join(chr(ord(x) ^ ord(y)) for (x,y) in izip(data, cycle(key)))
+
+
+def encode_key(key):
+    return urlsafe_b64encode(xor_crypt_string(key)).strip('=')
+
+
+def decode_key(key):
+    return xor_crypt_string(urlsafe_b64decode(str(key) + '=='))
+
+
+def urlunquote(quoted_url):
+    """
+    A wrapper for Python's urllib.unquote() function that can operate on
+    the result of django.utils.http.urlquote().
+    """
+    return force_unicode(unquote(smart_str(quoted_url)))
+urlunquote = allow_lazy(urlunquote, unicode)

private_media/views.py

 import os
-from base64 import urlsafe_b64decode
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import get_callable
 from django.http import Http404
 from django.views.static import serve
 from private_media.conf import settings
+from private_media.utils import decode_key, urlunquote
 
 
-def get_file(request, content_type_id, object_id, field_name, file_name):
+def get_file(request, content_type_id, object_pk, field_name, file_name):
     try:
         content_type = ContentType.objects.get_for_id(content_type_id)
         instance = content_type.get_object_for_this_type(**{
-            'pk': object_id,
+            'pk': object_pk,
             field_name: file_name,
         })
     except Exception, e:
     return response
 
 
-def get_file_b64(request, b64_key, file_name):
+def get_file_enc(request, enc_key, file_name):
     try:
-        key = urlsafe_b64decode(str(b64_key) + '==')
-        content_type_id, object_id, field_name = key.split('/', 2)
+        key = decode_key(enc_key)
+        content_type_id, object_pk, field_name = key.split('/', 2)
+        object_pk = urlunquote(object_pk)
     except Exception, e:
         raise Http404(e if settings.DEBUG else 'File not found.')
-    return get_file(request, content_type_id, object_id, field_name, file_name)
+    return get_file(request, content_type_id, object_pk, field_name, file_name)