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