client.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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 CodeMessageException
  16. from syutil.jsonutil import encode_canonical_json
  17. from twisted.internet import defer, reactor
  18. from twisted.web.client import (
  19. Agent, readBody, FileBodyProducer, PartialDownloadError
  20. )
  21. from twisted.web.http_headers import Headers
  22. from StringIO import StringIO
  23. import simplejson as json
  24. import logging
  25. import urllib
  26. logger = logging.getLogger(__name__)
  27. class SimpleHttpClient(object):
  28. """
  29. A simple, no-frills HTTP client with methods that wrap up common ways of
  30. using HTTP in Matrix
  31. """
  32. def __init__(self, hs):
  33. self.hs = hs
  34. # The default context factory in Twisted 14.0.0 (which we require) is
  35. # BrowserLikePolicyForHTTPS which will do regular cert validation
  36. # 'like a browser'
  37. self.agent = Agent(reactor)
  38. self.version_string = hs.version_string
  39. @defer.inlineCallbacks
  40. def post_urlencoded_get_json(self, uri, args={}):
  41. logger.debug("post_urlencoded_get_json args: %s", args)
  42. query_bytes = urllib.urlencode(args, True)
  43. response = yield self.agent.request(
  44. "POST",
  45. uri.encode("ascii"),
  46. headers=Headers({
  47. b"Content-Type": [b"application/x-www-form-urlencoded"],
  48. b"User-Agent": [self.version_string],
  49. }),
  50. bodyProducer=FileBodyProducer(StringIO(query_bytes))
  51. )
  52. body = yield readBody(response)
  53. defer.returnValue(json.loads(body))
  54. @defer.inlineCallbacks
  55. def post_json_get_json(self, uri, post_json):
  56. json_str = encode_canonical_json(post_json)
  57. logger.info("HTTP POST %s -> %s", json_str, uri)
  58. response = yield self.agent.request(
  59. "POST",
  60. uri.encode("ascii"),
  61. headers=Headers({
  62. "Content-Type": ["application/json"]
  63. }),
  64. bodyProducer=FileBodyProducer(StringIO(json_str))
  65. )
  66. body = yield readBody(response)
  67. defer.returnValue(json.loads(body))
  68. @defer.inlineCallbacks
  69. def get_json(self, uri, args={}):
  70. """ Gets some json from the given URI.
  71. Args:
  72. uri (str): The URI to request, not including query parameters
  73. args (dict): A dictionary used to create query strings, defaults to
  74. None.
  75. **Note**: The value of each key is assumed to be an iterable
  76. and *not* a string.
  77. Returns:
  78. Deferred: Succeeds when we get *any* 2xx HTTP response, with the
  79. HTTP body as JSON.
  80. Raises:
  81. On a non-2xx HTTP response. The response body will be used as the
  82. error message.
  83. """
  84. if len(args):
  85. query_bytes = urllib.urlencode(args, True)
  86. uri = "%s?%s" % (uri, query_bytes)
  87. response = yield self.agent.request(
  88. "GET",
  89. uri.encode("ascii"),
  90. headers=Headers({
  91. b"User-Agent": [self.version_string],
  92. })
  93. )
  94. body = yield readBody(response)
  95. if 200 <= response.code < 300:
  96. defer.returnValue(json.loads(body))
  97. else:
  98. # NB: This is explicitly not json.loads(body)'d because the contract
  99. # of CodeMessageException is a *string* message. Callers can always
  100. # load it into JSON if they want.
  101. raise CodeMessageException(response.code, body)
  102. @defer.inlineCallbacks
  103. def put_json(self, uri, json_body, args={}):
  104. """ Puts some json to the given URI.
  105. Args:
  106. uri (str): The URI to request, not including query parameters
  107. json_body (dict): The JSON to put in the HTTP body,
  108. args (dict): A dictionary used to create query strings, defaults to
  109. None.
  110. **Note**: The value of each key is assumed to be an iterable
  111. and *not* a string.
  112. Returns:
  113. Deferred: Succeeds when we get *any* 2xx HTTP response, with the
  114. HTTP body as JSON.
  115. Raises:
  116. On a non-2xx HTTP response.
  117. """
  118. if len(args):
  119. query_bytes = urllib.urlencode(args, True)
  120. uri = "%s?%s" % (uri, query_bytes)
  121. json_str = encode_canonical_json(json_body)
  122. response = yield self.agent.request(
  123. "PUT",
  124. uri.encode("ascii"),
  125. headers=Headers({
  126. b"User-Agent": [self.version_string],
  127. "Content-Type": ["application/json"]
  128. }),
  129. bodyProducer=FileBodyProducer(StringIO(json_str))
  130. )
  131. body = yield readBody(response)
  132. if 200 <= response.code < 300:
  133. defer.returnValue(json.loads(body))
  134. else:
  135. # NB: This is explicitly not json.loads(body)'d because the contract
  136. # of CodeMessageException is a *string* message. Callers can always
  137. # load it into JSON if they want.
  138. raise CodeMessageException(response.code, body)
  139. class CaptchaServerHttpClient(SimpleHttpClient):
  140. """
  141. Separate HTTP client for talking to google's captcha servers
  142. Only slightly special because accepts partial download responses
  143. """
  144. @defer.inlineCallbacks
  145. def post_urlencoded_get_raw(self, url, args={}):
  146. query_bytes = urllib.urlencode(args, True)
  147. response = yield self.agent.request(
  148. "POST",
  149. url.encode("ascii"),
  150. bodyProducer=FileBodyProducer(StringIO(query_bytes)),
  151. headers=Headers({
  152. b"Content-Type": [b"application/x-www-form-urlencoded"],
  153. b"User-Agent": [self.version_string],
  154. })
  155. )
  156. try:
  157. body = yield readBody(response)
  158. defer.returnValue(body)
  159. except PartialDownloadError as e:
  160. # twisted dislikes google's response, no content length.
  161. defer.returnValue(e.response)
  162. def _print_ex(e):
  163. if hasattr(e, "reasons") and e.reasons:
  164. for ex in e.reasons:
  165. _print_ex(ex)
  166. else:
  167. logger.exception(e)