Repository: CTFd/CTFd (python)

Issue:
Unsupported python3 method
In the challenges.py it is used unicode, an unsupported python3, method. This makes it impossible to submit flags.

This is the relevant line:

` provided_key = unicode(request.form['key'].strip())`



Pull request description:
Fixes #261 and generally supports Unicode better in Python 2

Patch:
diff --git a/.travis.yml b/.travis.yml
index 1a5f0f56..ceb7bb44 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
 language: python
 python:
     - 2.7
-    - 3.5
+    - 3.6
 install:
     - pip install -r development.txt
 script:
diff --git a/CTFd/__init__.py b/CTFd/__init__.py
index 43a8858f..63e07141 100644
--- a/CTFd/__init__.py
+++ b/CTFd/__init__.py
@@ -1,3 +1,4 @@
+import sys
 import os
 
 from distutils.version import StrictVersion
@@ -11,6 +12,11 @@ from six.moves import input
 from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp
 from CTFd import utils
 
+# Hack to support Unicode in Python 2 properly
+if sys.version_info[0] < 3:
+    reload(sys)
+    sys.setdefaultencoding("utf-8")
+
 __version__ = '1.0.2'
 
 
diff --git a/CTFd/challenges.py b/CTFd/challenges.py
index 9a139f85..c386b2aa 100644
--- a/CTFd/challenges.py
+++ b/CTFd/challenges.py
@@ -258,21 +258,21 @@ def chal(chalid):
                 db.session.close()
             logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data))
             # return '3' # Submitting too fast
-            return jsonify({'status': '3', 'message': "You're submitting keys too fast. Slow down."})
+            return jsonify({'status': 3, 'message': "You're submitting keys too fast. Slow down."})
 
         solves = Solves.query.filter_by(teamid=session['id'], chalid=chalid).first()
 
         # Challange not solved yet
         if not solves:
             chal = Challenges.query.filter_by(id=chalid).first_or_404()
-            provided_key = unicode(request.form['key'].strip())
+            provided_key = request.form['key'].strip()
             saved_keys = Keys.query.filter_by(chal=chal.id).all()
 
             # Hit max attempts
             max_tries = chal.max_attempts
             if max_tries and fails >= max_tries > 0:
                 return jsonify({
-                    'status': '0',
+                    'status': 0,
                     'message': "You have 0 tries remaining"
                 })
 
@@ -284,7 +284,7 @@ def chal(chalid):
                     db.session.commit()
                     db.session.close()
                 logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
-                return jsonify({'status': '1', 'message': 'Correct'})
+                return jsonify({'status': 1, 'message': 'Correct'})
 
             if utils.ctftime():
                 wrong = WrongKeys(teamid=session['id'], chalid=chalid, flag=provided_key)
@@ -298,17 +298,17 @@ def chal(chalid):
                 tries_str = 'tries'
                 if attempts_left == 1:
                     tries_str = 'try'
-                return jsonify({'status': '0', 'message': 'Incorrect. You have {} {} remaining.'.format(attempts_left, tries_str)})
+                return jsonify({'status': 0, 'message': 'Incorrect. You have {} {} remaining.'.format(attempts_left, tries_str)})
             else:
-                return jsonify({'status': '0', 'message': 'Incorrect'})
+                return jsonify({'status': 0, 'message': 'Incorrect'})
 
         # Challenge already solved
         else:
             logger.info("{0} submitted {1} with kpm {2} [ALREADY SOLVED]".format(*data))
             # return '2' # challenge was already solved
-            return jsonify({'status': '2', 'message': 'You already solved this'})
+            return jsonify({'status': 2, 'message': 'You already solved this'})
     else:
         return jsonify({
-            'status': '-1',
+            'status': -1,
             'message': "You must be logged in to solve a challenge"
         })
diff --git a/development.txt b/development.txt
index 39b4e543..cfa20356 100644
--- a/development.txt
+++ b/development.txt
@@ -3,4 +3,4 @@ coverage>=4.1
 mock>=2.0.0
 nose>=1.3.7
 rednose>=1.1.1
-pep8==1.7.0
+pep8>=1.7.0
diff --git a/populate.py b/populate.py
index 223c5574..08809f3c 100644
--- a/populate.py
+++ b/populate.py
@@ -231,7 +231,7 @@ if __name__ == '__main__':
         for x in range(AMT_CHALS_WITH_FILES):
             chal = random.randint(1, CHAL_AMOUNT)
             filename = gen_file()
-            md5hash = hashlib.md5(filename).hexdigest()
+            md5hash = hashlib.md5(filename.encode('utf-8')).hexdigest()
             db.session.add(Files(chal, md5hash + '/' + filename))
 
         db.session.commit()


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index b17db88a..5e9a92cf 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -86,7 +86,7 @@ def gen_file():
     pass
 
 
-def gen_key(db, chal, flag='flag', key_type=0):
+def gen_flag(db, chal, flag='flag', key_type=0):
     key = Keys(chal, flag, key_type)
     db.session.add(key)
     db.session.commit()
diff --git a/tests/test_user_facing.py b/tests/test_user_facing.py
index 8a489aeb..5a1bc8de 100644
--- a/tests/test_user_facing.py
+++ b/tests/test_user_facing.py
@@ -1,4 +1,7 @@
-from tests.helpers import create_ctfd, register_user, login_as_user, gen_challenge
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from tests.helpers import *
 from CTFd.models import Teams
 import json
 
@@ -205,3 +208,61 @@ def test_viewing_challenges():
         r = client.get('/chals')
         chals = json.loads(r.get_data(as_text=True))
         assert len(chals['game']) == 1
+
+
+def test_submitting_correct_flag():
+    """Test that correct flags are correct"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        client = login_as_user(app)
+        chal = gen_challenge(app.db)
+        flag = gen_flag(app.db, chal=chal.id, flag='flag')
+        with client.session_transaction() as sess:
+            data = {
+                "key": 'flag',
+                "nonce": sess.get('nonce')
+            }
+        r = client.post('/chal/{}'.format(chal.id), data=data)
+        assert r.status_code == 200
+        resp = json.loads(r.data.decode('utf8'))
+        assert resp.get('status') == 1 and resp.get('message') == "Correct"
+
+
+def test_submitting_incorrect_flag():
+    """Test that incorrect flags are incorrect"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        client = login_as_user(app)
+        chal = gen_challenge(app.db)
+        flag = gen_flag(app.db, chal=chal.id, flag='flag')
+        with client.session_transaction() as sess:
+            data = {
+                "key": 'notflag',
+                "nonce": sess.get('nonce')
+            }
+        r = client.post('/chal/{}'.format(chal.id), data=data)
+        assert r.status_code == 200
+        resp = json.loads(r.data.decode('utf8'))
+        assert resp.get('status') == 0 and resp.get('message') == "Incorrect"
+
+
+def test_submitting_unicode_flag():
+    """Test that users can submit a unicode flag"""
+    print("Test that users can submit a flag")
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        client = login_as_user(app)
+        chal = gen_challenge(app.db)
+        flag = gen_flag(app.db, chal=chal.id, flag=u'你好')
+        with client.session_transaction() as sess:
+            data = {
+                "key": '你好',
+                "nonce": sess.get('nonce')
+            }
+        r = client.post('/chal/{}'.format(chal.id), data=data)
+        assert r.status_code == 200
+        resp = json.loads(r.data.decode('utf8'))
+        assert resp.get('status') == 1 and resp.get('message') == "Correct"