sydent.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 OpenMarket Ltd
  3. # Copyright 2018 New Vector Ltd
  4. # Copyright 2019 The Matrix.org Foundation C.I.C.
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. from __future__ import absolute_import
  18. import gc
  19. from six.moves import configparser
  20. import copy
  21. import logging
  22. import logging.handlers
  23. import os
  24. import twisted.internet.reactor
  25. from twisted.internet import task
  26. from twisted.python import log
  27. from sydent.db.sqlitedb import SqliteDatabase
  28. from sydent.http.httpcommon import SslComponents
  29. from sydent.http.httpserver import (
  30. ClientApiHttpServer, ReplicationHttpsServer,
  31. InternalApiHttpServer,
  32. )
  33. from sydent.http.httpsclient import ReplicationHttpsClient
  34. from sydent.http.servlets.blindlysignstuffservlet import BlindlySignStuffServlet
  35. from sydent.http.servlets.pubkeyservlets import EphemeralPubkeyIsValidServlet, PubkeyIsValidServlet
  36. from sydent.http.servlets.termsservlet import TermsServlet
  37. from sydent.validators.emailvalidator import EmailValidator
  38. from sydent.validators.msisdnvalidator import MsisdnValidator
  39. from sydent.hs_federation.verifier import Verifier
  40. from sydent.util.hash import sha256_and_url_safe_base64
  41. from sydent.util.tokenutils import generateAlphanumericTokenOfLength
  42. from sydent.sign.ed25519 import SydentEd25519
  43. from sydent.http.servlets.emailservlet import EmailRequestCodeServlet, EmailValidateCodeServlet
  44. from sydent.http.servlets.msisdnservlet import MsisdnRequestCodeServlet, MsisdnValidateCodeServlet
  45. from sydent.http.servlets.lookupservlet import LookupServlet
  46. from sydent.http.servlets.bulklookupservlet import BulkLookupServlet
  47. from sydent.http.servlets.lookupv2servlet import LookupV2Servlet
  48. from sydent.http.servlets.hashdetailsservlet import HashDetailsServlet
  49. from sydent.http.servlets.pubkeyservlets import Ed25519Servlet
  50. from sydent.http.servlets.threepidbindservlet import ThreePidBindServlet
  51. from sydent.http.servlets.threepidunbindservlet import ThreePidUnbindServlet
  52. from sydent.http.servlets.replication import ReplicationPushServlet
  53. from sydent.http.servlets.getvalidated3pidservlet import GetValidated3pidServlet
  54. from sydent.http.servlets.store_invite_servlet import StoreInviteServlet
  55. from sydent.http.servlets.v1_servlet import V1Servlet
  56. from sydent.http.servlets.accountservlet import AccountServlet
  57. from sydent.http.servlets.registerservlet import RegisterServlet
  58. from sydent.http.servlets.logoutservlet import LogoutServlet
  59. from sydent.http.servlets.v2_servlet import V2Servlet
  60. from sydent.db.valsession import ThreePidValSessionStore
  61. from sydent.db.hashing_metadata import HashingMetadataStore
  62. from sydent.threepid.bind import ThreepidBinder
  63. from sydent.replication.pusher import Pusher
  64. logger = logging.getLogger(__name__)
  65. CONFIG_DEFAULTS = {
  66. 'general': {
  67. 'server.name': os.environ.get('SYDENT_SERVER_NAME', ''),
  68. 'log.path': '',
  69. 'log.level': 'INFO',
  70. 'pidfile.path': os.environ.get('SYDENT_PID_FILE', 'sydent.pid'),
  71. 'terms.path': '',
  72. 'address_lookup_limit': '10000', # Maximum amount of addresses in a single /lookup request
  73. # The following can be added to your local config file to enable prometheus
  74. # support.
  75. # 'prometheus_port': '8080', # The port to serve metrics on
  76. # 'prometheus_addr': '', # The address to bind to. Empty string means bind to all.
  77. # The following can be added to your local config file to enable sentry support.
  78. # 'sentry_dsn': 'https://...' # The DSN has configured in the sentry instance project.
  79. # Whether clients and homeservers can register an association using v1 endpoints.
  80. 'enable_v1_associations': 'true',
  81. 'delete_tokens_on_bind': 'true',
  82. },
  83. 'db': {
  84. 'db.file': os.environ.get('SYDENT_DB_PATH', 'sydent.db'),
  85. },
  86. 'http': {
  87. 'clientapi.http.bind_address': '::',
  88. 'clientapi.http.port': '8090',
  89. 'internalapi.http.bind_address': '::1',
  90. 'internalapi.http.port': '',
  91. 'replication.https.certfile': '',
  92. 'replication.https.cacert': '', # This should only be used for testing
  93. 'replication.https.bind_address': '::',
  94. 'replication.https.port': '4434',
  95. 'obey_x_forwarded_for': 'False',
  96. 'federation.verifycerts': 'True',
  97. 'verify_response_template': '',
  98. 'client_http_base': '',
  99. },
  100. 'email': {
  101. 'email.template': 'res/email.template',
  102. 'email.invite_template': 'res/invite.template',
  103. 'email.from': 'Sydent Validation <noreply@{hostname}>',
  104. 'email.subject': 'Your Validation Token',
  105. 'email.invite.subject': '%(sender_display_name)s has invited you to chat',
  106. 'email.smtphost': 'localhost',
  107. 'email.smtpport': '25',
  108. 'email.smtpusername': '',
  109. 'email.smtppassword': '',
  110. 'email.hostname': '',
  111. 'email.tlsmode': '0',
  112. # The web client location which will be used if it is not provided by
  113. # the homeserver.
  114. #
  115. # This should be the scheme and hostname only, see res/invite_template.eml
  116. # for the full URL that gets generated.
  117. 'email.default_web_client_location': 'https://app.element.io',
  118. # When a user is invited to a room via their email address, that invite is
  119. # displayed in the room list using an obfuscated version of the user's email
  120. # address. These config options determine how much of the email address to
  121. # obfuscate. Note that the '@' sign is always included.
  122. #
  123. # If the string is longer than a configured limit below, it is truncated to that limit
  124. # with '...' added. Otherwise:
  125. #
  126. # * If the string is longer than 5 characters, it is truncated to 3 characters + '...'
  127. # * If the string is longer than 1 character, it is truncated to 1 character + '...'
  128. # * If the string is 1 character long, it is converted to '...'
  129. #
  130. # This ensures that a full email address is never shown, even if it is extremely
  131. # short.
  132. #
  133. # The number of characters from the beginning to reveal of the email's username
  134. # portion (left of the '@' sign)
  135. 'email.third_party_invite_username_obfuscate_characters': '3',
  136. # The number of characters from the beginning to reveal of the email's domain
  137. # portion (right of the '@' sign)
  138. 'email.third_party_invite_domain_obfuscate_characters': '3',
  139. },
  140. 'sms': {
  141. 'bodyTemplate': 'Your code is {token}',
  142. 'username': '',
  143. 'password': '',
  144. },
  145. 'crypto': {
  146. 'ed25519.signingkey': '',
  147. },
  148. }
  149. class Sydent:
  150. def __init__(self, cfg, reactor=twisted.internet.reactor):
  151. self.reactor = reactor
  152. self.config_file = get_config_file_path()
  153. self.cfg = cfg
  154. logger.info("Starting Sydent server")
  155. self.pidfile = self.cfg.get('general', "pidfile.path");
  156. self.db = SqliteDatabase(self).db
  157. self.server_name = self.cfg.get('general', 'server.name')
  158. if self.server_name == '':
  159. self.server_name = os.uname()[1]
  160. logger.warn(("You had not specified a server name. I have guessed that this server is called '%s' "
  161. + "and saved this in the config file. If this is incorrect, you should edit server.name in "
  162. + "the config file.") % (self.server_name,))
  163. self.cfg.set('general', 'server.name', self.server_name)
  164. self.save_config()
  165. if self.cfg.has_option("general", "sentry_dsn"):
  166. # Only import and start sentry SDK if configured.
  167. import sentry_sdk
  168. sentry_sdk.init(
  169. dsn=self.cfg.get("general", "sentry_dsn"),
  170. )
  171. with sentry_sdk.configure_scope() as scope:
  172. scope.set_tag("sydent_server_name", self.server_name)
  173. if self.cfg.has_option("general", "prometheus_port"):
  174. import prometheus_client
  175. prometheus_client.start_http_server(
  176. port=self.cfg.getint("general", "prometheus_port"),
  177. addr=self.cfg.get("general", "prometheus_addr"),
  178. )
  179. self.enable_v1_associations = parse_cfg_bool(
  180. self.cfg.get("general", "enable_v1_associations")
  181. )
  182. self.delete_tokens_on_bind = parse_cfg_bool(
  183. self.cfg.get("general", "delete_tokens_on_bind")
  184. )
  185. self.default_web_client_location = self.cfg.get(
  186. "email", "email.default_web_client_location"
  187. )
  188. self.username_obfuscate_characters = int(self.cfg.get(
  189. "email", "email.third_party_invite_username_obfuscate_characters"
  190. ))
  191. self.domain_obfuscate_characters = int(self.cfg.get(
  192. "email", "email.third_party_invite_domain_obfuscate_characters"
  193. ))
  194. # See if a pepper already exists in the database
  195. # Note: This MUST be run before we start serving requests, otherwise lookups for
  196. # 3PID hashes may come in before we've completed generating them
  197. hashing_metadata_store = HashingMetadataStore(self)
  198. lookup_pepper = hashing_metadata_store.get_lookup_pepper()
  199. if not lookup_pepper:
  200. # No pepper defined in the database, generate one
  201. lookup_pepper = generateAlphanumericTokenOfLength(5)
  202. # Store it in the database and rehash 3PIDs
  203. hashing_metadata_store.store_lookup_pepper(sha256_and_url_safe_base64,
  204. lookup_pepper)
  205. self.validators = Validators()
  206. self.validators.email = EmailValidator(self)
  207. self.validators.msisdn = MsisdnValidator(self)
  208. self.keyring = Keyring()
  209. self.keyring.ed25519 = SydentEd25519(self).signing_key
  210. self.keyring.ed25519.alg = 'ed25519'
  211. self.sig_verifier = Verifier(self)
  212. self.servlets = Servlets()
  213. self.servlets.v1 = V1Servlet(self)
  214. self.servlets.v2 = V2Servlet(self)
  215. self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
  216. self.servlets.emailValidate = EmailValidateCodeServlet(self)
  217. self.servlets.msisdnRequestCode = MsisdnRequestCodeServlet(self)
  218. self.servlets.msisdnValidate = MsisdnValidateCodeServlet(self)
  219. self.servlets.lookup = LookupServlet(self)
  220. self.servlets.bulk_lookup = BulkLookupServlet(self)
  221. self.servlets.hash_details = HashDetailsServlet(self, lookup_pepper)
  222. self.servlets.lookup_v2 = LookupV2Servlet(self, lookup_pepper)
  223. self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
  224. self.servlets.pubkeyIsValid = PubkeyIsValidServlet(self)
  225. self.servlets.ephemeralPubkeyIsValid = EphemeralPubkeyIsValidServlet(self)
  226. self.servlets.threepidBind = ThreePidBindServlet(self)
  227. self.servlets.threepidUnbind = ThreePidUnbindServlet(self)
  228. self.servlets.replicationPush = ReplicationPushServlet(self)
  229. self.servlets.getValidated3pid = GetValidated3pidServlet(self)
  230. self.servlets.storeInviteServlet = StoreInviteServlet(self)
  231. self.servlets.blindlySignStuffServlet = BlindlySignStuffServlet(self)
  232. self.servlets.termsServlet = TermsServlet(self)
  233. self.servlets.accountServlet = AccountServlet(self)
  234. self.servlets.registerServlet = RegisterServlet(self)
  235. self.servlets.logoutServlet = LogoutServlet(self)
  236. self.threepidBinder = ThreepidBinder(self)
  237. self.sslComponents = SslComponents(self)
  238. self.clientApiHttpServer = ClientApiHttpServer(self)
  239. self.replicationHttpsServer = ReplicationHttpsServer(self)
  240. self.replicationHttpsClient = ReplicationHttpsClient(self)
  241. self.pusher = Pusher(self)
  242. # A dedicated validation session store just to clean up old sessions every N minutes
  243. self.cleanupValSession = ThreePidValSessionStore(self)
  244. cb = task.LoopingCall(self.cleanupValSession.deleteOldSessions)
  245. cb.clock = self.reactor
  246. cb.start(10 * 60.0)
  247. # workaround for https://github.com/getsentry/sentry-python/issues/803: we
  248. # disable automatic GC and run it periodically instead.
  249. gc.disable()
  250. cb = task.LoopingCall(run_gc)
  251. cb.clock = self.reactor
  252. cb.start(1.0)
  253. def save_config(self):
  254. fp = open(self.config_file, 'w')
  255. self.cfg.write(fp)
  256. fp.close()
  257. def run(self):
  258. self.clientApiHttpServer.setup()
  259. self.replicationHttpsServer.setup()
  260. self.pusher.setup()
  261. internalport = self.cfg.get('http', 'internalapi.http.port')
  262. if internalport:
  263. try:
  264. interface = self.cfg.get('http', 'internalapi.http.bind_address')
  265. except configparser.NoOptionError:
  266. interface = '::1'
  267. self.internalApiHttpServer = InternalApiHttpServer(self)
  268. self.internalApiHttpServer.setup(interface, int(internalport))
  269. if self.pidfile:
  270. with open(self.pidfile, 'w') as pidfile:
  271. pidfile.write(str(os.getpid()) + "\n")
  272. self.reactor.run()
  273. def ip_from_request(self, request):
  274. if (self.cfg.get('http', 'obey_x_forwarded_for') and
  275. request.requestHeaders.hasHeader("X-Forwarded-For")):
  276. return request.requestHeaders.getRawHeaders("X-Forwarded-For")[0]
  277. return request.getClientIP()
  278. class Validators:
  279. pass
  280. class Servlets:
  281. pass
  282. class Keyring:
  283. pass
  284. def parse_config_dict(config_dict):
  285. """Parse the given config from a dictionary, populating missing items and sections
  286. Args:
  287. config_dict (dict): the configuration dictionary to be parsed
  288. """
  289. # Build a config dictionary from the defaults merged with the given dictionary
  290. config = copy.deepcopy(CONFIG_DEFAULTS)
  291. for section, section_dict in config_dict.items():
  292. if section not in config:
  293. config[section] = {}
  294. for option in section_dict.keys():
  295. config[section][option] = config_dict[section][option]
  296. # Build a ConfigParser from the merged dictionary
  297. cfg = configparser.ConfigParser()
  298. for section, section_dict in config.items():
  299. cfg.add_section(section)
  300. for option, value in section_dict.items():
  301. cfg.set(section, option, value)
  302. return cfg
  303. def parse_config_file(config_file):
  304. """Parse the given config from a filepath, populating missing items and
  305. sections
  306. Args:
  307. config_file (str): the file to be parsed
  308. """
  309. # if the config file doesn't exist, prepopulate the config object
  310. # with the defaults, in the right section.
  311. #
  312. # otherwise, we have to put the defaults in the DEFAULT section,
  313. # to ensure that they don't override anyone's settings which are
  314. # in their config file in the default section (which is likely,
  315. # because sydent used to be braindead).
  316. use_defaults = not os.path.exists(config_file)
  317. cfg = configparser.ConfigParser()
  318. for sect, entries in CONFIG_DEFAULTS.items():
  319. cfg.add_section(sect)
  320. for k, v in entries.items():
  321. cfg.set(configparser.DEFAULTSECT if use_defaults else sect, k, v)
  322. cfg.read(config_file)
  323. return cfg
  324. def setup_logging(cfg):
  325. log_format = (
  326. "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s"
  327. " - %(message)s"
  328. )
  329. formatter = logging.Formatter(log_format)
  330. logPath = cfg.get('general', "log.path")
  331. if logPath != '':
  332. handler = logging.handlers.TimedRotatingFileHandler(
  333. logPath, when='midnight', backupCount=365
  334. )
  335. handler.setFormatter(formatter)
  336. def sighup(signum, stack):
  337. logger.info("Closing log file due to SIGHUP")
  338. handler.doRollover()
  339. logger.info("Opened new log file due to SIGHUP")
  340. else:
  341. handler = logging.StreamHandler()
  342. handler.setFormatter(formatter)
  343. rootLogger = logging.getLogger('')
  344. rootLogger.setLevel(cfg.get('general', 'log.level'))
  345. rootLogger.addHandler(handler)
  346. observer = log.PythonLoggingObserver()
  347. observer.start()
  348. def get_config_file_path():
  349. return os.environ.get('SYDENT_CONF', "sydent.conf")
  350. def parse_cfg_bool(value):
  351. return value.lower() == "true"
  352. def run_gc():
  353. threshold = gc.get_threshold()
  354. counts = gc.get_count()
  355. for i in reversed(range(len(threshold))):
  356. if threshold[i] < counts[i]:
  357. gc.collect(i)
  358. if __name__ == '__main__':
  359. cfg = parse_config_file(get_config_file_path())
  360. setup_logging(cfg)
  361. syd = Sydent(cfg)
  362. syd.run()