UiRequest.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import time, re, os, mimetypes, json
  2. from Config import config
  3. from Site import SiteManager
  4. from Ui.UiWebsocket import UiWebsocket
  5. status_texts = {
  6. 200: "200 OK",
  7. 400: "400 Bad Request",
  8. 403: "403 Forbidden",
  9. 404: "404 Not Found",
  10. }
  11. class UiRequest:
  12. def __init__(self, server = None):
  13. if server:
  14. self.server = server
  15. self.log = server.log
  16. self.get = {} # Get parameters
  17. self.env = {} # Enviroment settings
  18. self.start_response = None # Start response function
  19. # Call the request handler function base on path
  20. def route(self, path):
  21. if config.ui_restrict and self.env['REMOTE_ADDR'] != config.ui_restrict: # Restict Ui access by ip
  22. return self.error403()
  23. if path == "/":
  24. return self.actionIndex()
  25. elif path == "/favicon.ico":
  26. return self.actionFile("src/Ui/media/img/favicon.ico")
  27. # Media
  28. elif path.startswith("/uimedia/"):
  29. return self.actionUiMedia(path)
  30. elif path.startswith("/media"):
  31. return self.actionSiteMedia(path)
  32. # Websocket
  33. elif path == "/Websocket":
  34. return self.actionWebsocket()
  35. # Debug
  36. elif path == "/Debug" and config.debug:
  37. return self.actionDebug()
  38. elif path == "/Console" and config.debug:
  39. return self.actionConsole()
  40. # Test
  41. elif path == "/Test/Websocket":
  42. return self.actionFile("Data/temp/ws_test.html")
  43. elif path == "/Test/Stream":
  44. return self.actionTestStream()
  45. # Site media wrapper
  46. else:
  47. return self.actionWrapper(path)
  48. # Get mime by filename
  49. def getContentType(self, file_name):
  50. content_type = mimetypes.guess_type(file_name)[0]
  51. if not content_type:
  52. if file_name.endswith("json"): # Correct json header
  53. content_type = "application/json"
  54. else:
  55. content_type = "application/octet-stream"
  56. return content_type
  57. # Send response headers
  58. def sendHeader(self, status=200, content_type="text/html; charset=utf-8", extra_headers=[]):
  59. headers = []
  60. headers.append(("Version", "HTTP/1.1"))
  61. headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access
  62. headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")) # Allow json access
  63. headers.append(("Cache-Control", "no-cache, no-store, private, must-revalidate, max-age=0")) # No caching at all
  64. #headers.append(("Cache-Control", "public, max-age=604800")) # Cache 1 week
  65. headers.append(("Content-Type", content_type))
  66. for extra_header in extra_headers:
  67. headers.append(extra_header)
  68. self.start_response(status_texts[status], headers)
  69. # Renders a template
  70. def render(self, template_path, *args, **kwargs):
  71. #template = SimpleTemplate(open(template_path), lookup=[os.path.dirname(template_path)])
  72. #yield str(template.render(*args, **kwargs).encode("utf8"))
  73. template = open(template_path).read()
  74. yield template.format(**kwargs)
  75. # - Actions -
  76. # Redirect to an url
  77. def actionRedirect(self, url):
  78. self.start_response('301 Redirect', [('Location', url)])
  79. yield "Location changed: %s" % url
  80. def actionIndex(self):
  81. return self.actionRedirect("/"+config.homepage)
  82. # Render a file from media with iframe site wrapper
  83. def actionWrapper(self, path):
  84. if self.env.get("HTTP_X_REQUESTED_WITH"): return self.error403() # No ajax allowed on wrapper
  85. match = re.match("/(?P<site>[A-Za-z0-9]+)(?P<inner_path>/.*|$)", path)
  86. if match:
  87. inner_path = match.group("inner_path").lstrip("/")
  88. if not inner_path: inner_path = "index.html" # If inner path defaults to index.html
  89. site = self.server.sites.get(match.group("site"))
  90. if site and site.content and not site.bad_files: # Its downloaded
  91. title = site.content["title"]
  92. else:
  93. title = "Loading %s..." % match.group("site")
  94. site = SiteManager.need(match.group("site")) # Start download site
  95. if not site: self.error404()
  96. self.sendHeader(extra_headers=[("X-Frame-Options", "DENY")])
  97. return self.render("src/Ui/template/wrapper.html",
  98. inner_path=inner_path,
  99. address=match.group("site"),
  100. title=title,
  101. auth_key=site.settings["auth_key"],
  102. permissions=json.dumps(site.settings["permissions"]),
  103. show_loadingscreen=json.dumps(not os.path.isfile(site.getPath(inner_path))),
  104. homepage=config.homepage
  105. )
  106. else: # Bad url
  107. return self.error404(path)
  108. # Serve a media for site
  109. def actionSiteMedia(self, path):
  110. match = re.match("/media/(?P<site>[A-Za-z0-9]+)/(?P<inner_path>.*)", path)
  111. referer = self.env.get("HTTP_REFERER")
  112. if referer: # Only allow same site to receive media
  113. referer = re.sub("http://.*?/", "/", referer) # Remove server address
  114. referer = referer.replace("/media", "") # Media
  115. if not referer.startswith("/"+match.group("site")): return self.error403() # Referer not starts same address as requested path
  116. if match: # Looks like a valid path
  117. file_path = "data/%s/%s" % (match.group("site"), match.group("inner_path"))
  118. allowed_dir = os.path.abspath("data/%s" % match.group("site")) # Only files within data/sitehash allowed
  119. if ".." in file_path or not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir): # File not in allowed path
  120. return self.error403()
  121. else:
  122. if config.debug and file_path.split("/")[-1].startswith("all."): # When debugging merge *.css to all.css and *.js to all.js
  123. site = self.server.sites.get(match.group("site"))
  124. if site.settings["own"]:
  125. from Debug import DebugMedia
  126. DebugMedia.merge(file_path)
  127. if os.path.isfile(file_path): # File exits
  128. return self.actionFile(file_path)
  129. else: # File not exits, try to download
  130. site = SiteManager.need(match.group("site"), all_file=False)
  131. self.sendHeader(content_type=self.getContentType(file_path)) # ?? Get Exception without this
  132. result = site.needFile(match.group("inner_path")) # Wait until file downloads
  133. return self.actionFile(file_path)
  134. else: # Bad url
  135. return self.error404(path)
  136. # Serve a media for ui
  137. def actionUiMedia(self, path):
  138. match = re.match("/uimedia/(?P<inner_path>.*)", path)
  139. if match: # Looks like a valid path
  140. file_path = "src/Ui/media/%s" % match.group("inner_path")
  141. allowed_dir = os.path.abspath("src/Ui/media") # Only files within data/sitehash allowed
  142. if ".." in file_path or not os.path.dirname(os.path.abspath(file_path)).startswith(allowed_dir): # File not in allowed path
  143. return self.error403()
  144. else:
  145. if config.debug and match.group("inner_path").startswith("all."): # When debugging merge *.css to all.css and *.js to all.js
  146. from Debug import DebugMedia
  147. DebugMedia.merge(file_path)
  148. return self.actionFile(file_path)
  149. else: # Bad url
  150. return self.error400()
  151. # Stream a file to client
  152. def actionFile(self, file_path, block_size = 64*1024):
  153. if os.path.isfile(file_path):
  154. # Try to figure out content type by extension
  155. content_type = self.getContentType(file_path)
  156. self.sendHeader(content_type = content_type) # TODO: Dont allow external access: extra_headers=[("Content-Security-Policy", "default-src 'unsafe-inline' data: http://localhost:43110 ws://localhost:43110")]
  157. if self.env["REQUEST_METHOD"] != "OPTIONS":
  158. file = open(file_path, "rb")
  159. while 1:
  160. try:
  161. block = file.read(block_size)
  162. if block:
  163. yield block
  164. else:
  165. raise StopIteration
  166. except StopIteration:
  167. file.close()
  168. break
  169. else: # File not exits
  170. yield self.error404(file_path)
  171. # On websocket connection
  172. def actionWebsocket(self):
  173. ws = self.env.get("wsgi.websocket")
  174. if ws:
  175. auth_key = self.get["auth_key"]
  176. # Find site by auth_key
  177. site = None
  178. for site_check in self.server.sites.values():
  179. if site_check.settings["auth_key"] == auth_key: site = site_check
  180. if site: # Correct auth key
  181. ui_websocket = UiWebsocket(ws, site, self.server)
  182. site.websockets.append(ui_websocket) # Add to site websockets to allow notify on events
  183. ui_websocket.start()
  184. for site_check in self.server.sites.values(): # Remove websocket from every site (admin sites allowed to join other sites event channels)
  185. if ui_websocket in site_check.websockets:
  186. site_check.websockets.remove(ui_websocket)
  187. return "Bye."
  188. else: # No site found by auth key
  189. self.log.error("Auth key not found: %s" % auth_key)
  190. return self.error403()
  191. else:
  192. start_response("400 Bad Request", [])
  193. return "Not a websocket!"
  194. # Debug last error
  195. def actionDebug(self):
  196. # Raise last error from DebugHook
  197. import sys
  198. last_error = sys.modules["src.main"].DebugHook.last_error
  199. if last_error:
  200. raise last_error[0], last_error[1], last_error[2]
  201. else:
  202. self.sendHeader()
  203. yield "No error! :)"
  204. # Just raise an error to get console
  205. def actionConsole(self):
  206. raise Exception("Here is your console")
  207. # - Tests -
  208. def actionTestStream(self):
  209. self.sendHeader()
  210. yield " "*1080 # Overflow browser's buffer
  211. yield "He"
  212. time.sleep(1)
  213. yield "llo!"
  214. yield "Running websockets: %s" % len(self.server.websockets)
  215. self.server.sendMessage("Hello!")
  216. # - Errors -
  217. # Send bad request error
  218. def error400(self):
  219. self.sendHeader(400)
  220. return "Bad Request"
  221. # You are not allowed to access this
  222. def error403(self):
  223. self.sendHeader(403)
  224. return "Forbidden"
  225. # Send file not found error
  226. def error404(self, path = None):
  227. self.sendHeader(404)
  228. return "Not Found: %s" % path
  229. # - Reload for eaiser developing -
  230. def reload(self):
  231. import imp
  232. global UiWebsocket
  233. UiWebsocket = imp.load_source("UiWebsocket", "src/Ui/UiWebsocket.py").UiWebsocket