123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170 |
- import json
- import time
- import sys
- import os
- import shutil
- import re
- import copy
- import logging
- import gevent
- from Config import config
- from Site import SiteManager
- from Debug import Debug
- from util import QueryJson, RateLimit
- from Plugin import PluginManager
- from Translate import translate as _
- from util import helper
- from util import SafeRe
- from Content.ContentManager import VerifyError, SignError
- @PluginManager.acceptPlugins
- class UiWebsocket(object):
- admin_commands = set([
- "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteAdd", "siteListModifiedFiles", "siteSetSettingsValue",
- "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "serverGetWrapperNonce",
- "certSet", "certList", "configSet", "permissionAdd", "permissionRemove", "announcerStats", "userSetGlobalSettings"
- ])
- async_commands = set(["fileGet", "fileList", "dirList", "fileNeed", "serverPortcheck", "siteListModifiedFiles"])
- def __init__(self, ws, site, server, user, request):
- self.ws = ws
- self.site = site
- self.user = user
- self.log = site.log
- self.request = request
- self.permissions = []
- self.server = server
- self.next_message_id = 1
- self.waiting_cb = {} # Waiting for callback. Key: message_id, Value: function pointer
- self.channels = [] # Channels joined to
- self.state = {"sending": False} # Shared state of websocket connection
- self.send_queue = [] # Messages to send to client
- # Start listener loop
- def start(self):
- ws = self.ws
- if self.site.address == config.homepage and not self.site.page_requested:
- # Add open fileserver port message or closed port error to homepage at first request after start
- self.site.page_requested = True # Dont add connection notification anymore
- file_server = sys.modules["main"].file_server
- if not file_server.port_opened or file_server.tor_manager.start_onions is None:
- self.site.page_requested = False # Not ready yet, check next time
- else:
- try:
- self.addHomepageNotifications()
- except Exception, err:
- self.log.error("Uncaught Exception: " + Debug.formatException(err))
- for notification in self.site.notifications: # Send pending notification messages
- # send via WebSocket
- self.cmd("notification", notification)
- # just in case, log them to terminal
- if notification[0] == "error":
- self.log.error("\n*** %s\n" % self.dedent(notification[1]))
- self.site.notifications = []
- while True:
- try:
- if ws.closed:
- break
- else:
- message = ws.receive()
- except Exception, err:
- self.log.error("WebSocket receive error: %s" % Debug.formatException(err))
- break
- if message:
- try:
- req = json.loads(message)
- self.handleRequest(req)
- except Exception, err:
- if config.debug: # Allow websocket errors to appear on /Debug
- sys.modules["main"].DebugHook.handleError()
- self.log.error("WebSocket handleRequest error: %s \n %s" % (Debug.formatException(err), message))
- if not self.hasPlugin("Multiuser"):
- self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html"))
- def dedent(self, text):
- return re.sub("[\\r\\n\\x20\\t]+", " ", text.strip().replace("<br>", " "))
- def addHomepageNotifications(self):
- if not(self.hasPlugin("Multiuser")) and not(self.hasPlugin("UiPassword")):
- bind_ip = getattr(config, "ui_ip", "")
- whitelist = getattr(config, "ui_restrict", [])
- # binds to the Internet, no IP whitelist, no UiPassword, no Multiuser
- if ("0.0.0.0" == bind_ip or "*" == bind_ip) and (not whitelist):
- self.site.notifications.append([
- "error",
- _(u"You are not going to set up a public gateway. However, <b>your Web UI is<br>" +
- "open to the whole Internet.</b> " +
- "Please check your configuration.")
- ])
- file_server = sys.modules["main"].file_server
- if any(file_server.port_opened.values()):
- self.site.notifications.append([
- "done",
- _["Congratulations, your port <b>{0}</b> is opened.<br>You are a full member of the ZeroNet network!"].format(config.fileserver_port),
- 10000
- ])
- elif config.tor == "always" and file_server.tor_manager.start_onions:
- self.site.notifications.append([
- "done",
- _(u"""
- {_[Tor mode active, every connection using Onion route.]}<br>
- {_[Successfully started Tor onion hidden services.]}
- """),
- 10000
- ])
- elif config.tor == "always" and file_server.tor_manager.start_onions is not False:
- self.site.notifications.append([
- "error",
- _(u"""
- {_[Tor mode active, every connection using Onion route.]}<br>
- {_[Unable to start hidden services, please check your config.]}
- """),
- 0
- ])
- elif file_server.tor_manager.start_onions:
- self.site.notifications.append([
- "done",
- _(u"""
- {_[Successfully started Tor onion hidden services.]}<br>
- {_[For faster connections open <b>{0}</b> port on your router.]}
- """).format(config.fileserver_port),
- 10000
- ])
- else:
- self.site.notifications.append([
- "error",
- _(u"""
- {_[Your connection is restricted. Please, open <b>{0}</b> port on your router]}<br>
- {_[or configure Tor to become a full member of the ZeroNet network.]}
- """).format(config.fileserver_port),
- 0
- ])
- def hasPlugin(self, name):
- return name in PluginManager.plugin_manager.plugin_names
- # Has permission to run the command
- def hasCmdPermission(self, cmd):
- cmd = cmd[0].lower() + cmd[1:]
- if cmd in self.admin_commands and "ADMIN" not in self.permissions:
- return False
- else:
- return True
- # Has permission to access a site
- def hasSitePermission(self, address, cmd=None):
- if address != self.site.address and "ADMIN" not in self.site.settings["permissions"]:
- return False
- else:
- return True
- def hasFilePermission(self, inner_path):
- valid_signers = self.site.content_manager.getValidSigners(inner_path)
- return self.site.settings["own"] or self.user.getAuthAddress(self.site.address) in valid_signers
- # Event in a channel
- def event(self, channel, *params):
- if channel in self.channels: # We are joined to channel
- if channel == "siteChanged":
- site = params[0]
- site_info = self.formatSiteInfo(site, create_user=False)
- if len(params) > 1 and params[1]: # Extra data
- site_info.update(params[1])
- self.cmd("setSiteInfo", site_info)
- elif channel == "serverChanged":
- server_info = self.formatServerInfo()
- self.cmd("setServerInfo", server_info)
- elif channel == "announcerChanged":
- site = params[0]
- announcer_info = self.formatAnnouncerInfo(site)
- if len(params) > 1 and params[1]: # Extra data
- announcer_info.update(params[1])
- self.cmd("setAnnouncerInfo", announcer_info)
- # Send response to client (to = message.id)
- def response(self, to, result):
- self.send({"cmd": "response", "to": to, "result": result})
- # Send a command
- def cmd(self, cmd, params={}, cb=None):
- self.send({"cmd": cmd, "params": params}, cb)
- # Encode to json and send message
- def send(self, message, cb=None):
- message["id"] = self.next_message_id # Add message id to allow response
- self.next_message_id += 1
- if cb: # Callback after client responded
- self.waiting_cb[message["id"]] = cb
- self.send_queue.append(message)
- if self.state["sending"]:
- return # Already sending
- try:
- while self.send_queue:
- self.state["sending"] = True
- message = self.send_queue.pop(0)
- self.ws.send(json.dumps(message))
- self.state["sending"] = False
- except Exception, err:
- self.log.debug("Websocket send error: %s" % Debug.formatException(err))
- self.state["sending"] = False
- def getPermissions(self, req_id):
- permissions = self.site.settings["permissions"]
- if req_id >= 1000000: # Its a wrapper command, allow admin commands
- permissions = permissions[:]
- permissions.append("ADMIN")
- return permissions
- def asyncWrapper(self, func):
- def asyncErrorWatcher(func, *args, **kwargs):
- try:
- result = func(*args, **kwargs)
- if result is not None:
- self.response(args[0], result)
- except Exception, err:
- if config.debug: # Allow websocket errors to appear on /Debug
- sys.modules["main"].DebugHook.handleError()
- self.log.error("WebSocket handleRequest error: %s" % Debug.formatException(err))
- self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html"))
- def wrapper(*args, **kwargs):
- gevent.spawn(asyncErrorWatcher, func, *args, **kwargs)
- return wrapper
- # Handle incoming messages
- def handleRequest(self, req):
- cmd = req.get("cmd")
- params = req.get("params")
- self.permissions = self.getPermissions(req["id"])
- if cmd == "response": # It's a response to a command
- return self.actionResponse(req["to"], req["result"])
- elif not self.hasCmdPermission(cmd): # Admin commands
- return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd})
- else: # Normal command
- func_name = "action" + cmd[0].upper() + cmd[1:]
- func = getattr(self, func_name, None)
- if not func: # Unknown command
- self.response(req["id"], {"error": "Unknown command: %s" % cmd})
- return
- # Execute in parallel
- if cmd in self.async_commands:
- func = self.asyncWrapper(func)
- # Support calling as named, unnamed parameters and raw first argument too
- if type(params) is dict:
- result = func(req["id"], **params)
- elif type(params) is list:
- result = func(req["id"], *params)
- elif params:
- result = func(req["id"], params)
- else:
- result = func(req["id"])
- if result is not None:
- self.response(req["id"], result)
- # Format site info
- def formatSiteInfo(self, site, create_user=True):
- content = site.content_manager.contents.get("content.json", {})
- if content: # Remove unnecessary data transfer
- content = content.copy()
- content["files"] = len(content.get("files", {}))
- content["files_optional"] = len(content.get("files_optional", {}))
- content["includes"] = len(content.get("includes", {}))
- if "sign" in content:
- del(content["sign"])
- if "signs" in content:
- del(content["signs"])
- if "signers_sign" in content:
- del(content["signers_sign"])
- settings = site.settings.copy()
- del settings["wrapper_key"] # Dont expose wrapper key
- del settings["auth_key"] # Dont send auth key twice
- ret = {
- "auth_key": self.site.settings["auth_key"], # Obsolete, will be removed
- "auth_address": self.user.getAuthAddress(site.address, create=create_user),
- "cert_user_id": self.user.getCertUserId(site.address),
- "address": site.address,
- "settings": settings,
- "content_updated": site.content_updated,
- "bad_files": len(site.bad_files),
- "size_limit": site.getSizeLimit(),
- "next_size_limit": site.getNextSizeLimit(),
- "peers": max(site.settings.get("peers", 0), len(site.peers)),
- "started_task_num": site.worker_manager.started_task_num,
- "tasks": len(site.worker_manager.tasks),
- "workers": len(site.worker_manager.workers),
- "content": content
- }
- if site.settings["own"]:
- ret["privatekey"] = bool(self.user.getSiteData(site.address, create=create_user).get("privatekey"))
- if site.settings["serving"] and content:
- ret["peers"] += 1 # Add myself if serving
- return ret
- def formatServerInfo(self):
- file_server = sys.modules["main"].file_server
- if file_server.port_opened == {}:
- ip_external = None
- else:
- ip_external = any(file_server.port_opened.values())
- return {
- "ip_external": ip_external,
- "port_opened": file_server.port_opened,
- "platform": sys.platform,
- "fileserver_ip": config.fileserver_ip,
- "fileserver_port": config.fileserver_port,
- "tor_enabled": file_server.tor_manager.enabled,
- "tor_status": file_server.tor_manager.status,
- "tor_has_meek_bridges": file_server.tor_manager.has_meek_bridges,
- "tor_use_bridges": config.tor_use_bridges,
- "ui_ip": config.ui_ip,
- "ui_port": config.ui_port,
- "version": config.version,
- "rev": config.rev,
- "timecorrection": file_server.timecorrection,
- "language": config.language,
- "debug": config.debug,
- "plugins": PluginManager.plugin_manager.plugin_names,
- "user_settings": self.user.settings
- }
- def formatAnnouncerInfo(self, site):
- return {"address": site.address, "stats": site.announcer.stats}
- # - Actions -
- def actionAs(self, to, address, cmd, params=[]):
- if not self.hasSitePermission(address, cmd=cmd):
- return self.response(to, "No permission for site %s" % address)
- req_self = copy.copy(self)
- req_self.site = self.server.sites.get(address)
- req_self.hasCmdPermission = self.hasCmdPermission # Use the same permissions as current site
- req_obj = super(UiWebsocket, req_self)
- req = {"id": to, "cmd": cmd, "params": params}
- req_obj.handleRequest(req)
- # Do callback on response {"cmd": "response", "to": message_id, "result": result}
- def actionResponse(self, to, result):
- if to in self.waiting_cb:
- self.waiting_cb[to](result) # Call callback function
- else:
- self.log.error("Websocket callback not found: %s, %s" % (to, result))
- # Send a simple pong answer
- def actionPing(self, to):
- self.response(to, "pong")
- # Send site details
- def actionSiteInfo(self, to, file_status=None):
- ret = self.formatSiteInfo(self.site)
- if file_status: # Client queries file status
- if self.site.storage.isFile(file_status): # File exist, add event done
- ret["event"] = ("file_done", file_status)
- self.response(to, ret)
- # Join to an event channel
- def actionChannelJoin(self, to, channels):
- if type(channels) != list:
- channels = [channels]
- for channel in channels:
- if channel not in self.channels:
- self.channels.append(channel)
- # Server variables
- def actionServerInfo(self, to):
- back = self.formatServerInfo()
- self.response(to, back)
- # Create a new wrapper nonce that allows to load html file
- def actionServerGetWrapperNonce(self, to):
- wrapper_nonce = self.request.getWrapperNonce()
- self.response(to, wrapper_nonce)
- def actionAnnouncerInfo(self, to):
- back = self.formatAnnouncerInfo(self.site)
- self.response(to, back)
- def actionAnnouncerStats(self, to):
- back = {}
- trackers = self.site.announcer.getTrackers()
- for site in self.server.sites.values():
- for tracker, stats in site.announcer.stats.iteritems():
- if tracker not in trackers:
- continue
- if tracker not in back:
- back[tracker] = {}
- is_latest_data = bool(stats["time_request"] > back[tracker].get("time_request", 0) and stats["status"])
- for key, val in stats.iteritems():
- if key.startswith("num_"):
- back[tracker][key] = back[tracker].get(key, 0) + val
- elif is_latest_data:
- back[tracker][key] = val
- return back
- # Sign content.json
- def actionSiteSign(self, to, privatekey=None, inner_path="content.json", remove_missing_optional=False, update_changed_files=False, response_ok=True):
- self.log.debug("Signing: %s" % inner_path)
- site = self.site
- extend = {} # Extended info for signing
- # Change to the file's content.json
- file_info = site.content_manager.getFileInfo(inner_path)
- if not inner_path.endswith("content.json"):
- if not file_info:
- raise Exception("Invalid content.json file: %s" % inner_path)
- inner_path = file_info["content_inner_path"]
- # Add certificate to user files
- is_user_content = file_info and ("cert_signers" in file_info or "cert_signers_pattern" in file_info)
- if is_user_content and privatekey is None:
- cert = self.user.getCert(self.site.address)
- extend["cert_auth_type"] = cert["auth_type"]
- extend["cert_user_id"] = self.user.getCertUserId(site.address)
- extend["cert_sign"] = cert["cert_sign"]
- self.log.debug("Extending content.json with cert %s" % extend["cert_user_id"])
- if not self.hasFilePermission(inner_path):
- self.log.error("SiteSign error: you don't own this site & site owner doesn't allow you to do so.")
- return self.response(to, {"error": "Forbidden, you can only modify your own sites"})
- if privatekey == "stored": # Get privatekey from sites.json
- privatekey = self.user.getSiteData(self.site.address).get("privatekey")
- if not privatekey: # Get privatekey from users.json auth_address
- privatekey = self.user.getAuthPrivatekey(self.site.address)
- # Signing
- # Reload content.json, ignore errors to make it up-to-date
- site.content_manager.loadContent(inner_path, add_bad_files=False, force=True)
- # Sign using private key sent by user
- try:
- site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional)
- except (VerifyError, SignError) as err:
- self.cmd("notification", ["error", _["Content signing failed"] + "<br><small>%s</small>" % err])
- self.response(to, {"error": "Site sign failed: %s" % err})
- self.log.error("Site sign failed: %s: %s" % (inner_path, Debug.formatException(err)))
- return
- except Exception as err:
- self.cmd("notification", ["error", _["Content signing error"] + "<br><small>%s</small>" % Debug.formatException(err)])
- self.response(to, {"error": "Site sign error: %s" % Debug.formatException(err)})
- self.log.error("Site sign error: %s: %s" % (inner_path, Debug.formatException(err)))
- return
- site.content_manager.loadContent(inner_path, add_bad_files=False) # Load new content.json, ignore errors
- if update_changed_files:
- self.site.updateWebsocket(file_done=inner_path)
- if response_ok:
- self.response(to, "ok")
- else:
- return inner_path
- # Sign and publish content.json
- def actionSitePublish(self, to, privatekey=None, inner_path="content.json", sign=True, remove_missing_optional=False, update_changed_files=False):
- if sign:
- inner_path = self.actionSiteSign(
- to, privatekey, inner_path, response_ok=False,
- remove_missing_optional=remove_missing_optional, update_changed_files=update_changed_files
- )
- if not inner_path:
- return
- # Publishing
- if not self.site.settings["serving"]: # Enable site if paused
- self.site.settings["serving"] = True
- self.site.saveSettings()
- self.site.announce()
- if inner_path not in self.site.content_manager.contents:
- return self.response(to, {"error": "File %s not found" % inner_path})
- event_name = "publish %s %s" % (self.site.address, inner_path)
- called_instantly = RateLimit.isAllowed(event_name, 30)
- thread = RateLimit.callAsync(event_name, 30, self.doSitePublish, self.site, inner_path) # Only publish once in 30 seconds
- notification = "linked" not in dir(thread) # Only display notification on first callback
- thread.linked = True
- if called_instantly: # Allowed to call instantly
- # At the end callback with request id and thread
- self.cmd("progress", ["publish", _["Content published to {0}/{1} peers."].format(0, 5), 0])
- thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=notification))
- else:
- self.cmd(
- "notification",
- ["info", _["Content publish queued for {0:.0f} seconds."].format(RateLimit.delayLeft(event_name, 30)), 5000]
- )
- self.response(to, "ok")
- # At the end display notification
- thread.link(lambda thread: self.cbSitePublish(to, self.site, thread, notification, callback=False))
- def doSitePublish(self, site, inner_path):
- def cbProgress(published, limit):
- progress = int(float(published) / limit * 100)
- self.cmd("progress", [
- "publish",
- _["Content published to {0}/{1} peers."].format(published, limit),
- progress
- ])
- diffs = site.content_manager.getDiffs(inner_path)
- back = site.publish(limit=5, inner_path=inner_path, diffs=diffs, cb_progress=cbProgress)
- if back == 0: # Failed to publish to anyone
- self.cmd("progress", ["publish", _["Content publish failed."], -100])
- else:
- cbProgress(back, back)
- return back
- # Callback of site publish
- def cbSitePublish(self, to, site, thread, notification=True, callback=True):
- published = thread.value
- if published > 0: # Successfully published
- if notification:
- # self.cmd("notification", ["done", _["Content published to {0} peers."].format(published), 5000])
- site.updateWebsocket() # Send updated site data to local websocket clients
- if callback:
- self.response(to, "ok")
- else:
- if len(site.peers) == 0:
- if any(sys.modules["main"].file_server.port_opened.values()) or sys.modules["main"].file_server.tor_manager.start_onions:
- if notification:
- self.cmd("notification", ["info", _["No peers found, but your content is ready to access."]])
- if callback:
- self.response(to, "ok")
- else:
- if notification:
- self.cmd("notification", [
- "info",
- _(u"""{_[Your network connection is restricted. Please, open <b>{0}</b> port]}<br>
- {_[on your router to make your site accessible for everyone.]}""").format(config.fileserver_port)
- ])
- if callback:
- self.response(to, {"error": "Port not opened."})
- else:
- if notification:
- self.response(to, {"error": "Content publish failed."})
- def actionSiteReload(self, to, inner_path):
- self.site.content_manager.loadContent(inner_path, add_bad_files=False)
- self.site.storage.verifyFiles(quick_check=True)
- self.site.updateWebsocket()
- return "ok"
- # Write a file to disk
- def actionFileWrite(self, to, inner_path, content_base64, ignore_bad_files=False):
- valid_signers = self.site.content_manager.getValidSigners(inner_path)
- auth_address = self.user.getAuthAddress(self.site.address)
- if not self.hasFilePermission(inner_path):
- self.log.error("FileWrite forbidden %s not in valid_signers %s" % (auth_address, valid_signers))
- return self.response(to, {"error": "Forbidden, you can only modify your own files"})
- # Try not to overwrite files currently in sync
- content_inner_path = re.sub("^(.*)/.*?$", "\\1/content.json", inner_path) # Also check the content.json from same directory
- if (self.site.bad_files.get(inner_path) or self.site.bad_files.get(content_inner_path)) and not ignore_bad_files:
- found = self.site.needFile(inner_path, update=True, priority=10)
- if not found:
- self.cmd(
- "confirm",
- [_["This file still in sync, if you write it now, then the previous content may be lost."], _["Write content anyway"]],
- lambda (res): self.actionFileWrite(to, inner_path, content_base64, ignore_bad_files=True)
- )
- return False
- try:
- import base64
- content = base64.b64decode(content_base64)
- # Save old file to generate patch later
- if (
- inner_path.endswith(".json") and not inner_path.endswith("content.json") and
- self.site.storage.isFile(inner_path) and not self.site.storage.isFile(inner_path + "-old")
- ):
- try:
- self.site.storage.rename(inner_path, inner_path + "-old")
- except Exception:
- # Rename failed, fall back to standard file write
- f_old = self.site.storage.open(inner_path, "rb")
- f_new = self.site.storage.open(inner_path + "-old", "wb")
- shutil.copyfileobj(f_old, f_new)
- self.site.storage.write(inner_path, content)
- except Exception, err:
- self.log.error("File write error: %s" % Debug.formatException(err))
- return self.response(to, {"error": "Write error: %s" % Debug.formatException(err)})
- if inner_path.endswith("content.json"):
- self.site.content_manager.loadContent(inner_path, add_bad_files=False, force=True)
- self.response(to, "ok")
- # Send sitechanged to other local users
- for ws in self.site.websockets:
- if ws != self:
- ws.event("siteChanged", self.site, {"event": ["file_done", inner_path]})
- def actionFileDelete(self, to, inner_path):
- if not self.hasFilePermission(inner_path):
- self.log.error("File delete error: you don't own this site & you are not approved by the owner.")
- return self.response(to, {"error": "Forbidden, you can only modify your own files"})
- need_delete = True
- file_info = self.site.content_manager.getFileInfo(inner_path)
- if file_info and file_info.get("optional"):
- # Non-existing optional files won't be removed from content.json, so we have to do it manually
- self.log.debug("Deleting optional file: %s" % inner_path)
- relative_path = file_info["relative_path"]
- content_json = self.site.storage.loadJson(file_info["content_inner_path"])
- if relative_path in content_json.get("files_optional", {}):
- del content_json["files_optional"][relative_path]
- self.site.storage.writeJson(file_info["content_inner_path"], content_json)
- self.site.content_manager.loadContent(file_info["content_inner_path"], add_bad_files=False, force=True)
- need_delete = self.site.storage.isFile(inner_path) # File sill exists after removing from content.json (owned site)
- if need_delete:
- try:
- self.site.storage.delete(inner_path)
- except Exception, err:
- self.log.error("File delete error: %s" % err)
- return self.response(to, {"error": "Delete error: %s" % err})
- self.response(to, "ok")
- # Send sitechanged to other local users
- for ws in self.site.websockets:
- if ws != self:
- ws.event("siteChanged", self.site, {"event": ["file_deleted", inner_path]})
- # Find data in json files
- def actionFileQuery(self, to, dir_inner_path, query=None):
- # s = time.time()
- dir_path = self.site.storage.getPath(dir_inner_path)
- rows = list(QueryJson.query(dir_path, query or ""))
- # self.log.debug("FileQuery %s %s done in %s" % (dir_inner_path, query, time.time()-s))
- return self.response(to, rows)
- # List files in directory
- def actionFileList(self, to, inner_path):
- try:
- return list(self.site.storage.walk(inner_path))
- except Exception as err:
- return {"error": str(err)}
- # List directories in a directory
- def actionDirList(self, to, inner_path):
- try:
- return list(self.site.storage.list(inner_path))
- except Exception as err:
- return {"error": str(err)}
- # Sql query
- def actionDbQuery(self, to, query, params=None, wait_for=None):
- if config.debug or config.verbose:
- s = time.time()
- rows = []
- try:
- res = self.site.storage.query(query, params)
- except Exception, err: # Response the error to client
- self.log.error("DbQuery error: %s" % err)
- return self.response(to, {"error": str(err)})
- # Convert result to dict
- for row in res:
- rows.append(dict(row))
- if config.verbose and time.time() - s > 0.1: # Log slow query
- self.log.debug("Slow query: %s (%.3fs)" % (query, time.time() - s))
- return self.response(to, rows)
- # Return file content
- def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300):
- try:
- if required or inner_path in self.site.bad_files:
- with gevent.Timeout(timeout):
- self.site.needFile(inner_path, priority=6)
- body = self.site.storage.read(inner_path, "rb")
- except Exception, err:
- self.log.error("%s fileGet error: %s" % (inner_path, err))
- body = None
- if body and format == "base64":
- import base64
- body = base64.b64encode(body)
- self.response(to, body)
- def actionFileNeed(self, to, inner_path, timeout=300):
- try:
- with gevent.Timeout(timeout):
- self.site.needFile(inner_path, priority=6)
- except Exception, err:
- return self.response(to, {"error": str(err)})
- return self.response(to, "ok")
- def actionFileRules(self, to, inner_path, use_my_cert=False, content=None):
- if not content: # No content defined by function call
- content = self.site.content_manager.contents.get(inner_path)
- if not content: # File not created yet
- cert = self.user.getCert(self.site.address)
- if cert and cert["auth_address"] in self.site.content_manager.getValidSigners(inner_path):
- # Current selected cert if valid for this site, add it to query rules
- content = {}
- content["cert_auth_type"] = cert["auth_type"]
- content["cert_user_id"] = self.user.getCertUserId(self.site.address)
- content["cert_sign"] = cert["cert_sign"]
- rules = self.site.content_manager.getRules(inner_path, content)
- if inner_path.endswith("content.json") and rules:
- if content:
- rules["current_size"] = len(json.dumps(content)) + sum([file["size"] for file in content.get("files", {}).values()])
- else:
- rules["current_size"] = 0
- return self.response(to, rules)
- # Add certificate to user
- def actionCertAdd(self, to, domain, auth_type, auth_user_name, cert):
- try:
- res = self.user.addCert(self.user.getAuthAddress(self.site.address), domain, auth_type, auth_user_name, cert)
- if res is True:
- self.cmd(
- "notification",
- ["done", _("{_[New certificate added]:} <b>{auth_type}/{auth_user_name}@{domain}</b>.")]
- )
- self.user.setCert(self.site.address, domain)
- self.site.updateWebsocket(cert_changed=domain)
- self.response(to, "ok")
- elif res is False:
- # Display confirmation of change
- cert_current = self.user.certs[domain]
- body = _("{_[Your current certificate]:} <b>{cert_current[auth_type]}/{cert_current[auth_user_name]}@{domain}</b>")
- self.cmd(
- "confirm",
- [body, _("Change it to {auth_type}/{auth_user_name}@{domain}")],
- lambda (res): self.cbCertAddConfirm(to, domain, auth_type, auth_user_name, cert)
- )
- else:
- self.response(to, "Not changed")
- except Exception, err:
- self.log.error("CertAdd error: Exception - %s (%s)" % (err.message, Debug.formatException(err)))
- self.response(to, {"error": err.message})
- def cbCertAddConfirm(self, to, domain, auth_type, auth_user_name, cert):
- self.user.deleteCert(domain)
- self.user.addCert(self.user.getAuthAddress(self.site.address), domain, auth_type, auth_user_name, cert)
- self.cmd(
- "notification",
- ["done", _("Certificate changed to: <b>{auth_type}/{auth_user_name}@{domain}</b>.")]
- )
- self.user.setCert(self.site.address, domain)
- self.site.updateWebsocket(cert_changed=domain)
- self.response(to, "ok")
- # Select certificate for site
- def actionCertSelect(self, to, accepted_domains=[], accept_any=False, accepted_pattern=None):
- accounts = []
- accounts.append(["", _["No certificate"], ""]) # Default option
- active = "" # Make it active if no other option found
- # Add my certs
- auth_address = self.user.getAuthAddress(self.site.address) # Current auth address
- site_data = self.user.getSiteData(self.site.address) # Current auth address
- if not accepted_domains and not accepted_pattern: # Accept any if no filter defined
- accept_any = True
- for domain, cert in self.user.certs.items():
- if auth_address == cert["auth_address"] and domain == site_data.get("cert"):
- active = domain
- title = cert["auth_user_name"] + "@" + domain
- accepted_pattern_match = accepted_pattern and SafeRe.match(accepted_pattern, domain)
- if domain in accepted_domains or accept_any or accepted_pattern_match:
- accounts.append([domain, title, ""])
- else:
- accounts.append([domain, title, "disabled"])
- # Render the html
- body = "<span style='padding-bottom: 5px; display: inline-block'>" + _["Select account you want to use in this site:"] + "</span>"
- # Accounts
- for domain, account, css_class in accounts:
- if domain == active:
- css_class += " active" # Currently selected option
- title = _(u"<b>%s</b> <small>({_[currently selected]})</small>") % account
- else:
- title = "<b>%s</b>" % account
- body += "<a href='#Select+account' class='select select-close cert %s' title='%s'>%s</a>" % (css_class, domain, title)
- # More available providers
- more_domains = [domain for domain in accepted_domains if domain not in self.user.certs] # Domains we not displayed yet
- if more_domains:
- # body+= "<small style='margin-top: 10px; display: block'>Accepted authorization providers by the site:</small>"
- body += "<div style='background-color: #F7F7F7; margin-right: -30px'>"
- for domain in more_domains:
- body += _(u"""
- <a href='/{domain}' target='_top' class='select'>
- <small style='float: right; margin-right: 40px; margin-top: -1px'>{_[Register]} »</small>{domain}
- </a>
- """)
- body += "</div>"
- script = """
- $(".notification .select.cert").on("click", function() {
- $(".notification .select").removeClass('active')
- zeroframe.response(%s, this.title)
- return false
- })
- """ % self.next_message_id
- self.cmd("notification", ["ask", body], lambda domain: self.actionCertSet(to, domain))
- self.cmd("injectScript", script)
- # - Admin actions -
- def actionPermissionAdd(self, to, permission):
- if permission not in self.site.settings["permissions"]:
- self.site.settings["permissions"].append(permission)
- self.site.saveSettings()
- self.site.updateWebsocket(permission_added=permission)
- self.response(to, "ok")
- def actionPermissionRemove(self, to, permission):
- self.site.settings["permissions"].remove(permission)
- self.site.saveSettings()
- self.site.updateWebsocket(permission_removed=permission)
- self.response(to, "ok")
- def actionPermissionDetails(self, to, permission):
- if permission == "ADMIN":
- self.response(to, _["Modify your client's configuration and access all site"] + " <span style='color: red'>" + _["(Dangerous!)"] + "</span>")
- elif permission == "NOSANDBOX":
- self.response(to, _["Modify your client's configuration and access all site"] + " <span style='color: red'>" + _["(Dangerous!)"] + "</span>")
- else:
- self.response(to, "")
- # Set certificate that used for authenticate user for site
- def actionCertSet(self, to, domain):
- self.user.setCert(self.site.address, domain)
- self.site.updateWebsocket(cert_changed=domain)
- self.response(to, "ok")
- # List user's certificates
- def actionCertList(self, to):
- back = []
- auth_address = self.user.getAuthAddress(self.site.address)
- for domain, cert in self.user.certs.items():
- back.append({
- "auth_address": cert["auth_address"],
- "auth_type": cert["auth_type"],
- "auth_user_name": cert["auth_user_name"],
- "domain": domain,
- "selected": cert["auth_address"] == auth_address
- })
- return back
- # List all site info
- def actionSiteList(self, to, connecting_sites=False):
- ret = []
- SiteManager.site_manager.load() # Reload sites
- for site in self.server.sites.values():
- if not site.content_manager.contents.get("content.json") and not connecting_sites:
- continue # Incomplete site
- ret.append(self.formatSiteInfo(site, create_user=False)) # Dont generate the auth_address on listing
- self.response(to, ret)
- # Join to an event channel on all sites
- def actionChannelJoinAllsite(self, to, channel):
- if channel not in self.channels: # Add channel to channels
- self.channels.append(channel)
- for site in self.server.sites.values(): # Add websocket to every channel
- if self not in site.websockets:
- site.websockets.append(self)
- # Update site content.json
- def actionSiteUpdate(self, to, address, check_files=False, since=None, announce=False):
- def updateThread():
- site.update(announce=announce, check_files=check_files, since=since)
- self.response(to, "Updated")
- site = self.server.sites.get(address)
- if site and (site.address == self.site.address or "ADMIN" in self.site.settings["permissions"]):
- if not site.settings["serving"]:
- site.settings["serving"] = True
- site.saveSettings()
- gevent.spawn(updateThread)
- else:
- self.response(to, {"error": "Unknown site: %s" % address})
- # Pause site serving
- def actionSitePause(self, to, address):
- site = self.server.sites.get(address)
- if site:
- site.settings["serving"] = False
- site.saveSettings()
- site.updateWebsocket()
- site.worker_manager.stopWorkers()
- self.response(to, "Paused")
- else:
- self.response(to, {"error": "Unknown site: %s" % address})
- # Resume site serving
- def actionSiteResume(self, to, address):
- site = self.server.sites.get(address)
- if site:
- site.settings["serving"] = True
- site.saveSettings()
- gevent.spawn(site.update, announce=True)
- time.sleep(0.001) # Wait for update thread starting
- site.updateWebsocket()
- self.response(to, "Resumed")
- else:
- self.response(to, {"error": "Unknown site: %s" % address})
- def actionSiteDelete(self, to, address):
- site = self.server.sites.get(address)
- if site:
- site.delete()
- self.user.deleteSiteData(address)
- self.response(to, "Deleted")
- import gc
- gc.collect(2)
- else:
- self.response(to, {"error": "Unknown site: %s" % address})
- def cbSiteClone(self, to, address, root_inner_path="", target_address=None):
- self.cmd("notification", ["info", _["Cloning site..."]])
- site = self.server.sites.get(address)
- if target_address:
- target_site = self.server.sites.get(target_address)
- privatekey = self.user.getSiteData(target_site.address).get("privatekey")
- site.clone(target_address, privatekey, root_inner_path=root_inner_path)
- self.cmd("notification", ["done", _["Site source code upgraded!"]])
- site.publish()
- else:
- # Generate a new site from user's bip32 seed
- new_address, new_address_index, new_site_data = self.user.getNewSiteData()
- new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index, root_inner_path=root_inner_path)
- new_site.settings["own"] = True
- new_site.saveSettings()
- self.cmd("notification", ["done", _["Site cloned"]])
- self.cmd("redirect", "/%s" % new_address)
- gevent.spawn(new_site.announce)
- return "ok"
- def actionSiteClone(self, to, address, root_inner_path="", target_address=None):
- if not SiteManager.site_manager.isAddress(address):
- self.response(to, {"error": "Not a site: %s" % address})
- return
- if not self.server.sites.get(address):
- # Don't expose site existence
- return
- site = self.server.sites.get(address)
- if site.bad_files:
- for bad_inner_path in site.bad_files.keys():
- is_user_file = "cert_signers" in site.content_manager.getRules(bad_inner_path)
- if not is_user_file:
- self.cmd("notification", ["error", _["Clone error: Site still in sync"]])
- return {"error": "Site still in sync"}
- if "ADMIN" in self.getPermissions(to):
- self.cbSiteClone(to, address, root_inner_path, target_address)
- else:
- self.cmd(
- "confirm",
- [_["Clone site <b>%s</b>?"] % address, _["Clone"]],
- lambda (res): self.cbSiteClone(to, address, root_inner_path, target_address)
- )
- def actionSiteSetLimit(self, to, size_limit):
- self.site.settings["size_limit"] = int(size_limit)
- self.site.saveSettings()
- self.response(to, "ok")
- self.site.updateWebsocket()
- self.site.download(blind_includes=True)
- def actionSiteAdd(self, to, address):
- site_manager = SiteManager.site_manager
- if address in site_manager.sites:
- return {"error": "Site already added"}
- else:
- if site_manager.need(address):
- return "ok"
- else:
- return {"error": "Invalid address"}
- def actionSiteListModifiedFiles(self, to, content_inner_path="content.json"):
- content = self.site.content_manager.contents[content_inner_path]
- min_mtime = content.get("modified", 0)
- site_path = self.site.storage.directory
- modified_files = []
- # Load cache if not signed since last modified check
- if content.get("modified", 0) < self.site.settings["cache"].get("time_modified_files_check"):
- min_mtime = self.site.settings["cache"].get("time_modified_files_check")
- modified_files = self.site.settings["cache"].get("modified_files", [])
- inner_paths = [content_inner_path] + content.get("includes", {}).keys() + content.get("files", {}).keys()
- for relative_inner_path in inner_paths:
- inner_path = helper.getDirname(content_inner_path) + relative_inner_path
- try:
- is_mtime_newer = os.path.getmtime(self.site.storage.getPath(inner_path)) > min_mtime + 1
- if is_mtime_newer:
- if inner_path.endswith("content.json"):
- is_modified = self.site.content_manager.isModified(inner_path)
- else:
- previous_size = content["files"][inner_path]["size"]
- is_same_size = self.site.storage.getSize(inner_path) == previous_size
- ext = inner_path.rsplit(".", 1)[-1]
- is_text_file = ext in ["json", "txt", "html", "js", "css"]
- if is_same_size:
- if is_text_file:
- is_modified = self.site.content_manager.isModified(inner_path) # Check sha512 hash
- else:
- is_modified = False
- else:
- is_modified = True
- # Check ran, modified back to original value, but in the cache
- if not is_modified and inner_path in modified_files:
- modified_files.remove(inner_path)
- else:
- is_modified = False
- except Exception as err:
- if not self.site.storage.isFile(inner_path): # File deleted
- is_modified = True
- else:
- raise err
- if is_modified and inner_path not in modified_files:
- modified_files.append(inner_path)
- self.site.settings["cache"]["time_modified_files_check"] = time.time()
- self.site.settings["cache"]["modified_files"] = modified_files
- return {"modified_files": modified_files}
- def actionSiteSetSettingsValue(self, to, key, value):
- if key not in ["modified_files_notification"]:
- return {"error": "Can't change this key"}
- self.site.settings[key] = value
- return "ok"
- def actionUserGetSettings(self, to):
- settings = self.user.sites.get(self.site.address, {}).get("settings", {})
- self.response(to, settings)
- def actionUserSetSettings(self, to, settings):
- self.user.setSiteSettings(self.site.address, settings)
- self.response(to, "ok")
- def actionUserGetGlobalSettings(self, to):
- settings = self.user.settings
- self.response(to, settings)
- def actionUserSetGlobalSettings(self, to, settings):
- self.user.settings = settings
- self.user.save()
- self.response(to, "ok")
- def actionServerUpdate(self, to):
- self.cmd("updating")
- sys.modules["main"].update_after_shutdown = True
- SiteManager.site_manager.save()
- sys.modules["main"].file_server.stop()
- sys.modules["main"].ui_server.stop()
- def actionServerPortcheck(self, to):
- file_server = sys.modules["main"].file_server
- file_server.portCheck()
- self.response(to, file_server.port_opened)
- def actionServerShutdown(self, to, restart=False):
- if restart:
- sys.modules["main"].restart_after_shutdown = True
- sys.modules["main"].file_server.stop()
- sys.modules["main"].ui_server.stop()
- def actionServerShowdirectory(self, to, directory="backup", inner_path=""):
- if self.request.env["REMOTE_ADDR"] != "127.0.0.1":
- return self.response(to, {"error": "Only clients from 127.0.0.1 allowed to run this command"})
- import webbrowser
- if directory == "backup":
- path = os.path.abspath(config.data_dir)
- elif directory == "log":
- path = os.path.abspath(config.log_dir)
- elif directory == "site":
- path = os.path.abspath(self.site.storage.getPath(helper.getDirname(inner_path)))
- if os.path.isdir(path):
- self.log.debug("Opening: %s" % path)
- webbrowser.open('file://' + path)
- return self.response(to, "ok")
- else:
- return self.response(to, {"error": "Not a directory"})
- def actionConfigSet(self, to, key, value):
- if key not in config.keys_api_change_allowed:
- self.response(to, {"error": "Forbidden you cannot set this config key"})
- return
- if key == "open_browser":
- if value not in ["default_browser", "False"]:
- self.response(to, {"error": "Forbidden: Invalid value"})
- return
- # Remove empty lines from lists
- if type(value) is list:
- value = [line for line in value if line]
- config.saveValue(key, value)
- if key not in config.keys_restart_need:
- if value is None: # Default value
- setattr(config, key, config.parser.get_default(key))
- setattr(config.arguments, key, config.parser.get_default(key))
- else:
- setattr(config, key, value)
- setattr(config.arguments, key, value)
- else:
- config.need_restart = True
- config.pending_changes[key] = value
- if key == "language":
- import Translate
- for translate in Translate.translates:
- translate.setLanguage(value)
- message = _["You have successfully changed the web interface's language!"] + "<br>"
- message += _["Due to the browser's caching, the full transformation could take some minute."]
- self.cmd("notification", ["done", message, 10000])
- if key == "tor_use_bridges":
- if value is None:
- value = False
- else:
- value = True
- tor_manager = sys.modules["main"].file_server.tor_manager
- tor_manager.request("SETCONF UseBridges=%i" % value)
- if key == "trackers_file":
- config.loadTrackersFile()
- if key == "log_level":
- logging.getLogger('').setLevel(logging.getLevelName(config.log_level))
- if key == "ip_external":
- gevent.spawn(sys.modules["main"].file_server.portCheck)
- self.response(to, "ok")
|