Commits

Yuri van der Meer committed fef7bbf

First update in three years: refactored using Django class-based view and using urllib2 instead of httplib2

Comments (0)

Files changed (7)

httpproxy/admin.py

 class ResponseInline(admin.StackedInline):
     model = Response
 
+
 class RequestParameterInline(admin.TabularInline):
     model = RequestParameter
     extra = 1
 
+
 class RequestAdmin(admin.ModelAdmin):
-    list_display = ('domain', 'port', 'path', 'querystring_display', 'date')
-    list_filter = ('domain', 'port')
-    inlines = [RequestParameterInline, ResponseInline]
+    list_display = ('method', 'domain', 'port', 'path', 'querystring_display', 'date')
+    list_filter = ('method', 'domain', 'port')
+    inlines = (RequestParameterInline, ResponseInline)
+
 
 class ResponseAdmin(admin.ModelAdmin):
     list_display = ('request_domain', 'request_path', 'request_querystring', 'status', 'content_type')

httpproxy/decorators.py

 
 from django.core.urlresolvers import reverse
 
-from httpproxy import settings
-from httpproxy.recorder import ProxyRecorder
-
-
-proxy = ProxyRecorder(settings.PROXY_DOMAIN, settings.PROXY_PORT)
-
-def normalize_request(fn):
-    """
-    Updates all path-related info in the original request object with the url
-    given to the proxy.
-    
-    This way, any further processing of the proxy'd request can just ignore
-    the url given to the proxy and use request.path safely instead.
-    """
-    def decorate(request, url, *args, **kwargs):
-        if not url.startswith('/'):
-            url = u'/' + url
-        request.path = url
-        request.path_info = url
-        request.META['PATH_INFO'] = url
-        return fn(request, *args, **kwargs)
-    return decorate
-
-def record(fn):
-    """
-    Decorator for recording the request being made and its response.
-    """
-    def decorate(request, *args, **kwargs):
-        
-        # Make the actual live request as usual
-        response = fn(request, *args, **kwargs)
-        
-        # Record the request and response
-        proxy.record(request, response)
-
-        return response
-    return decorate
-
-def play(fn):
-    """
-    Decorator for playing back the response to a request, based on a
-    previously recorded request/response.
-    """
-    def decorate(request, *args, **kwargs):
-        return proxy.playback(request)
-    return decorate
 
 REWRITE_REGEX = re.compile(r'((?:src|action|href)=["\'])/')
 
     """
     def decorate(request, *args, **kwargs):
         response = fn(request, *args, **kwargs)
-        proxy_root = reverse('httpproxy.views.proxy', 
+        proxy_root = reverse('httpproxy.views.proxy',
             kwargs={'url': ''}
         )
         response.content = REWRITE_REGEX.sub(r'\1%s' % proxy_root, response.content)

httpproxy/exceptions.py

 from django.http import Http404
 """
-Some generic exceptions that can occur in the Django HTTP Proxy.
+Some generic exceptions that can occur in Django HTTP Proxy
 """
 
 class UnkownProxyMode(Exception):
     pass
 
+
 class ResponseUnsupported(Exception):
     pass
 
+
 class RequestNotRecorded(Http404):
     pass
 

httpproxy/models.py

 
 
 class Request(models.Model):
-    domain = models.CharField(_('domain'), max_length=100)  
+    method = models.CharField(_('method'), max_length=20)
+    domain = models.CharField(_('domain'), max_length=100)
     port = models.PositiveSmallIntegerField(default=80)
     path = models.CharField(_('path'), max_length=250)
     date = models.DateTimeField(auto_now=True)
     querykey = models.CharField(_('query key'), max_length=255, editable=False)
-    
+
     @property
     def querystring(self):
         return self.parameters.urlencode()
     querystring_display.short_description = 'querystring'
 
     def __unicode__(self):
-        output = u'%s:%d%s' % (self.domain, self.port, self.path)
+        output = u'%s %s:%d%s' % \
+                (self.method, self.domain, self.port, self.path)
         if self.querystring:
             output += '?%s' % self.querystring
         return output[:50] # TODO add elipsed if truncating
     class Meta:
         verbose_name = _('request')
         verbose_name_plural = _('requests')
-        unique_together = ('domain', 'port', 'path', 'querykey')
+        unique_together = ('method', 'domain', 'port', 'path', 'querykey')
         get_latest_by = 'date'
 
 
 class RequestParameterManager(models.Manager):
-    
+
     def urlencode(self):
         output = []
         for param in self.values('name', 'value'):
             output.extend([urlencode({param['name']: param['value']})])
         return '&'.join(output)
-    
+
 
 class RequestParameter(models.Model):
     REQUEST_TYPES = (
         ordering = ('order',)
         verbose_name = _('request parameter')
         verbose_name_plural = _('request parameters')
-    
+
 
 class Response(models.Model):
     request = models.OneToOneField(Request, verbose_name=_('request'))
     status = models.PositiveSmallIntegerField(default=200)
     content_type = models.CharField(_('content type'), max_length=200)
     content = models.TextField(_('content'))
-    
+
     @property
     def request_domain(self):
         return self.request.domain
     @property
     def request_path(self):
         return self.request.path
-    
+
     @property
     def request_querystring(self):
         return self.request.querystring

httpproxy/recorder.py

 from httpproxy.models import Request, Response
 
 
-RESPONSE_TYPES_SUPPORTED = (
-    'application/javascript',
-    'application/xml',
-    'text/css',
-    'text/html',
-    'text/javascript',
-    'text/plain',
-    'text/xml',
-)
-
-logger = logging.getLogger('httpproxy')
-logger.setLevel(logging.DEBUG)
+logger = logging.getLogger(__name__)
 
 class ProxyRecorder(object):
     """
     Facilitates recording and playback of Django HTTP requests and responses.
     """
-    
+
+    response_types_supported = (
+        'application/javascript',
+        'application/xml',
+        'text/css',
+        'text/html',
+        'text/javascript',
+        'text/plain',
+        'text/xml',
+    )
+
     def __init__(self, domain, port):
         super(ProxyRecorder, self).__init__()
         self.domain, self.port = domain, port
-    
+
     def record(self, request, response):
         """
         Attempts to record the request and the corresponding response.
             self.record_response(recorded_request, response)
         elif not settings.PROXY_IGNORE_UNSUPPORTED:
             raise ResponseUnsupported('Response of type "%s" could not be recorded.' % response['Content-Type'])
