UiRequest.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. import time
  2. import re
  3. import os
  4. import mimetypes
  5. import json
  6. import cgi
  7. import gevent
  8. from Config import config
  9. from Site import SiteManager
  10. from User import UserManager
  11. from Plugin import PluginManager
  12. from Ui.UiWebsocket import UiWebsocket
  13. from Crypt import CryptHash
  14. from util import helper
  15. status_texts = {
  16. 200: "200 OK",
  17. 206: "206 Partial Content",
  18. 400: "400 Bad Request",
  19. 403: "403 Forbidden",
  20. 404: "404 Not Found",
  21. 500: "500 Internal Server Error",
  22. }
  23. class SecurityError(Exception):
  24. pass
  25. @PluginManager.acceptPlugins
  26. class UiRequest(object):
  27. def __init__(self, server, get, env, start_response):
  28. if server:
  29. self.server = server
  30. self.log = server.log
  31. self.get = get # Get parameters
  32. self.env = env # Enviroment settings
  33. # ['CONTENT_LENGTH', 'CONTENT_TYPE', 'GATEWAY_INTERFACE', 'HTTP_ACCEPT', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE',
  34. # 'HTTP_COOKIE', 'HTTP_CACHE_CONTROL', 'HTTP_HOST', 'HTTP_HTTPS', 'HTTP_ORIGIN', 'HTTP_PROXY_CONNECTION', 'HTTP_REFERER',
  35. # 'HTTP_USER_AGENT', 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REMOTE_PORT', 'REQUEST_METHOD', 'SCRIPT_NAME',
  36. # 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SOFTWARE', 'werkzeug.request', 'wsgi.errors',
  37. # 'wsgi.input', 'wsgi.multiprocess', 'wsgi.multithread', 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version']
  38. self.start_response = start_response # Start response function
  39. self.user = None
  40. self.script_nonce = None # Nonce for script tags in wrapper html
  41. def learnHost(self, host):
  42. self.server.allowed_hosts.add(host)
  43. self.server.log.info("Added %s as allowed host" % host)
  44. def isHostAllowed(self, host):
  45. if host in self.server.allowed_hosts:
  46. return True
  47. # Allow any IP address as they are not affected by DNS rebinding
  48. # attacks
  49. if helper.isIp(host):
  50. self.learnHost(host)
  51. return True
  52. if ":" in host and helper.isIp(host.rsplit(":", 1)[0]): # Test without port
  53. self.learnHost(host)
  54. return True
  55. if self.isProxyRequest(): # Support for chrome extension proxy
  56. if self.server.site_manager.isDomain(host):
  57. return True
  58. else:
  59. return False
  60. return False
  61. # Call the request handler function base on path
  62. def route(self, path):
  63. # Restict Ui access by ip
  64. if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict:
  65. return self.error403(details=False)
  66. # Check if host allowed to do request
  67. if not self.isHostAllowed(self.env.get("HTTP_HOST")):
  68. return self.error403("Invalid host: %s" % self.env.get("HTTP_HOST"), details=False)
  69. # Prepend .bit host for transparent proxy
  70. if self.server.site_manager.isDomain(self.env.get("HTTP_HOST")):
  71. path = re.sub("^/", "/" + self.env.get("HTTP_HOST") + "/", path)
  72. path = re.sub("^http://zero[/]+", "/", path) # Remove begining http://zero/ for chrome extension
  73. path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access
  74. # Sanitize request url
  75. path = path.replace("\\", "/")
  76. if "../" in path or "./" in path:
  77. return self.error403("Invalid path: %s" % path)
  78. if self.env["REQUEST_METHOD"] == "OPTIONS":
  79. if "/" not in path.strip("/"):
  80. content_type = self.getContentType("index.html")
  81. else:
  82. content_type = self.getContentType(path)
  83. extra_headers = {"Access-Control-Allow-Origin": "null"}
  84. self.sendHeader(content_type=content_type, extra_headers=extra_headers, noscript=True)
  85. return ""
  86. if path == "/":
  87. return self.actionIndex()
  88. elif path == "/favicon.ico":
  89. return self.actionFile("src/Ui/media/img/favicon.ico")
  90. # Internal functions
  91. elif "/ZeroNet-Internal/" in path:
  92. path = re.sub(".*?/ZeroNet-Internal/", "/", path)
  93. func = getattr(self, "action" + path.strip("/"), None) # Check if we have action+request_path function
  94. if func:
  95. return func()
  96. else:
  97. return self.error404(path)
  98. # Media
  99. elif path.startswith("/uimedia/"):
  100. return self.actionUiMedia(path)
  101. elif "/uimedia/" in path:
  102. # uimedia within site dir (for chrome extension)
  103. path = re.sub(".*?/uimedia/", "/uimedia/", path)
  104. return self.actionUiMedia(path)
  105. # Websocket
  106. elif path == "/Websocket":
  107. return self.actionWebsocket()
  108. # Debug
  109. elif path == "/Debug" and config.debug:
  110. return self.actionDebug()
  111. elif path == "/Console" and config.debug:
  112. return self.actionConsole()
  113. # Wrapper-less static files
  114. elif path.startswith("/raw/"):
  115. return self.actionSiteMedia(path.replace("/raw", "/media", 1), header_noscript=True)
  116. elif path.startswith("/add/"):
  117. return self.actionSiteAdd()
  118. # Site media wrapper
  119. else:
  120. if self.get.get("wrapper_nonce"):
  121. if self.get["wrapper_nonce"] in self.server.wrapper_nonces:
  122. self.server.wrapper_nonces.remove(self.get["wrapper_nonce"])
  123. return self.actionSiteMedia("/media" + path) # Only serve html files with frame
  124. else:
  125. self.server.log.warning("Invalid wrapper nonce: %s" % self.get["wrapper_nonce"])
  126. body = self.actionWrapper(path)
  127. else:
  128. body = self.actionWrapper(path)
  129. if body:
  130. return body
  131. else:
  132. func = getattr(self, "action" + path.strip("/"), None) # Check if we have action+request_path function
  133. if func:
  134. return func()
  135. else:
  136. return self.error404(path)
  137. # The request is proxied by chrome extension or a transparent proxy
  138. def isProxyRequest(self):
  139. return self.env["PATH_INFO"].startswith("http://") or (self.server.allow_trans_proxy and self.server.site_manager.isDomain(self.env.get("HTTP_HOST")))
  140. def isWebSocketRequest(self):
  141. return self.env.get("HTTP_UPGRADE") == "websocket"
  142. def isAjaxRequest(self):
  143. return self.env.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
  144. # Get mime by filename
  145. def getContentType(self, file_name):
  146. content_type = mimetypes.guess_type(file_name)[0]
  147. if content_type:
  148. content_type = content_type.lower()
  149. if file_name.endswith(".css"): # Force correct css content type
  150. content_type = "text/css"
  151. if not content_type:
  152. if file_name.endswith(".json"): # Correct json header
  153. content_type = "application/json"
  154. else:
  155. content_type = "application/octet-stream"
  156. return content_type
  157. # Return: <dict> Posted variables
  158. def getPosted(self):
  159. if self.env['REQUEST_METHOD'] == "POST":
  160. return dict(cgi.parse_qsl(
  161. self.env['wsgi.input'].readline().decode()
  162. ))
  163. else:
  164. return {}
  165. # Return: <dict> Cookies based on self.env
  166. def getCookies(self):
  167. raw_cookies = self.env.get('HTTP_COOKIE')
  168. if raw_cookies:
  169. cookies = cgi.parse_qsl(raw_cookies)
  170. return {key.strip(): val for key, val in cookies}
  171. else:
  172. return {}
  173. def getCurrentUser(self):
  174. if self.user:
  175. return self.user # Cache
  176. self.user = UserManager.user_manager.get() # Get user
  177. if not self.user:
  178. self.user = UserManager.user_manager.create()
  179. return self.user
  180. def getRequestUrl(self):
  181. if self.isProxyRequest():
  182. if self.env["PATH_INFO"].startswith("http://zero/"):
  183. return self.env["PATH_INFO"]
  184. else: # Add http://zero to direct domain access
  185. return self.env["PATH_INFO"].replace("http://", "http://zero/", 1)
  186. else:
  187. return self.env["wsgi.url_scheme"] + "://" + self.env["HTTP_HOST"] + self.env["PATH_INFO"]
  188. def getReferer(self):
  189. referer = self.env.get("HTTP_REFERER")
  190. if referer and self.isProxyRequest() and not referer.startswith("http://zero/"):
  191. return referer.replace("http://", "http://zero/", 1)
  192. else:
  193. return referer
  194. def isScriptNonceSupported(self):
  195. user_agent = self.env.get("HTTP_USER_AGENT")
  196. if "Edge/" in user_agent:
  197. is_script_nonce_supported = False
  198. elif "Safari/" in user_agent and "Chrome/" not in user_agent:
  199. is_script_nonce_supported = False
  200. else:
  201. is_script_nonce_supported = True
  202. return is_script_nonce_supported
  203. # Send response headers
  204. def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, script_nonce=None, extra_headers=[]):
  205. headers = {}
  206. headers["Version"] = "HTTP/1.1"
  207. headers["Connection"] = "Keep-Alive"
  208. headers["Keep-Alive"] = "max=25, timeout=30"
  209. headers["X-Frame-Options"] = "SAMEORIGIN"
  210. is_referer_allowed = False
  211. if self.env.get("HTTP_REFERER"):
  212. if self.isSameOrigin(self.getReferer(), self.getRequestUrl()):
  213. is_referer_allowed = True
  214. elif self.getReferer() == "%s://%s/" % (self.env["wsgi.url_scheme"], self.env["HTTP_HOST"]): # Origin-only referer
  215. is_referer_allowed = True
  216. if content_type != "text/html" and is_referer_allowed:
  217. headers["Access-Control-Allow-Origin"] = "*" # Allow load font files from css
  218. if noscript:
  219. 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';"
  220. elif script_nonce and self.isScriptNonceSupported():
  221. 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)
  222. if allow_ajax:
  223. headers["Access-Control-Allow-Origin"] = "null"
  224. if self.env["REQUEST_METHOD"] == "OPTIONS":
  225. # Allow json access
  226. headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range"
  227. headers["Access-Control-Allow-Credentials"] = "true"
  228. if content_type == "text/html":
  229. content_type = "text/html; charset=utf-8"
  230. if content_type == "text/plain":
  231. content_type = "text/plain; charset=utf-8"
  232. # Download instead of display file types that can be dangerous
  233. if re.findall("/svg|/xml|/x-shockwave-flash|/pdf", content_type):
  234. headers["Content-Disposition"] = "attachment"
  235. cacheable_type = (
  236. content_type == "text/css" or content_type.startswith("image") or content_type.startswith("video") or
  237. self.env["REQUEST_METHOD"] == "OPTIONS" or content_type == "application/javascript"
  238. )
  239. if status in (200, 206) and cacheable_type: # Cache Css, Js, Image files for 10min
  240. headers["Cache-Control"] = "public, max-age=600" # Cache 10 min
  241. else:
  242. headers["Cache-Control"] = "no-cache, no-store, private, must-revalidate, max-age=0" # No caching at all
  243. headers["Content-Type"] = content_type
  244. headers.update(extra_headers)
  245. return self.start_response(status_texts[status], headers.items())
  246. # Renders a template
  247. def render(self, template_path, *args, **kwargs):
  248. template = open(template_path).read()
  249. def renderReplacer(m):
  250. return "%s" % kwargs.get(m.group(1), "")
  251. template_rendered = re.sub("{(.*?)}", renderReplacer, template)
  252. return template_rendered.encode("utf8")
  253. # - Actions -
  254. # Redirect to an url
  255. def actionRedirect(self, url):
  256. self.start_response('301 Redirect', [('Location', str(url))])
  257. yield "Location changed: %s" % url
  258. def actionIndex(self):
  259. return self.actionRedirect("/" + config.homepage)
  260. # Render a file from media with iframe site wrapper
  261. def actionWrapper(self, path, extra_headers=None):
  262. if not extra_headers:
  263. extra_headers = {}
  264. script_nonce = self.getScriptNonce()
  265. match = re.match("/(?P<address>[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
  266. just_added = False
  267. if match:
  268. address = match.group("address")
  269. inner_path = match.group("inner_path").lstrip("/")
  270. if not inner_path or path.endswith("/"): # It's a directory
  271. content_type = self.getContentType("index.html")
  272. else: # It's a file
  273. content_type = self.getContentType(inner_path)
  274. is_html_file = "html" in content_type or "xhtml" in content_type
  275. if not is_html_file:
  276. return self.actionSiteMedia("/media" + path) # Serve non-html files without wrapper
  277. if self.isAjaxRequest():
  278. return self.error403("Ajax request not allowed to load wrapper") # No ajax allowed on wrapper
  279. if self.isWebSocketRequest():
  280. return self.error403("WebSocket request not allowed to load wrapper") # No websocket
  281. if "text/html" not in self.env.get("HTTP_ACCEPT", ""):
  282. return self.error403("Invalid Accept header to load wrapper")
  283. if "prefetch" in self.env.get("HTTP_X_MOZ", "") or "prefetch" in self.env.get("HTTP_PURPOSE", ""):
  284. return self.error403("Prefetch not allowed to load wrapper")
  285. site = SiteManager.site_manager.get(address)
  286. if (
  287. site and site.content_manager.contents.get("content.json") and
  288. (not site.getReachableBadFiles() or site.settings["own"])
  289. ): # Its downloaded or own
  290. title = site.content_manager.contents["content.json"]["title"]
  291. else:
  292. title = "Loading %s..." % address
  293. site = SiteManager.site_manager.get(address)
  294. if site: # Already added, but not downloaded
  295. if time.time() - site.announcer.time_last_announce > 5:
  296. site.log.debug("Reannouncing site...")
  297. gevent.spawn(site.update, announce=True)
  298. else: # If not added yet
  299. site = SiteManager.site_manager.need(address)
  300. just_added = True
  301. if not site:
  302. return False
  303. self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
  304. min_last_announce = (time.time() - site.announcer.time_last_announce) / 60
  305. if min_last_announce > 60 and site.settings["serving"] and not just_added:
  306. site.log.debug("Site requested, but not announced recently (last %.0fmin ago). Updating..." % min_last_announce)
  307. gevent.spawn(site.update, announce=True)
  308. return iter([self.renderWrapper(site, path, inner_path, title, extra_headers, script_nonce=script_nonce)])
  309. # Make response be sent at once (see https://github.com/HelloZeroNet/ZeroNet/issues/1092)
  310. else: # Bad url
  311. return False
  312. def getSiteUrl(self, address):
  313. if self.isProxyRequest():
  314. return "http://zero/" + address
  315. else:
  316. return "/" + address
  317. def processQueryString(self, site, query_string):
  318. match = re.search("zeronet_peers=(.*?)(&|$)", query_string)
  319. if match:
  320. query_string = query_string.replace(match.group(0), "")
  321. num_added = 0
  322. for peer in match.group(1).split(","):
  323. if not re.match(".*?:[0-9]+$", peer):
  324. continue
  325. ip, port = peer.rsplit(":", 1)
  326. if site.addPeer(ip, int(port), source="query_string"):
  327. num_added += 1
  328. site.log.debug("%s peers added by query string" % num_added)
  329. return query_string
  330. def renderWrapper(self, site, path, inner_path, title, extra_headers, show_loadingscreen=None, script_nonce=None):
  331. file_inner_path = inner_path
  332. if not file_inner_path:
  333. file_inner_path = "index.html" # If inner path defaults to index.html
  334. if file_inner_path.endswith("/"):
  335. file_inner_path = file_inner_path + "index.html"
  336. address = re.sub("/.*", "", path.lstrip("/"))
  337. if self.isProxyRequest() and (not path or "/" in path[1:]):
  338. if self.env["HTTP_HOST"] == "zero":
  339. root_url = "/" + address + "/"
  340. file_url = "/" + address + "/" + inner_path
  341. else:
  342. file_url = "/" + inner_path
  343. root_url = "/"
  344. else:
  345. file_url = "/" + address + "/" + inner_path
  346. root_url = "/" + address + "/"
  347. if self.isProxyRequest():
  348. self.server.allowed_ws_origins.add(self.env["HTTP_HOST"])
  349. # Wrapper variable inits
  350. body_style = ""
  351. meta_tags = ""
  352. postmessage_nonce_security = "false"
  353. wrapper_nonce = self.getWrapperNonce()
  354. inner_query_string = self.processQueryString(site, self.env.get("QUERY_STRING", ""))
  355. if inner_query_string:
  356. inner_query_string = "?%s&wrapper_nonce=%s" % (inner_query_string, wrapper_nonce)
  357. elif "?" in inner_path:
  358. inner_query_string = "&wrapper_nonce=%s" % wrapper_nonce
  359. else:
  360. inner_query_string = "?wrapper_nonce=%s" % wrapper_nonce
  361. if self.isProxyRequest(): # Its a remote proxy request
  362. if self.env["REMOTE_ADDR"] == "127.0.0.1": # Local client, the server address also should be 127.0.0.1
  363. server_url = "http://127.0.0.1:%s" % self.env["SERVER_PORT"]
  364. else: # Remote client, use SERVER_NAME as server's real address
  365. server_url = "http://%s:%s" % (self.env["SERVER_NAME"], self.env["SERVER_PORT"])
  366. homepage = "http://zero/" + config.homepage
  367. else: # Use relative path
  368. server_url = ""
  369. homepage = "/" + config.homepage
  370. user = self.getCurrentUser()
  371. if user:
  372. theme = user.settings.get("theme", "light")
  373. else:
  374. theme = "light"
  375. themeclass = "theme-%-6s" % re.sub("[^a-z]", "", theme)
  376. if site.content_manager.contents.get("content.json"): # Got content.json
  377. content = site.content_manager.contents["content.json"]
  378. if content.get("background-color"):
  379. background_color = content.get("background-color-%s" % theme, content["background-color"])
  380. body_style += "background-color: %s;" % cgi.escape(background_color, True)
  381. if content.get("viewport"):
  382. meta_tags += '<meta name="viewport" id="viewport" content="%s">' % cgi.escape(content["viewport"], True)
  383. if content.get("favicon"):
  384. meta_tags += '<link rel="icon" href="%s%s">' % (root_url, cgi.escape(content["favicon"], True))
  385. if content.get("postmessage_nonce_security"):
  386. postmessage_nonce_security = "true"
  387. sandbox_permissions = ""
  388. if "NOSANDBOX" in site.settings["permissions"]:
  389. sandbox_permissions += " allow-same-origin"
  390. if show_loadingscreen is None:
  391. show_loadingscreen = not site.storage.isFile(file_inner_path)
  392. return self.render(
  393. "src/Ui/template/wrapper.html",
  394. server_url=server_url,
  395. inner_path=inner_path,
  396. file_url=re.escape(file_url),
  397. file_inner_path=re.escape(file_inner_path),
  398. address=site.address,
  399. title=cgi.escape(title, True),
  400. body_style=body_style,
  401. meta_tags=meta_tags,
  402. query_string=re.escape(inner_query_string),
  403. wrapper_key=site.settings["wrapper_key"],
  404. ajax_key=site.settings["ajax_key"],
  405. wrapper_nonce=wrapper_nonce,
  406. postmessage_nonce_security=postmessage_nonce_security,
  407. permissions=json.dumps(site.settings["permissions"]),
  408. show_loadingscreen=json.dumps(show_loadingscreen),
  409. sandbox_permissions=sandbox_permissions,
  410. rev=config.rev,
  411. lang=config.language,
  412. homepage=homepage,
  413. themeclass=themeclass,
  414. script_nonce=script_nonce
  415. )
  416. # Create a new wrapper nonce that allows to get one html file without the wrapper
  417. def getWrapperNonce(self):
  418. wrapper_nonce = CryptHash.random()
  419. self.server.wrapper_nonces.append(wrapper_nonce)
  420. return wrapper_nonce
  421. def getScriptNonce(self):
  422. if not self.script_nonce:
  423. self.script_nonce = CryptHash.random(encoding="base64")
  424. return self.script_nonce
  425. # Create a new wrapper nonce that allows to get one site
  426. def getAddNonce(self):
  427. add_nonce = CryptHash.random()
  428. self.server.add_nonces.append(add_nonce)
  429. return add_nonce
  430. def isSameOrigin(self, url_a, url_b):
  431. if not url_a or not url_b:
  432. return False
  433. origin_a = re.sub("http[s]{0,1}://(.*?/.*?/).*", "\\1", url_a)
  434. origin_b = re.sub("http[s]{0,1}://(.*?/.*?/).*", "\\1", url_b)
  435. return origin_a == origin_b
  436. # Return {address: 1Site.., inner_path: /data/users.json} from url path
  437. def parsePath(self, path):
  438. path = path.replace("\\", "/")
  439. path = path.replace("/index.html/", "/") # Base Backward compatibility fix
  440. if path.endswith("/"):
  441. path = path + "index.html"
  442. if "../" in path or "./" in path:
  443. raise SecurityError("Invalid path")
  444. match = re.match("/media/(?P<address>[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P<inner_path>/.*|$)", path)
  445. if match:
  446. path_parts = match.groupdict()
  447. path_parts["request_address"] = path_parts["address"] # Original request address (for Merger sites)
  448. path_parts["inner_path"] = path_parts["inner_path"].lstrip("/")
  449. if not path_parts["inner_path"]:
  450. path_parts["inner_path"] = "index.html"
  451. return path_parts
  452. else:
  453. return None
  454. # Serve a media for site
  455. def actionSiteMedia(self, path, header_length=True, header_noscript=False):
  456. try:
  457. path_parts = self.parsePath(path)
  458. except SecurityError as err:
  459. return self.error403(err)
  460. if not path_parts:
  461. return self.error404(path)
  462. address = path_parts["address"]
  463. file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"])
  464. if config.debug and file_path.split("/")[-1].startswith("all."):
  465. # If debugging merge *.css to all.css and *.js to all.js
  466. site = self.server.sites.get(address)
  467. if site and site.settings["own"]:
  468. from Debug import DebugMedia
  469. DebugMedia.merge(file_path)
  470. if not address or address == ".":
  471. return self.error403(path_parts["inner_path"])
  472. header_allow_ajax = False
  473. if self.get.get("ajax_key"):
  474. site = SiteManager.site_manager.get(path_parts["request_address"])
  475. if self.get["ajax_key"] == site.settings["ajax_key"]:
  476. header_allow_ajax = True
  477. else:
  478. return self.error403("Invalid ajax_key")
  479. file_size = helper.getFilesize(file_path)
  480. if file_size is not None:
  481. 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)
  482. elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect
  483. if path_parts["inner_path"]:
  484. return self.actionRedirect("./%s/" % path_parts["inner_path"].split("/")[-1])
  485. else:
  486. return self.actionRedirect("./%s/" % path_parts["address"])
  487. else: # File not exists, try to download
  488. if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading
  489. return self.actionSiteAddPrompt(path)
  490. site = SiteManager.site_manager.need(address)
  491. if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites
  492. return self.actionFile("src/Ui/media/img/favicon.ico")
  493. result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads
  494. if result:
  495. file_size = helper.getFilesize(file_path)
  496. 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)
  497. else:
  498. self.log.debug("File not found: %s" % path_parts["inner_path"])
  499. return self.error404(path_parts["inner_path"])
  500. # Serve a media for ui
  501. def actionUiMedia(self, path):
  502. match = re.match("/uimedia/(?P<inner_path>.*)", path)
  503. if match: # Looks like a valid path
  504. file_path = "src/Ui/media/%s" % match.group("inner_path")
  505. allowed_dir = os.path.abspath("src/Ui/media") # Only files within data/sitehash allowed
  506. if ".." in file_path or not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir):
  507. # File not in allowed path
  508. return self.error403()
  509. else:
  510. if config.debug and match.group("inner_path").startswith("all."):
  511. # If debugging merge *.css to all.css and *.js to all.js
  512. from Debug import DebugMedia
  513. DebugMedia.merge(file_path)
  514. return self.actionFile(file_path, header_length=False) # Dont's send site to allow plugins append content
  515. else: # Bad url
  516. return self.error400()
  517. def actionSiteAdd(self):
  518. post = dict(cgi.parse_qsl(self.env["wsgi.input"].read()))
  519. if post["add_nonce"] not in self.server.add_nonces:
  520. return self.error403("Add nonce error.")
  521. self.server.add_nonces.remove(post["add_nonce"])
  522. SiteManager.site_manager.need(post["address"])
  523. return self.actionRedirect(post["url"])
  524. def actionSiteAddPrompt(self, path):
  525. path_parts = self.parsePath(path)
  526. if not path_parts or not self.server.site_manager.isAddress(path_parts["address"]):
  527. return self.error404(path)
  528. self.sendHeader(200, "text/html", noscript=True)
  529. template = open("src/Ui/template/site_add.html").read()
  530. template = template.replace("{url}", cgi.escape(self.env["PATH_INFO"], True))
  531. template = template.replace("{address}", path_parts["address"])
  532. template = template.replace("{add_nonce}", self.getAddNonce())
  533. return template
  534. def replaceHtmlVariables(self, block, path_parts):
  535. user = self.getCurrentUser()
  536. themeclass = "theme-%-6s" % re.sub("[^a-z]", "", user.settings.get("theme", "light"))
  537. block = block.replace("{themeclass}", themeclass.encode("utf8"))
  538. if path_parts:
  539. site = self.server.sites.get(path_parts.get("address"))
  540. if site.settings["own"]:
  541. modified = int(time.time())
  542. else:
  543. modified = int(site.content_manager.contents["content.json"]["modified"])
  544. block = block.replace("{site_modified}", str(modified))
  545. return block
  546. # Stream a file to client
  547. 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):
  548. if file_size is None:
  549. file_size = helper.getFilesize(file_path)
  550. if file_size is not None:
  551. # Try to figure out content type by extension
  552. content_type = self.getContentType(file_path)
  553. range = self.env.get("HTTP_RANGE")
  554. range_start = None
  555. is_html_file = file_path.endswith(".html")
  556. if is_html_file:
  557. header_length = False
  558. if send_header:
  559. extra_headers = {}
  560. extra_headers["Accept-Ranges"] = "bytes"
  561. if header_length:
  562. extra_headers["Content-Length"] = str(file_size)
  563. if range:
  564. range_start = int(re.match(".*?([0-9]+)", range).group(1))
  565. if re.match(".*?-([0-9]+)", range):
  566. range_end = int(re.match(".*?-([0-9]+)", range).group(1)) + 1
  567. else:
  568. range_end = file_size
  569. extra_headers["Content-Length"] = str(range_end - range_start)
  570. extra_headers["Content-Range"] = "bytes %s-%s/%s" % (range_start, range_end - 1, file_size)
  571. if range:
  572. status = 206
  573. else:
  574. status = 200
  575. self.sendHeader(status, content_type=content_type, noscript=header_noscript, allow_ajax=header_allow_ajax, extra_headers=extra_headers)
  576. if self.env["REQUEST_METHOD"] != "OPTIONS":
  577. if not file_obj:
  578. file_obj = open(file_path, "rb")
  579. if range_start:
  580. file_obj.seek(range_start)
  581. while 1:
  582. try:
  583. block = file_obj.read(block_size)
  584. if is_html_file:
  585. block = self.replaceHtmlVariables(block, path_parts)
  586. if block:
  587. yield block
  588. else:
  589. raise StopIteration
  590. except StopIteration:
  591. file_obj.close()
  592. break
  593. else: # File not exists
  594. yield self.error404(file_path)
  595. # On websocket connection
  596. def actionWebsocket(self):
  597. ws = self.env.get("wsgi.websocket")
  598. if ws:
  599. # Allow only same-origin websocket requests
  600. origin = self.env.get("HTTP_ORIGIN")
  601. host = self.env.get("HTTP_HOST")
  602. # Allow only same-origin websocket requests
  603. if origin:
  604. origin_host = origin.split("://", 1)[-1]
  605. if origin_host != host and origin_host not in self.server.allowed_ws_origins:
  606. ws.send(json.dumps({"error": "Invalid origin: %s" % origin}))
  607. return self.error403("Invalid origin: %s" % origin)
  608. # Find site by wrapper_key
  609. wrapper_key = self.get["wrapper_key"]
  610. site = None
  611. for site_check in self.server.sites.values():
  612. if site_check.settings["wrapper_key"] == wrapper_key:
  613. site = site_check
  614. if site: # Correct wrapper key
  615. try:
  616. user = self.getCurrentUser()
  617. except Exception, err:
  618. self.log.error("Error in data/user.json: %s" % err)
  619. return self.error500()
  620. if not user:
  621. self.log.error("No user found")
  622. return self.error403()
  623. ui_websocket = UiWebsocket(ws, site, self.server, user, self)
  624. site.websockets.append(ui_websocket) # Add to site websockets to allow notify on events
  625. self.server.websockets.append(ui_websocket)
  626. ui_websocket.start()
  627. self.server.websockets.remove(ui_websocket)
  628. for site_check in self.server.sites.values():
  629. # Remove websocket from every site (admin sites allowed to join other sites event channels)
  630. if ui_websocket in site_check.websockets:
  631. site_check.websockets.remove(ui_websocket)
  632. return "Bye."
  633. else: # No site found by wrapper key
  634. self.log.error("Wrapper key not found: %s" % wrapper_key)
  635. return self.error403()
  636. else:
  637. self.start_response("400 Bad Request", [])
  638. return "Not a websocket!"
  639. # Debug last error
  640. def actionDebug(self):
  641. # Raise last error from DebugHook
  642. import sys
  643. last_error = sys.modules["main"].DebugHook.last_error
  644. if last_error:
  645. raise last_error[0], last_error[1], last_error[2]
  646. else:
  647. self.sendHeader()
  648. return "No error! :)"
  649. # Just raise an error to get console
  650. def actionConsole(self):
  651. import sys
  652. sites = self.server.sites
  653. main = sys.modules["main"]
  654. def bench(code, times=100, init=None):
  655. sites = self.server.sites
  656. main = sys.modules["main"]
  657. s = time.time()
  658. if init:
  659. eval(compile(init, '<string>', 'exec'), globals(), locals())
  660. for _ in range(times):
  661. back = eval(code, globals(), locals())
  662. return ["%s run: %.3fs" % (times, time.time() - s), back]
  663. raise Exception("Here is your console")
  664. # - Tests -
  665. def actionTestStream(self):
  666. self.sendHeader()
  667. yield " " * 1080 # Overflow browser's buffer
  668. yield "He"
  669. time.sleep(1)
  670. yield "llo!"
  671. # yield "Running websockets: %s" % len(self.server.websockets)
  672. # self.server.sendMessage("Hello!")
  673. # - Errors -
  674. # Send bad request error
  675. def error400(self, message=""):
  676. self.sendHeader(400, noscript=True)
  677. return self.formatError("Bad Request", message)
  678. # You are not allowed to access this
  679. def error403(self, message="", details=True):
  680. self.sendHeader(403, noscript=True)
  681. self.log.error("Error 403: %s" % message)
  682. return self.formatError("Forbidden", message, details=details)
  683. # Send file not found error
  684. def error404(self, path=""):
  685. self.sendHeader(404, noscript=True)
  686. return self.formatError("Not Found", path.encode("utf8"), details=False)
  687. # Internal server error
  688. def error500(self, message=":("):
  689. self.sendHeader(500, noscript=True)
  690. return self.formatError("Server error", message)
  691. def formatError(self, title, message, details=True):
  692. import sys
  693. import gevent
  694. if details and config.debug:
  695. details = {key: val for key, val in self.env.items() if hasattr(val, "endswith") and "COOKIE" not in key}
  696. details["version_zeronet"] = "%s r%s" % (config.version, config.rev)
  697. details["version_python"] = sys.version
  698. details["version_gevent"] = gevent.__version__
  699. details["plugins"] = PluginManager.plugin_manager.plugin_names
  700. arguments = {key: val for key, val in vars(config.arguments).items() if "password" not in key}
  701. details["arguments"] = arguments
  702. return """
  703. <style>
  704. * { font-family: Consolas, Monospace; color: #333 }
  705. pre { padding: 10px; background-color: #EEE }
  706. </style>
  707. <h1>%s</h1>
  708. <h2>%s</h3>
  709. <h3>Please <a href="https://github.com/HelloZeroNet/ZeroNet/issues" target="_top">report it</a> if you think this an error.</h3>
  710. <h4>Details:</h4>
  711. <pre>%s</pre>
  712. """ % (title, cgi.escape(message), cgi.escape(json.dumps(details, indent=4, sort_keys=True)))
  713. else:
  714. return """
  715. <h1>%s</h1>
  716. <h2>%s</h3>
  717. """ % (title, cgi.escape(message))