Repository: liberapay/liberapay.com

Issue:
Prevent users from creating usernames with extensions in them.
I have claimed ownership of liberapay.com on https://keybase.io/edoverflow by creating a user with `keybase.txt` as the username and placing the verification snippet in the user's statement as you can see below (https://liberapay.com/keybase.txt):

![image](https://user-images.githubusercontent.com/18099289/35120564-d26274aa-fc98-11e7-884f-b972fdc44efe.png)

![image](https://user-images.githubusercontent.com/18099289/35120581-e233b146-fc98-11e7-826d-40d54a46601c.png)

In order to fix this issue we should disallow usernames containing file extensions in them. 


Prevent users from creating usernames with extensions in them.
I have claimed ownership of liberapay.com on https://keybase.io/edoverflow by creating a user with `keybase.txt` as the username and placing the verification snippet in the user's statement as you can see below (https://liberapay.com/keybase.txt):

![image](https://user-images.githubusercontent.com/18099289/35120564-d26274aa-fc98-11e7-884f-b972fdc44efe.png)

![image](https://user-images.githubusercontent.com/18099289/35120581-e233b146-fc98-11e7-826d-40d54a46601c.png)

In order to fix this issue we should disallow usernames containing file extensions in them. 




Discussion:
I also managed to claim ownership of liberapay.org:

![image](https://user-images.githubusercontent.com/18099289/35158882-c1adf30a-fd38-11e7-93b8-57a814762e69.png)

I suggest rejecting names ending with any of: `.txt`, `.html`, `.htm`, `.json`, `.xml`.
I also managed to claim ownership of liberapay.org:

![image](https://user-images.githubusercontent.com/18099289/35158882-c1adf30a-fd38-11e7-93b8-57a814762e69.png)

I suggest rejecting names ending with any of: `.txt`, `.html`, `.htm`, `.json`, `.xml`.

Resolving patch:
diff --git a/liberapay/constants.py b/liberapay/constants.py
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -305,6 +305,7 @@ def make_standard_tip(label, weekly, currency):
 TAKE_THROTTLING_THRESHOLD = {k: Money('1.00', k) for k in ('EUR', 'USD')}
 
 USERNAME_MAX_SIZE = 32
+USERNAME_SUFFIX_BLACKLIST = set('.txt .html .htm .json .xml'.split())
 
 ZERO = {c: Money(D_ZERO, c) for c in ('EUR', 'USD', None)}
 
diff --git a/liberapay/exceptions.py b/liberapay/exceptions.py
--- a/liberapay/exceptions.py
+++ b/liberapay/exceptions.py
@@ -92,6 +92,10 @@ class UsernameBeginsWithRestrictedCharacter(ProblemChangingUsername):
     def msg(self, _):
         return _("The username '{0}' begins with a restricted character.", *self.args)
 
+class UsernameEndsWithForbiddenSuffix(ProblemChangingUsername):
+    def msg(self, _):
+        return _("The username '{0}' ends with the forbidden suffix '{1}'.", *self.args)
+
 class TooManyUsernameChanges(ProblemChangingUsername):
     code = 429
     def msg(self, _):
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -25,7 +25,8 @@
     EMAIL_VERIFICATION_TIMEOUT, EVENTS,
     PASSWORD_MAX_SIZE, PASSWORD_MIN_SIZE, PAYMENT_SLUGS,
     PERIOD_CONVERSION_RATES, PRIVILEGES,
-    SESSION, SESSION_REFRESH, SESSION_TIMEOUT, USERNAME_MAX_SIZE, ZERO
+    SESSION, SESSION_REFRESH, SESSION_TIMEOUT,
+    USERNAME_MAX_SIZE, USERNAME_SUFFIX_BLACKLIST, ZERO,
 )
 from liberapay.exceptions import (
     BadAmount,
@@ -48,6 +49,7 @@
     UsernameAlreadyTaken,
     UsernameBeginsWithRestrictedCharacter,
     UsernameContainsInvalidCharacters,
+    UsernameEndsWithForbiddenSuffix,
     UsernameIsEmpty,
     UsernameIsRestricted,
     UsernameTooLong,
@@ -1458,6 +1460,10 @@ def change_username(self, suggested, cursor=None, recorder=None):
         if suggested[0] == '.':
             raise UsernameBeginsWithRestrictedCharacter(suggested)
 
+        suffix = suggested[suggested.rfind('.'):]
+        if suffix in USERNAME_SUFFIX_BLACKLIST:
+            raise UsernameEndsWithForbiddenSuffix(suggested, suffix)
+
         lowercased = suggested.lower()
 
         if lowercased in website.restricted_usernames:


Test patch:
diff --git a/tests/py/test_settings.py b/tests/py/test_settings.py
--- a/tests/py/test_settings.py
+++ b/tests/py/test_settings.py
@@ -98,6 +98,13 @@ def test_too_long(self):
         assert r.code == 400
         assert "The username &#39;%s&#39; is too long." % username in r.text, r.text
 
+    def test_forbidden_suffix(self):
+        username = "keybase.txt"
+        r = self.change_username(username)
+        assert r.code == 400
+        expected = "The username &#39;%s&#39; ends with the forbidden suffix &#39;.txt&#39;." % username
+        assert expected in r.text, r.text
+
     def test_change_team_name(self):
         team = self.make_participant(None, kind='group')
         team.change_username('team')