-    
+
     def record_request(self, request):
         """
         Saves the provided request, including its parameters.
         """
         logger.info('Recording: GET "%s"' % self._request_string(request))
-        
+
         recorded_request, created = Request.objects.get_or_create(
-            domain=self.domain,
-            port=self.port,
-            path=request.path,
-            querykey=self._get_query_key(request),
-        )
-        
+                method=request.method, domain=self.domain, port=self.port,
+                path=request.path, querykey=self._get_query_key(request))
+
         self.record_request_parameters(request, recorded_request)
-        
+
         # Update the timestamp on the existing recorded request
         if not created:
             recorded_request.save()
-        
+
         return recorded_request
-    
+
     def record_request_parameters(self, request, recorded_request):
         """
         Records the request parameters for the recorded request.
-        
+
         The order field is set to reflect the order in which the QueryDict
         returns the GET parameters.
         """
         position = 1
         for name, values_list in request.GET.lists():
             for value in values_list:
-                recorded_request.parameters.create(
-                    order=position,
-                    name=name,
-                    value=value,
-                )
+                recorded_request.parameters.create(order=position, name=name,
+                        value=value)
                 position += 1
-    
+
     def record_response(self, recorded_request, response):
         """
         Records a response so it can be replayed at a later stage.
-        
+
         The recorded response is linked to a previously recorded request and
         its request parameters to allow for reverse-finding the recorded
         response given the recorded request object.
         """
-        
+
         # Delete the previously recorded response, if any
         try:
             recorded_request.response.delete()
         except Response.DoesNotExist:
             pass
-        
+
         # Extract the encoding from the response
         content_type = response['Content-Type']
         encoding = content_type.partition('charset=')[-1] or 'utf-8'
 
         # Record the new response
-        Response.objects.create(
-            request=recorded_request,
-            status=response.status_code,
-            content_type=content_type,
-            content=response.content.decode(encoding), 
-        )
-    
+        Response.objects.create(request=recorded_request,
+                status=response.status_code, content_type=content_type,
+                content=response.content.decode(encoding))
+
     def playback(self, request):
         """
         Returns a previously recorded response based on the provided request.
         """
         try:
-            matching_request = Request.objects.filter(
-                domain=self.domain,
-                port=self.port,
-                path=request.path,
-                querykey=self._get_query_key(request)
-            ).latest()
+            matching_request = Request.objects.filter(method=request.method,
+                    domain=self.domain, port=self.port, path=request.path,
+                    querykey=self._get_query_key(request)).latest()
         except Request.DoesNotExist, e:
-            raise RequestNotRecorded('The request made has not been recorded yet. Please run httpproxy in "record" mode first.')
-        
+            raise RequestNotRecorded('The request made has not been ' \
+                    'recorded yet. Please run httpproxy in "record" mode ' \
+                    'first.')
+
         logger.info('Playback: GET "%s"' % self._request_string(request))
         response = matching_request.response # TODO handle "no response" situation
         encoding = self._get_encoding(response.content_type)
-        
-        return HttpResponse(
-            response.content.encode(encoding), 
-            status=response.status, 
-            mimetype=response.content_type
-        )
-    
+
+        return HttpResponse(response.content.encode(encoding),
+                status=response.status, mimetype=response.content_type)
+
     def response_supported(self, response):
-        return response['Content-Type'].partition(';')[0] in RESPONSE_TYPES_SUPPORTED
-    
+        return response['Content-Type'].partition(';')[0] \
+                in self.response_types_supported
+
     def _get_encoding(self, content_type):
         """
         Extracts the character encoding from an HTTP Content-Type header.
         """
         return content_type.partition('charset=')[-1] or 'utf-8'
