server.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 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 syutil.jsonutil import (
  16. encode_canonical_json, encode_pretty_printed_json
  17. )
  18. from synapse.api.errors import (
  19. cs_exception, SynapseError, CodeMessageException
  20. )
  21. from synapse.util.logcontext import LoggingContext
  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. logger = logging.getLogger(__name__)
  29. class HttpServer(object):
  30. """ Interface for registering callbacks on a HTTP server
  31. """
  32. def register_path(self, method, path_pattern, callback):
  33. """ Register a callback that get's fired if we receive a http request
  34. with the given method for a path that matches the given regex.
  35. If the regex contains groups these get's passed to the calback via
  36. an unpacked tuple.
  37. Args:
  38. method (str): The method to listen to.
  39. path_pattern (str): The regex used to match requests.
  40. callback (function): The function to fire if we receive a matched
  41. request. The first argument will be the request object and
  42. subsequent arguments will be any matched groups from the regex.
  43. This should return a tuple of (code, response).
  44. """
  45. pass
  46. class JsonResource(HttpServer, resource.Resource):
  47. """ This implements the HttpServer interface and provides JSON support for
  48. Resources.
  49. Register callbacks via register_path()
  50. """
  51. isLeaf = True
  52. _PathEntry = collections.namedtuple("_PathEntry", ["pattern", "callback"])
  53. def __init__(self):
  54. resource.Resource.__init__(self)
  55. self.path_regexs = {}
  56. def register_path(self, method, path_pattern, callback):
  57. self.path_regexs.setdefault(method, []).append(
  58. self._PathEntry(path_pattern, callback)
  59. )
  60. def start_listening(self, port):
  61. """ Registers the http server with the twisted reactor.
  62. Args:
  63. port (int): The port to listen on.
  64. """
  65. reactor.listenTCP(port, server.Site(self))
  66. # Gets called by twisted
  67. def render(self, request):
  68. """ This get's called by twisted every time someone sends us a request.
  69. """
  70. self._async_render_with_logging_context(request)
  71. return server.NOT_DONE_YET
  72. _request_id = 0
  73. @defer.inlineCallbacks
  74. def _async_render_with_logging_context(self, request):
  75. request_id = "%s-%s" % (request.method, JsonResource._request_id)
  76. JsonResource._request_id += 1
  77. with LoggingContext(request_id) as request_context:
  78. request_context.request = request_id
  79. yield self._async_render(request)
  80. @defer.inlineCallbacks
  81. def _async_render(self, request):
  82. """ This get's called by twisted every time someone sends us a request.
  83. This checks if anyone has registered a callback for that method and
  84. path.
  85. """
  86. try:
  87. # Just say yes to OPTIONS.
  88. if request.method == "OPTIONS":
  89. self._send_response(request, 200, {})
  90. return
  91. # Loop through all the registered callbacks to check if the method
  92. # and path regex match
  93. for path_entry in self.path_regexs.get(request.method, []):
  94. m = path_entry.pattern.match(request.path)
  95. if m:
  96. # We found a match! Trigger callback and then return the
  97. # returned response. We pass both the request and any
  98. # matched groups from the regex to the callback.
  99. code, response = yield path_entry.callback(
  100. request,
  101. *m.groups()
  102. )
  103. self._send_response(request, code, response)
  104. return
  105. # Huh. No one wanted to handle that? Fiiiiiine. Send 400.
  106. self._send_response(
  107. request,
  108. 400,
  109. {"error": "Unrecognized request"}
  110. )
  111. except CodeMessageException as e:
  112. if isinstance(e, SynapseError):
  113. logger.info("%s SynapseError: %s - %s", request, e.code, e.msg)
  114. else:
  115. logger.exception(e)
  116. self._send_response(
  117. request,
  118. e.code,
  119. cs_exception(e),
  120. response_code_message=e.response_code_message
  121. )
  122. except Exception as e:
  123. logger.exception(e)
  124. self._send_response(
  125. request,
  126. 500,
  127. {"error": "Internal server error"}
  128. )
  129. def _send_response(self, request, code, response_json_object,
  130. response_code_message=None):
  131. # could alternatively use request.notifyFinish() and flip a flag when
  132. # the Deferred fires, but since the flag is RIGHT THERE it seems like
  133. # a waste.
  134. if request._disconnected:
  135. logger.warn(
  136. "Not sending response to request %s, already disconnected.",
  137. request)
  138. return
  139. # TODO: Only enable CORS for the requests that need it.
  140. respond_with_json(request, code, response_json_object, send_cors=True,
  141. response_code_message=response_code_message,
  142. pretty_print=self._request_user_agent_is_curl)
  143. @staticmethod
  144. def _request_user_agent_is_curl(request):
  145. user_agents = request.requestHeaders.getRawHeaders(
  146. "User-Agent", default=[]
  147. )
  148. for user_agent in user_agents:
  149. if "curl" in user_agent:
  150. return True
  151. return False
  152. class RootRedirect(resource.Resource):
  153. """Redirects the root '/' path to another path."""
  154. def __init__(self, path):
  155. resource.Resource.__init__(self)
  156. self.url = path
  157. def render_GET(self, request):
  158. return redirectTo(self.url, request)
  159. def getChild(self, name, request):
  160. if len(name) == 0:
  161. return self # select ourselves as the child to render
  162. return resource.Resource.getChild(self, name, request)
  163. def respond_with_json(request, code, json_object, send_cors=False,
  164. response_code_message=None, pretty_print=False):
  165. if not pretty_print:
  166. json_bytes = encode_pretty_printed_json(json_object)
  167. else:
  168. json_bytes = encode_canonical_json(json_object)
  169. return respond_with_json_bytes(request, code, json_bytes, send_cors,
  170. response_code_message=response_code_message)
  171. def respond_with_json_bytes(request, code, json_bytes, send_cors=False,
  172. response_code_message=None):
  173. """Sends encoded JSON in response to the given request.
  174. Args:
  175. request (twisted.web.http.Request): The http request to respond to.
  176. code (int): The HTTP response code.
  177. json_bytes (bytes): The json bytes to use as the response body.
  178. send_cors (bool): Whether to send Cross-Origin Resource Sharing headers
  179. http://www.w3.org/TR/cors/
  180. Returns:
  181. twisted.web.server.NOT_DONE_YET"""
  182. request.setResponseCode(code, message=response_code_message)
  183. request.setHeader(b"Content-Type", b"application/json")
  184. if send_cors:
  185. request.setHeader("Access-Control-Allow-Origin", "*")
  186. request.setHeader("Access-Control-Allow-Methods",
  187. "GET, POST, PUT, DELETE, OPTIONS")
  188. request.setHeader("Access-Control-Allow-Headers",
  189. "Origin, X-Requested-With, Content-Type, Accept")
  190. request.write(json_bytes)
  191. request.finish()
  192. return NOT_DONE_YET