您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

253 行
7.1KB

  1. import web
  2. import os
  3. import hmac
  4. import base64
  5. import struct
  6. import hashlib
  7. import random
  8. import string
  9. import time
  10. import bcrypt
  11. import logging
  12. import smtplib
  13. from email.message import EmailMessage
  14. from email.headerregistry import Address
  15. from uuid import uuid4
  16. logger = logging.getLogger(__name__)
  17. def sendmail(subject, message, to_name, to_email, from_name="Beelance", from_email="beelance@ntnu.no"):
  18. try:
  19. msg = EmailMessage()
  20. msg['From'] = Address(from_name, from_email)
  21. msg['To'] = Address(to_name, to_email)
  22. msg['Subject'] = subject
  23. msg.set_content(message)
  24. logger.info("Sending email: %s", msg)
  25. with get_smtp() as smtp:
  26. smtp.set_debuglevel(2)
  27. smtp.send_message(msg)
  28. except Exception:
  29. logging.exception("Exception when sending email")
  30. def get_smtp(timeout=2):
  31. smtp_server = os.getenv("smtp_server", default="molde.idi.ntnu.no") + ":25"
  32. return smtplib.SMTP(smtp_server, timeout=timeout)
  33. def get_render(path='templates/', globals={}, **kwargs):
  34. default_globals = {
  35. 'session': web.config._session,
  36. 'csrf_field': csrf_field,
  37. }
  38. return web.template.render(
  39. path,
  40. globals={**default_globals, **globals},
  41. **kwargs,
  42. )
  43. def get_nav_bar(session):
  44. """
  45. Generates the page nav bar
  46. :return: The navigation bar HTML markup
  47. """
  48. result = '<nav>'
  49. result += ' <ul>'
  50. result += ' <li><h1 id="title">Beelance2</h1></li>'
  51. result += ' <li><a href="/">Home</a></li>'
  52. if session.username:
  53. result += ' <li><a href="logout">Logout</a></li>'
  54. result += ' <li><a href="new_project">New</a></li>'
  55. else:
  56. result += ' <li><a href="register">Register</a></li>'
  57. result += ' <li><a href="login">Login</a></li>'
  58. result += ' <li><a href="open_projects">Projects</a></li>'
  59. result += ' </ul>'
  60. result += '</nav>'
  61. return result
  62. def get_element_count(data, element):
  63. """
  64. Determine the number of tasks created by removing
  65. the four other elements from count and divide by the
  66. number of variables in one task.
  67. :param data: The data object from web.input
  68. :return: The number of tasks opened by the client
  69. """
  70. task_count = 0
  71. while True:
  72. try:
  73. data[element+str(task_count)]
  74. task_count += 1
  75. except:
  76. break
  77. return task_count
  78. def csrf_token():
  79. """
  80. Get the CSRF token for the session
  81. """
  82. session = web.ctx.session
  83. if 'csrf_token' not in session:
  84. session.csrf_token = uuid4().hex
  85. return session.csrf_token
  86. def csrf_field():
  87. """
  88. Return a HTML form field for the CSRF token
  89. """
  90. return f'<input type="hidden" name="csrf_token" value="{csrf_token()}" />'
  91. def csrf_protected(f):
  92. """
  93. Decorate a function to do a CSRF check.
  94. """
  95. def decorated(*args, **kwargs):
  96. session = web.ctx.session
  97. inp = web.input()
  98. if not ('csrf_token' in inp and inp.csrf_token == session.pop('csrf_token', None)):
  99. raise web.HTTPError(
  100. '400 Bad request',
  101. {'content-type': 'text/html'},
  102. 'Cross-site request forgery attempt',
  103. )
  104. return f(*args, **kwargs)
  105. return decorated
  106. def is_common_password(password):
  107. """Helper function that checks various common passwords."""
  108. def common_sequences(n):
  109. # Check sequences of the same number
  110. for i in range(n):
  111. for j in range(n):
  112. yield ''.join([str(i) for _ in range(j)])
  113. # Check incrementing sequences
  114. for i in range(n):
  115. # Starting at 0
  116. seq = ''.join([str(j) for j in range(i)])
  117. yield seq
  118. # Starting at 1
  119. yield seq[1:]
  120. # Decrementing
  121. # Starting at 0
  122. yield seq[::-1]
  123. # Starting at 1
  124. yield seq[1::-1]
  125. common_passwords = [
  126. 'password', 'qwerty', 'iloveyou', '123123', 'abc123', 'admin',
  127. 'passwrod', 'password1', 'beelance', 'beelance2'
  128. ]
  129. if password in common_passwords or password in common_sequences(12):
  130. return True
  131. return False
  132. def password_weakness(password, username):
  133. """
  134. Check if the password fulfills the password policy.
  135. The policy is:
  136. - At least 8 characters, but not more than 70 (due to bcrypt)
  137. - Does not overlap with the username
  138. - Not a common password
  139. :param password: The password to check
  140. :param username: The username of the user (used to check similarity)
  141. :return: The most important weakness of the password, or None if it fulfills the policy
  142. """
  143. if len(password) < 8:
  144. return "The password must be at least 5 characters long."
  145. elif len(password) > 70:
  146. return "The password can't be longer than 70 characters."
  147. elif password in username or username in password:
  148. return "The password can't overlap with your username."
  149. elif is_common_password(password):
  150. return "The password is too common. Choose something more unique."
  151. return None
  152. def hash_password(password):
  153. """
  154. Create a hash of the given password, with a random salt.
  155. :param password: The password to hash
  156. :return: The generated hash
  157. """
  158. return bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())
  159. def check_password(password, password_hash):
  160. """
  161. Check if the entered password matches the hashed password.
  162. :param password: The password to check
  163. :param password_hash: The password hash to check against
  164. :return: True if the password matches, or False if it doesn't
  165. """
  166. return password and password_hash and bcrypt.checkpw(password.encode('UTF-8'), password_hash.encode('UTF-8'))
  167. # Authenticator code copied from https://github.com/jakobsn/google-authenticator.
  168. # The license for this code can be found in the file LICENSE.google-authenticator
  169. def get_hotp_token(secret, intervals_no):
  170. """This is where the magic happens."""
  171. key = base64.b32decode(normalize(secret), True) # True is to fold lower into uppercase
  172. msg = struct.pack(">Q", intervals_no)
  173. h = bytearray(hmac.new(key, msg, hashlib.sha1).digest())
  174. o = h[19] & 15
  175. h = str((struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000)
  176. return prefix0(h)
  177. def get_totp_token(secret):
  178. """The TOTP token is just a HOTP token seeded with every 30 seconds."""
  179. return get_hotp_token(secret, intervals_no=int(time.time())//30)
  180. def normalize(key):
  181. """Normalizes secret by removing spaces and padding with = to a multiple of 8"""
  182. k2 = key.strip().replace(' ', '')
  183. # k2 = k2.upper() # skipped b/c b32decode has a foldcase argument
  184. if len(k2) % 8 != 0:
  185. k2 += '=' * (8 - len(k2) % 8)
  186. return k2
  187. def prefix0(h):
  188. """Prefixes code with leading zeros if missing."""
  189. if len(h) < 6:
  190. h = '0'*(6-len(h)) + h
  191. return h
  192. # End of code copied from https://github.com/jakobsn/google-authenticator.
  193. def generate_authenticator_secret():
  194. length = 16
  195. alphabet = string.ascii_uppercase
  196. return ''.join(random.SystemRandom().choice(alphabet) for _ in range(length))