You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

211 lines
6.5 KiB

  1. # Copyright 2014-2016 OpenMarket Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import urllib
  16. from pprint import pformat
  17. from typing import Optional
  18. from twisted.internet import defer, reactor
  19. from twisted.web.client import Agent, readBody
  20. from twisted.web.http_headers import Headers
  21. class HttpClient:
  22. """Interface for talking json over http"""
  23. def put_json(self, url, data):
  24. """Sends the specifed json data using PUT
  25. Args:
  26. url (str): The URL to PUT data to.
  27. data (dict): A dict containing the data that will be used as
  28. the request body. This will be encoded as JSON.
  29. Returns:
  30. Deferred: Succeeds when we get a 2xx HTTP response. The result
  31. will be the decoded JSON body.
  32. """
  33. def get_json(self, url, args=None):
  34. """Gets some json from the given host homeserver and path
  35. Args:
  36. url (str): The URL to GET data from.
  37. args (dict): A dictionary used to create query strings, defaults to
  38. None.
  39. **Note**: The value of each key is assumed to be an iterable
  40. and *not* a string.
  41. Returns:
  42. Deferred: Succeeds when we get a 2xx HTTP response. The result
  43. will be the decoded JSON body.
  44. """
  45. class TwistedHttpClient(HttpClient):
  46. """Wrapper around the twisted HTTP client api.
  47. Attributes:
  48. agent (twisted.web.client.Agent): The twisted Agent used to send the
  49. requests.
  50. """
  51. def __init__(self):
  52. self.agent = Agent(reactor)
  53. @defer.inlineCallbacks
  54. def put_json(self, url, data):
  55. response = yield self._create_put_request(
  56. url, data, headers_dict={"Content-Type": ["application/json"]}
  57. )
  58. body = yield readBody(response)
  59. defer.returnValue((response.code, body))
  60. @defer.inlineCallbacks
  61. def get_json(self, url, args=None):
  62. if args:
  63. # generates a list of strings of form "k=v".
  64. qs = urllib.urlencode(args, True)
  65. url = "%s?%s" % (url, qs)
  66. response = yield self._create_get_request(url)
  67. body = yield readBody(response)
  68. defer.returnValue(json.loads(body))
  69. def _create_put_request(self, url, json_data, headers_dict: Optional[dict] = None):
  70. """Wrapper of _create_request to issue a PUT request"""
  71. headers_dict = headers_dict or {}
  72. if "Content-Type" not in headers_dict:
  73. raise defer.error(RuntimeError("Must include Content-Type header for PUTs"))
  74. return self._create_request(
  75. "PUT", url, producer=_JsonProducer(json_data), headers_dict=headers_dict
  76. )
  77. def _create_get_request(self, url, headers_dict: Optional[dict] = None):
  78. """Wrapper of _create_request to issue a GET request"""
  79. return self._create_request("GET", url, headers_dict=headers_dict or {})
  80. @defer.inlineCallbacks
  81. def do_request(
  82. self,
  83. method,
  84. url,
  85. data=None,
  86. qparams=None,
  87. jsonreq=True,
  88. headers: Optional[dict] = None,
  89. ):
  90. headers = headers or {}
  91. if qparams:
  92. url = "%s?%s" % (url, urllib.urlencode(qparams, True))
  93. if jsonreq:
  94. prod = _JsonProducer(data)
  95. headers["Content-Type"] = ["application/json"]
  96. else:
  97. prod = _RawProducer(data)
  98. if method in ["POST", "PUT"]:
  99. response = yield self._create_request(
  100. method, url, producer=prod, headers_dict=headers
  101. )
  102. else:
  103. response = yield self._create_request(method, url)
  104. body = yield readBody(response)
  105. defer.returnValue(json.loads(body))
  106. @defer.inlineCallbacks
  107. def _create_request(
  108. self, method, url, producer=None, headers_dict: Optional[dict] = None
  109. ):
  110. """Creates and sends a request to the given url"""
  111. headers_dict = headers_dict or {}
  112. headers_dict["User-Agent"] = ["Synapse Cmd Client"]
  113. retries_left = 5
  114. print("%s to %s with headers %s" % (method, url, headers_dict))
  115. if self.verbose and producer:
  116. if "password" in producer.data:
  117. temp = producer.data["password"]
  118. producer.data["password"] = "[REDACTED]"
  119. print(json.dumps(producer.data, indent=4))
  120. producer.data["password"] = temp
  121. else:
  122. print(json.dumps(producer.data, indent=4))
  123. while True:
  124. try:
  125. response = yield self.agent.request(
  126. method, url.encode("UTF8"), Headers(headers_dict), producer
  127. )
  128. break
  129. except Exception as e:
  130. print("uh oh: %s" % e)
  131. if retries_left:
  132. yield self.sleep(2 ** (5 - retries_left))
  133. retries_left -= 1
  134. else:
  135. raise e
  136. if self.verbose:
  137. print("Status %s %s" % (response.code, response.phrase))
  138. print(pformat(list(response.headers.getAllRawHeaders())))
  139. defer.returnValue(response)
  140. def sleep(self, seconds):
  141. d = defer.Deferred()
  142. reactor.callLater(seconds, d.callback, seconds)
  143. return d
  144. class _RawProducer:
  145. def __init__(self, data):
  146. self.data = data
  147. self.body = data
  148. self.length = len(self.body)
  149. def startProducing(self, consumer):
  150. consumer.write(self.body)
  151. return defer.succeed(None)
  152. def pauseProducing(self):
  153. pass
  154. def stopProducing(self):
  155. pass
  156. class _JsonProducer:
  157. """Used by the twisted http client to create the HTTP body from json"""
  158. def __init__(self, jsn):
  159. self.data = jsn
  160. self.body = json.dumps(jsn).encode("utf8")
  161. self.length = len(self.body)
  162. def startProducing(self, consumer):
  163. consumer.write(self.body)
  164. return defer.succeed(None)
  165. def pauseProducing(self):
  166. pass
  167. def stopProducing(self):
  168. pass