Repository: CTFd/CTFd
Pull request #343 (2017-08-09 08:12:37)

Toggle username_password inputs depending on mail_useauth and clean get_smtp util
This cleans up the config.html file a little to remove spaces in favor of tabs for HTML. 

Also change get_smtp to properly support SSL instead of just TLS. This was tested with Google SMTP on port 465 and 587 to make sure we don't regress. 

Also change the username and password inputs for the mailserver config to only appear if the use_mailauth checkbox is checked. 


Patch:
diff --git a/CTFd/auth.py b/CTFd/auth.py
index ab9aeff5..97017aaa 100644
--- a/CTFd/auth.py
+++ b/CTFd/auth.py
@@ -2,7 +2,6 @@ import logging
 import os
 import re
 import time
-import urllib
 
 from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint
 from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature
@@ -25,8 +24,10 @@ def confirm_user(data=None):
     # User is confirming email account
     if data and request.method == "GET":
         try:
-            s = Signer(app.config['SECRET_KEY'])
-            email = s.unsign(urllib.unquote_plus(data.decode('base64')))
+            s = TimedSerializer(app.config['SECRET_KEY'])
+            email = s.loads(utils.base64decode(data, urldecode=True), max_age=1800)
+        except BadTimeSignature:
+            return render_template('confirm.html', errors=['Your confirmation link has expired'])
         except BadSignature:
             return render_template('confirm.html', errors=['Your confirmation link seems wrong'])
         except:
@@ -82,7 +83,7 @@ def reset_password(data=None):
     if data is not None and request.method == "POST":
         try:
             s = TimedSerializer(app.config['SECRET_KEY'])
-            name = s.loads(urllib.unquote_plus(data.decode('base64')), max_age=1800)
+            name = s.loads(utils.base64decode(data, urldecode=True), max_age=1800)
         except BadTimeSignature:
             return render_template('reset_password.html', errors=['Your link has expired'])
         except:
@@ -110,7 +111,7 @@ Did you initiate a password reset?
 
 {0}/{1}
 
-""".format(url_for('auth.reset_password', _external=True), urllib.quote_plus(token.encode('base64')))
+""".format(url_for('auth.reset_password', _external=True), utils.base64encode(token, urlencode=True))
 
         utils.sendmail(email, text)
 
diff --git a/CTFd/config.py b/CTFd/config.py
index 7a7591eb..64c74c23 100644
--- a/CTFd/config.py
+++ b/CTFd/config.py
@@ -119,7 +119,9 @@ class Config(object):
 
 
 class TestingConfig(Config):
+    SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA'
     PRESERVE_CONTEXT_ON_EXCEPTION = False
     TESTING = True
     DEBUG = True
     SQLALCHEMY_DATABASE_URI = 'sqlite://'
+    SERVER_NAME = 'localhost'
diff --git a/CTFd/themes/admin/templates/config.html b/CTFd/themes/admin/templates/config.html
index 1658ea02..164a9cad 100644
--- a/CTFd/themes/admin/templates/config.html
+++ b/CTFd/themes/admin/templates/config.html
@@ -97,8 +97,8 @@
 							Prevent team name changes
 						</label>
 					</div>
-			   </div>
-			   <div role="tabpanel" class="tab-pane" id="email-section">
+				</div>
+				<div role="tabpanel" class="tab-pane" id="email-section">
 					<ul class="nav nav-tabs" role="tablist">
 						<li role="presentation" class="active">
 							<a href="#mailserver" aria-controls="mailserver" role="tab" data-toggle="tab">Mail Server</a>
@@ -111,8 +111,8 @@
 					<div class="form-group">
 						<label for="start">Mail From Address:</label>
 						<input class="form-control" id='mailfrom_addr' name='mailfrom_addr' type='text'
-							   placeholder="Email address used to send email"
-							   {% if mailfrom_addr is defined and mailfrom_addr != None %}value="{{ mailfrom_addr }}"{% endif %}>
+								placeholder="Email address used to send email"
+								{% if mailfrom_addr is defined and mailfrom_addr != None %}value="{{ mailfrom_addr }}"{% endif %}>
 					</div>
 
 					<div class="tab-content">
@@ -121,33 +121,35 @@
 							<div class="form-group">
 								<label for="start">Mail Server Address:</label>
 								<input class="form-control" id='mail_server' name='mail_server' type='text'
