Matrix Docker Ansible eploy
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

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