UiWebsocket.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170
  1. import json
  2. import time
  3. import sys
  4. import os
  5. import shutil
  6. import re
  7. import copy
  8. import logging
  9. import gevent
  10. from Config import config
  11. from Site import SiteManager
  12. from Debug import Debug
  13. from util import QueryJson, RateLimit
  14. from Plugin import PluginManager
  15. from Translate import translate as _
  16. from util import helper
  17. from util import SafeRe
  18. from Content.ContentManager import VerifyError, SignError
  19. @PluginManager.acceptPlugins
  20. class UiWebsocket(object):
  21. admin_commands = set([
  22. "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteAdd", "siteListModifiedFiles", "siteSetSettingsValue",
  23. "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "serverGetWrapperNonce",
  24. "certSet", "certList", "configSet", "permissionAdd", "permissionRemove", "announcerStats", "userSetGlobalSettings"
  25. ])
  26. async_commands = set(["fileGet", "fileList", "dirList", "fileNeed", "serverPortcheck", "siteListModifiedFiles"])
  27. def __init__(self, ws, site, server, user, request):
  28. self.ws = ws
  29. self.site = site
  30. self.user = user
  31. self.log = site.log
  32. self.request = request
  33. self.permissions = []
  34. self.server = server
  35. self.next_message_id = 1
  36. self.waiting_cb = {} # Waiting for callback. Key: message_id, Value: function pointer
  37. self.channels = [] # Channels joined to
  38. self.state = {"sending": False} # Shared state of websocket connection
  39. self.send_queue = [] # Messages to send to client
  40. # Start listener loop
  41. def start(self):
  42. ws = self.ws
  43. if self.site.address == config.homepage and not self.site.page_requested:
  44. # Add open fileserver port message or closed port error to homepage at first request after start
  45. self.site.page_requested = True # Dont add connection notification anymore
  46. file_server = sys.modules["main"].file_server
  47. if not file_server.port_opened or file_server.tor_manager.start_onions is None:
  48. self.site.page_requested = False # Not ready yet, check next time
  49. else:
  50. try:
  51. self.addHomepageNotifications()
  52. except Exception, err:
  53. self.log.error("Uncaught Exception: " + Debug.formatException(err))
  54. for notification in self.site.notifications: # Send pending notification messages
  55. # send via WebSocket
  56. self.cmd("notification", notification)
  57. # just in case, log them to terminal
  58. if notification[0] == "error":
  59. self.log.error("\n*** %s\n" % self.dedent(notification[1]))
  60. self.site.notifications = []
  61. while True:
  62. try:
  63. if ws.closed:
  64. break
  65. else:
  66. message = ws.receive()
  67. except Exception, err:
  68. self.log.error("WebSocket receive error: %s" % Debug.formatException(err))
  69. break
  70. if message:
  71. try:
  72. req = json.loads(message)
  73. self.handleRequest(req)
  74. except Exception, err:
  75. if config.debug: # Allow websocket errors to appear on /Debug
  76. sys.modules["main"].DebugHook.handleError()
  77. self.log.error("WebSocket handleRequest error: %s \n %s" % (Debug.formatException(err), message))
  78. if not self.hasPlugin("Multiuser"):
  79. self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html"))
  80. def dedent(self, text):
  81. return re.sub("[\\r\\n\\x20\\t]+", " ", text.strip().replace("<br>", " "))
  82. def addHomepageNotifications(self):
  83. if not(self.hasPlugin("Multiuser")) and not(self.hasPlugin("UiPassword")):
  84. bind_ip = getattr(config, "ui_ip", "")
  85. whitelist = getattr(config, "ui_restrict", [])
  86. # binds to the Internet, no IP whitelist, no UiPassword, no Multiuser
  87. if ("0.0.0.0" == bind_ip or "*" == bind_ip) and (not whitelist):
  88. self.site.notifications.append([
  89. "error",
  90. _(u"You are not going to set up a public gateway. However, <b>your Web UI is<br>" +
  91. "open to the whole Internet.</b> " +
  92. "Please check your configuration.")
  93. ])
  94. file_server = sys.modules["main"].file_server
  95. if any(file_server.port_opened.values()):
  96. self.site.notifications.append([
  97. "done",
  98. _["Congratulations, your port <b>{0}</b> is opened.<br>You are a full member of the ZeroNet network!"].format(config.fileserver_port),
  99. 10000
  100. ])
  101. elif config.tor == "always" and file_server.tor_manager.start_onions:
  102. self.site.notifications.append([
  103. "done",
  104. _(u"""
  105. {_[Tor mode active, every connection using Onion route.]}<br>
  106. {_[Successfully started Tor onion hidden services.]}
  107. """),
  108. 10000
  109. ])
  110. elif config.tor == "always" and file_server.tor_manager.start_onions is not False:
  111. self.site.notifications.append([
  112. "error",
  113. _(u"""
  114. {_[Tor mode active, every connection using Onion route.]}<br>
  115. {_[Unable to start hidden services, please check your config.]}
  116. """),
  117. 0
  118. ])
  119. elif file_server.tor_manager.start_onions:
  120. self.site.notifications.append([
  121. "done",
  122. _(u"""
  123. {_[Successfully started Tor onion hidden services.]}<br>
  124. {_[For faster connections open <b>{0}</b> port on your router.]}
  125. """).format(config.fileserver_port),
  126. 10000
  127. ])
  128. else:
  129. self.site.notifications.append([
  130. "error",
  131. _(u"""
  132. {_[Your connection is restricted. Please, open <b>{0}</b> port on your router]}<br>
  133. {_[or configure Tor to become a full member of the ZeroNet network.]}
  134. """).format(config.fileserver_port),
  135. 0
  136. ])
  137. def hasPlugin(self, name):
  138. return name in PluginManager.plugin_manager.plugin_names
  139. # Has permission to run the command
  140. def hasCmdPermission(self, cmd):
  141. cmd = cmd[0].lower() + cmd[1:]
  142. if cmd in self.admin_commands and "ADMIN" not in self.permissions:
  143. return False
  144. else:
  145. return True
  146. # Has permission to access a site
  147. def hasSitePermission(self, address, cmd=None):
  148. if address != self.site.address and "ADMIN" not in self.site.settings["permissions"]:
  149. return False
  150. else:
  151. return True
  152. def hasFilePermission(self, inner_path):
  153. valid_signers = self.site.content_manager.getValidSigners(inner_path)
  154. return self.site.settings["own"] or self.user.getAuthAddress(self.site.address) in valid_signers
  155. # Event in a channel
  156. def event(self, channel, *params):
  157. if channel in self.channels: # We are joined to channel
  158. if channel == "siteChanged":
  159. site = params[0]
  160. site_info = self.formatSiteInfo(site, create_user=False)
  161. if len(params) > 1 and params[1]: # Extra data
  162. site_info.update(params[1])
  163. self.cmd("setSiteInfo", site_info)
  164. elif channel == "serverChanged":
  165. server_info = self.formatServerInfo()
  166. self.cmd("setServerInfo", server_info)
  167. elif channel == "announcerChanged":
  168. site = params[0]
  169. announcer_info = self.formatAnnouncerInfo(site)
  170. if len(params) > 1 and params[1]: # Extra data
  171. announcer_info.update(params[1])
  172. self.cmd("setAnnouncerInfo", announcer_info)
  173. # Send response to client (to = message.id)
  174. def response(self, to, result):
  175. self.send({"cmd": "response", "to": to, "result": result})
  176. # Send a command
  177. def cmd(self, cmd, params={}, cb=None):
  178. self.send({"cmd": cmd, "params": params}, cb)
  179. # Encode to json and send message
  180. def send(self, message, cb=None):
  181. message["id"] = self.next_message_id # Add message id to allow response
  182. self.next_message_id += 1
  183. if cb: # Callback after client responded
  184. self.waiting_cb[message["id"]] = cb
  185. self.send_queue.append(message)
  186. if self.state["sending"]:
  187. return # Already sending
  188. try:
  189. while self.send_queue:
  190. self.state["sending"] = True
  191. message = self.send_queue.pop(0)
  192. self.ws.send(json.dumps(message))
  193. self.state["sending"] = False
  194. except Exception, err:
  195. self.log.debug("Websocket send error: %s" % Debug.formatException(err))
  196. self.state["sending"] = False
  197. def getPermissions(self, req_id):
  198. permissions = self.site.settings["permissions"]
  199. if req_id >= 1000000: # Its a wrapper command, allow admin commands
  200. permissions = permissions[:]
  201. permissions.append("ADMIN")
  202. return permissions
  203. def asyncWrapper(self, func):
  204. def asyncErrorWatcher(func, *args, **kwargs):
  205. try:
  206. result = func(*args, **kwargs)
  207. if result is not None:
  208. self.response(args[0], result)
  209. except Exception, err:
  210. if config.debug: # Allow websocket errors to appear on /Debug
  211. sys.modules["main"].DebugHook.handleError()
  212. self.log.error("WebSocket handleRequest error: %s" % Debug.formatException(err))
  213. self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html"))
  214. def wrapper(*args, **kwargs):
  215. gevent.spawn(asyncErrorWatcher, func, *args, **kwargs)
  216. return wrapper
  217. # Handle incoming messages
  218. def handleRequest(self, req):
  219. cmd = req.get("cmd")
  220. params = req.get("params")
  221. self.permissions = self.getPermissions(req["id"])
  222. if cmd == "response": # It's a response to a command
  223. return self.actionResponse(req["to"], req["result"])
  224. elif not self.hasCmdPermission(cmd): # Admin commands
  225. return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd})
  226. else: # Normal command
  227. func_name = "action" + cmd[0].upper() + cmd[1:]
  228. func = getattr(self, func_name, None)
  229. if not func: # Unknown command
  230. self.response(req["id"], {"error": "Unknown command: %s" % cmd})
  231. return
  232. # Execute in parallel
  233. if cmd in self.async_commands:
  234. func = self.asyncWrapper(func)
  235. # Support calling as named, unnamed parameters and raw first argument too
  236. if type(params) is dict:
  237. result = func(req["id"], **params)
  238. elif type(params) is list:
  239. result = func(req["id"], *params)
  240. elif params:
  241. result = func(req["id"], params)
  242. else:
  243. result = func(req["id"])
  244. if result is not None:
  245. self.response(req["id"], result)
  246. # Format site info
  247. def formatSiteInfo(self, site, create_user=True):
  248. content = site.content_manager.contents.get("content.json", {})
  249. if content: # Remove unnecessary data transfer
  250. content = content.copy()
  251. content["files"] = len(content.get("files", {}))
  252. content["files_optional"] = len(content.get("files_optional", {}))
  253. content["includes"] = len(content.get("includes", {}))
  254. if "sign" in content:
  255. del(content["sign"])
  256. if "signs" in content:
  257. del(content["signs"])
  258. if "signers_sign" in content:
  259. del(content["signers_sign"])
  260. settings = site.settings.copy()
  261. del settings["wrapper_key"] # Dont expose wrapper key
  262. del settings["auth_key"] # Dont send auth key twice
  263. ret = {
  264. "auth_key": self.site.settings["auth_key"], # Obsolete, will be removed
  265. "auth_address": self.user.getAuthAddress(site.address, create=create_user),
  266. "cert_user_id": self.user.getCertUserId(site.address),
  267. "address": site.address,
  268. "settings": settings,
  269. "content_updated": site.content_updated,
  270. "bad_files": len(site.bad_files),
  271. "size_limit": site.getSizeLimit(),
  272. "next_size_limit": site.getNextSizeLimit(),
  273. "peers": max(site.settings.get("peers", 0), len(site.peers)),
  274. "started_task_num": site.worker_manager.started_task_num,
  275. "tasks": len(site.worker_manager.tasks),
  276. "workers": len(site.worker_manager.workers),
  277. "content": content
  278. }
  279. if site.settings["own"]:
  280. ret["privatekey"] = bool(self.user.getSiteData(site.address, create=create_user).get("privatekey"))
  281. if site.settings["serving"] and content:
  282. ret["peers"] += 1 # Add myself if serving
  283. return ret
  284. def formatServerInfo(self):
  285. file_server = sys.modules["main"].file_server
  286. if file_server.port_opened == {}:
  287. ip_external = None
  288. else:
  289. ip_external = any(file_server.port_opened.values())
  290. return {
  291. "ip_external": ip_external,
  292. "port_opened": file_server.port_opened,
  293. "platform": sys.platform,
  294. "fileserver_ip": config.fileserver_ip,
  295. "fileserver_port": config.fileserver_port,
  296. "tor_enabled": file_server.tor_manager.enabled,
  297. "tor_status": file_server.tor_manager.status,
  298. "tor_has_meek_bridges": file_server.tor_manager.has_meek_bridges,
  299. "tor_use_bridges": config.tor_use_bridges,
  300. "ui_ip": config.ui_ip,
  301. "ui_port": config.ui_port,
  302. "version": config.version,
  303. "rev": config.rev,
  304. "timecorrection": file_server.timecorrection,
  305. "language": config.language,
  306. "debug": config.debug,
  307. "plugins": PluginManager.plugin_manager.plugin_names,
  308. "user_settings": self.user.settings
  309. }
  310. def formatAnnouncerInfo(self, site):
  311. return {"address": site.address, "stats": site.announcer.stats}
  312. # - Actions -
  313. def actionAs(self, to, address, cmd, params=[]):
  314. if not self.hasSitePermission(address, cmd=cmd):
  315. return self.response(to, "No permission for site %s" % address)
  316. req_self = copy.copy(self)
  317. req_self.site = self.server.sites.get(address)
  318. req_self.hasCmdPermission = self.hasCmdPermission # Use the same permissions as current site
  319. req_obj = super(UiWebsocket, req_self)
  320. req = {"id": to, "cmd": cmd, "params": params}
  321. req_obj.handleRequest(req)
  322. # Do callback on response {"cmd": "response", "to": message_id, "result": result}
  323. def actionResponse(self, to, result):
  324. if to in self.waiting_cb:
  325. self.waiting_cb[to](result) # Call callback function
  326. else:
  327. self.log.error("Websocket callback not found: %s, %s" % (to, result))
  328. # Send a simple pong answer
  329. def actionPing(self, to):
  330. self.response(to, "pong")
  331. # Send site details
  332. def actionSiteInfo(self, to, file_status=None):
  333. ret = self.formatSiteInfo(self.site)
  334. if file_status: # Client queries file status
  335. if self.site.storage.isFile(file_status): # File exist, add event done
  336. ret["event"] = ("file_done", file_status)
  337. self.response(to, ret)
  338. # Join to an event channel
  339. def actionChannelJoin(self, to, channels):
  340. if type(channels) != list:
  341. channels = [channels]
  342. for channel in channels:
  343. if channel not in self.channels:
  344. self.channels.append(channel)
  345. # Server variables
  346. def actionServerInfo(self, to):
  347. back = self.formatServerInfo()
  348. self.response(to, back)
  349. # Create a new wrapper nonce that allows to load html file
  350. def actionServerGetWrapperNonce(self, to):
  351. wrapper_nonce = self.request.getWrapperNonce()
  352. self.response(to, wrapper_nonce)
  353. def actionAnnouncerInfo(self, to):
  354. back = self.formatAnnouncerInfo(self.site)
  355. self.response(to, back)
  356. def actionAnnouncerStats(self, to):
  357. back = {}
  358. trackers = self.site.announcer.getTrackers()
  359. for site in self.server.sites.values():
  360. for tracker, stats in site.announcer.stats.iteritems():
  361. if tracker not in trackers:
  362. continue
  363. if tracker not in back:
  364. back[tracker] = {}
  365. is_latest_data = bool(stats["time_request"] > back[tracker].get("time_request", 0) and stats["status"])
  366. for key, val in stats.iteritems():
  367. if key.startswith("num_"):
  368. back[tracker][key] = back[tracker].get(key, 0) + val
  369. elif is_latest_data:
  370. back[tracker][key] = val
  371. return back
  372. # Sign content.json
  373. def actionSiteSign(self, to, privatekey=None, inner_path="content.json", remove_missing_optional=False, update_changed_files=False, response_ok=True):
  374. self.log.debug("Signing: %s" % inner_path)
  375. site = self.site
  376. extend = {} # Extended info for signing
  377. # Change to the file's content.json
  378. file_info = site.content_manager.getFileInfo(inner_path)
  379. if not inner_path.endswith("content.json"):
  380. if not file_info:
  381. raise Exception("Invalid content.json file: %s" % inner_path)
  382. inner_path = file_info["content_inner_path"]
  383. # Add certificate to user files
  384. is_user_content = file_info and ("cert_signers" in file_info or "cert_signers_pattern" in file_info)
  385. if is_user_content and privatekey is None:
  386. cert = self.user.getCert(self.site.address)
  387. extend["cert_auth_type"] = cert["auth_type"]
  388. extend["cert_user_id"] = self.user.getCertUserId(site.address)
  389. extend["cert_sign"] = cert["cert_sign"]
  390. self.log.debug("Extending content.json with cert %s" % extend["cert_user_id"])
  391. if not self.hasFilePermission(inner_path):
  392. self.log.error("SiteSign error: you don't own this site & site owner doesn't allow you to do so.")
  393. return self.response(to, {"error": "Forbidden, you can only modify your own sites"})
  394. if privatekey == "stored": # Get privatekey from sites.json
  395. privatekey = self.user.getSiteData(self.site.address).get("privatekey")
  396. if not privatekey: # Get privatekey from users.json auth_address
  397. privatekey = self.user.getAuthPrivatekey(self.site.address)
  398. # Signing
  399. # Reload content.json, ignore errors to make it up-to-date
  400. site.content_manager.loadContent(inner_path, add_bad_files=False, force=True)
  401. # Sign using private key sent by user
  402. try:
  403. site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional)
  404. except (VerifyError, SignError) as err:
  405. self.cmd("notification", ["error", _["Content signing failed"] + "<br><small>%s</small>" % err])
  406. self.response(to, {"error": "Site sign failed: %s" % err})
  407. self.log.error("Site sign failed: %s: %s" % (inner_path, Debug.formatException(err)))
  408. return
  409. except Exception as err:
  410. self.cmd("notification", ["error", _["Content signing error"] + "<br><small>%s</small>" % Debug.formatException(err)])
  411. self.response(to, {"error": "Site sign error: %s" % Debug.formatException(err)})
  412. self.log.error("Site sign error: %s: %s" % (inner_path, Debug.formatException(err)))
  413. return
  414. site.content_manager.loadContent(inner_path, add_bad_files=False) # Load new content.json, ignore errors
  415. if update_changed_files:
  416. self.site.updateWebsocket(file_done=inner_path)
  417. if response_ok:
  418. self.response(to, "ok")
  419. else:
  420. return inner_path
  421. # Sign and publish content.json
  422. def actionSitePublish(self, to, privatekey=None, inner_path="content.json", sign=True, remove_missing_optional=False, update_changed_files=False):
  423. if sign:
  424. inner_path = self.actionSiteSign(
  425. to, privatekey, inner_path, response_ok=False,
  426. remove_missing_optional=remove_missing_optional, update_changed_files=update_changed_files
  427. )
  428. if not inner_path:
  429. return
  430. # Publishing
  431. if not self.site.settings["serving"]: # Enable site if paused
  432. self.site.settings["serving"] = True
  433. self.site.saveSettings()
  434. self.site.announce()
  435. if inner_path not in self.site.content_manager.contents:
  436. return self.response(to, {"error": "File %s not found" % inner_path})
  437. event_name = "publish %s %s" % (self.site.address, inner_path)
  438. called_instantly = RateLimit.isAllowed(event_name, 30)
  439. thread = RateLimit.callAsync(event_name, 30, self.doSitePublish, self.site, inner_path) # Only publish once in 30 seconds
  440. notification = "linked" not in dir(thread) # Only display notification on first callback
  441. thread.linked = True
  442. if called_instantly: # Allowed to call instantly
  443. # At the end callback with request id and thread
  444. self.cmd("progress", ["publish", _["Content published to {0}/{1} peers."].format(0, 5), 0])
  445. thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=notification))
  446. else:
  447. self.cmd(
  448. "notification",
  449. ["info", _["Content publish queued for {0:.0f} seconds."].format(RateLimit.delayLeft(event_name, 30)), 5000]
  450. )
  451. self.response(to, "ok")
  452. # At the end display notification
  453. thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=False))
  454. def doSitePublish(self, site, inner_path):
  455. def cbProgress(published, limit):
  456. progress = int(float(published) / limit * 100)
  457. self.cmd("progress", [
  458. "publish",
  459. _["Content published to {0}/{1} peers."].format(published, limit),
  460. progress
  461. ])
  462. diffs = site.content_manager.getDiffs(inner_path)
  463. back = site.publish(limit=5, inner_path=inner_path, diffs=diffs, cb_progress=cbProgress)
  464. if back == 0: # Failed to publish to anyone
  465. self.cmd("progress", ["publish", _["Content publish failed."], -100])
  466. else:
  467. cbProgress(back, back)
  468. return back
  469. # Callback of site publish
  470. def cbSitePublish(self, to, site, thread, notification=True, callback=True):
  471. published = thread.value
  472. if published > 0: # Successfully published
  473. if notification:
  474. # self.cmd("notification", ["done", _["Content published to {0} peers."].format(published), 5000])
  475. site.updateWebsocket() # Send updated site data to local websocket clients
  476. if callback:
  477. self.response(to, "ok")
  478. else:
  479. if len(site.peers) == 0:
  480. if any(sys.modules["main"].file_server.port_opened.values()) or sys.modules["main"].file_server.tor_manager.start_onions:
  481. if notification:
  482. self.cmd("notification", ["info", _["No peers found, but your content is ready to access."]])
  483. if callback:
  484. self.response(to, "ok")
  485. else:
  486. if notification:
  487. self.cmd("notification", [
  488. "info",
  489. _(u"""{_[Your network connection is restricted. Please, open <b>{0}</b> port]}<br>
  490. {_[on your router to make your site accessible for everyone.]}""").format(config.fileserver_port)
  491. ])
  492. if callback:
  493. self.response(to, {"error": "Port not opened."})
  494. else:
  495. if notification:
  496. self.response(to, {"error": "Content publish failed."})
  497. def actionSiteReload(self, to, inner_path):
  498. self.site.content_manager.loadContent(inner_path, add_bad_files=False)
  499. self.site.storage.verifyFiles(quick_check=True)
  500. self.site.updateWebsocket()
  501. return "ok"
  502. # Write a file to disk
  503. def actionFileWrite(self, to, inner_path, content_base64, ignore_bad_files=False):
  504. valid_signers = self.site.content_manager.getValidSigners(inner_path)
  505. auth_address = self.user.getAuthAddress(self.site.address)
  506. if not self.hasFilePermission(inner_path):
  507. self.log.error("FileWrite forbidden %s not in valid_signers %s" % (auth_address, valid_signers))
  508. return self.response(to, {"error": "Forbidden, you can only modify your own files"})
  509. # Try not to overwrite files currently in sync
  510. content_inner_path = re.sub("^(.*)/.*?$", "\\1/content.json", inner_path) # Also check the content.json from same directory
  511. if (self.site.bad_files.get(inner_path) or self.site.bad_files.get(content_inner_path)) and not ignore_bad_files:
  512. found = self.site.needFile(inner_path, update=True, priority=10)
  513. if not found:
  514. self.cmd(
  515. "confirm",
  516. [_["This file still in sync, if you write it now, then the previous content may be lost."], _["Write content anyway"]],
  517. lambda (res): self.actionFileWrite(to, inner_path, content_base64, ignore_bad_files=True)
  518. )
  519. return False
  520. try:
  521. import base64
  522. content = base64.b64decode(content_base64)
  523. # Save old file to generate patch later
  524. if (
  525. inner_path.endswith(".json") and not inner_path.endswith("content.json") and
  526. self.site.storage.isFile(inner_path) and not self.site.storage.isFile(inner_path + "-old")
  527. ):
  528. try:
  529. self.site.storage.rename(inner_path, inner_path + "-old")
  530. except Exception:
  531. # Rename failed, fall back to standard file write
  532. f_old = self.site.storage.open(inner_path, "rb")
  533. f_new = self.site.storage.open(inner_path + "-old", "wb")
  534. shutil.copyfileobj(f_old, f_new)
  535. self.site.storage.write(inner_path, content)
  536. except Exception, err:
  537. self.log.error("File write error: %s" % Debug.formatException(err))
  538. return self.response(to, {"error": "Write error: %s" % Debug.formatException(err)})
  539. if inner_path.endswith("content.json"):
  540. self.site.content_manager.loadContent(inner_path, add_bad_files=False, force=True)
  541. self.response(to, "ok")
  542. # Send sitechanged to other local users
  543. for ws in self.site.websockets:
  544. if ws != self:
  545. ws.event("siteChanged", self.site, {"event": ["file_done", inner_path]})
  546. def actionFileDelete(self, to, inner_path):
  547. if not self.hasFilePermission(inner_path):
  548. self.log.error("File delete error: you don't own this site & you are not approved by the owner.")
  549. return self.response(to, {"error": "Forbidden, you can only modify your own files"})
  550. need_delete = True
  551. file_info = self.site.content_manager.getFileInfo(inner_path)
  552. if file_info and file_info.get("optional"):
  553. # Non-existing optional files won't be removed from content.json, so we have to do it manually
  554. self.log.debug("Deleting optional file: %s" % inner_path)
  555. relative_path = file_info["relative_path"]
  556. content_json = self.site.storage.loadJson(file_info["content_inner_path"])
  557. if relative_path in content_json.get("files_optional", {}):
  558. del content_json["files_optional"][relative_path]
  559. self.site.storage.writeJson(file_info["content_inner_path"], content_json)
  560. self.site.content_manager.loadContent(file_info["content_inner_path"], add_bad_files=False, force=True)
  561. need_delete = self.site.storage.isFile(inner_path) # File sill exists after removing from content.json (owned site)
  562. if need_delete:
  563. try:
  564. self.site.storage.delete(inner_path)
  565. except Exception, err:
  566. self.log.error("File delete error: %s" % err)
  567. return self.response(to, {"error": "Delete error: %s" % err})
  568. self.response(to, "ok")
  569. # Send sitechanged to other local users
  570. for ws in self.site.websockets:
  571. if ws != self:
  572. ws.event("siteChanged", self.site, {"event": ["file_deleted", inner_path]})
  573. # Find data in json files
  574. def actionFileQuery(self, to, dir_inner_path, query=None):
  575. # s = time.time()
  576. dir_path = self.site.storage.getPath(dir_inner_path)
  577. rows = list(QueryJson.query(dir_path, query or ""))
  578. # self.log.debug("FileQuery %s %s done in %s" % (dir_inner_path, query, time.time()-s))
  579. return self.response(to, rows)
  580. # List files in directory
  581. def actionFileList(self, to, inner_path):
  582. try:
  583. return list(self.site.storage.walk(inner_path))
  584. except Exception as err:
  585. return {"error": str(err)}
  586. # List directories in a directory
  587. def actionDirList(self, to, inner_path):
  588. try:
  589. return list(self.site.storage.list(inner_path))
  590. except Exception as err:
  591. return {"error": str(err)}
  592. # Sql query
  593. def actionDbQuery(self, to, query, params=None, wait_for=None):
  594. if config.debug or config.verbose:
  595. s = time.time()
  596. rows = []
  597. try:
  598. res = self.site.storage.query(query, params)
  599. except Exception, err: # Response the error to client
  600. self.log.error("DbQuery error: %s" % err)
  601. return self.response(to, {"error": str(err)})
  602. # Convert result to dict
  603. for row in res:
  604. rows.append(dict(row))
  605. if config.verbose and time.time() - s > 0.1: # Log slow query
  606. self.log.debug("Slow query: %s (%.3fs)" % (query, time.time() - s))
  607. return self.response(to, rows)
  608. # Return file content
  609. def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300):
  610. try:
  611. if required or inner_path in self.site.bad_files:
  612. with gevent.Timeout(timeout):
  613. self.site.needFile(inner_path, priority=6)
  614. body = self.site.storage.read(inner_path, "rb")
  615. except Exception, err:
  616. self.log.error("%s fileGet error: %s" % (inner_path, err))
  617. body = None
  618. if body and format == "base64":
  619. import base64
  620. body = base64.b64encode(body)
  621. self.response(to, body)
  622. def actionFileNeed(self, to, inner_path, timeout=300):
  623. try:
  624. with gevent.Timeout(timeout):
  625. self.site.needFile(inner_path, priority=6)
  626. except Exception, err:
  627. return self.response(to, {"error": str(err)})
  628. return self.response(to, "ok")
  629. def actionFileRules(self, to, inner_path, use_my_cert=False, content=None):
  630. if not content: # No content defined by function call
  631. content = self.site.content_manager.contents.get(inner_path)
  632. if not content: # File not created yet
  633. cert = self.user.getCert(self.site.address)
  634. if cert and cert["auth_address"] in self.site.content_manager.getValidSigners(inner_path):
  635. # Current selected cert if valid for this site, add it to query rules
  636. content = {}
  637. content["cert_auth_type"] = cert["auth_type"]
  638. content["cert_user_id"] = self.user.getCertUserId(self.site.address)
  639. content["cert_sign"] = cert["cert_sign"]
  640. rules = self.site.content_manager.getRules(inner_path, content)
  641. if inner_path.endswith("content.json") and rules:
  642. if content:
  643. rules["current_size"] = len(json.dumps(content)) + sum([file["size"] for file in content.get("files", {}).values()])
  644. else:
  645. rules["current_size"] = 0
  646. return self.response(to, rules)
  647. # Add certificate to user
  648. def actionCertAdd(self, to, domain, auth_type, auth_user_name, cert):
  649. try:
  650. res = self.user.addCert(self.user.getAuthAddress(self.site.address), domain, auth_type, auth_user_name, cert)
  651. if res is True:
  652. self.cmd(
  653. "notification",
  654. ["done", _("{_[New certificate added]:} <b>{auth_type}/{auth_user_name}@{domain}</b>.")]
  655. )
  656. self.user.setCert(self.site.address, domain)
  657. self.site.updateWebsocket(cert_changed=domain)
  658. self.response(to, "ok")
  659. elif res is False:
  660. # Display confirmation of change
  661. cert_current = self.user.certs[domain]
  662. body = _("{_[Your current certificate]:} <b>{cert_current[auth_type]}/{cert_current[auth_user_name]}@{domain}</b>")
  663. self.cmd(
  664. "confirm",
  665. [body, _("Change it to {auth_type}/{auth_user_name}@{domain}")],
  666. lambda (res): self.cbCertAddConfirm(to, domain, auth_type, auth_user_name, cert)
  667. )
  668. else:
  669. self.response(to, "Not changed")
  670. except Exception, err:
  671. self.log.error("CertAdd error: Exception - %s (%s)" % (err.message, Debug.formatException(err)))
  672. self.response(to, {"error": err.message})
  673. def cbCertAddConfirm(self, to, domain, auth_type, auth_user_name, cert):
  674. self.user.deleteCert(domain)
  675. self.user.addCert(self.user.getAuthAddress(self.site.address), domain, auth_type, auth_user_name, cert)
  676. self.cmd(
  677. "notification",
  678. ["done", _("Certificate changed to: <b>{auth_type}/{auth_user_name}@{domain}</b>.")]
  679. )
  680. self.user.setCert(self.site.address, domain)
  681. self.site.updateWebsocket(cert_changed=domain)
  682. self.response(to, "ok")
  683. # Select certificate for site
  684. def actionCertSelect(self, to, accepted_domains=[], accept_any=False, accepted_pattern=None):
  685. accounts = []
  686. accounts.append(["", _["No certificate"], ""]) # Default option
  687. active = "" # Make it active if no other option found
  688. # Add my certs
  689. auth_address = self.user.getAuthAddress(self.site.address) # Current auth address
  690. site_data = self.user.getSiteData(self.site.address) # Current auth address
  691. if not accepted_domains and not accepted_pattern: # Accept any if no filter defined
  692. accept_any = True
  693. for domain, cert in self.user.certs.items():
  694. if auth_address == cert["auth_address"] and domain == site_data.get("cert"):
  695. active = domain
  696. title = cert["auth_user_name"] + "@" + domain
  697. accepted_pattern_match = accepted_pattern and SafeRe.match(accepted_pattern, domain)
  698. if domain in accepted_domains or accept_any or accepted_pattern_match:
  699. accounts.append([domain, title, ""])
  700. else:
  701. accounts.append([domain, title, "disabled"])
  702. # Render the html
  703. body = "<span style='padding-bottom: 5px; display: inline-block'>" + _["Select account you want to use in this site:"] + "</span>"
  704. # Accounts
  705. for domain, account, css_class in accounts:
  706. if domain == active:
  707. css_class += " active" # Currently selected option
  708. title = _(u"<b>%s</b> <small>({_[currently selected]})</small>") % account
  709. else:
  710. title = "<b>%s</b>" % account
  711. body += "<a href='#Select+account' class='select select-close cert %s' title='%s'>%s</a>" % (css_class, domain, title)
  712. # More available providers
  713. more_domains = [domain for domain in accepted_domains if domain not in self.user.certs] # Domains we not displayed yet
  714. if more_domains:
  715. # body+= "<small style='margin-top: 10px; display: block'>Accepted authorization providers by the site:</small>"
  716. body += "<div style='background-color: #F7F7F7; margin-right: -30px'>"
  717. for domain in more_domains:
  718. body += _(u"""
  719. <a href='/{domain}' target='_top' class='select'>
  720. <small style='float: right; margin-right: 40px; margin-top: -1px'>{_[Register]} &raquo;</small>{domain}
  721. </a>
  722. """)
  723. body += "</div>"
  724. script = """
  725. $(".notification .select.cert").on("click", function() {
  726. $(".notification .select").removeClass('active')
  727. zeroframe.response(%s, this.title)
  728. return false
  729. })
  730. """ % self.next_message_id
  731. self.cmd("notification", ["ask", body], lambda domain: self.actionCertSet(to, domain))
  732. self.cmd("injectScript", script)
  733. # - Admin actions -
  734. def actionPermissionAdd(self, to, permission):
  735. if permission not in self.site.settings["permissions"]:
  736. self.site.settings["permissions"].append(permission)
  737. self.site.saveSettings()
  738. self.site.updateWebsocket(permission_added=permission)
  739. self.response(to, "ok")
  740. def actionPermissionRemove(self, to, permission):
  741. self.site.settings["permissions"].remove(permission)
  742. self.site.saveSettings()
  743. self.site.updateWebsocket(permission_removed=permission)
  744. self.response(to, "ok")
  745. def actionPermissionDetails(self, to, permission):
  746. if permission == "ADMIN":
  747. self.response(to, _["Modify your client's configuration and access all site"] + " <span style='color: red'>" + _["(Dangerous!)"] + "</span>")
  748. elif permission == "NOSANDBOX":
  749. self.response(to, _["Modify your client's configuration and access all site"] + " <span style='color: red'>" + _["(Dangerous!)"] + "</span>")
  750. else:
  751. self.response(to, "")
  752. # Set certificate that used for authenticate user for site
  753. def actionCertSet(self, to, domain):
  754. self.user.setCert(self.site.address, domain)
  755. self.site.updateWebsocket(cert_changed=domain)
  756. self.response(to, "ok")
  757. # List user's certificates
  758. def actionCertList(self, to):
  759. back = []
  760. auth_address = self.user.getAuthAddress(self.site.address)
  761. for domain, cert in self.user.certs.items():
  762. back.append({
  763. "auth_address": cert["auth_address"],
  764. "auth_type": cert["auth_type"],
  765. "auth_user_name": cert["auth_user_name"],
  766. "domain": domain,
  767. "selected": cert["auth_address"] == auth_address
  768. })
  769. return back
  770. # List all site info
  771. def actionSiteList(self, to, connecting_sites=False):
  772. ret = []
  773. SiteManager.site_manager.load() # Reload sites
  774. for site in self.server.sites.values():
  775. if not site.content_manager.contents.get("content.json") and not connecting_sites:
  776. continue # Incomplete site
  777. ret.append(self.formatSiteInfo(site, create_user=False)) # Dont generate the auth_address on listing
  778. self.response(to, ret)
  779. # Join to an event channel on all sites
  780. def actionChannelJoinAllsite(self, to, channel):
  781. if channel not in self.channels: # Add channel to channels
  782. self.channels.append(channel)
  783. for site in self.server.sites.values(): # Add websocket to every channel
  784. if self not in site.websockets:
  785. site.websockets.append(self)
  786. # Update site content.json
  787. def actionSiteUpdate(self, to, address, check_files=False, since=None, announce=False):
  788. def updateThread():
  789. site.update(announce=announce, check_files=check_files, since=since)
  790. self.response(to, "Updated")
  791. site = self.server.sites.get(address)
  792. if site and (site.address == self.site.address or "ADMIN" in self.site.settings["permissions"]):
  793. if not site.settings["serving"]:
  794. site.settings["serving"] = True
  795. site.saveSettings()
  796. gevent.spawn(updateThread)
  797. else:
  798. self.response(to, {"error": "Unknown site: %s" % address})
  799. # Pause site serving
  800. def actionSitePause(self, to, address):
  801. site = self.server.sites.get(address)
  802. if site:
  803. site.settings["serving"] = False
  804. site.saveSettings()
  805. site.updateWebsocket()
  806. site.worker_manager.stopWorkers()
  807. self.response(to, "Paused")
  808. else:
  809. self.response(to, {"error": "Unknown site: %s" % address})
  810. # Resume site serving
  811. def actionSiteResume(self, to, address):
  812. site = self.server.sites.get(address)
  813. if site:
  814. site.settings["serving"] = True
  815. site.saveSettings()
  816. gevent.spawn(site.update, announce=True)
  817. time.sleep(0.001) # Wait for update thread starting
  818. site.updateWebsocket()
  819. self.response(to, "Resumed")
  820. else:
  821. self.response(to, {"error": "Unknown site: %s" % address})
  822. def actionSiteDelete(self, to, address):
  823. site = self.server.sites.get(address)
  824. if site:
  825. site.delete()
  826. self.user.deleteSiteData(address)
  827. self.response(to, "Deleted")
  828. import gc
  829. gc.collect(2)
  830. else:
  831. self.response(to, {"error": "Unknown site: %s" % address})
  832. def cbSiteClone(self, to, address, root_inner_path="", target_address=None):
  833. self.cmd("notification", ["info", _["Cloning site..."]])
  834. site = self.server.sites.get(address)
  835. if target_address:
  836. target_site = self.server.sites.get(target_address)
  837. privatekey = self.user.getSiteData(target_site.address).get("privatekey")
  838. site.clone(target_address, privatekey, root_inner_path=root_inner_path)
  839. self.cmd("notification", ["done", _["Site source code upgraded!"]])
  840. site.publish()
  841. else:
  842. # Generate a new site from user's bip32 seed
  843. new_address, new_address_index, new_site_data = self.user.getNewSiteData()
  844. new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index, root_inner_path=root_inner_path)
  845. new_site.settings["own"] = True
  846. new_site.saveSettings()
  847. self.cmd("notification", ["done", _["Site cloned"]])
  848. self.cmd("redirect", "/%s" % new_address)
  849. gevent.spawn(new_site.announce)
  850. return "ok"
  851. def actionSiteClone(self, to, address, root_inner_path="", target_address=None):
  852. if not SiteManager.site_manager.isAddress(address):
  853. self.response(to, {"error": "Not a site: %s" % address})
  854. return
  855. if not self.server.sites.get(address):
  856. # Don't expose site existence
  857. return
  858. site = self.server.sites.get(address)
  859. if site.bad_files:
  860. for bad_inner_path in site.bad_files.keys():
  861. is_user_file = "cert_signers" in site.content_manager.getRules(bad_inner_path)
  862. if not is_user_file:
  863. self.cmd("notification", ["error", _["Clone error: Site still in sync"]])
  864. return {"error": "Site still in sync"}
  865. if "ADMIN" in self.getPermissions(to):
  866. self.cbSiteClone(to, address, root_inner_path, target_address)
  867. else:
  868. self.cmd(
  869. "confirm",
  870. [_["Clone site <b>%s</b>?"] % address, _["Clone"]],
  871. lambda (res): self.cbSiteClone(to, address, root_inner_path, target_address)
  872. )
  873. def actionSiteSetLimit(self, to, size_limit):
  874. self.site.settings["size_limit"] = int(size_limit)
  875. self.site.saveSettings()
  876. self.response(to, "ok")
  877. self.site.updateWebsocket()
  878. self.site.download(blind_includes=True)
  879. def actionSiteAdd(self, to, address):
  880. site_manager = SiteManager.site_manager
  881. if address in site_manager.sites:
  882. return {"error": "Site already added"}
  883. else:
  884. if site_manager.need(address):
  885. return "ok"
  886. else:
  887. return {"error": "Invalid address"}
  888. def actionSiteListModifiedFiles(self, to, content_inner_path="content.json"):
  889. content = self.site.content_manager.contents[content_inner_path]
  890. min_mtime = content.get("modified", 0)
  891. site_path = self.site.storage.directory
  892. modified_files = []
  893. # Load cache if not signed since last modified check
  894. if content.get("modified", 0) < self.site.settings["cache"].get("time_modified_files_check"):
  895. min_mtime = self.site.settings["cache"].get("time_modified_files_check")
  896. modified_files = self.site.settings["cache"].get("modified_files", [])
  897. inner_paths = [content_inner_path] + content.get("includes", {}).keys() + content.get("files", {}).keys()
  898. for relative_inner_path in inner_paths:
  899. inner_path = helper.getDirname(content_inner_path) + relative_inner_path
  900. try:
  901. is_mtime_newer = os.path.getmtime(self.site.storage.getPath(inner_path)) > min_mtime + 1
  902. if is_mtime_newer:
  903. if inner_path.endswith("content.json"):
  904. is_modified = self.site.content_manager.isModified(inner_path)
  905. else:
  906. previous_size = content["files"][inner_path]["size"]
  907. is_same_size = self.site.storage.getSize(inner_path) == previous_size
  908. ext = inner_path.rsplit(".", 1)[-1]
  909. is_text_file = ext in ["json", "txt", "html", "js", "css"]
  910. if is_same_size:
  911. if is_text_file:
  912. is_modified = self.site.content_manager.isModified(inner_path) # Check sha512 hash
  913. else:
  914. is_modified = False
  915. else:
  916. is_modified = True
  917. # Check ran, modified back to original value, but in the cache
  918. if not is_modified and inner_path in modified_files:
  919. modified_files.remove(inner_path)
  920. else:
  921. is_modified = False
  922. except Exception as err:
  923. if not self.site.storage.isFile(inner_path): # File deleted
  924. is_modified = True
  925. else:
  926. raise err
  927. if is_modified and inner_path not in modified_files:
  928. modified_files.append(inner_path)
  929. self.site.settings["cache"]["time_modified_files_check"] = time.time()
  930. self.site.settings["cache"]["modified_files"] = modified_files
  931. return {"modified_files": modified_files}
  932. def actionSiteSetSettingsValue(self, to, key, value):
  933. if key not in ["modified_files_notification"]:
  934. return {"error": "Can't change this key"}
  935. self.site.settings[key] = value
  936. return "ok"
  937. def actionUserGetSettings(self, to):
  938. settings = self.user.sites.get(self.site.address, {}).get("settings", {})
  939. self.response(to, settings)
  940. def actionUserSetSettings(self, to, settings):
  941. self.user.setSiteSettings(self.site.address, settings)
  942. self.response(to, "ok")
  943. def actionUserGetGlobalSettings(self, to):
  944. settings = self.user.settings
  945. self.response(to, settings)
  946. def actionUserSetGlobalSettings(self, to, settings):
  947. self.user.settings = settings
  948. self.user.save()
  949. self.response(to, "ok")
  950. def actionServerUpdate(self, to):
  951. self.cmd("updating")
  952. sys.modules["main"].update_after_shutdown = True
  953. SiteManager.site_manager.save()
  954. sys.modules["main"].file_server.stop()
  955. sys.modules["main"].ui_server.stop()
  956. def actionServerPortcheck(self, to):
  957. file_server = sys.modules["main"].file_server
  958. file_server.portCheck()
  959. self.response(to, file_server.port_opened)
  960. def actionServerShutdown(self, to, restart=False):
  961. if restart:
  962. sys.modules["main"].restart_after_shutdown = True
  963. sys.modules["main"].file_server.stop()
  964. sys.modules["main"].ui_server.stop()
  965. def actionServerShowdirectory(self, to, directory="backup", inner_path=""):
  966. if self.request.env["REMOTE_ADDR"] != "127.0.0.1":
  967. return self.response(to, {"error": "Only clients from 127.0.0.1 allowed to run this command"})
  968. import webbrowser
  969. if directory == "backup":
  970. path = os.path.abspath(config.data_dir)
  971. elif directory == "log":
  972. path = os.path.abspath(config.log_dir)
  973. elif directory == "site":
  974. path = os.path.abspath(self.site.storage.getPath(helper.getDirname(inner_path)))
  975. if os.path.isdir(path):
  976. self.log.debug("Opening: %s" % path)
  977. webbrowser.open('file://' + path)
  978. return self.response(to, "ok")
  979. else:
  980. return self.response(to, {"error": "Not a directory"})
  981. def actionConfigSet(self, to, key, value):
  982. if key not in config.keys_api_change_allowed:
  983. self.response(to, {"error": "Forbidden you cannot set this config key"})
  984. return
  985. if key == "open_browser":
  986. if value not in ["default_browser", "False"]:
  987. self.response(to, {"error": "Forbidden: Invalid value"})
  988. return
  989. # Remove empty lines from lists
  990. if type(value) is list:
  991. value = [line for line in value if line]
  992. config.saveValue(key, value)
  993. if key not in config.keys_restart_need:
  994. if value is None: # Default value
  995. setattr(config, key, config.parser.get_default(key))
  996. setattr(config.arguments, key, config.parser.get_default(key))
  997. else:
  998. setattr(config, key, value)
  999. setattr(config.arguments, key, value)
  1000. else:
  1001. config.need_restart = True
  1002. config.pending_changes[key] = value
  1003. if key == "language":
  1004. import Translate
  1005. for translate in Translate.translates:
  1006. translate.setLanguage(value)
  1007. message = _["You have successfully changed the web interface's language!"] + "<br>"
  1008. message += _["Due to the browser's caching, the full transformation could take some minute."]
  1009. self.cmd("notification", ["done", message, 10000])
  1010. if key == "tor_use_bridges":
  1011. if value is None:
  1012. value = False
  1013. else:
  1014. value = True
  1015. tor_manager = sys.modules["main"].file_server.tor_manager
  1016. tor_manager.request("SETCONF UseBridges=%i" % value)
  1017. if key == "trackers_file":
  1018. config.loadTrackersFile()
  1019. if key == "log_level":
  1020. logging.getLogger('').setLevel(logging.getLevelName(config.log_level))
  1021. if key == "ip_external":
  1022. gevent.spawn(sys.modules["main"].file_server.portCheck)
  1023. self.response(to, "ok")