Matrix Docker Ansible eploy
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

186 linhas
7.3 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. """A stand-in homeserver for Molecule scenarios.
  5. Most components in this playbook talk to a homeserver while starting up and
  6. exit if it is unreachable, so a scenario cannot get them running without one.
  7. Standing up a real Synapse for every role would dominate the run time and drag
  8. in Postgres, and the scenarios are not testing Synapse - they are testing that
  9. the role's configuration reaches the component and that it starts.
  10. So this answers the handful of endpoints components touch during startup, with
  11. the blandest plausible response in each case. It is deliberately permissive: an
  12. unknown path returns `{}` with a 200 rather than a 404, because the goal is to
  13. get the component past its startup checks, not to model the Matrix spec.
  14. What it is NOT: an authentication check, a room state machine, or anything a
  15. scenario should assert *about*. Assert on what the role rendered and on what the
  16. component reports about itself. If a scenario starts needing this stub to behave
  17. like a real homeserver, that scenario has outgrown what these tests are for.
  18. """
  19. import json
  20. import os
  21. import re
  22. import sys
  23. import time
  24. from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
  25. from urllib.parse import parse_qs, urlparse
  26. SERVER_NAME = os.environ.get("STUB_SERVER_NAME", "molecule.local")
  27. PORT = int(os.environ.get("STUB_PORT", "8008"))
  28. # Rooms reported as already joined. Components that resolve a room mapping at
  29. # startup (matrix-alertmanager-receiver, for one) fail if the rooms they were
  30. # configured with are missing, so a scenario passes its own room IDs in.
  31. JOINED_ROOMS = [r for r in os.environ.get("STUB_JOINED_ROOMS", "").split(",") if r]
  32. USER_ID = os.environ.get("STUB_USER_ID", f"@stub:{SERVER_NAME}")
  33. # Longest a /sync call is held open. Long-polling clients (anything on
  34. # matrix-sdk: baibot and the other bots) ask for a 30s timeout and immediately
  35. # ask again when the call returns, so answering instantly would spin them into a
  36. # hot loop that eats the test machine. Honouring the requested timeout, capped
  37. # here, keeps an idle bot idle.
  38. SYNC_MAX_HOLD_SECONDS = 30
  39. class Handler(BaseHTTPRequestHandler):
  40. protocol_version = "HTTP/1.1"
  41. def _send(self, payload, status=200):
  42. body = json.dumps(payload).encode()
  43. self.send_response(status)
  44. self.send_header("Content-Type", "application/json")
  45. self.send_header("Content-Length", str(len(body)))
  46. self.end_headers()
  47. self.wfile.write(body)
  48. def _route(self):
  49. parsed = urlparse(self.path)
  50. path = parsed.path
  51. # A client that syncs (every bot here does) needs a `next_batch` back or
  52. # the response will not deserialize, and it needs the call to block for
  53. # the timeout it asked for or it will hammer this stub. Nothing is ever
  54. # reported: an idle bot is what a scenario wants.
  55. if path.endswith("/sync"):
  56. requested_ms = parse_qs(parsed.query).get("timeout", ["0"])[0]
  57. try:
  58. hold = min(int(requested_ms) / 1000.0, SYNC_MAX_HOLD_SECONDS)
  59. except ValueError:
  60. hold = 0
  61. if hold > 0:
  62. time.sleep(hold)
  63. return {"next_batch": "molecule-stub-batch"}
  64. # Before the generic `/upload` below: this one is the end-to-end
  65. # encryption key upload, and the client insists on the key counts.
  66. if path.endswith("/keys/upload"):
  67. return {"one_time_key_counts": {}}
  68. # Media. A bot that sets its own avatar asks for the upload limits first
  69. # and refuses to proceed without them, then uploads and expects an MXC
  70. # URI back.
  71. if path.endswith("/media/config") or path.endswith("/media/v3/config"):
  72. return {"m.upload.size": 10485760}
  73. if path.endswith("/upload"):
  74. return {"content_uri": f"mxc://{SERVER_NAME}/molecule-stub-media"}
  75. # Sync filters are uploaded before the first sync and referenced by id.
  76. if path.endswith("/filter"):
  77. return {"filter_id": "molecule-stub-filter"}
  78. if path.endswith("/joined_rooms"):
  79. return {"joined_rooms": JOINED_ROOMS}
  80. if path.endswith("/whoami"):
  81. return {"user_id": USER_ID, "device_id": "STUBDEVICE"}
  82. if path.endswith("/versions"):
  83. return {
  84. "versions": ["v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6"],
  85. "unstable_features": {},
  86. }
  87. if path.endswith("/capabilities"):
  88. return {"capabilities": {}}
  89. # Bots that authenticate with a username and password rather than as an
  90. # appservice with a token log in here. Matched loosely on purpose:
  91. # clients differ on the API version prefix (matrix-nio has shipped both
  92. # /_matrix/client/r0/login and /_matrix/client/v3/login over time), and a
  93. # login that falls through to the catch-all `{}` below looks to the
  94. # client like bad credentials.
  95. if path.endswith("/login"):
  96. return {
  97. "user_id": USER_ID,
  98. "access_token": "stub_access_token",
  99. "device_id": "STUBDEVICE",
  100. "home_server": SERVER_NAME,
  101. }
  102. if path.endswith("/createRoom"):
  103. return {"room_id": f"!stub-room:{SERVER_NAME}"}
  104. if re.search(r"/rooms/[^/]+/join$", path) or path.endswith("/join"):
  105. return {"room_id": f"!stub-room:{SERVER_NAME}"}
  106. if "/send/" in path or "/state/" in path:
  107. return {"event_id": f"$stub-event:{SERVER_NAME}"}
  108. if path.endswith("/register"):
  109. return {
  110. "user_id": USER_ID,
  111. "access_token": "stub_access_token",
  112. "device_id": "STUBDEVICE",
  113. "home_server": SERVER_NAME,
  114. }
  115. if path.endswith("/profile") or "/profile/" in path:
  116. return {"displayname": "stub"}
  117. if path.startswith("/_matrix/key/"):
  118. return {"server_name": SERVER_NAME, "verify_keys": {}, "old_verify_keys": {}}
  119. if path.startswith("/.well-known/matrix/client"):
  120. return {"m.homeserver": {"base_url": f"http://{SERVER_NAME}:{PORT}"}}
  121. if path.startswith("/.well-known/matrix/server"):
  122. return {"m.server": f"{SERVER_NAME}:{PORT}"}
  123. if path.endswith("/health") or path.endswith("/_matrix/federation/v1/version"):
  124. return {"server": {"name": "molecule-stub", "version": "0"}}
  125. # Anything unrecognised: an empty object, so a component doing a startup
  126. # probe of an endpoint not listed here still gets past it.
  127. return {}
  128. def do_GET(self):
  129. self._send(self._route())
  130. def do_POST(self):
  131. length = int(self.headers.get("Content-Length") or 0)
  132. if length:
  133. self.rfile.read(length)
  134. self._send(self._route())
  135. def do_PUT(self):
  136. self.do_POST()
  137. def do_DELETE(self):
  138. self._send({})
  139. def log_message(self, fmt, *args):
  140. # Quiet by default; STUB_VERBOSE=1 when a scenario will not start and you
  141. # need to see what the component is actually asking for.
  142. if os.environ.get("STUB_VERBOSE"):
  143. sys.stderr.write("stub: " + (fmt % args) + "\n")
  144. if __name__ == "__main__":
  145. ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()