import time import re import os import mimetypes import json import cgi 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 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", } @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 # Call the request handler function base on path def route(self, path): if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict: # Restict Ui access by ip return self.error403(details=False) 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 if self.env["REQUEST_METHOD"] == "OPTIONS": self.sendHeader() return "" if path == "/": return self.actionIndex() elif path.endswith("favicon.ico"): return self.actionFile("src/Ui/media/img/favicon.ico") # 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) elif path.startswith("/media"): return self.actionSiteMedia(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() # Site media wrapper else: if self.get.get("wrapper_nonce"): return self.actionSiteMedia("/media" + path) # Only serve html files with frame else: body = self.actionWrapper(path) if body: return body else: func = getattr(self, "action" + path.lstrip("/"), 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 def isProxyRequest(self): return self.env["PATH_INFO"].startswith("http://") 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 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: 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: 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 # Send response headers def sendHeader(self, status=200, content_type="text/html", extra_headers=[]): if content_type == "text/html": content_type = "text/html; charset=utf-8" headers = [] headers.append(("Version", "HTTP/1.1")) headers.append(("Connection", "Keep-Alive")) headers.append(("Keep-Alive", "max=25, timeout=30")) headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access # headers.append(("Content-Security-Policy", "default-src 'self' data: 'unsafe-inline' ws://127.0.0.1:* http://127.0.0.1:* wss://tracker.webtorrent.io; sandbox allow-same-origin allow-top-navigation allow-scripts")) # Only local connections if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Cookie")) headers.append(("Access-Control-Allow-Credentials", "true")) 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.append(("Cache-Control", "public, max-age=600")) # Cache 10 min else: headers.append(("Cache-Control", "no-cache, no-store, private, must-revalidate, max-age=0")) # No caching at all headers.append(("Content-Type", content_type)) for extra_header in extra_headers: headers.append(extra_header) return self.start_response(status_texts[status], headers) # Renders a template def render(self, template_path, *args, **kwargs): template = open(template_path).read().decode("utf8") return template.format(**kwargs).encode("utf8") # - Actions - # Redirect to an url def actionRedirect(self, url): self.start_response('301 Redirect', [('Location', 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 = [] match = re.match("/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) if match: address = match.group("address") inner_path = match.group("inner_path").lstrip("/") if "." in inner_path and not inner_path.endswith(".html"): return self.actionSiteMedia("/media" + path) # Only serve html files with frame if self.env.get("HTTP_X_REQUESTED_WITH"): return self.error403("Ajax request not allowed to load wrapper") # No ajax allowed on 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.need(address) # Start download site if not site: return False self.sendHeader(extra_headers=extra_headers[:]) return iter([self.renderWrapper(site, path, inner_path, title, extra_headers)]) # Dont know why wrapping with iter necessary, but without it around 100x slower else: # Bad url return False def renderWrapper(self, site, path, inner_path, title, extra_headers): file_inner_path = inner_path if not file_inner_path: file_inner_path = "index.html" # If inner path defaults to index.html address = re.sub("/.*", "", path.lstrip("/")) if self.isProxyRequest() and (not path or "/" in path[1:]): file_url = re.sub(".*/", "", inner_path) else: file_url = "/" + address + "/" + inner_path # Wrapper variable inits query_string = "" body_style = "" meta_tags = "" wrapper_nonce = self.getWrapperNonce() if self.env.get("QUERY_STRING"): query_string = "?%s&wrapper_nonce=%s" % (self.env["QUERY_STRING"], wrapper_nonce) else: 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 if site.content_manager.contents.get("content.json"): # Got content.json content = site.content_manager.contents["content.json"] if content.get("background-color"): body_style += "background-color: %s;" % \ cgi.escape(site.content_manager.contents["content.json"]["background-color"], True) if content.get("viewport"): meta_tags += '' % cgi.escape(content["viewport"], True) if site.settings.get("own"): sandbox_permissions = "allow-modals" # For coffeescript compile errors else: sandbox_permissions = "" return self.render( "src/Ui/template/wrapper.html", server_url=server_url, inner_path=inner_path, file_url=file_url, file_inner_path=file_inner_path, address=site.address, title=title, body_style=body_style, meta_tags=meta_tags, query_string=query_string, wrapper_key=site.settings["wrapper_key"], permissions=json.dumps(site.settings["permissions"]), show_loadingscreen=json.dumps(not site.storage.isFile(file_inner_path)), sandbox_permissions=sandbox_permissions, rev=config.rev, homepage=homepage ) # 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 # Returns if media request allowed from that referer def isMediaRequestAllowed(self, site_address, referer): if not re.sub("^http[s]{0,1}://", "", referer).startswith(self.env["HTTP_HOST"]): return False referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address return referer_path.startswith("/" + site_address) # Serve a media for site def actionSiteMedia(self, path): path = path.replace("/index.html/", "/") # Base Backward compatibility fix if path.endswith("/"): path = path + "index.html" match = re.match("/media/(?P
[A-Za-z0-9\._-]+)/(?P.*)", path) # Check wrapper nonce content_type = self.getContentType(path) if "htm" in content_type: # Valid nonce must present to render html files wrapper_nonce = self.get.get("wrapper_nonce") if wrapper_nonce not in self.server.wrapper_nonces: return self.error403("Wrapper nonce error. Please reload the page.") self.server.wrapper_nonces.remove(self.get["wrapper_nonce"]) referer = self.env.get("HTTP_REFERER") if referer and match: # Only allow same site to receive media if not self.isMediaRequestAllowed(match.group("address"), referer): return self.error403("Media referrer error") # Referrer not starts same address as requested path if match: # Looks like a valid path address = match.group("address") file_path = "%s/%s/%s" % (config.data_dir, address, match.group("inner_path")) allowed_dir = os.path.abspath("%s/%s" % (config.data_dir, address)) # Only files within data/sitehash allowed data_dir = os.path.abspath("data") # No files from data/ allowed if ( ".." in file_path or not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir) or allowed_dir == data_dir ): # File not in allowed path return self.error403() else: 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.settings["own"]: from Debug import DebugMedia DebugMedia.merge(file_path) if os.path.isfile(file_path): # File exits return self.actionFile(file_path) else: # File not exits, try to download site = SiteManager.site_manager.need(address, all_file=False) result = site.needFile(match.group("inner_path"), priority=5) # Wait until file downloads if result: return self.actionFile(file_path) else: self.log.debug("File not found: %s" % match.group("inner_path")) # Site larger than allowed, re-add wrapper nonce to allow reload if site.settings.get("size", 0) > site.getSizeLimit() * 1024 * 1024: self.server.wrapper_nonces.append(self.get.get("wrapper_nonce")) return self.error404(match.group("inner_path")) else: # Bad url return self.error404(path) # Serve a media for ui def actionUiMedia(self, path): match = re.match("/uimedia/(?P.*)", 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) else: # Bad url return self.error400() # Stream a file to client def actionFile(self, file_path, block_size=64 * 1024, send_header=True): if os.path.isfile(file_path): # Try to figure out content type by extension content_type = self.getContentType(file_path) # TODO: Dont allow external access: extra_headers= # [("Content-Security-Policy", "default-src 'unsafe-inline' data: http://localhost:43110 ws://localhost:43110")] range = self.env.get("HTTP_RANGE") range_start = None if send_header: extra_headers = {} file_size = os.path.getsize(file_path) extra_headers["Accept-Ranges"] = "bytes" 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, extra_headers=extra_headers.items()) if self.env["REQUEST_METHOD"] != "OPTIONS": file = open(file_path, "rb") if range_start: file.seek(range_start) while 1: try: block = file.read(block_size) if block: yield block else: raise StopIteration except StopIteration: file.close() break else: # File not exits yield self.error404(file_path) # On websocket connection def actionWebsocket(self): ws = self.env.get("wsgi.websocket") if ws: wrapper_key = self.get["wrapper_key"] # Find site by 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 user = self.getCurrentUser() 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 ui_websocket.start() 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"] 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) return self.formatError("Bad Request", message) # You are not allowed to access this def error403(self, message="", details=True): self.sendHeader(403) return self.formatError("Forbidden", message, details=details) # Send file not found error def error404(self, path=""): self.sendHeader(404) return self.formatError("Not Found", path.encode("utf8"), details=False) # Internal server error def error500(self, message=":("): self.sendHeader(500) return self.formatError("Server error", cgi.escape(message)) def formatError(self, title, message, details=True): import sys import gevent if details: 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 """

%s

%s

Please report it if you think this an error.

Details:

%s
""" % (title, message, json.dumps(details, indent=4, sort_keys=True)) else: return """

%s

%s

""" % (title, message) # - Reload for eaiser developing - # def reload(): # import imp, sys # global UiWebsocket # UiWebsocket = imp.load_source("UiWebsocket", "src/Ui/UiWebsocket.py").UiWebsocket # reload(sys.modules["User.UserManager"]) # UserManager.reloadModule() # self.user = UserManager.user_manager.getCurrent()