__init__.py 8.7 KB

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