MultiuserPlugin.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import re
  2. import sys
  3. import json
  4. from Config import config
  5. from Plugin import PluginManager
  6. from Crypt import CryptBitcoin
  7. from . import UserPlugin
  8. from util.Flag import flag
  9. from Translate import translate as _
  10. # We can only import plugin host clases after the plugins are loaded
  11. @PluginManager.afterLoad
  12. def importPluginnedClasses():
  13. global UserManager
  14. from User import UserManager
  15. try:
  16. local_master_addresses = set(json.load(open("%s/users.json" % config.data_dir)).keys()) # Users in users.json
  17. except Exception as err:
  18. local_master_addresses = set()
  19. @PluginManager.registerTo("UiRequest")
  20. class UiRequestPlugin(object):
  21. def __init__(self, *args, **kwargs):
  22. self.user_manager = UserManager.user_manager
  23. super(UiRequestPlugin, self).__init__(*args, **kwargs)
  24. def parsePath(self, path):
  25. return super(UiRequestPlugin, self).parsePath(path)
  26. # Create new user and inject user welcome message if necessary
  27. # Return: Html body also containing the injection
  28. def actionWrapper(self, path, extra_headers=None):
  29. match = re.match("/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
  30. if not match:
  31. return False
  32. inner_path = match.group("inner_path").lstrip("/")
  33. html_request = "." not in inner_path or inner_path.endswith(".html") # Only inject html to html requests
  34. user_created = False
  35. if html_request:
  36. user = self.getCurrentUser() # Get user from cookie
  37. if not user: # No user found by cookie
  38. user = self.user_manager.create()
  39. user_created = True
  40. else:
  41. user = None
  42. # Disable new site creation if --multiuser_no_new_sites enabled
  43. if config.multiuser_no_new_sites:
  44. path_parts = self.parsePath(path)
  45. if not self.server.site_manager.get(match.group("address")) and (not user or user.master_address not in local_master_addresses):
  46. self.sendHeader(404)
  47. return self.formatError("Not Found", "Adding new sites disabled on this proxy", details=False)
  48. if user_created:
  49. if not extra_headers:
  50. extra_headers = {}
  51. extra_headers['Set-Cookie'] = "master_address=%s;path=/;max-age=2592000;" % user.master_address # = 30 days
  52. loggedin = self.get.get("login") == "done"
  53. back_generator = super(UiRequestPlugin, self).actionWrapper(path, extra_headers) # Get the wrapper frame output
  54. if not back_generator: # Wrapper error or not string returned, injection not possible
  55. return False
  56. elif loggedin:
  57. back = next(back_generator)
  58. inject_html = """
  59. <!-- Multiser plugin -->
  60. <script nonce="{script_nonce}">
  61. setTimeout(function() {
  62. zeroframe.cmd("wrapperNotification", ["done", "{message}<br><small>You have been logged in successfully</small>", 5000])
  63. }, 1000)
  64. </script>
  65. </body>
  66. </html>
  67. """.replace("\t", "")
  68. if user.master_address in local_master_addresses:
  69. message = "Hello master!"
  70. else:
  71. message = "Hello again!"
  72. inject_html = inject_html.replace("{message}", message)
  73. inject_html = inject_html.replace("{script_nonce}", self.getScriptNonce())
  74. return iter([re.sub(b"</body>\s*</html>\s*$", inject_html.encode(), back)]) # Replace the </body></html> tags with the injection
  75. else: # No injection necessary
  76. return back_generator
  77. # Get the current user based on request's cookies
  78. # Return: User object or None if no match
  79. def getCurrentUser(self):
  80. cookies = self.getCookies()
  81. user = None
  82. if "master_address" in cookies:
  83. users = self.user_manager.list()
  84. user = users.get(cookies["master_address"])
  85. return user
  86. @PluginManager.registerTo("UiWebsocket")
  87. class UiWebsocketPlugin(object):
  88. def __init__(self, *args, **kwargs):
  89. if config.multiuser_no_new_sites:
  90. flag.no_multiuser(self.actionMergerSiteAdd)
  91. super(UiWebsocketPlugin, self).__init__(*args, **kwargs)
  92. # Let the page know we running in multiuser mode
  93. def formatServerInfo(self):
  94. server_info = super(UiWebsocketPlugin, self).formatServerInfo()
  95. server_info["multiuser"] = True
  96. if "ADMIN" in self.site.settings["permissions"]:
  97. server_info["master_address"] = self.user.master_address
  98. is_multiuser_admin = config.multiuser_local or self.user.master_address in local_master_addresses
  99. server_info["multiuser_admin"] = is_multiuser_admin
  100. return server_info
  101. # Show current user's master seed
  102. @flag.admin
  103. def actionUserShowMasterSeed(self, to):
  104. message = "<b style='padding-top: 5px; display: inline-block'>Your unique private key:</b>"
  105. message += "<div style='font-size: 84%%; background-color: #FFF0AD; padding: 5px 8px; margin: 9px 0px'>%s</div>" % self.user.master_seed
  106. message += "<small>(Save it, you can access your account using this information)</small>"
  107. self.cmd("notification", ["info", message])
  108. # Logout user
  109. @flag.admin
  110. def actionUserLogout(self, to):
  111. message = "<b>You have been logged out.</b> <a href='#Login' class='button' id='button_notification'>Login to another account</a>"
  112. self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :)
  113. script = "document.cookie = 'master_address=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';"
  114. script += "$('#button_notification').on('click', function() { zeroframe.cmd(\"userLoginForm\", []); });"
  115. self.cmd("injectScript", script)
  116. # Delete from user_manager
  117. user_manager = UserManager.user_manager
  118. if self.user.master_address in user_manager.users:
  119. if not config.multiuser_local:
  120. del user_manager.users[self.user.master_address]
  121. self.response(to, "Successful logout")
  122. else:
  123. self.response(to, "User not found")
  124. @flag.admin
  125. def actionUserSet(self, to, master_address):
  126. user_manager = UserManager.user_manager
  127. user = user_manager.get(master_address)
  128. if not user:
  129. raise Exception("No user found")
  130. script = "document.cookie = 'master_address=%s;path=/;max-age=2592000;';" % master_address
  131. script += "zeroframe.cmd('wrapperReload', ['login=done']);"
  132. self.cmd("notification", ["done", "Successful login, reloading page..."])
  133. self.cmd("injectScript", script)
  134. self.response(to, "ok")
  135. @flag.admin
  136. def actionUserSelectForm(self, to):
  137. if not config.multiuser_local:
  138. raise Exception("Only allowed in multiuser local mode")
  139. user_manager = UserManager.user_manager
  140. body = "<span style='padding-bottom: 5px; display: inline-block'>" + "Change account:" + "</span>"
  141. for master_address, user in user_manager.list().items():
  142. is_active = self.user.master_address == master_address
  143. if user.certs:
  144. first_cert = next(iter(user.certs.keys()))
  145. title = "%s@%s" % (user.certs[first_cert]["auth_user_name"], first_cert)
  146. else:
  147. title = user.master_address
  148. if len(user.sites) < 2 and not is_active: # Avoid listing ad-hoc created users
  149. continue
  150. if is_active:
  151. css_class = "active"
  152. else:
  153. css_class = "noclass"
  154. body += "<a href='#Select+user' class='select select-close user %s' title='%s'>%s</a>" % (css_class, user.master_address, title)
  155. script = """
  156. $(".notification .select.user").on("click", function() {
  157. $(".notification .select").removeClass('active')
  158. zeroframe.response(%s, this.title)
  159. return false
  160. })
  161. """ % self.next_message_id
  162. self.cmd("notification", ["ask", body], lambda master_address: self.actionUserSet(to, master_address))
  163. self.cmd("injectScript", script)
  164. # Show login form
  165. def actionUserLoginForm(self, to):
  166. self.cmd("prompt", ["<b>Login</b><br>Your private key:", "password", "Login"], self.responseUserLogin)
  167. # Login form submit
  168. def responseUserLogin(self, master_seed):
  169. user_manager = UserManager.user_manager
  170. user = user_manager.get(CryptBitcoin.privatekeyToAddress(master_seed))
  171. if not user:
  172. user = user_manager.create(master_seed=master_seed)
  173. if user.master_address:
  174. script = "document.cookie = 'master_address=%s;path=/;max-age=2592000;';" % user.master_address
  175. script += "zeroframe.cmd('wrapperReload', ['login=done']);"
  176. self.cmd("notification", ["done", "Successful login, reloading page..."])
  177. self.cmd("injectScript", script)
  178. else:
  179. self.cmd("notification", ["error", "Error: Invalid master seed"])
  180. self.actionUserLoginForm(0)
  181. def hasCmdPermission(self, cmd):
  182. flags = flag.db.get(self.getCmdFuncName(cmd), ())
  183. is_public_proxy_user = not config.multiuser_local and self.user.master_address not in local_master_addresses
  184. if is_public_proxy_user and "no_multiuser" in flags:
  185. self.cmd("notification", ["info", _("This function ({cmd}) is disabled on this proxy!")])
  186. return False
  187. else:
  188. return super(UiWebsocketPlugin, self).hasCmdPermission(cmd)
  189. def actionCertAdd(self, *args, **kwargs):
  190. super(UiWebsocketPlugin, self).actionCertAdd(*args, **kwargs)
  191. master_seed = self.user.master_seed
  192. message = """
  193. <style>
  194. .masterseed {
  195. font-size: 85%; background-color: #FFF0AD; padding: 5px 8px; margin: 9px 0px; width: 100%;
  196. box-sizing: border-box; border: 0px; text-align: center; cursor: pointer;
  197. }
  198. </style>
  199. <b>Hello, welcome to ZeroProxy!</b><div style='margin-top: 8px'>A new, unique account created for you:</div>
  200. <input type='text' class='masterseed' id='button_notification_masterseed' value='Click here to show' readonly/>
  201. <div style='text-align: center; font-size: 85%; margin-bottom: 10px;'>
  202. or <a href='#Download' id='button_notification_download'
  203. class='masterseed_download' download='zeronet_private_key.backup'>Download backup as text file</a>
  204. </div>
  205. <div>
  206. This is your private key, <b>save it</b>, so you can login next time.<br>
  207. <b>Warning: Without this key, your account will be lost forever!</b>
  208. </div><br>
  209. <a href='#' class='button' style='margin-left: 0px'>Ok, Saved it!</a><br><br>
  210. <small>This site allows you to browse ZeroNet content, but if you want to secure your account <br>
  211. and help to keep the network alive, then please run your own <a href='https://zeronet.io' target='_blank'>ZeroNet client</a>.</small>
  212. """
  213. self.cmd("notification", ["info", message])
  214. script = """
  215. $("#button_notification_masterseed").on("click", function() {
  216. this.value = "{master_seed}"; this.setSelectionRange(0,100);
  217. })
  218. $("#button_notification_download").on("mousedown", function() {
  219. this.href = window.URL.createObjectURL(new Blob(["ZeroNet user master seed:\\r\\n{master_seed}"]))
  220. })
  221. """.replace("{master_seed}", master_seed)
  222. self.cmd("injectScript", script)
  223. def actionPermissionAdd(self, to, permission):
  224. is_public_proxy_user = not config.multiuser_local and self.user.master_address not in local_master_addresses
  225. if permission == "NOSANDBOX" and is_public_proxy_user:
  226. self.cmd("notification", ["info", "You can't disable sandbox on this proxy!"])
  227. self.response(to, {"error": "Denied by proxy"})
  228. return False
  229. else:
  230. return super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission)
  231. @PluginManager.registerTo("ConfigPlugin")
  232. class ConfigPlugin(object):
  233. def createArguments(self):
  234. group = self.parser.add_argument_group("Multiuser plugin")
  235. group.add_argument('--multiuser_local', help="Enable unsafe Ui functions and write users to disk", action='store_true')
  236. group.add_argument('--multiuser_no_new_sites', help="Denies adding new sites by normal users", action='store_true')
  237. return super(ConfigPlugin, self).createArguments()