123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853 |
- import time
- import re
- import os
- import mimetypes
- import json
- import cgi
- import gevent
- from Config import config
- from Site import SiteManager
- from User import UserManager
- from Plugin import PluginManager
- from Ui.UiWebsocket import UiWebsocket
- from Crypt import CryptHash
- from util import helper
- status_texts = {
- 200: "200 OK",
- 206: "206 Partial Content",
- 400: "400 Bad Request",
- 403: "403 Forbidden",
- 404: "404 Not Found",
- 500: "500 Internal Server Error",
- }
- class SecurityError(Exception):
- pass
- @PluginManager.acceptPlugins
- class UiRequest(object):
- def __init__(self, server, get, env, start_response):
- if server:
- self.server = server
- self.log = server.log
- self.get = get # Get parameters
- self.env = env # Enviroment settings
- # ['CONTENT_LENGTH', 'CONTENT_TYPE', 'GATEWAY_INTERFACE', 'HTTP_ACCEPT', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE',
- # 'HTTP_COOKIE', 'HTTP_CACHE_CONTROL', 'HTTP_HOST', 'HTTP_HTTPS', 'HTTP_ORIGIN', 'HTTP_PROXY_CONNECTION', 'HTTP_REFERER',
- # 'HTTP_USER_AGENT', 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REMOTE_PORT', 'REQUEST_METHOD', 'SCRIPT_NAME',
- # 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SOFTWARE', 'werkzeug.request', 'wsgi.errors',
- # 'wsgi.input', 'wsgi.multiprocess', 'wsgi.multithread', 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version']
- self.start_response = start_response # Start response function
- self.user = None
- self.script_nonce = None # Nonce for script tags in wrapper html
- def learnHost(self, host):
- self.server.allowed_hosts.add(host)
- self.server.log.info("Added %s as allowed host" % host)
- def isHostAllowed(self, host):
- if host in self.server.allowed_hosts:
- return True
- # Allow any IP address as they are not affected by DNS rebinding
- # attacks
- if helper.isIp(host):
- self.learnHost(host)
- return True
- if ":" in host and helper.isIp(host.rsplit(":", 1)[0]): # Test without port
- self.learnHost(host)
- return True
- if self.isProxyRequest(): # Support for chrome extension proxy
- if self.server.site_manager.isDomain(host):
- return True
- else:
- return False
- return False
- # Call the request handler function base on path
- def route(self, path):
- # Restict Ui access by ip
- if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:
- return self.error403(details=False)
- # Check if host allowed to do request
- if not self.isHostAllowed(self.env.get("HTTP_HOST")):
- return self.error403("Invalid host: %s" % self.env.get("HTTP_HOST"), details=False)
- # Prepend .bit host for transparent proxy
- if self.server.site_manager.isDomain(self.env.get("HTTP_HOST")):
- path = re.sub("^/", "/" + self.env.get("HTTP_HOST") + "/", path)
- path = re.sub("^http://zero[/]+", "/", path) # Remove begining http://zero/ for chrome extension
- path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access
- # Sanitize request url
- path = path.replace("\\", "/")
- if "../" in path or "./" in path:
- return self.error403("Invalid path: %s" % path)
- if self.env["REQUEST_METHOD"] == "OPTIONS":
- if "/" not in path.strip("/"):
- content_type = self.getContentType("index.html")
- else:
- content_type = self.getContentType(path)
- extra_headers = {"Access-Control-Allow-Origin": "null"}
- self.sendHeader(content_type=content_type, extra_headers=extra_headers, noscript=True)
- return ""
- if path == "/":
- return self.actionIndex()
- elif path == "/favicon.ico":
- return self.actionFile("src/Ui/media/img/favicon.ico")
- # Internal functions
- elif "/ZeroNet-Internal/" in path:
- path = re.sub(".*?/ZeroNet-Internal/", "/", path)
- func = getattr(self, "action" + path.strip("/"), None) # Check if we have action+request_path function
- if func:
- return func()
- else:
- return self.error404(path)
- # Media
- elif path.startswith("/uimedia/"):
- return self.actionUiMedia(path)
- elif "/uimedia/" in path:
- # uimedia within site dir (for chrome extension)
- path = re.sub(".*?/uimedia/", "/uimedia/", path)
- return self.actionUiMedia(path)
- # Websocket
- elif path == "/Websocket":
- return self.actionWebsocket()
- # Debug
- elif path == "/Debug" and config.debug:
- return self.actionDebug()
- elif path == "/Console" and config.debug:
- return self.actionConsole()
- # Wrapper-less static files
- elif path.startswith("/raw/"):
- return self.actionSiteMedia(path.replace("/raw", "/media", 1), header_noscript=True)
- elif path.startswith("/add/"):
- return self.actionSiteAdd()
- # Site media wrapper
- else:
- if self.get.get("wrapper_nonce"):
- if self.get["wrapper_nonce"] in self.server.wrapper_nonces:
- self.server.wrapper_nonces.remove(self.get["wrapper_nonce"])
- return self.actionSiteMedia("/media" + path) # Only serve html files with frame
- else:
- self.server.log.warning("Invalid wrapper nonce: %s" % self.get["wrapper_nonce"])
- body = self.actionWrapper(path)
- else:
- body = self.actionWrapper(path)
- if body:
- return body
- else:
- func = getattr(self, "action" + path.strip("/"), None) # Check if we have action+request_path function
- if func:
- return func()
- else:
- return self.error404(path)
- # The request is proxied by chrome extension or a transparent proxy
- def isProxyRequest(self):
- return self.env["PATH_INFO"].startswith("http://") or (self.server.allow_trans_proxy and self.server.site_manager.isDomain(self.env.get("HTTP_HOST")))
- def isWebSocketRequest(self):
- return self.env.get("HTTP_UPGRADE") == "websocket"
- def isAjaxRequest(self):
- return self.env.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
- # Get mime by filename
- def getContentType(self, file_name):
- content_type = mimetypes.guess_type(file_name)[0]
- if content_type:
- content_type = content_type.lower()
- if file_name.endswith(".css"): # Force correct css content type
- content_type = "text/css"
- if not content_type:
- if file_name.endswith(".json"): # Correct json header
- content_type = "application/json"
- else:
- content_type = "application/octet-stream"
- return content_type
- # Return: <dict> Posted variables
- def getPosted(self):
- if self.env['REQUEST_METHOD'] == "POST":
- return dict(cgi.parse_qsl(
- self.env['wsgi.input'].readline().decode()
- ))
- else:
- return {}
- # Return: <dict> Cookies based on self.env
- def getCookies(self):
- raw_cookies = self.env.get('HTTP_COOKIE')
- if raw_cookies:
- cookies = cgi.parse_qsl(raw_cookies)
- return {key.strip(): val for key, val in cookies}
- else:
- return {}
- def getCurrentUser(self):
- if self.user:
- return self.user # Cache
- self.user = UserManager.user_manager.get() # Get user
- if not self.user:
- self.user = UserManager.user_manager.create()
- return self.user
- def getRequestUrl(self):
- if self.isProxyRequest():
- if self.env["PATH_INFO"].startswith("http://zero/"):
- return self.env["PATH_INFO"]
- else: # Add http://zero to direct domain access
- return self.env["PATH_INFO"].replace("http://", "http://zero/", 1)
- else:
- return self.env["wsgi.url_scheme"] + "://" + self.env["HTTP_HOST"] + self.env["PATH_INFO"]
- def getReferer(self):
- referer = self.env.get("HTTP_REFERER")
- if referer and self.isProxyRequest() and not referer.startswith("http://zero/"):
- return referer.replace("http://", "http://zero/", 1)
- else:
- return referer
- def isScriptNonceSupported(self):
- user_agent = self.env.get("HTTP_USER_AGENT")
- if "Edge/" in user_agent:
- is_script_nonce_supported = False
- elif "Safari/" in user_agent and "Chrome/" not in user_agent:
- is_script_nonce_supported = False
- else:
- is_script_nonce_supported = True
- return is_script_nonce_supported
- # Send response headers
- def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
- headers = {}
- headers["Version"] = "HTTP/1.1"
- headers["Connection"] = "Keep-Alive"
- headers["Keep-Alive"] = "max=25, timeout=30"
- headers["X-Frame-Options"] = "SAMEORIGIN"
- is_referer_allowed = False
- if self.env.get("HTTP_REFERER"):
- if self.isSameOrigin(self.getReferer(), self.getRequestUrl()):
- is_referer_allowed = True
- elif self.getReferer() == "%s://%s/" % (self.env["wsgi.url_scheme"], self.env["HTTP_HOST"]): # Origin-only referer
- is_referer_allowed = True
- if content_type != "text/html" and is_referer_allowed:
- headers["Access-Control-Allow-Origin"] = "*" # Allow load font files from css
- if noscript:
- headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';"
- elif script_nonce and self.isScriptNonceSupported():
- headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self'; style-src 'self' 'unsafe-inline'; connect-src *; frame-src 'self'".format(script_nonce)
- if allow_ajax:
- headers["Access-Control-Allow-Origin"] = "null"
- if self.env["REQUEST_METHOD"] == "OPTIONS":
- # Allow json access
- headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range"
- headers["Access-Control-Allow-Credentials"] = "true"
- if content_type == "text/html":
- content_type = "text/html; charset=utf-8"
- if content_type == "text/plain":
- content_type = "text/plain; charset=utf-8"
- # Download instead of display file types that can be dangerous
- if re.findall("/svg|/xml|/x-shockwave-flash|/pdf", content_type):
- headers["Content-Disposition"] = "attachment"
- cacheable_type = (
- content_type == "text/css" or content_type.startswith("image") or content_type.startswith("video") or
- self.env["REQUEST_METHOD"] == "OPTIONS" or content_type == "application/javascript"
- )
- if status in (200, 206) and cacheable_type: # Cache Css, Js, Image files for 10min
- headers["Cache-Control"] = "public, max-age=600" # Cache 10 min
- else:
- headers["Cache-Control"] = "no-cache, no-store, private, must-revalidate, max-age=0" # No caching at all
- headers["Content-Type"] = content_type
- headers.update(extra_headers)
- return self.start_response(status_texts[status], headers.items())
- # Renders a template
- def render(self, template_path, *args, **kwargs):
- template = open(template_path).read()
- def renderReplacer(m):
- return "%s" % kwargs.get(m.group(1), "")
- template_rendered = re.sub("{(.*?)}", renderReplacer, template)
- return template_rendered.encode("utf8")
- # - Actions -
- # Redirect to an url
- def actionRedirect(self, url):
- self.start_response('301 Redirect', [('Location', str(url))])
- yield "Location changed: %s" % url
- def actionIndex(self):
- return self.actionRedirect("/" + config.homepage)
- # Render a file from media with iframe site wrapper
- def actionWrapper(self, path, extra_headers=None):
- if not extra_headers:
- extra_headers = {}
- script_nonce = self.getScriptNonce()
- match = re.match("/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
- just_added = False
- if match:
- address = match.group("address")
- inner_path = match.group("inner_path").lstrip("/")
- if not inner_path or path.endswith("/"): # It's a directory
- content_type = self.getContentType("index.html")
- else: # It's a file
- content_type = self.getContentType(inner_path)
- is_html_file = "html" in content_type or "xhtml" in content_type
- if not is_html_file:
- return self.actionSiteMedia("/media" + path) # Serve non-html files without wrapper
- if self.isAjaxRequest():
- return self.error403("Ajax request not allowed to load wrapper") # No ajax allowed on wrapper
- if self.isWebSocketRequest():
- return self.error403("WebSocket request not allowed to load wrapper") # No websocket
- if "text/html" not in self.env.get("HTTP_ACCEPT", ""):
- return self.error403("Invalid Accept header to load wrapper")
- if "prefetch" in self.env.get("HTTP_X_MOZ", "") or "prefetch" in self.env.get("HTTP_PURPOSE", ""):
- return self.error403("Prefetch not allowed to load wrapper")
- site = SiteManager.site_manager.get(address)
- if (
- site and site.content_manager.contents.get("content.json") and
- (not site.getReachableBadFiles() or site.settings["own"])
- ): # Its downloaded or own
- title = site.content_manager.contents["content.json"]["title"]
- else:
- title = "Loading %s..." % address
- site = SiteManager.site_manager.get(address)
- if site: # Already added, but not downloaded
- if time.time() - site.announcer.time_last_announce > 5:
- site.log.debug("Reannouncing site...")
- gevent.spawn(site.update, announce=True)
- else: # If not added yet
- site = SiteManager.site_manager.need(address)
- just_added = True
- if not site:
- return False
- self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
- min_last_announce = (time.time() - site.announcer.time_last_announce) / 60
- if min_last_announce > 60 and site.settings["serving"] and not just_added:
- site.log.debug("Site requested, but not announced recently (last %.0fmin ago). Updating..." % min_last_announce)
- gevent.spawn(site.update, announce=True)
- return iter([self.renderWrapper(site, path, inner_path, title, extra_headers, script_nonce=script_nonce)])
- # Make response be sent at once (see https://github.com/HelloZeroNet/ZeroNet/issues/1092)
- else: # Bad url
- return False
- def getSiteUrl(self, address):
- if self.isProxyRequest():
- return "http://zero/" + address
- else:
- return "/" + address
- def processQueryString(self, site, query_string):
- match = re.search("zeronet_peers=(.*?)(&|$)", query_string)
- if match:
- query_string = query_string.replace(match.group(0), "")
- num_added = 0
- for peer in match.group(1).split(","):
- if not re.match(".*?:[0-9]+$", peer):
- continue
- ip, port = peer.rsplit(":", 1)
- if site.addPeer(ip, int(port), source="query_string"):
- num_added += 1
- site.log.debug("%s peers added by query string" % num_added)
- return query_string
- def renderWrapper(self, site, path, inner_path, title, extra_headers, show_loadingscreen=None, script_nonce=None):
- file_inner_path = inner_path
- if not file_inner_path:
- file_inner_path = "index.html" # If inner path defaults to index.html
- if file_inner_path.endswith("/"):
- file_inner_path = file_inner_path + "index.html"
- address = re.sub("/.*", "", path.lstrip("/"))
- if self.isProxyRequest() and (not path or "/" in path[1:]):
- if self.env["HTTP_HOST"] == "zero":
- root_url = "/" + address + "/"
- file_url = "/" + address + "/" + inner_path
- else:
- file_url = "/" + inner_path
- root_url = "/"
- else:
- file_url = "/" + address + "/" + inner_path
- root_url = "/" + address + "/"
- if self.isProxyRequest():
- self.server.allowed_ws_origins.add(self.env["HTTP_HOST"])
- # Wrapper variable inits
- body_style = ""
- meta_tags = ""
- postmessage_nonce_security = "false"
- wrapper_nonce = self.getWrapperNonce()
- inner_query_string = self.processQueryString(site, self.env.get("QUERY_STRING", ""))
- if inner_query_string:
- inner_query_string = "?%s&wrapper_nonce=%s" % (inner_query_string, wrapper_nonce)
- elif "?" in inner_path:
- inner_query_string = "&wrapper_nonce=%s" % wrapper_nonce
- else:
- inner_query_string = "?wrapper_nonce=%s" % wrapper_nonce
- if self.isProxyRequest(): # Its a remote proxy request
- if self.env["REMOTE_ADDR"] == "127.0.0.1": # Local client, the server address also should be 127.0.0.1
- server_url = "http://127.0.0.1:%s" % self.env["SERVER_PORT"]
- else: # Remote client, use SERVER_NAME as server's real address
- server_url = "http://%s:%s" % (self.env["SERVER_NAME"], self.env["SERVER_PORT"])
- homepage = "http://zero/" + config.homepage
- else: # Use relative path
- server_url = ""
- homepage = "/" + config.homepage
- user = self.getCurrentUser()
- if user:
- theme = user.settings.get("theme", "light")
- else:
- theme = "light"
- themeclass = "theme-%-6s" % re.sub("[^a-z]", "", theme)
- if site.content_manager.contents.get("content.json"): # Got content.json
- content = site.content_manager.contents["content.json"]
- if content.get("background-color"):
- background_color = content.get("background-color-%s" % theme, content["background-color"])
- body_style += "background-color: %s;" % cgi.escape(background_color, True)
- if content.get("viewport"):
- meta_tags += '<meta name="viewport" id="viewport" content="%s">' % cgi.escape(content["viewport"], True)
- if content.get("favicon"):
- meta_tags += '<link rel="icon" href="%s%s">' % (root_url, cgi.escape(content["favicon"], True))
- if content.get("postmessage_nonce_security"):
- postmessage_nonce_security = "true"
- sandbox_permissions = ""
- if "NOSANDBOX" in site.settings["permissions"]:
- sandbox_permissions += " allow-same-origin"
- if show_loadingscreen is None:
- show_loadingscreen = not site.storage.isFile(file_inner_path)
- return self.render(
- "src/Ui/template/wrapper.html",
- server_url=server_url,
- inner_path=inner_path,
- file_url=re.escape(file_url),
- file_inner_path=re.escape(file_inner_path),
- address=site.address,
- title=cgi.escape(title, True),
- body_style=body_style,
- meta_tags=meta_tags,
- query_string=re.escape(inner_query_string),
- wrapper_key=site.settings["wrapper_key"],
- ajax_key=site.settings["ajax_key"],
- wrapper_nonce=wrapper_nonce,
- postmessage_nonce_security=postmessage_nonce_security,
- permissions=json.dumps(site.settings["permissions"]),
- show_loadingscreen=json.dumps(show_loadingscreen),
- sandbox_permissions=sandbox_permissions,
- rev=config.rev,
- lang=config.language,
- homepage=homepage,
- themeclass=themeclass,
- script_nonce=script_nonce
- )
- # Create a new wrapper nonce that allows to get one html file without the wrapper
- def getWrapperNonce(self):
- wrapper_nonce = CryptHash.random()
- self.server.wrapper_nonces.append(wrapper_nonce)
- return wrapper_nonce
- def getScriptNonce(self):
- if not self.script_nonce:
- self.script_nonce = CryptHash.random(encoding="base64")
- return self.script_nonce
- # Create a new wrapper nonce that allows to get one site
- def getAddNonce(self):
- add_nonce = CryptHash.random()
- self.server.add_nonces.append(add_nonce)
- return add_nonce
- def isSameOrigin(self, url_a, url_b):
- if not url_a or not url_b:
- return False
- origin_a = re.sub("http[s]{0,1}://(.*?/.*?/).*", "\\1", url_a)
- origin_b = re.sub("http[s]{0,1}://(.*?/.*?/).*", "\\1", url_b)
- return origin_a == origin_b
- # Return {address: 1Site.., inner_path: /data/users.json} from url path
- def parsePath(self, path):
- path = path.replace("\\", "/")
- path = path.replace("/index.html/", "/") # Base Backward compatibility fix
- if path.endswith("/"):
- path = path + "index.html"
- if "../" in path or "./" in path:
- raise SecurityError("Invalid path")
- match = re.match("/media/(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
- if match:
- path_parts = match.groupdict()
- path_parts["request_address"] = path_parts["address"] # Original request address (for Merger sites)
- path_parts["inner_path"] = path_parts["inner_path"].lstrip("/")
- if not path_parts["inner_path"]:
- path_parts["inner_path"] = "index.html"
- return path_parts
- else:
- return None
- # Serve a media for site
- def actionSiteMedia(self, path, header_length=True, header_noscript=False):
- try:
- path_parts = self.parsePath(path)
- except SecurityError as err:
- return self.error403(err)
- if not path_parts:
- return self.error404(path)
- address = path_parts["address"]
- file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"])
- if config.debug and file_path.split("/")[-1].startswith("all."):
- # If debugging merge *.css to all.css and *.js to all.js
- site = self.server.sites.get(address)
- if site and site.settings["own"]:
- from Debug import DebugMedia
- DebugMedia.merge(file_path)
- if not address or address == ".":
- return self.error403(path_parts["inner_path"])
- header_allow_ajax = False
- if self.get.get("ajax_key"):
- site = SiteManager.site_manager.get(path_parts["request_address"])
- if self.get["ajax_key"] == site.settings["ajax_key"]:
- header_allow_ajax = True
- else:
- return self.error403("Invalid ajax_key")
- file_size = helper.getFilesize(file_path)
- if file_size is not None:
- return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax, file_size=file_size, path_parts=path_parts)
- elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect
- if path_parts["inner_path"]:
- return self.actionRedirect("./%s/" % path_parts["inner_path"].split("/")[-1])
- else:
- return self.actionRedirect("./%s/" % path_parts["address"])
- else: # File not exists, try to download
- if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading
- return self.actionSiteAddPrompt(path)
- site = SiteManager.site_manager.need(address)
- if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites
- return self.actionFile("src/Ui/media/img/favicon.ico")
- result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads
- if result:
- file_size = helper.getFilesize(file_path)
- return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax, file_size=file_size, path_parts=path_parts)
- else:
- self.log.debug("File not found: %s" % path_parts["inner_path"])
- return self.error404(path_parts["inner_path"])
- # Serve a media for ui
- def actionUiMedia(self, path):
- match = re.match("/uimedia/(?P<inner_path>.*)", path)
- if match: # Looks like a valid path
- file_path = "src/Ui/media/%s" % match.group("inner_path")
- allowed_dir = os.path.abspath("src/Ui/media") # Only files within data/sitehash allowed
- if ".." in file_path or not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir):
- # File not in allowed path
- return self.error403()
- else:
- if config.debug and match.group("inner_path").startswith("all."):
- # If debugging merge *.css to all.css and *.js to all.js
- from Debug import DebugMedia
- DebugMedia.merge(file_path)
- return self.actionFile(file_path, header_length=False) # Dont's send site to allow plugins append content
- else: # Bad url
- return self.error400()
- def actionSiteAdd(self):
- post = dict(cgi.parse_qsl(self.env["wsgi.input"].read()))
- if post["add_nonce"] not in self.server.add_nonces:
- return self.error403("Add nonce error.")
- self.server.add_nonces.remove(post["add_nonce"])
- SiteManager.site_manager.need(post["address"])
- return self.actionRedirect(post["url"])
- def actionSiteAddPrompt(self, path):
- path_parts = self.parsePath(path)
- if not path_parts or not self.server.site_manager.isAddress(path_parts["address"]):
- return self.error404(path)
- self.sendHeader(200, "text/html", noscript=True)
- template = open("src/Ui/template/site_add.html").read()
- template = template.replace("{url}", cgi.escape(self.env["PATH_INFO"], True))
- template = template.replace("{address}", path_parts["address"])
- template = template.replace("{add_nonce}", self.getAddNonce())
- return template
- def replaceHtmlVariables(self, block, path_parts):
- user = self.getCurrentUser()
- themeclass = "theme-%-6s" % re.sub("[^a-z]", "", user.settings.get("theme", "light"))
- block = block.replace("{themeclass}", themeclass.encode("utf8"))
- if path_parts:
- site = self.server.sites.get(path_parts.get("address"))
- if site.settings["own"]:
- modified = int(time.time())
- else:
- modified = int(site.content_manager.contents["content.json"]["modified"])
- block = block.replace("{site_modified}", str(modified))
- return block
- # Stream a file to client
- def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False, header_allow_ajax=False, file_size=None, file_obj=None, path_parts=None):
- if file_size is None:
- file_size = helper.getFilesize(file_path)
- if file_size is not None:
- # Try to figure out content type by extension
- content_type = self.getContentType(file_path)
- range = self.env.get("HTTP_RANGE")
- range_start = None
- is_html_file = file_path.endswith(".html")
- if is_html_file:
- header_length = False
- if send_header:
- extra_headers = {}
- extra_headers["Accept-Ranges"] = "bytes"
- if header_length:
- extra_headers["Content-Length"] = str(file_size)
- if range:
- range_start = int(re.match(".*?([0-9]+)", range).group(1))
- if re.match(".*?-([0-9]+)", range):
- range_end = int(re.match(".*?-([0-9]+)", range).group(1)) + 1
- else:
- range_end = file_size
- extra_headers["Content-Length"] = str(range_end - range_start)
- extra_headers["Content-Range"] = "bytes %s-%s/%s" % (range_start, range_end - 1, file_size)
- if range:
- status = 206
- else:
- status = 200
- self.sendHeader(status, content_type=content_type, noscript=header_noscript, allow_ajax=header_allow_ajax, extra_headers=extra_headers)
- if self.env["REQUEST_METHOD"] != "OPTIONS":
- if not file_obj:
- file_obj = open(file_path, "rb")
- if range_start:
- file_obj.seek(range_start)
- while 1:
- try:
- block = file_obj.read(block_size)
- if is_html_file:
- block = self.replaceHtmlVariables(block, path_parts)
- if block:
- yield block
- else:
- raise StopIteration
- except StopIteration:
- file_obj.close()
- break
- else: # File not exists
- yield self.error404(file_path)
- # On websocket connection
- def actionWebsocket(self):
- ws = self.env.get("wsgi.websocket")
- if ws:
- # Allow only same-origin websocket requests
- origin = self.env.get("HTTP_ORIGIN")
- host = self.env.get("HTTP_HOST")
- # Allow only same-origin websocket requests
- if origin:
- origin_host = origin.split("://", 1)[-1]
- if origin_host != host and origin_host not in self.server.allowed_ws_origins:
- ws.send(json.dumps({"error": "Invalid origin: %s" % origin}))
- return self.error403("Invalid origin: %s" % origin)
- # Find site by wrapper_key
- wrapper_key = self.get["wrapper_key"]
- site = None
- for site_check in self.server.sites.values():
- if site_check.settings["wrapper_key"] == wrapper_key:
- site = site_check
- if site: # Correct wrapper key
- try:
- user = self.getCurrentUser()
- except Exception, err:
- self.log.error("Error in data/user.json: %s" % err)
- return self.error500()
- if not user:
- self.log.error("No user found")
- return self.error403()
- ui_websocket = UiWebsocket(ws, site, self.server, user, self)
- site.websockets.append(ui_websocket) # Add to site websockets to allow notify on events
- self.server.websockets.append(ui_websocket)
- ui_websocket.start()
- self.server.websockets.remove(ui_websocket)
- for site_check in self.server.sites.values():
- # Remove websocket from every site (admin sites allowed to join other sites event channels)
- if ui_websocket in site_check.websockets:
- site_check.websockets.remove(ui_websocket)
- return "Bye."
- else: # No site found by wrapper key
- self.log.error("Wrapper key not found: %s" % wrapper_key)
- return self.error403()
- else:
- self.start_response("400 Bad Request", [])
- return "Not a websocket!"
- # Debug last error
- def actionDebug(self):
- # Raise last error from DebugHook
- import sys
- last_error = sys.modules["main"].DebugHook.last_error
- if last_error:
- raise last_error[0], last_error[1], last_error[2]
- else:
- self.sendHeader()
- return "No error! :)"
- # Just raise an error to get console
- def actionConsole(self):
- import sys
- sites = self.server.sites
- main = sys.modules["main"]
- def bench(code, times=100, init=None):
- sites = self.server.sites
- main = sys.modules["main"]
- s = time.time()
- if init:
- eval(compile(init, '<string>', 'exec'), globals(), locals())
- for _ in range(times):
- back = eval(code, globals(), locals())
- return ["%s run: %.3fs" % (times, time.time() - s), back]
- raise Exception("Here is your console")
- # - Tests -
- def actionTestStream(self):
- self.sendHeader()
- yield " " * 1080 # Overflow browser's buffer
- yield "He"
- time.sleep(1)
- yield "llo!"
- # yield "Running websockets: %s" % len(self.server.websockets)
- # self.server.sendMessage("Hello!")
- # - Errors -
- # Send bad request error
- def error400(self, message=""):
- self.sendHeader(400, noscript=True)
- return self.formatError("Bad Request", message)
- # You are not allowed to access this
- def error403(self, message="", details=True):
- self.sendHeader(403, noscript=True)
- self.log.error("Error 403: %s" % message)
- return self.formatError("Forbidden", message, details=details)
- # Send file not found error
- def error404(self, path=""):
- self.sendHeader(404, noscript=True)
- return self.formatError("Not Found", path.encode("utf8"), details=False)
- # Internal server error
- def error500(self, message=":("):
- self.sendHeader(500, noscript=True)
- return self.formatError("Server error", message)
- def formatError(self, title, message, details=True):
- import sys
- import gevent
- if details and config.debug:
- details = {key: val for key, val in self.env.items() if hasattr(val, "endswith") and "COOKIE" not in key}
- details["version_zeronet"] = "%s r%s" % (config.version, config.rev)
- details["version_python"] = sys.version
- details["version_gevent"] = gevent.__version__
- details["plugins"] = PluginManager.plugin_manager.plugin_names
- arguments = {key: val for key, val in vars(config.arguments).items() if "password" not in key}
- details["arguments"] = arguments
- return """
- <style>
- * { font-family: Consolas, Monospace; color: #333 }
- pre { padding: 10px; background-color: #EEE }
- </style>
- <h1>%s</h1>
- <h2>%s</h3>
- <h3>Please <a href="https://github.com/HelloZeroNet/ZeroNet/issues" target="_top">report it</a> if you think this an error.</h3>
- <h4>Details:</h4>
- <pre>%s</pre>
- """ % (title, cgi.escape(message), cgi.escape(json.dumps(details, indent=4, sort_keys=True)))
- else:
- return """
- <h1>%s</h1>
- <h2>%s</h3>
- """ % (title, cgi.escape(message))
|