-        
+
     def _request_string(self, request):
         """
         Helper for getting a string representation of a request.
         """
         return '%(domain)s:%(port)d%(path)s' % {
-            'domain': self.domain, 
-            'port': self.port, 
+            'domain': self.domain,
+            'port': self.port,
             'path': request.get_full_path()
         }
-    
+
     def _get_query_key(self, request):
         """
         Returns an MD5 has of the request's query parameters.
         """
         querystring = request.GET.urlencode()
         return md5_constructor(querystring).hexdigest()
-    
+

httpproxy/views.py

-import httplib2
- 
+import logging
+import urllib2
+from urlparse import urlparse
+
 from django.http import HttpResponse
+from django.views.generic import View
 
-from httpproxy import settings
-from httpproxy.exceptions import UnkownProxyMode
-from httpproxy.decorators import normalize_request, rewrite_response
- 
+from httpproxy.recorder import ProxyRecorder
 
-PROXY_FORMAT = u'http://%s:%d%s' % (settings.PROXY_DOMAIN, settings.PROXY_PORT, u'%s')
 
-def proxy(request):
-    conn = httplib2.Http()
-    url = request.path
-    
-    # Optionally provide authentication for server
-    try:
-        conn.add_credentials(settings.PROXY_USER, settings.PROXY_PASSWORD)
-    except AttributeError:
-        pass
-    
-    if request.method == 'GET':
-        url_ending = '%s?%s' % (url, request.GET.urlencode())
-        url = PROXY_FORMAT % url_ending
-        response, content = conn.request(url, request.method)
-    elif request.method == 'POST':
-        url = PROXY_FORMAT % url
-        data = request.POST.urlencode()
-        response, content = conn.request(url, request.method, data)
-    return HttpResponse(content, status=int(response['status']), mimetype=response['content-type'])
+logger = logging.getLogger(__name__)
 
+class HttpProxy(View):
 
-if settings.PROXY_MODE is not None:
-    proxy_mode = settings.PROXY_MODE
-    try:
-        decorator = getattr(__import__('httpproxy' + '.decorators', fromlist=proxy_mode), proxy_mode)
-    except AttributeError, e:
-        raise UnkownProxyMode('The proxy mode "%s" could not be found. Please specify a corresponding decorator function in "%s.decorators".' % (proxy_mode, 'httpproxy'))
-    else:
-        proxy = decorator(proxy)
+    mode = None
+    base_url = None
+    msg = 'Response body: \n%s'
 
-if settings.PROXY_REWRITE_RESPONSES:
-    proxy = rewrite_response(proxy)
+    def dispatch(self, request, url, *args, **kwargs):
+        self.url = url
+        request = self.normalize_request(request)
+        if self.mode == 'play':
+            return self.play(request)
 
-# The request object should always be normalized
-proxy = normalize_request(proxy)
+        response = super(HttpProxy, self).dispatch(request, *args, **kwargs)
+        if self.mode == 'record':
+            self.record(response)
+        return response
+
+    def normalize_request(self, request):
+        """
+        Updates all path-related info in the original request object with the url
+        given to the proxy
+
+        This way, any further processing of the proxy'd request can just ignore
+        the url given to the proxy and use request.path safely instead.
+        """
+        if not self.url.startswith('/'):
+            self.url = '/' + self.url
+        request.path = self.url
+        request.path_info = self.url
+        request.META['PATH_INFO'] = self.url
+        return request
+
+    def play(self, request):
+        """
+        Plays back the response to a request, based on a previously recorded
+        request/response
+        """
+        return self.get_recorder().playback(request)
+
+    def record(self, response):
+        """
+        Records the request being made and its response
+        """
+        self.get_recorder().record(self.request, response)
+
+    def get_recorder(self):
+        url = urlparse(self.base_url)
+        return ProxyRecorder(domain=url.hostname, port=(url.port or 80))
+
+    def get(self, request, *args, **kwargs):
+        request_url = self.get_full_url(self.url)
+        request = self.create_request(request_url)
+        response = urllib2.urlopen(request)
+        try:
+            response_body = response.read()
+            status = response.getcode()
+            logger.debug(self.msg % response_body)
+        except urllib2.HTTPError, e:
+            response_body = e.read()
+            logger.error(self.msg % response_body)
+            status = e.code
+        return HttpResponse(response_body, status=status,
+                content_type=response.headers['content-type'])
+
+    def get_full_url(self, url):
+        """
+        Constructs the full URL to be requested
+        """
+        param_str = self.request.GET.urlencode()
+        request_url = u'%s/%s' % (self.base_url, url)
+        request_url += '?%s' % param_str if param_str else ''
+        return request_url
+
+    def create_request(self, url, body=None, headers={}):
+        request = urllib2.Request(url, body, headers)
+        logger.info('%s %s' % (request.get_method(), request.get_full_url()))
+        return request

requirements/libs.txt

 Django==1.1.1
-httplib2==0.5.0