sydent.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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. import ConfigParser
  18. import logging
  19. import logging.handlers
  20. import os
  21. import pickle
  22. import twisted.internet.reactor
  23. from twisted.internet import task
  24. from twisted.python import log
  25. from db.sqlitedb import SqliteDatabase
  26. from http.httpcommon import SslComponents
  27. from http.httpserver import (
  28. ClientApiHttpServer, ReplicationHttpsServer,
  29. InternalApiHttpServer,
  30. )
  31. from http.httpsclient import ReplicationHttpsClient
  32. from http.servlets.blindlysignstuffservlet import BlindlySignStuffServlet
  33. from http.servlets.pubkeyservlets import EphemeralPubkeyIsValidServlet, PubkeyIsValidServlet
  34. from http.servlets.termsservlet import TermsServlet
  35. from validators.emailvalidator import EmailValidator
  36. from validators.msisdnvalidator import MsisdnValidator
  37. from hs_federation.verifier import Verifier
  38. from util.hash import sha256_and_url_safe_base64
  39. from util.tokenutils import generateAlphanumericTokenOfLength
  40. from sign.ed25519 import SydentEd25519
  41. from http.servlets.emailservlet import EmailRequestCodeServlet, EmailValidateCodeServlet
  42. from http.servlets.msisdnservlet import MsisdnRequestCodeServlet, MsisdnValidateCodeServlet
  43. from http.servlets.lookupservlet import LookupServlet
  44. from http.servlets.bulklookupservlet import BulkLookupServlet
  45. from http.servlets.lookupv2servlet import LookupV2Servlet
  46. from http.servlets.hashdetailsservlet import HashDetailsServlet
  47. from http.servlets.pubkeyservlets import Ed25519Servlet
  48. from http.servlets.threepidbindservlet import ThreePidBindServlet
  49. from http.servlets.threepidunbindservlet import ThreePidUnbindServlet
  50. from http.servlets.replication import ReplicationPushServlet
  51. from http.servlets.getvalidated3pidservlet import GetValidated3pidServlet
  52. from http.servlets.store_invite_servlet import StoreInviteServlet
  53. from http.servlets.v1_servlet import V1Servlet
  54. from http.servlets.accountservlet import AccountServlet
  55. from http.servlets.registerservlet import RegisterServlet
  56. from http.servlets.logoutservlet import LogoutServlet
  57. from http.servlets.v2_servlet import V2Servlet
  58. from db.valsession import ThreePidValSessionStore
  59. from db.hashing_metadata import HashingMetadataStore
  60. from threepid.bind import ThreepidBinder
  61. from replication.pusher import Pusher
  62. logger = logging.getLogger(__name__)
  63. CONFIG_DEFAULTS = {
  64. 'general': {
  65. 'server.name': '',
  66. 'log.path': '',
  67. 'log.level': 'INFO',
  68. 'pidfile.path': 'sydent.pid',
  69. 'terms.path': '',
  70. 'address_lookup_limit': '10000', # Maximum amount of addresses in a single /lookup request
  71. # The following can be added to your local config file to enable prometheus
  72. # support.
  73. # 'prometheus_port': '8080', # The port to serve metrics on
  74. # 'prometheus_addr': '', # The address to bind to. Empty string means bind to all.
  75. # The following can be added to your local config file to enable sentry support.
  76. # 'sentry_dsn': 'https://...' # The DSN has configured in the sentry instance project.
  77. },
  78. 'db': {
  79. 'db.file': 'sydent.db',
  80. },
  81. 'http': {
  82. 'clientapi.http.bind_address': '::',
  83. 'clientapi.http.port': '8090',
  84. # internalapi.http.bind_address defaults to '::1'
  85. 'internalapi.http.port': '',
  86. 'replication.https.certfile': '',
  87. 'replication.https.cacert': '', # This should only be used for testing
  88. 'replication.https.bind_address': '::',
  89. 'replication.https.port': '4434',
  90. 'obey_x_forwarded_for': 'False',
  91. 'federation.verifycerts': 'True',
  92. },
  93. 'email': {
  94. 'email.template': 'res/email.template',
  95. 'email.from': 'Sydent Validation <noreply@{hostname}>',
  96. 'email.subject': 'Your Validation Token',
  97. 'email.invite.subject': '%(sender_display_name)s has invited you to chat',
  98. 'email.smtphost': 'localhost',
  99. 'email.smtpport': '25',
  100. 'email.smtpusername': '',
  101. 'email.smtppassword': '',
  102. 'email.hostname': '',
  103. 'email.tlsmode': '0',
  104. },
  105. 'sms': {
  106. 'bodyTemplate': 'Your code is {token}',
  107. },
  108. 'crypto': {
  109. 'ed25519.signingkey': '',
  110. },
  111. }
  112. class Sydent:
  113. def __init__(self):
  114. self.config_file = os.environ.get('SYDENT_CONF', "sydent.conf")
  115. self.cfg = parse_config(self.config_file)
  116. log_format = (
  117. "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s"
  118. " - %(message)s"
  119. )
  120. formatter = logging.Formatter(log_format)
  121. logPath = self.cfg.get('general', "log.path")
  122. if logPath != '':
  123. handler = logging.handlers.TimedRotatingFileHandler(
  124. logPath, when='midnight', backupCount=365
  125. )
  126. handler.setFormatter(formatter)
  127. def sighup(signum, stack):
  128. logger.info("Closing log file due to SIGHUP")
  129. handler.doRollover()
  130. logger.info("Opened new log file due to SIGHUP")
  131. else:
  132. handler = logging.StreamHandler()
  133. handler.setFormatter(formatter)
  134. rootLogger = logging.getLogger('')
  135. rootLogger.setLevel(self.cfg.get('general', 'log.level'))
  136. rootLogger.addHandler(handler)
  137. logger.info("Starting Sydent server")
  138. self.pidfile = self.cfg.get('general', "pidfile.path");
  139. observer = log.PythonLoggingObserver()
  140. observer.start()
  141. self.db = SqliteDatabase(self).db
  142. self.server_name = self.cfg.get('general', 'server.name')
  143. if self.server_name == '':
  144. self.server_name = os.uname()[1]
  145. logger.warn(("You had not specified a server name. I have guessed that this server is called '%s' "
  146. + "and saved this in the config file. If this is incorrect, you should edit server.name in "
  147. + "the config file.") % (self.server_name,))
  148. self.cfg.set('general', 'server.name', self.server_name)
  149. self.save_config()
  150. if self.cfg.has_option("general", "sentry_dsn"):
  151. # Only import and start sentry SDK if configured.
  152. import sentry_sdk
  153. sentry_sdk.init(
  154. dsn=self.cfg.get("general", "sentry_dsn"),
  155. )
  156. with sentry_sdk.configure_scope() as scope:
  157. scope.set_tag("sydent_server_name", self.server_name)
  158. if self.cfg.has_option("general", "prometheus_port"):
  159. import prometheus_client
  160. prometheus_client.start_http_server(
  161. port=self.cfg.getint("general", "prometheus_port"),
  162. addr=self.cfg.get("general", "prometheus_addr"),
  163. )
  164. # See if a pepper already exists in the database
  165. # Note: This MUST be run before we start serving requests, otherwise lookups for
  166. # 3PID hashes may come in before we've completed generating them
  167. hashing_metadata_store = HashingMetadataStore(self)
  168. lookup_pepper = hashing_metadata_store.get_lookup_pepper()
  169. if not lookup_pepper:
  170. # No pepper defined in the database, generate one
  171. lookup_pepper = generateAlphanumericTokenOfLength(5)
  172. # Store it in the database and rehash 3PIDs
  173. hashing_metadata_store.store_lookup_pepper(sha256_and_url_safe_base64,
  174. lookup_pepper)
  175. self.validators = Validators()
  176. self.validators.email = EmailValidator(self)
  177. self.validators.msisdn = MsisdnValidator(self)
  178. self.keyring = Keyring()
  179. self.keyring.ed25519 = SydentEd25519(self).signing_key
  180. self.keyring.ed25519.alg = 'ed25519'
  181. self.sig_verifier = Verifier(self)
  182. self.servlets = Servlets()
  183. self.servlets.v1 = V1Servlet(self)
  184. self.servlets.v2 = V2Servlet(self)
  185. self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
  186. self.servlets.emailValidate = EmailValidateCodeServlet(self)
  187. self.servlets.msisdnRequestCode = MsisdnRequestCodeServlet(self)
  188. self.servlets.msisdnValidate = MsisdnValidateCodeServlet(self)
  189. self.servlets.lookup = LookupServlet(self)
  190. self.servlets.bulk_lookup = BulkLookupServlet(self)
  191. self.servlets.hash_details = HashDetailsServlet(self, lookup_pepper)
  192. self.servlets.lookup_v2 = LookupV2Servlet(self, lookup_pepper)
  193. self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
  194. self.servlets.pubkeyIsValid = PubkeyIsValidServlet(self)
  195. self.servlets.ephemeralPubkeyIsValid = EphemeralPubkeyIsValidServlet(self)
  196. self.servlets.threepidBind = ThreePidBindServlet(self)
  197. self.servlets.threepidUnbind = ThreePidUnbindServlet(self)
  198. self.servlets.replicationPush = ReplicationPushServlet(self)
  199. self.servlets.getValidated3pid = GetValidated3pidServlet(self)
  200. self.servlets.storeInviteServlet = StoreInviteServlet(self)
  201. self.servlets.blindlySignStuffServlet = BlindlySignStuffServlet(self)
  202. self.servlets.termsServlet = TermsServlet(self)
  203. self.servlets.accountServlet = AccountServlet(self)
  204. self.servlets.registerServlet = RegisterServlet(self)
  205. self.servlets.logoutServlet = LogoutServlet(self)
  206. self.threepidBinder = ThreepidBinder(self)
  207. self.sslComponents = SslComponents(self)
  208. self.clientApiHttpServer = ClientApiHttpServer(self)
  209. self.replicationHttpsServer = ReplicationHttpsServer(self)
  210. self.replicationHttpsClient = ReplicationHttpsClient(self)
  211. self.pusher = Pusher(self)
  212. # A dedicated validation session store just to clean up old sessions every N minutes
  213. self.cleanupValSession = ThreePidValSessionStore(self)
  214. cb = task.LoopingCall(self.cleanupValSession.deleteOldSessions)
  215. cb.start(10 * 60.0)
  216. def save_config(self):
  217. fp = open(self.config_file, 'w')
  218. self.cfg.write(fp)
  219. fp.close()
  220. def run(self):
  221. self.clientApiHttpServer.setup()
  222. self.replicationHttpsServer.setup()
  223. self.pusher.setup()
  224. internalport = self.cfg.get('http', 'internalapi.http.port')
  225. if internalport:
  226. try:
  227. interface = self.cfg.get('http', 'internalapi.http.bind_address')
  228. except ConfigParser.NoOptionError:
  229. interface = '::1'
  230. self.internalApiHttpServer = InternalApiHttpServer(self)
  231. self.internalApiHttpServer.setup(interface, int(internalport))
  232. if self.pidfile:
  233. with open(self.pidfile, 'w') as pidfile:
  234. pidfile.write(str(os.getpid()) + "\n")
  235. twisted.internet.reactor.run()
  236. def ip_from_request(self, request):
  237. if (self.cfg.get('http', 'obey_x_forwarded_for') and
  238. request.requestHeaders.hasHeader("X-Forwarded-For")):
  239. return request.requestHeaders.getRawHeaders("X-Forwarded-For")[0]
  240. return request.getClientIP()
  241. class Validators:
  242. pass
  243. class Servlets:
  244. pass
  245. class Keyring:
  246. pass
  247. def parse_config(config_file):
  248. """Parse the given config file, populating missing items and sections
  249. Args:
  250. config_file (str): the file to be parsed
  251. """
  252. cfg = ConfigParser.SafeConfigParser()
  253. # if the config file doesn't exist, prepopulate the config object
  254. # with the defaults, in the right section.
  255. if not os.path.exists(config_file):
  256. for sect, entries in CONFIG_DEFAULTS.items():
  257. cfg.add_section(sect)
  258. for k, v in entries.items():
  259. cfg.set(sect, k, v)
  260. else:
  261. # otherwise, we have to put the defaults in the DEFAULT section,
  262. # to ensure that they don't override anyone's settings which are
  263. # in their config file in the default section (which is likely,
  264. # because sydent used to be braindead).
  265. for sect, entries in CONFIG_DEFAULTS.items():
  266. cfg.add_section(sect)
  267. for k, v in entries.items():
  268. cfg.set(ConfigParser.DEFAULTSECT, k, v)
  269. cfg.read(config_file)
  270. return cfg
  271. if __name__ == '__main__':
  272. syd = Sydent()
  273. syd.run()