http.py 6.3 KB

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