server.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014, 2015 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from synapse.api.errors import (
  16. cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError
  17. )
  18. from synapse.util.logcontext import LoggingContext
  19. from syutil.jsonutil import (
  20. encode_canonical_json, encode_pretty_printed_json
  21. )
  22. from twisted.internet import defer, reactor
  23. from twisted.web import server, resource
  24. from twisted.web.server import NOT_DONE_YET
  25. from twisted.web.util import redirectTo
  26. import collections
  27. import logging
  28. import urllib
  29. logger = logging.getLogger(__name__)
  30. class HttpServer(object):
  31. """ Interface for registering callbacks on a HTTP server
  32. """
  33. def register_path(self, method, path_pattern, callback):
  34. """ Register a callback that get's fired if we receive a http request
  35. with the given method for a path that matches the given regex.
  36. If the regex contains groups these get's passed to the calback via
  37. an unpacked tuple.
  38. Args:
  39. method (str): The method to listen to.
  40. path_pattern (str): The regex used to match requests.
  41. callback (function): The function to fire if we receive a matched
  42. request. The first argument will be the request object and
  43. subsequent arguments will be any matched groups from the regex.
  44. This should return a tuple of (code, response).
  45. """
  46. pass
  47. class JsonResource(HttpServer, resource.Resource):
  48. """ This implements the HttpServer interface and provides JSON support for
  49. Resources.
  50. Register callbacks via register_path()
  51. """
  52. isLeaf = True
  53. _PathEntry = collections.namedtuple("_PathEntry", ["pattern", "callback"])
  54. def __init__(self, hs):
  55. resource.Resource.__init__(self)
  56. self.clock = hs.get_clock()
  57. self.path_regexs = {}
  58. self.version_string = hs.version_string
  59. def register_path(self, method, path_pattern, callback):
  60. self.path_regexs.setdefault(method, []).append(
  61. self._PathEntry(path_pattern, callback)
  62. )
  63. def start_listening(self, port):
  64. """ Registers the http server with the twisted reactor.
  65. Args:
  66. port (int): The port to listen on.
  67. """
  68. reactor.listenTCP(port, server.Site(self))
  69. # Gets called by twisted
  70. def render(self, request):
  71. """ This get's called by twisted every time someone sends us a request.
  72. """
  73. self._async_render_with_logging_context(request)
  74. return server.NOT_DONE_YET
  75. _request_id = 0
  76. @defer.inlineCallbacks
  77. def _async_render_with_logging_context(self, request):
  78. request_id = "%s-%s" % (request.method, JsonResource._request_id)
  79. JsonResource._request_id += 1
  80. with LoggingContext(request_id) as request_context:
  81. request_context.request = request_id
  82. yield self._async_render(request)
  83. @defer.inlineCallbacks
  84. def _async_render(self, request):
  85. """ This get's called by twisted every time someone sends us a request.
  86. This checks if anyone has registered a callback for that method and
  87. path.
  88. """
  89. code = None
  90. start = self.clock.time_msec()
  91. try:
  92. # Just say yes to OPTIONS.
  93. if request.method == "OPTIONS":
  94. self._send_response(request, 200, {})
  95. return
  96. # Loop through all the registered callbacks to check if the method
  97. # and path regex match
  98. for path_entry in self.path_regexs.get(request.method, []):
  99. m = path_entry.pattern.match(request.path)
  100. if not m:
  101. continue
  102. # We found a match! Trigger callback and then return the
  103. # returned response. We pass both the request and any
  104. # matched groups from the regex to the callback.
  105. args = [
  106. urllib.unquote(u).decode("UTF-8") for u in m.groups()
  107. ]
  108. logger.info(
  109. "Received request: %s %s",
  110. request.method, request.path
  111. )
  112. code, response = yield path_entry.callback(
  113. request,
  114. *args
  115. )
  116. self._send_response(request, code, response)
  117. return
  118. # Huh. No one wanted to handle that? Fiiiiiine. Send 400.
  119. raise UnrecognizedRequestError()
  120. except CodeMessageException as e:
  121. if isinstance(e, SynapseError):
  122. logger.info("%s SynapseError: %s - %s", request, e.code, e.msg)
  123. else:
  124. logger.exception(e)
  125. code = e.code
  126. self._send_response(
  127. request,
  128. code,
  129. cs_exception(e),
  130. response_code_message=e.response_code_message
  131. )
  132. except Exception as e:
  133. logger.exception(e)
  134. self._send_response(
  135. request,
  136. 500,
  137. {"error": "Internal server error"}
  138. )
  139. finally:
  140. code = str(code) if code else "-"
  141. end = self.clock.time_msec()
  142. logger.info(
  143. "Processed request: %dms %s %s %s",
  144. end-start, code, request.method, request.path
  145. )
  146. def _send_response(self, request, code, response_json_object,
  147. response_code_message=None):
  148. # could alternatively use request.notifyFinish() and flip a flag when
  149. # the Deferred fires, but since the flag is RIGHT THERE it seems like
  150. # a waste.
  151. if request._disconnected:
  152. logger.warn(
  153. "Not sending response to request %s, already disconnected.",
  154. request)
  155. return
  156. # TODO: Only enable CORS for the requests that need it.
  157. respond_with_json(
  158. request, code, response_json_object,
  159. send_cors=True,
  160. response_code_message=response_code_message,
  161. pretty_print=self._request_user_agent_is_curl,
  162. version_string=self.version_string,
  163. )
  164. @staticmethod
  165. def _request_user_agent_is_curl(request):
  166. user_agents = request.requestHeaders.getRawHeaders(
  167. "User-Agent", default=[]
  168. )
  169. for user_agent in user_agents:
  170. if "curl" in user_agent:
  171. return True
  172. return False
  173. class RootRedirect(resource.Resource):
  174. """Redirects the root '/' path to another path."""
  175. def __init__(self, path):
  176. resource.Resource.__init__(self)
  177. self.url = path
  178. def render_GET(self, request):
  179. return redirectTo(self.url, request)
  180. def getChild(self, name, request):
  181. if len(name) == 0:
  182. return self # select ourselves as the child to render
  183. return resource.Resource.getChild(self, name, request)
  184. def respond_with_json(request, code, json_object, send_cors=False,
  185. response_code_message=None, pretty_print=False,
  186. version_string=""):
  187. if not pretty_print:
  188. json_bytes = encode_pretty_printed_json(json_object)
  189. else:
  190. json_bytes = encode_canonical_json(json_object)
  191. return respond_with_json_bytes(
  192. request, code, json_bytes,
  193. send_cors=send_cors,
  194. response_code_message=response_code_message,
  195. version_string=version_string
  196. )
  197. def respond_with_json_bytes(request, code, json_bytes, send_cors=False,
  198. version_string="", response_code_message=None):
  199. """Sends encoded JSON in response to the given request.
  200. Args:
  201. request (twisted.web.http.Request): The http request to respond to.
  202. code (int): The HTTP response code.
  203. json_bytes (bytes): The json bytes to use as the response body.
  204. send_cors (bool): Whether to send Cross-Origin Resource Sharing headers
  205. http://www.w3.org/TR/cors/
  206. Returns:
  207. twisted.web.server.NOT_DONE_YET"""
  208. request.setResponseCode(code, message=response_code_message)
  209. request.setHeader(b"Content-Type", b"application/json")
  210. request.setHeader(b"Server", version_string)
  211. request.setHeader(b"Content-Length", b"%d" % (len(json_bytes),))
  212. if send_cors:
  213. request.setHeader("Access-Control-Allow-Origin", "*")
  214. request.setHeader("Access-Control-Allow-Methods",
  215. "GET, POST, PUT, DELETE, OPTIONS")
  216. request.setHeader("Access-Control-Allow-Headers",
  217. "Origin, X-Requested-With, Content-Type, Accept")
  218. request.write(json_bytes)
  219. request.finish()
  220. return NOT_DONE_YET