Repository: jazzband/django-axes

Issue:
Issue with IE8 user agent
Hard to believe but this user agent exist:

```

Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; BTRS123646; SIMBAR={948FDCDC-90AF-11E1-BE08-005056C00008}; GTB7.4; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0; .NET CLR 1.1.4322)
```

Asking how long it is:

```
>>> len('Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; BTRS123646; SIMBAR={948FDCDC-90AF-11E1-BE08-005056C00008}; GTB7.4; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0; .NET CLR 1.1.4322)')
272
```

So it will be better to increase the max_length to avoid the issue



Discussion:

Resolving patch:
diff --git a/axes/decorators.py b/axes/decorators.py
--- a/axes/decorators.py
+++ b/axes/decorators.py
@@ -160,7 +160,7 @@ def get_user_attempts(request):
     username = request.POST.get('username', None)
 
     if USE_USER_AGENT:
-        ua = request.META.get('HTTP_USER_AGENT', '<unknown>')
+        ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
         attempts = AccessAttempt.objects.filter(
             user_agent=ua, ip_address=ip, username=username, trusted=True
         )
@@ -237,7 +237,7 @@ def decorated_login(request, *args, **kwargs):
             )
 
             access_log = AccessLog.objects.create(
-                user_agent=request.META.get('HTTP_USER_AGENT', '<unknown>'),
+                user_agent=request.META.get('HTTP_USER_AGENT', '<unknown>')[:255],
                 ip_address=get_ip(request),
                 username=request.POST.get('username', None),
                 http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
@@ -368,7 +368,7 @@ def check_request(request, login_unsuccessful):
 
 def create_new_failure_records(request, failures):
     ip = get_ip(request)
-    ua = request.META.get('HTTP_USER_AGENT', '<unknown>')
+    ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
     username = request.POST.get('username', None)
 
     params = {
@@ -397,7 +397,7 @@ def create_new_failure_records(request, failures):
 
 def create_new_trusted_record(request):
     ip = get_ip(request)
-    ua = request.META.get('HTTP_USER_AGENT', '<unknown>')
+    ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
     username = request.POST.get('username', None)
 
     if not username:
diff --git a/axes/models.py b/axes/models.py
--- a/axes/models.py
+++ b/axes/models.py
@@ -1,41 +1,72 @@
 from django.db import models
-#import signals
-FAILURES_DESC = 'Failed Logins'
 
-#XXX TODO
-# set unique by user_agent, ip
-# make user agent, ip indexed fields
 
 class CommonAccess(models.Model):
-    user_agent = models.CharField(max_length=255)
-    ip_address = models.IPAddressField('IP Address', null=True)
-    username = models.CharField(max_length=255, null=True)
+    user_agent = models.CharField(
+        max_length=255,
+    )
+
+    ip_address = models.IPAddressField(
+        verbose_name='IP Address',
+        null=True,
+    )
+
+    username = models.CharField(
+        max_length=255,
+        null=True,
+    )
 
     # Once a user logs in from an ip, that combination is trusted and not
     # locked out in case of a distributed attack
-    trusted = models.BooleanField(default=False)
-    http_accept = models.CharField('HTTP Accept', max_length=1025)
-    path_info = models.CharField('Path', max_length=255)
-    attempt_time = models.DateTimeField(auto_now_add=True)
+    trusted = models.BooleanField(
+        default=False,
+    )
+
+    http_accept = models.CharField(
+        verbose_name='HTTP Accept',
+        max_length=1025,
+    )
+
+    path_info = models.CharField(
+        verbose_name='Path',
+        max_length=255,
+    )
+
+    attempt_time = models.DateTimeField(
+        auto_now_add=True,
+    )
 
     class Meta:
         abstract = True
         ordering = ['-attempt_time']
 
+
 class AccessAttempt(CommonAccess):
-    get_data = models.TextField('GET Data')
-    post_data = models.TextField('POST Data')
-    failures_since_start = models.PositiveIntegerField(FAILURES_DESC)
+    get_data = models.TextField(
+        verbose_name='GET Data',
+    )
 
-    def __unicode__(self):
-        return u'Attempted Access: %s' % self.attempt_time
+    post_data = models.TextField(
+        verbose_name='POST Data',
+    )
+
+    failures_since_start = models.PositiveIntegerField(
+        verbose_name='Failed Logins',
+    )
 
     @property
     def failures(self):
         return self.failures_since_start
 
+    def __unicode__(self):
+        return u'Attempted Access: %s' % self.attempt_time
+
+
 class AccessLog(CommonAccess):
-    logout_time = models.DateTimeField(null=True, blank=True)
+    logout_time = models.DateTimeField(
+        null=True,
+        blank=True,
+    )
 
     def __unicode__(self):
         return u'Access Log for %s @ %s' % (self.username, self.attempt_time)


Test patch:
diff --git a/axes/tests.py b/axes/tests.py
--- a/axes/tests.py
+++ b/axes/tests.py
@@ -30,11 +30,11 @@ def _random_username(self, existing_username=False):
 
         return self._generate_random_string()
 
-    def _login(self, existing_username=False):
+    def _login(self, existing_username=False, user_agent='test-browser'):
         response = self.client.post(reverse('admin:index'), {
             'username': self._random_username(existing_username),
             'password': self._generate_random_string(),
-        })
+        }, HTTP_USER_AGENT=user_agent)
 
         return response
 
@@ -101,3 +101,28 @@ def test_valid_login(self):
             'password': valid_username
         })
         self.assertNotIn(LOGIN_FORM_KEY, response.context)
+
+    def test_long_user_agent_valid(self):
+        """Tests if can handle a long user agent
+        """
+        long_user_agent = 'ie6' * 1024
+        valid_username = self._random_username(existing_username=True)
+        response = self.client.post(reverse('admin:index'), {
+            'username': valid_username,
+            'password': valid_username
+        }, HTTP_USER_AGENT=long_user_agent)
+        self.assertNotIn(LOGIN_FORM_KEY, response.context)
+
+    def test_long_user_agent_not_valid(self):
+        """Tests if can handle a long user agent with failure
+        """
+        long_user_agent = 'ie6' * 1024
+        for i in range(0, FAILURE_LIMIT):
+            response = self._login(
+                existing_username=False,
+                user_agent=long_user_agent,
+            )
+            self.assertContains(response, LOGIN_FORM_KEY)
+
+        response = self._login()
+        self.assertContains(response, self.LOCKED_MESSAGE)