|
- import web
- import os
- import hmac
- import base64
- import struct
- import hashlib
- import random
- import string
- import time
- import bcrypt
- import logging
- import smtplib
- from email.message import EmailMessage
- from email.headerregistry import Address
- from uuid import uuid4
-
- logger = logging.getLogger(__name__)
-
-
- def sendmail(subject, message, to_name, to_email, from_name="Beelance", from_email="beelance@ntnu.no"):
- try:
- msg = EmailMessage()
- msg['From'] = Address(from_name, from_email)
- msg['To'] = Address(to_name, to_email)
- msg['Subject'] = subject
- msg.set_content(message)
-
- logger.info("Sending email: %s", msg)
-
- with get_smtp() as smtp:
- smtp.set_debuglevel(2)
- smtp.send_message(msg)
- except Exception:
- logging.exception("Exception when sending email")
-
-
- def get_smtp(timeout=2):
- smtp_server = os.getenv("smtp_server", default="molde.idi.ntnu.no") + ":25"
- return smtplib.SMTP(smtp_server, timeout=timeout)
-
-
- def get_render(path='templates/', globals={}, **kwargs):
- default_globals = {
- 'session': web.config._session,
- 'csrf_field': csrf_field,
- }
-
- return web.template.render(
- path,
- globals={**default_globals, **globals},
- **kwargs,
- )
-
-
- def get_nav_bar(session):
- """
- Generates the page nav bar
-
- :return: The navigation bar HTML markup
- """
- result = '<nav>'
- result += ' <ul>'
- result += ' <li><h1 id="title">Beelance2</h1></li>'
- result += ' <li><a href="/">Home</a></li>'
- if session.username:
- result += ' <li><a href="logout">Logout</a></li>'
- result += ' <li><a href="new_project">New</a></li>'
- else:
- result += ' <li><a href="register">Register</a></li>'
- result += ' <li><a href="login">Login</a></li>'
- result += ' <li><a href="open_projects">Projects</a></li>'
- result += ' </ul>'
- result += '</nav>'
- return result
-
-
- def get_element_count(data, element):
- """
- Determine the number of tasks created by removing
- the four other elements from count and divide by the
- number of variables in one task.
-
- :param data: The data object from web.input
- :return: The number of tasks opened by the client
- """
- task_count = 0
- while True:
- try:
- data[element+str(task_count)]
- task_count += 1
- except:
- break
- return task_count
-
-
- def csrf_token():
- """
- Get the CSRF token for the session
- """
- session = web.ctx.session
-
- if 'csrf_token' not in session:
- session.csrf_token = uuid4().hex
-
- return session.csrf_token
-
-
- def csrf_field():
- """
- Return a HTML form field for the CSRF token
- """
- return f'<input type="hidden" name="csrf_token" value="{csrf_token()}" />'
-
-
- def csrf_protected(f):
- """
- Decorate a function to do a CSRF check.
- """
- def decorated(*args, **kwargs):
- session = web.ctx.session
- inp = web.input()
- if not ('csrf_token' in inp and inp.csrf_token == session.pop('csrf_token', None)):
- raise web.HTTPError(
- '400 Bad request',
- {'content-type': 'text/html'},
- 'Cross-site request forgery attempt',
- )
- return f(*args, **kwargs)
-
- return decorated
-
-
- def is_common_password(password):
- """Helper function that checks various common passwords."""
- def common_sequences(n):
- # Check sequences of the same number
- for i in range(n):
- for j in range(n):
- yield ''.join([str(i) for _ in range(j)])
-
- # Check incrementing sequences
- for i in range(n):
- # Starting at 0
- seq = ''.join([str(j) for j in range(i)])
- yield seq
- # Starting at 1
- yield seq[1:]
-
- # Decrementing
- # Starting at 0
- yield seq[::-1]
- # Starting at 1
- yield seq[1::-1]
-
- common_passwords = [
- 'password', 'qwerty', 'iloveyou', '123123', 'abc123', 'admin',
- 'passwrod', 'password1', 'beelance', 'beelance2'
- ]
-
- if password in common_passwords or password in common_sequences(12):
- return True
-
- return False
-
-
- def password_weakness(password, username):
- """
- Check if the password fulfills the password policy.
-
- The policy is:
- - At least 8 characters, but not more than 70 (due to bcrypt)
- - Does not overlap with the username
- - Not a common password
-
- :param password: The password to check
- :param username: The username of the user (used to check similarity)
- :return: The most important weakness of the password, or None if it fulfills the policy
- """
- if len(password) < 8:
- return "The password must be at least 5 characters long."
- elif len(password) > 70:
- return "The password can't be longer than 70 characters."
- elif password in username or username in password:
- return "The password can't overlap with your username."
- elif is_common_password(password):
- return "The password is too common. Choose something more unique."
-
- return None
-
-
- def hash_password(password):
- """
- Create a hash of the given password, with a random salt.
-
- :param password: The password to hash
- :return: The generated hash
- """
- return bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())
-
-
- def check_password(password, password_hash):
- """
- Check if the entered password matches the hashed password.
-
- :param password: The password to check
- :param password_hash: The password hash to check against
- :return: True if the password matches, or False if it doesn't
- """
- return password and password_hash and bcrypt.checkpw(password.encode('UTF-8'), password_hash.encode('UTF-8'))
-
-
- # Authenticator code copied from https://github.com/jakobsn/google-authenticator.
- # The license for this code can be found in the file LICENSE.google-authenticator
-
- def get_hotp_token(secret, intervals_no):
- """This is where the magic happens."""
- key = base64.b32decode(normalize(secret), True) # True is to fold lower into uppercase
- msg = struct.pack(">Q", intervals_no)
- h = bytearray(hmac.new(key, msg, hashlib.sha1).digest())
- o = h[19] & 15
- h = str((struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000)
- return prefix0(h)
-
-
- def get_totp_token(secret):
- """The TOTP token is just a HOTP token seeded with every 30 seconds."""
- return get_hotp_token(secret, intervals_no=int(time.time())//30)
-
-
- def normalize(key):
- """Normalizes secret by removing spaces and padding with = to a multiple of 8"""
- k2 = key.strip().replace(' ', '')
- # k2 = k2.upper() # skipped b/c b32decode has a foldcase argument
- if len(k2) % 8 != 0:
- k2 += '=' * (8 - len(k2) % 8)
- return k2
-
-
- def prefix0(h):
- """Prefixes code with leading zeros if missing."""
- if len(h) < 6:
- h = '0'*(6-len(h)) + h
- return h
-
- # End of code copied from https://github.com/jakobsn/google-authenticator.
-
-
- def generate_authenticator_secret():
- length = 16
- alphabet = string.ascii_uppercase
-
- return ''.join(random.SystemRandom().choice(alphabet) for _ in range(length))
|