A web server for doing quick budgeting
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

231 lines
8.6 KiB

  1. #!/usr/bin/env python3
  2. import tornado.ioloop
  3. import tornado.web
  4. import tornado.websocket
  5. import tornado.httpclient
  6. import tornado.gen
  7. import os
  8. import random
  9. import webbrowser
  10. # Require a username and password in order to use the web interface. See ReadMe.org for details.
  11. #enable_authentication = False
  12. enable_authentication = True
  13. # Allow anyone to create an account even after the first account has been created
  14. # This is a security vulnerability that should be considered before being enabled.
  15. # Do you want anyone to be able to use the service provided by your server?
  16. enable_subsequent_account_creation = False
  17. # enable_subsequent_account_creation = True
  18. # If "next" isn't specified from login, redirect here after login instead
  19. landingPage = "/"
  20. if enable_authentication:
  21. import PasswordManager
  22. # List of valid user ids (used to compare user cookie)
  23. authenticated_users = []
  24. # See https://github.com/tornadoweb/tornado/blob/stable/demos/blog/blog.py
  25. # https://www.tornadoweb.org/en/stable/guide/security.html
  26. def login_get_current_user(handler):
  27. if enable_authentication:
  28. cookie = handler.get_secure_cookie("user")
  29. if cookie in authenticated_users:
  30. return cookie
  31. else:
  32. print("Bad/expired cookie received")
  33. return None
  34. else:
  35. return "authentication_disabled"
  36. class AuthHandler(tornado.web.RequestHandler):
  37. def get_current_user(self):
  38. return login_get_current_user(self)
  39. class LoginNewAccountHandler(AuthHandler):
  40. def get(self):
  41. if not enable_subsequent_account_creation:
  42. self.redirect("/login")
  43. else:
  44. # New password setup
  45. self.render("templates/LoginCreate.html",
  46. next=self.get_argument("next", landingPage),
  47. xsrf_form_html=self.xsrf_form_html())
  48. class LoginHandler(AuthHandler):
  49. def get(self):
  50. if not enable_authentication:
  51. self.redirect("/")
  52. else:
  53. if PasswordManager.havePasswordsBeenSet():
  54. self.render("templates/Login.html",
  55. next=self.get_argument("next", landingPage),
  56. xsrf_form_html=self.xsrf_form_html(),
  57. create_accounts_html = ("" if not enable_subsequent_account_creation
  58. else '<p>Go <a href="/createNewAccount">here</a> to create an account.</p>'))
  59. else:
  60. # New password setup
  61. self.render("templates/LoginCreate.html",
  62. next=self.get_argument("next", landingPage),
  63. xsrf_form_html=self.xsrf_form_html())
  64. def post(self):
  65. global authenticated_users
  66. # Test password
  67. print("Attempting to authorize user {}...".format(self.get_argument("name")))
  68. if (enable_authentication
  69. and PasswordManager.verify(self.get_argument("name"), self.get_argument("password"))):
  70. # Generate new authenticated user session
  71. randomGenerator = random.SystemRandom()
  72. cookieSecret = str(randomGenerator.getrandbits(128))
  73. authenticated_user = self.get_argument("name") + "_" + cookieSecret
  74. authenticated_user = authenticated_user.encode()
  75. authenticated_users.append(authenticated_user)
  76. # Set the cookie on the user's side
  77. self.set_secure_cookie("user", authenticated_user)
  78. print("Authenticated user {}".format(self.get_argument("name")))
  79. # Let them in
  80. self.redirect(self.get_argument("next", landingPage))
  81. else:
  82. print("Refused user {} (password doesn't match any in database)"
  83. .format(self.get_argument("name")))
  84. self.redirect("/login")
  85. class LogoutHandler(AuthHandler):
  86. @tornado.web.authenticated
  87. def get(self):
  88. global authenticated_users
  89. if enable_authentication:
  90. print("User {} logging out".format(self.current_user))
  91. if self.current_user in authenticated_users:
  92. authenticated_users.remove(self.current_user)
  93. self.redirect("/login")
  94. else:
  95. self.redirect("/")
  96. class CreateAccountHandler(AuthHandler):
  97. def get(self):
  98. pass
  99. def post(self):
  100. if not enable_authentication:
  101. self.redirect("/")
  102. else:
  103. print("Attempting to set password")
  104. if not enable_subsequent_account_creation and PasswordManager.havePasswordsBeenSet():
  105. print("Rejected: Accounts cannot be created via the server if one already has been made.")
  106. elif self.get_argument("password") != self.get_argument("password_verify"):
  107. print("Rejected: password doesn't match verify field!")
  108. else:
  109. result = PasswordManager.createAccount(self.get_argument("name"),
  110. self.get_argument("password"))
  111. if not result[0]:
  112. print("Failed: {}".format(result[1]))
  113. print("Success: {}".format(result[1]))
  114. self.redirect("/login")
  115. class AuthedStaticHandler(tornado.web.StaticFileHandler):
  116. def get_current_user(self):
  117. return login_get_current_user(self)
  118. @tornado.web.authenticated
  119. def prepare(self):
  120. pass
  121. class HomeHandler(AuthHandler):
  122. @tornado.web.authenticated
  123. def get(self):
  124. self.render('webInterface/index.html')
  125. class ExampleWebSocket(tornado.websocket.WebSocketHandler):
  126. connections = set()
  127. def open(self):
  128. global userSessionData
  129. currentUser = login_get_current_user(self)
  130. if not currentUser:
  131. # Failed authorization
  132. return None
  133. self.connections.add(self)
  134. def on_message(self, message):
  135. currentUser = login_get_current_user(self)
  136. if not currentUser:
  137. # Failed authorization
  138. return None
  139. def on_close(self):
  140. self.connections.remove(self)
  141. #
  142. # Startup
  143. #
  144. def make_app():
  145. # Each time the server starts up, invalidate all cookies
  146. randomGenerator = random.SystemRandom()
  147. cookieSecret = str(randomGenerator.getrandbits(128))
  148. return tornado.web.Application([
  149. (r'/', HomeHandler),
  150. # Login
  151. (r'/login', LoginHandler),
  152. (r'/logout', LogoutHandler),
  153. (r'/createNewAccount', LoginNewAccountHandler),
  154. (r'/finishCreateAccount', CreateAccountHandler),
  155. # (r'/ExampleWebSocket', ExampleWebSocket),
  156. # Static files
  157. (r'/webInterface/(.*)', AuthedStaticHandler, {'path' : 'webInterface'}),
  158. # Files served regardless of whether the user is authenticated. Only login page resources
  159. # should be in this folder, because anyone can see them
  160. (r'/webInterfaceNoAuth/(.*)', tornado.web.StaticFileHandler, {'path' : 'webInterfaceNoAuth'}),
  161. ],
  162. xsrf_cookies=True,
  163. cookie_secret=cookieSecret,
  164. login_url="/login")
  165. if __name__ == '__main__':
  166. port = 8888
  167. print('\nStarting Authenticated Server on port {}...'.format(port))
  168. app = make_app()
  169. # Generating a self-signing certificate:
  170. # openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem
  171. # (from https://jupyter-notebook.readthedocs.io/en/latest/public_server.html)
  172. # I then had to tell Firefox to trust this certificate even though it is self-signing (because
  173. # I want a free certificate for this non-serious project)
  174. useSSL = True
  175. if useSSL:
  176. if os.path.exists("certificates/server.crt.pem"):
  177. app.listen(port, ssl_options={"certfile":"certificates/server.crt.pem",
  178. "keyfile":"certificates/server.crt.key"})
  179. else:
  180. print('\n\tERROR: Certificates non-existent! Run ./Generate_Certificates.sh to create them')
  181. else:
  182. # Show the warning only if SSL is not enabled
  183. print('\n\tWARNING: Do NOT run this server on the internet (e.g. port-forwarded)'
  184. ' nor when\n\t connected to an insecure LAN! It is not protected against malicious use.\n')
  185. app.listen(port)
  186. browseUrl ="{}://localhost:{}".format('https' if useSSL else 'http', port)
  187. print("Attempting to launch user's default browser to {}".format(browseUrl))
  188. webbrowser.open(browseUrl)
  189. ioLoop = tornado.ioloop.IOLoop.current()
  190. ioLoop.start()