__init__.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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. import logging
  16. import json
  17. import copy
  18. import functools
  19. from twisted.internet import defer
  20. from twisted.web import server
  21. from sydent.util import json_decoder
  22. logger = logging.getLogger(__name__)
  23. class MatrixRestError(Exception):
  24. """
  25. Handled by the jsonwrap wrapper. Any servlets that don't use this
  26. wrapper should catch this exception themselves.
  27. """
  28. def __init__(self, httpStatus, errcode, error):
  29. super(Exception, self).__init__(error)
  30. self.httpStatus = httpStatus
  31. self.errcode = errcode
  32. self.error = error
  33. def get_args(request, args, required=True):
  34. """
  35. Helper function to get arguments for an HTTP request.
  36. Currently takes args from the top level keys of a json object or
  37. www-form-urlencoded for backwards compatibility on v1 endpoints only.
  38. Returns a tuple (error, args) where if error is non-null,
  39. the request is malformed. Otherwise, args contains the
  40. parameters passed.
  41. :param request: The request received by the servlet.
  42. :type request: twisted.web.server.Request
  43. :param args: The args to look for in the request's parameters.
  44. :type args: tuple[unicode]
  45. :param required: Whether to raise a MatrixRestError with 400
  46. M_MISSING_PARAMS if an argument is not found.
  47. :type required: bool
  48. :raises: MatrixRestError if required is True and a given parameter
  49. was not found in the request's query parameters.
  50. :return: A dict containing the requested args and their values. String values
  51. are of type unicode.
  52. :rtype: dict[unicode, any]
  53. """
  54. v1_path = request.path.startswith(b'/_matrix/identity/api/v1')
  55. request_args = None
  56. # for v1 paths, only look for json args if content type is json
  57. if (
  58. request.method in (b'POST', b'PUT') and (
  59. not v1_path or (
  60. request.requestHeaders.hasHeader('Content-Type') and
  61. request.requestHeaders.getRawHeaders('Content-Type')[0].startswith('application/json')
  62. )
  63. )
  64. ):
  65. try:
  66. # json.loads doesn't allow bytes in Python 3.5
  67. request_args = json_decoder.decode(request.content.read().decode("UTF-8"))
  68. except ValueError:
  69. raise MatrixRestError(400, 'M_BAD_JSON', 'Malformed JSON')
  70. # If we didn't get anything from that, and it's a v1 api path, try the request args
  71. # (element-web's usage of the ed25519 sign servlet currently involves
  72. # sending the params in the query string with a json body of 'null')
  73. if request_args is None and (v1_path or request.method == b'GET'):
  74. request_args_bytes = copy.copy(request.args)
  75. # Twisted supplies everything as an array because it's valid to
  76. # supply the same params multiple times with www-form-urlencoded
  77. # params. This make it incompatible with the json object though,
  78. # so we need to convert one of them. Since this is the
  79. # backwards-compat option, we convert this one.
  80. request_args = {}
  81. for k, v in request_args_bytes.items():
  82. if isinstance(v, list) and len(v) == 1:
  83. try:
  84. request_args[k.decode("UTF-8")] = v[0].decode("UTF-8")
  85. except UnicodeDecodeError:
  86. # Get a version of the key that has non-UTF-8 characters replaced by
  87. # their \xNN escape sequence so it doesn't raise another exception.
  88. safe_k = k.decode("UTF-8", errors="backslashreplace")
  89. raise MatrixRestError(
  90. 400,
  91. 'M_INVALID_PARAM',
  92. "Parameter %s and its value must be valid UTF-8" % safe_k,
  93. )
  94. elif request_args is None:
  95. request_args = {}
  96. if required:
  97. # Check for any missing arguments
  98. missing = []
  99. for a in args:
  100. if a not in request_args:
  101. missing.append(a)
  102. if len(missing) > 0:
  103. request.setResponseCode(400)
  104. msg = "Missing parameters: "+(",".join(missing))
  105. raise MatrixRestError(400, 'M_MISSING_PARAMS', msg)
  106. return request_args
  107. def jsonwrap(f):
  108. @functools.wraps(f)
  109. def inner(self, request, *args, **kwargs):
  110. """
  111. Runs a web handler function with the given request and parameters, then
  112. converts its result into JSON and returns it. If an error happens, also sets
  113. the HTTP response code.
  114. :param self: The current object.
  115. :param request: The request to process.
  116. :type request: twisted.web.server.Request
  117. :param args: The arguments to pass to the function.
  118. :param kwargs: The keyword arguments to pass to the function.
  119. :return: The JSON payload to send as a response to the request.
  120. :rtype bytes
  121. """
  122. try:
  123. request.setHeader("Content-Type", "application/json")
  124. return dict_to_json_bytes(f(self, request, *args, **kwargs))
  125. except MatrixRestError as e:
  126. request.setResponseCode(e.httpStatus)
  127. return dict_to_json_bytes({"errcode": e.errcode, "error": e.error})
  128. except Exception:
  129. logger.exception("Exception processing request")
  130. request.setHeader("Content-Type", "application/json")
  131. request.setResponseCode(500)
  132. return dict_to_json_bytes({
  133. "errcode": "M_UNKNOWN",
  134. "error": "Internal Server Error",
  135. })
  136. return inner
  137. def deferjsonwrap(f):
  138. def reqDone(resp, request):
  139. """
  140. Converts the given response content into JSON and encodes it to bytes, then
  141. writes it as the response to the given request with the right headers.
  142. :param resp: The response content to convert to JSON and encode.
  143. :type resp: dict[str, any]
  144. :param request: The request to respond to.
  145. :type request: twisted.web.server.Request
  146. """
  147. request.setHeader("Content-Type", "application/json")
  148. request.write(dict_to_json_bytes(resp))
  149. request.finish()
  150. def reqErr(failure, request):
  151. """
  152. Logs the given failure. If the failure is a MatrixRestError, writes a response
  153. using the info it contains, otherwise responds with 500 Internal Server Error.
  154. :param failure: The failure to process.
  155. :type failure: twisted.python.failure.Failure
  156. :param request: The request to respond to.
  157. :type request: twisted.web.server.Request
  158. """
  159. request.setHeader("Content-Type", "application/json")
  160. if failure.check(MatrixRestError) is not None:
  161. request.setResponseCode(failure.value.httpStatus)
  162. request.write(dict_to_json_bytes({'errcode': failure.value.errcode, 'error': failure.value.error}))
  163. else:
  164. logger.error("Request processing failed: %r, %s", failure, failure.getTraceback())
  165. request.setResponseCode(500)
  166. request.write(dict_to_json_bytes({'errcode': 'M_UNKNOWN', 'error': 'Internal Server Error'}))
  167. request.finish()
  168. def inner(*args, **kwargs):
  169. """
  170. Runs an asynchronous web handler function with the given arguments and add
  171. reqDone and reqErr as the resulting Deferred's callbacks.
  172. :param args: The arguments to pass to the function.
  173. :param kwargs: The keyword arguments to pass to the function.
  174. :return: A special code to tell the servlet that the response isn't ready yet
  175. and will come later.
  176. :rtype: int
  177. """
  178. request = args[1]
  179. d = defer.maybeDeferred(f, *args, **kwargs)
  180. d.addCallback(reqDone, request)
  181. d.addErrback(reqErr, request)
  182. return server.NOT_DONE_YET
  183. return inner
  184. def send_cors(request):
  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. def dict_to_json_bytes(content):
  190. """
  191. Converts a dict into JSON and encodes it to bytes.
  192. :param content:
  193. :type content: dict[any, any]
  194. :return: The JSON bytes.
  195. :rtype: bytes
  196. """
  197. return json.dumps(content).encode("UTF-8")