-									   placeholder="Mail Server Address"
-									   {% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
+										placeholder="Mail Server Address"
+										{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
 							</div>
 
 							<div class="form-group">
 								<label for="start">Mail Server Port:</label>
 								<input class="form-control" id='mail_port' name='mail_port' type='text'
-									   placeholder="Mail Server Port"
-									   {% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
+										placeholder="Mail Server Port"
+										{% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
 							</div>
 							<div class="checkbox">
-                                                                <label>
-                                                                        <input id="mail_useauth" name="mail_useauth" type="checkbox" {% if mail_useauth %}checked{% endif %}>
-                                                                        Use SMTP Authentication?
-                                                                </label>
-                                                        </div>
-							<div class="form-group">
-								<label for="start">Username:</label>
-								<input class="form-control" id='mail_username' name='mail_username' type='text'
-									   placeholder="Username"
-									   {% if mail_username is defined and mail_username != None %}value="{{ mail_username }}"{% endif %}>
+							<label>
+								<input id="mail_useauth" name="mail_useauth" type="checkbox" {% if mail_useauth %}checked{% endif %}>
+									Specify Username and Password
+								</label>
 							</div>
-							<div class="form-group">
-								<label for="start">Password:</label>
-								<input class="form-control" id='mail_password' name='mail_password' type='password'
-									   placeholder="Password"
-									   {% if mail_password is defined and mail_password != None %}value="{{ mail_password }}"{% endif %}>
+							<div id="mail_username_password">
+								<div class="form-group">
+									<label for="start">Username:</label>
+									<input class="form-control" id='mail_username' name='mail_username' type='text'
+											placeholder="Username"
+											{% if mail_username is defined and mail_username != None %}value="{{ mail_username }}"{% endif %}>
+								</div>
+								<div class="form-group">
+									<label for="start">Password:</label>
+									<input class="form-control" id='mail_password' name='mail_password' type='password'
+											placeholder="Password"
+											{% if mail_password is defined and mail_password != None %}value="{{ mail_password }}"{% endif %}>
+								</div>
 							</div>
 							<div class="checkbox">
 								<label>
@@ -167,14 +169,14 @@
 							<div class="form-group">
 								<label for="start">Mailgun API Base URL:</label>
 								<input class="form-control" id='mg_base_url' name='mg_base_url' type='text'
-									   placeholder="Mailgun API Base URL"
-									   {% if mg_base_url is defined and mg_base_url != None %}value="{{ mg_base_url }}"{% endif %}>
+										placeholder="Mailgun API Base URL"
+										{% if mg_base_url is defined and mg_base_url != None %}value="{{ mg_base_url }}"{% endif %}>
 							</div>
 							<div class="form-group">
 								<label for="start">Mailgun API Key:</label>
 								<input class="form-control" id='mg_api_key' name='mg_api_key' type='text'
-									   placeholder="Mailgun API Key"
-									   {% if mg_api_key is defined and mg_api_key != None %}value="{{ mg_api_key }}"{% endif %}>
+										placeholder="Mailgun API Key"
+										{% if mg_api_key is defined and mg_api_key != None %}value="{{ mg_api_key }}"{% endif %}>
 							</div>
 						</div>
 					</div>
@@ -237,20 +239,20 @@
 								<div class="form-group col-xs-12">
 									<label for="start-local">Local Time:</label>
 									<input class="form-control" id='start-local' type='text'
-										   placeholder="Start Date (Local Time)" readonly>
+											placeholder="Start Date (Local Time)" readonly>
 								</div>
 
 								<div class="form-group col-xs-12">
 									<label for="start-zonetime">Timezone Time:</label>
 									<input class="form-control" id='start-zonetime' type='text'
-										   placeholder="Start Date (Timezone Time)" readonly>
+											placeholder="Start Date (Timezone Time)" readonly>
 								</div>
 
 								<div class="form-group col-xs-12">
 									<label for="start">UTC Timestamp:</label>
 									<input class="form-control" id='start' name='start' type='text'
-									   placeholder="Start Date (UTC timestamp)"
-									   {% if start is defined and start != None %}value="{{ start }}"{% endif %} readonly>
+										placeholder="Start Date (UTC timestamp)"
+										{% if start is defined and start != None %}value="{{ start }}"{% endif %} readonly>
 								</div>
 							</div>
 						</div>
@@ -259,13 +261,13 @@
 								<div class="form-group col-xs-2">
 									<label for="end-month">Month:</label>
 									<input class="form-control end-date" id='end-month' name='end-month' min="1" max="12"
-										   type='number'>
+											type='number'>
 								</div>
 
 								<div class="form-group col-xs-2">
 									<label for="end-day">Day:</label>
 									<input class="form-control end-date" id='end-day' name='end-day' min="1" max="31"
-										   type='number'>
+											type='number'>
 								</div>
 
 								<div class="form-group col-xs-2">
@@ -276,13 +278,13 @@
 								<div class="form-group col-xs-2">
 									<label for="end-hour">Hour:</label>
 									<input class="form-control end-date" id='end-hour' name='end-hour' min="0" max="23"
-										   type='number'>
+											type='number'>
 								</div>
 
 								<div class="form-group col-xs-2">
 									<label for="end-minute">Minute:</label>
 									<input class="form-control end-date" id='end-minute' name='end-minute' min="0"
-										   max="59" type='number'>
+											max="59" type='number'>
 								</div>
 
 								<div class="form-group col-xs-4">
@@ -301,20 +303,20 @@
 								<div class="form-group col-xs-12">
 									<label for="end-local">Local Time:</label>
 									<input class="form-control" id='end-local' type='text'
-										   placeholder="End Date (Local Time)" readonly>
+											placeholder="End Date (Local Time)" readonly>
 								</div>
 
 								<div class="form-group col-xs-12">
 									<label for="end-zonetime">Timezone Time:</label>
 									<input class="form-control" id='end-zonetime' type='text'
-										   placeholder="End Date (Timezone Time)" readonly>
+											placeholder="End Date (Timezone Time)" readonly>
 								</div>
 
 								<div class="form-group col-xs-12">
 									<label for="end">UTC Timestamp:</label>
 									<input class="form-control" id='end' name='end' type='text'
-										   placeholder="End Date (UTC timestamp)"
-										   {% if end is defined and end != None %}value="{{ end }}"{% endif %} readonly>
+											placeholder="End Date (UTC timestamp)"
+											{% if end is defined and end != None %}value="{{ end }}"{% endif %} readonly>
 								</div>
 							</div>
 						</div>
@@ -606,7 +608,6 @@
 		});
 
 		$(function () {
-
 			var hash = window.location.hash;
 			if (hash) {
 				hash = hash.replace("<>[]'\"", "");
@@ -621,8 +622,7 @@
 			var start = $('#start').val();
 			var end = $('#end').val();
 			var freeze = $('#freeze').val();
-			console.log(start);
-			console.log(end);
+
 			if (start){
 				load_timestamp('start', start);
 			}
@@ -632,6 +632,11 @@
 			if (freeze) {
 				load_timestamp('freeze', freeze);
 			}
+
+			// Toggle username and password based on stored value
+			$('#mail_useauth').change(function () {
+				$('#mail_username_password').toggle(this.checked);
+			}).change();
 		});
 	</script>
 {% endblock %}
diff --git a/CTFd/utils.py b/CTFd/utils.py
index 2bdaeec0..29e4b18e 100644
--- a/CTFd/utils.py
+++ b/CTFd/utils.py
@@ -1,3 +1,4 @@
+import base64
 import datetime
 import functools
 import hashlib
@@ -16,7 +17,6 @@ import subprocess
 import sys
 import tempfile
 import time
-import urllib
 import dataset
 import zipfile
 import io
@@ -25,8 +25,8 @@ from email.mime.text import MIMEText
 from flask import current_app as app, request, redirect, url_for, session, render_template, abort
 from flask_caching import Cache
 from flask_migrate import Migrate, upgrade as migrate_upgrade, stamp as migrate_stamp
-from itsdangerous import Signer
-from six.moves.urllib.parse import urlparse, urljoin
+from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature
+from six.moves.urllib.parse import urlparse, urljoin, quote, unquote
 from werkzeug.utils import secure_filename
 
 from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Containers, ip2long, long2ip
@@ -437,11 +437,14 @@ def mailserver():
 
 
 def get_smtp(host, port, username=None, password=None, TLS=None, SSL=None, auth=None):
-    smtp = smtplib.SMTP(host, port)
-    smtp.ehlo()
+    if SSL is None:
+        smtp = smtplib.SMTP(host, port)
+    else:
+        smtp = smtplib.SMTP_SSL(host, port)
+
     if TLS:
         smtp.starttls()
-        smtp.ehlo()
+
     if auth:
         smtp.login(username, password)
     return smtp
@@ -495,11 +498,12 @@ def sendmail(addr, text):
 
 
 def verify_email(addr):
-    s = Signer(app.config['SECRET_KEY'])
-    token = s.sign(addr)
-    text = """Please click the following link to confirm your email address for {}: {}""".format(
-        get_config('ctf_name'),
-        url_for('auth.confirm_user', _external=True) + '/' + urllib.quote_plus(token.encode('base64'))
+    s = TimedSerializer(app.config['SECRET_KEY'])
+    token = s.dumps(addr)
+    text = """Please click the following link to confirm your email address for {ctf_name}: {url}/{token}""".format(
+        ctf_name=get_config('ctf_name'),
+        url=url_for('auth.confirm_user', _external=True),
+        token=base64encode(token, urlencode=True)
     )
     sendmail(addr, text)
 
@@ -522,6 +526,28 @@ def sha512(string):
     return hashlib.sha512(string).hexdigest()
 
 
+def base64encode(s, urlencode=False):
+    if six.PY3 and isinstance(s, six.string_types):
+        s = s.encode('utf-8')
+    encoded = base64.urlsafe_b64encode(s)
+    if six.PY3:
+        encoded = encoded.decode('utf-8')
+    if urlencode:
+        encoded = quote(encoded)
+    return encoded
+
+
+def base64decode(s, urldecode=False):
+    if urldecode:
+        s = unquote(s)
+    if six.PY3 and isinstance(s, six.string_types):
+        s = s.encode('utf-8')
+    decoded = base64.urlsafe_b64decode(s)
+    if six.PY3:
+        decoded = decoded.decode('utf-8')
+    return decoded
+
+
 @cache.memoize()
 def can_create_container():
     try:


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index f09b1ce8..16bbac12 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -3,7 +3,8 @@
 
 from tests.helpers import *
 from CTFd.models import ip2long, long2ip
-from CTFd.utils import get_config, set_config, override_template, sendmail
+from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email
+from CTFd.utils import base64encode, base64decode
 from mock import patch
 import json
 
@@ -41,6 +42,20 @@ def test_long2ip_ipv6():
     assert long2ip(42540616829182469433547762482097946625) == '2001:658:22a:cafe:200::1'
 
 
+def test_base64encode():
+    """The base64encode wrapper works properly"""
+    assert base64encode('abc123') == 'YWJjMTIz'
+    assert base64encode('😆') == '8J-Yhg=='
+    assert base64encode('😆', urlencode=True) == '8J-Yhg%3D%3D'
+
+
+def test_base64decode():
+    """The base64decode wrapper works properly"""
+    assert base64decode('YWJjMTIz') == 'abc123'
+    assert base64decode('8J-Yhg==') == '😆'
+    assert base64decode('8J-Yhg%3D%3D', urldecode=True) == '😆'
+
+
 def test_override_template():
     """Does override_template work properly for regular themes"""
     app = create_ctfd()
@@ -93,3 +108,37 @@ def test_sendmail_with_smtp(mock_smtp):
 
         mock_smtp.return_value.sendmail.assert_called_once_with(from_addr, [to_addr], email_msg.as_string())
     destroy_ctfd(app)
+
+
+@patch('smtplib.SMTP')
+def test_verify_email(mock_smtp):
+    """Does verify_email send emails"""
+    from email.mime.text import MIMEText
+    app = create_ctfd()
+    with app.app_context():
+        set_config('mail_server', 'localhost')
+        set_config('mail_port', 25)
+        set_config('mail_username', 'username')
+        set_config('mail_password', 'password')
+        set_config('verify_emails', True)
+
+        from_addr = get_config('mailfrom_addr') or app.config.get('MAILFROM_ADDR')
+        to_addr = 'user@user.com'
+
+        verify_email(to_addr)
+
+        # This is currently not actually validated
+        msg = ("Please click the following link to confirm"
+               " your email address for CTFd:"
+               " http://localhost/confirm/InVzZXJAdXNlci5jb20iLkRHbGFZUS5XUURfQzBub3pGZkFMRlIyeGxDS1BCMjZETlk%3D")
+
+        ctf_name = get_config('ctf_name')
+        email_msg = MIMEText(msg)
+        email_msg['Subject'] = "Message from {0}".format(ctf_name)
+        email_msg['From'] = from_addr
+        email_msg['To'] = to_addr
+
+        # Need to freeze time to predict the value of the itsdangerous token.
+        # For now just assert that sendmail was called.
+        mock_smtp.return_value.sendmail.assert_called()
+    destroy_ctfd(app)