1
0

server.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. import json
  2. import logging
  3. from collections import deque
  4. from io import SEEK_END, BytesIO
  5. from typing import Callable, Iterable, MutableMapping, Optional, Tuple, Union
  6. import attr
  7. from typing_extensions import Deque
  8. from zope.interface import implementer
  9. from twisted.internet import address, threads, udp
  10. from twisted.internet._resolver import SimpleResolverComplexifier
  11. from twisted.internet.defer import Deferred, fail, succeed
  12. from twisted.internet.error import DNSLookupError
  13. from twisted.internet.interfaces import (
  14. IReactorPluggableNameResolver,
  15. IReactorTCP,
  16. IResolverSimple,
  17. )
  18. from twisted.python.failure import Failure
  19. from twisted.test.proto_helpers import AccumulatingProtocol, MemoryReactorClock
  20. from twisted.web.http_headers import Headers
  21. from twisted.web.resource import IResource
  22. from twisted.web.server import Site
  23. from synapse.http.site import SynapseRequest
  24. from synapse.util import Clock
  25. from tests.utils import setup_test_homeserver as _sth
  26. logger = logging.getLogger(__name__)
  27. class TimedOutException(Exception):
  28. """
  29. A web query timed out.
  30. """
  31. @attr.s
  32. class FakeChannel:
  33. """
  34. A fake Twisted Web Channel (the part that interfaces with the
  35. wire).
  36. """
  37. site = attr.ib(type=Site)
  38. _reactor = attr.ib()
  39. result = attr.ib(type=dict, default=attr.Factory(dict))
  40. _producer = None
  41. @property
  42. def json_body(self):
  43. return json.loads(self.text_body)
  44. @property
  45. def text_body(self) -> str:
  46. """The body of the result, utf-8-decoded.
  47. Raises an exception if the request has not yet completed.
  48. """
  49. if not self.is_finished:
  50. raise Exception("Request not yet completed")
  51. return self.result["body"].decode("utf8")
  52. def is_finished(self) -> bool:
  53. """check if the response has been completely received"""
  54. return self.result.get("done", False)
  55. @property
  56. def code(self):
  57. if not self.result:
  58. raise Exception("No result yet.")
  59. return int(self.result["code"])
  60. @property
  61. def headers(self) -> Headers:
  62. if not self.result:
  63. raise Exception("No result yet.")
  64. h = Headers()
  65. for i in self.result["headers"]:
  66. h.addRawHeader(*i)
  67. return h
  68. def writeHeaders(self, version, code, reason, headers):
  69. self.result["version"] = version
  70. self.result["code"] = code
  71. self.result["reason"] = reason
  72. self.result["headers"] = headers
  73. def write(self, content):
  74. assert isinstance(content, bytes), "Should be bytes! " + repr(content)
  75. if "body" not in self.result:
  76. self.result["body"] = b""
  77. self.result["body"] += content
  78. def registerProducer(self, producer, streaming):
  79. self._producer = producer
  80. self.producerStreaming = streaming
  81. def _produce():
  82. if self._producer:
  83. self._producer.resumeProducing()
  84. self._reactor.callLater(0.1, _produce)
  85. if not streaming:
  86. self._reactor.callLater(0.0, _produce)
  87. def unregisterProducer(self):
  88. if self._producer is None:
  89. return
  90. self._producer = None
  91. def requestDone(self, _self):
  92. self.result["done"] = True
  93. def getPeer(self):
  94. # We give an address so that getClientIP returns a non null entry,
  95. # causing us to record the MAU
  96. return address.IPv4Address("TCP", "127.0.0.1", 3423)
  97. def getHost(self):
  98. return None
  99. @property
  100. def transport(self):
  101. return self
  102. def await_result(self, timeout: int = 100) -> None:
  103. """
  104. Wait until the request is finished.
  105. """
  106. self._reactor.run()
  107. x = 0
  108. while not self.is_finished():
  109. # If there's a producer, tell it to resume producing so we get content
  110. if self._producer:
  111. self._producer.resumeProducing()
  112. x += 1
  113. if x > timeout:
  114. raise TimedOutException("Timed out waiting for request to finish.")
  115. self._reactor.advance(0.1)
  116. def extract_cookies(self, cookies: MutableMapping[str, str]) -> None:
  117. """Process the contents of any Set-Cookie headers in the response
  118. Any cookines found are added to the given dict
  119. """
  120. for h in self.headers.getRawHeaders("Set-Cookie"):
  121. parts = h.split(";")
  122. k, v = parts[0].split("=", maxsplit=1)
  123. cookies[k] = v
  124. class FakeSite:
  125. """
  126. A fake Twisted Web Site, with mocks of the extra things that
  127. Synapse adds.
  128. """
  129. server_version_string = b"1"
  130. site_tag = "test"
  131. access_logger = logging.getLogger("synapse.access.http.fake")
  132. def __init__(self, resource: IResource):
  133. """
  134. Args:
  135. resource: the resource to be used for rendering all requests
  136. """
  137. self._resource = resource
  138. def getResourceFor(self, request):
  139. return self._resource
  140. def make_request(
  141. reactor,
  142. site: Site,
  143. method,
  144. path,
  145. content=b"",
  146. access_token=None,
  147. request=SynapseRequest,
  148. shorthand=True,
  149. federation_auth_origin=None,
  150. content_is_form=False,
  151. await_result: bool = True,
  152. custom_headers: Optional[
  153. Iterable[Tuple[Union[bytes, str], Union[bytes, str]]]
  154. ] = None,
  155. ) -> FakeChannel:
  156. """
  157. Make a web request using the given method, path and content, and render it
  158. Returns the fake Channel object which records the response to the request.
  159. Args:
  160. site: The twisted Site to use to render the request
  161. method (bytes/unicode): The HTTP request method ("verb").
  162. path (bytes/unicode): The HTTP path, suitably URL encoded (e.g.
  163. escaped UTF-8 & spaces and such).
  164. content (bytes or dict): The body of the request. JSON-encoded, if
  165. a dict.
  166. shorthand: Whether to try and be helpful and prefix the given URL
  167. with the usual REST API path, if it doesn't contain it.
  168. federation_auth_origin (bytes|None): if set to not-None, we will add a fake
  169. Authorization header pretenting to be the given server name.
  170. content_is_form: Whether the content is URL encoded form data. Adds the
  171. 'Content-Type': 'application/x-www-form-urlencoded' header.
  172. custom_headers: (name, value) pairs to add as request headers
  173. await_result: whether to wait for the request to complete rendering. If true,
  174. will pump the reactor until the the renderer tells the channel the request
  175. is finished.
  176. Returns:
  177. channel
  178. """
  179. if not isinstance(method, bytes):
  180. method = method.encode("ascii")
  181. if not isinstance(path, bytes):
  182. path = path.encode("ascii")
  183. # Decorate it to be the full path, if we're using shorthand
  184. if (
  185. shorthand
  186. and not path.startswith(b"/_matrix")
  187. and not path.startswith(b"/_synapse")
  188. ):
  189. if path.startswith(b"/"):
  190. path = path[1:]
  191. path = b"/_matrix/client/r0/" + path
  192. if not path.startswith(b"/"):
  193. path = b"/" + path
  194. if isinstance(content, dict):
  195. content = json.dumps(content).encode("utf8")
  196. if isinstance(content, str):
  197. content = content.encode("utf8")
  198. channel = FakeChannel(site, reactor)
  199. req = request(channel)
  200. req.content = BytesIO(content)
  201. # Twisted expects to be at the end of the content when parsing the request.
  202. req.content.seek(SEEK_END)
  203. if access_token:
  204. req.requestHeaders.addRawHeader(
  205. b"Authorization", b"Bearer " + access_token.encode("ascii")
  206. )
  207. if federation_auth_origin is not None:
  208. req.requestHeaders.addRawHeader(
  209. b"Authorization",
  210. b"X-Matrix origin=%s,key=,sig=" % (federation_auth_origin,),
  211. )
  212. if content:
  213. if content_is_form:
  214. req.requestHeaders.addRawHeader(
  215. b"Content-Type", b"application/x-www-form-urlencoded"
  216. )
  217. else:
  218. # Assume the body is JSON
  219. req.requestHeaders.addRawHeader(b"Content-Type", b"application/json")
  220. if custom_headers:
  221. for k, v in custom_headers:
  222. req.requestHeaders.addRawHeader(k, v)
  223. req.parseCookies()
  224. req.requestReceived(method, path, b"1.1")
  225. if await_result:
  226. channel.await_result()
  227. return channel
  228. @implementer(IReactorPluggableNameResolver)
  229. class ThreadedMemoryReactorClock(MemoryReactorClock):
  230. """
  231. A MemoryReactorClock that supports callFromThread.
  232. """
  233. def __init__(self):
  234. self.threadpool = ThreadPool(self)
  235. self._tcp_callbacks = {}
  236. self._udp = []
  237. lookups = self.lookups = {}
  238. self._thread_callbacks = deque() # type: Deque[Callable[[], None]]()
  239. @implementer(IResolverSimple)
  240. class FakeResolver:
  241. def getHostByName(self, name, timeout=None):
  242. if name not in lookups:
  243. return fail(DNSLookupError("OH NO: unknown %s" % (name,)))
  244. return succeed(lookups[name])
  245. self.nameResolver = SimpleResolverComplexifier(FakeResolver())
  246. super().__init__()
  247. def listenUDP(self, port, protocol, interface="", maxPacketSize=8196):
  248. p = udp.Port(port, protocol, interface, maxPacketSize, self)
  249. p.startListening()
  250. self._udp.append(p)
  251. return p
  252. def callFromThread(self, callback, *args, **kwargs):
  253. """
  254. Make the callback fire in the next reactor iteration.
  255. """
  256. cb = lambda: callback(*args, **kwargs)
  257. # it's not safe to call callLater() here, so we append the callback to a
  258. # separate queue.
  259. self._thread_callbacks.append(cb)
  260. def getThreadPool(self):
  261. return self.threadpool
  262. def add_tcp_client_callback(self, host, port, callback):
  263. """Add a callback that will be invoked when we receive a connection
  264. attempt to the given IP/port using `connectTCP`.
  265. Note that the callback gets run before we return the connection to the
  266. client, which means callbacks cannot block while waiting for writes.
  267. """
  268. self._tcp_callbacks[(host, port)] = callback
  269. def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
  270. """Fake L{IReactorTCP.connectTCP}.
  271. """
  272. conn = super().connectTCP(
  273. host, port, factory, timeout=timeout, bindAddress=None
  274. )
  275. callback = self._tcp_callbacks.get((host, port))
  276. if callback:
  277. callback()
  278. return conn
  279. def advance(self, amount):
  280. # first advance our reactor's time, and run any "callLater" callbacks that
  281. # makes ready
  282. super().advance(amount)
  283. # now run any "callFromThread" callbacks
  284. while True:
  285. try:
  286. callback = self._thread_callbacks.popleft()
  287. except IndexError:
  288. break
  289. callback()
  290. # check for more "callLater" callbacks added by the thread callback
  291. # This isn't required in a regular reactor, but it ends up meaning that
  292. # our database queries can complete in a single call to `advance` [1] which
  293. # simplifies tests.
  294. #
  295. # [1]: we replace the threadpool backing the db connection pool with a
  296. # mock ThreadPool which doesn't really use threads; but we still use
  297. # reactor.callFromThread to feed results back from the db functions to the
  298. # main thread.
  299. super().advance(0)
  300. class ThreadPool:
  301. """
  302. Threadless thread pool.
  303. """
  304. def __init__(self, reactor):
  305. self._reactor = reactor
  306. def start(self):
  307. pass
  308. def stop(self):
  309. pass
  310. def callInThreadWithCallback(self, onResult, function, *args, **kwargs):
  311. def _(res):
  312. if isinstance(res, Failure):
  313. onResult(False, res)
  314. else:
  315. onResult(True, res)
  316. d = Deferred()
  317. d.addCallback(lambda x: function(*args, **kwargs))
  318. d.addBoth(_)
  319. self._reactor.callLater(0, d.callback, True)
  320. return d
  321. def setup_test_homeserver(cleanup_func, *args, **kwargs):
  322. """
  323. Set up a synchronous test server, driven by the reactor used by
  324. the homeserver.
  325. """
  326. server = _sth(cleanup_func, *args, **kwargs)
  327. # Make the thread pool synchronous.
  328. clock = server.get_clock()
  329. for database in server.get_datastores().databases:
  330. pool = database._db_pool
  331. def runWithConnection(func, *args, **kwargs):
  332. return threads.deferToThreadPool(
  333. pool._reactor,
  334. pool.threadpool,
  335. pool._runWithConnection,
  336. func,
  337. *args,
  338. **kwargs,
  339. )
  340. def runInteraction(interaction, *args, **kwargs):
  341. return threads.deferToThreadPool(
  342. pool._reactor,
  343. pool.threadpool,
  344. pool._runInteraction,
  345. interaction,
  346. *args,
  347. **kwargs,
  348. )
  349. pool.runWithConnection = runWithConnection
  350. pool.runInteraction = runInteraction
  351. pool.threadpool = ThreadPool(clock._reactor)
  352. pool.running = True
  353. # We've just changed the Databases to run DB transactions on the same
  354. # thread, so we need to disable the dedicated thread behaviour.
  355. server.get_datastores().main.USE_DEDICATED_DB_THREADS_FOR_EVENT_FETCHING = False
  356. return server
  357. def get_clock():
  358. clock = ThreadedMemoryReactorClock()
  359. hs_clock = Clock(clock)
  360. return clock, hs_clock
  361. @attr.s(cmp=False)
  362. class FakeTransport:
  363. """
  364. A twisted.internet.interfaces.ITransport implementation which sends all its data
  365. straight into an IProtocol object: it exists to connect two IProtocols together.
  366. To use it, instantiate it with the receiving IProtocol, and then pass it to the
  367. sending IProtocol's makeConnection method:
  368. server = HTTPChannel()
  369. client.makeConnection(FakeTransport(server, self.reactor))
  370. If you want bidirectional communication, you'll need two instances.
  371. """
  372. other = attr.ib()
  373. """The Protocol object which will receive any data written to this transport.
  374. :type: twisted.internet.interfaces.IProtocol
  375. """
  376. _reactor = attr.ib()
  377. """Test reactor
  378. :type: twisted.internet.interfaces.IReactorTime
  379. """
  380. _protocol = attr.ib(default=None)
  381. """The Protocol which is producing data for this transport. Optional, but if set
  382. will get called back for connectionLost() notifications etc.
  383. """
  384. disconnecting = False
  385. disconnected = False
  386. connected = True
  387. buffer = attr.ib(default=b"")
  388. producer = attr.ib(default=None)
  389. autoflush = attr.ib(default=True)
  390. def getPeer(self):
  391. return None
  392. def getHost(self):
  393. return None
  394. def loseConnection(self, reason=None):
  395. if not self.disconnecting:
  396. logger.info("FakeTransport: loseConnection(%s)", reason)
  397. self.disconnecting = True
  398. if self._protocol:
  399. self._protocol.connectionLost(reason)
  400. # if we still have data to write, delay until that is done
  401. if self.buffer:
  402. logger.info(
  403. "FakeTransport: Delaying disconnect until buffer is flushed"
  404. )
  405. else:
  406. self.connected = False
  407. self.disconnected = True
  408. def abortConnection(self):
  409. logger.info("FakeTransport: abortConnection()")
  410. if not self.disconnecting:
  411. self.disconnecting = True
  412. if self._protocol:
  413. self._protocol.connectionLost(None)
  414. self.disconnected = True
  415. def pauseProducing(self):
  416. if not self.producer:
  417. return
  418. self.producer.pauseProducing()
  419. def resumeProducing(self):
  420. if not self.producer:
  421. return
  422. self.producer.resumeProducing()
  423. def unregisterProducer(self):
  424. if not self.producer:
  425. return
  426. self.producer = None
  427. def registerProducer(self, producer, streaming):
  428. self.producer = producer
  429. self.producerStreaming = streaming
  430. def _produce():
  431. d = self.producer.resumeProducing()
  432. d.addCallback(lambda x: self._reactor.callLater(0.1, _produce))
  433. if not streaming:
  434. self._reactor.callLater(0.0, _produce)
  435. def write(self, byt):
  436. if self.disconnecting:
  437. raise Exception("Writing to disconnecting FakeTransport")
  438. self.buffer = self.buffer + byt
  439. # always actually do the write asynchronously. Some protocols (notably the
  440. # TLSMemoryBIOProtocol) get very confused if a read comes back while they are
  441. # still doing a write. Doing a callLater here breaks the cycle.
  442. if self.autoflush:
  443. self._reactor.callLater(0.0, self.flush)
  444. def writeSequence(self, seq):
  445. for x in seq:
  446. self.write(x)
  447. def flush(self, maxbytes=None):
  448. if not self.buffer:
  449. # nothing to do. Don't write empty buffers: it upsets the
  450. # TLSMemoryBIOProtocol
  451. return
  452. if self.disconnected:
  453. return
  454. if getattr(self.other, "transport") is None:
  455. # the other has no transport yet; reschedule
  456. if self.autoflush:
  457. self._reactor.callLater(0.0, self.flush)
  458. return
  459. if maxbytes is not None:
  460. to_write = self.buffer[:maxbytes]
  461. else:
  462. to_write = self.buffer
  463. logger.info("%s->%s: %s", self._protocol, self.other, to_write)
  464. try:
  465. self.other.dataReceived(to_write)
  466. except Exception as e:
  467. logger.exception("Exception writing to protocol: %s", e)
  468. return
  469. self.buffer = self.buffer[len(to_write) :]
  470. if self.buffer and self.autoflush:
  471. self._reactor.callLater(0.0, self.flush)
  472. if not self.buffer and self.disconnecting:
  473. logger.info("FakeTransport: Buffer now empty, completing disconnect")
  474. self.disconnected = True
  475. def connect_client(reactor: IReactorTCP, client_id: int) -> AccumulatingProtocol:
  476. """
  477. Connect a client to a fake TCP transport.
  478. Args:
  479. reactor
  480. factory: The connecting factory to build.
  481. """
  482. factory = reactor.tcpClients.pop(client_id)[2]
  483. client = factory.buildProtocol(None)
  484. server = AccumulatingProtocol()
  485. server.makeConnection(FakeTransport(client, reactor))
  486. client.makeConnection(FakeTransport(server, reactor))
  487. return client, server