sydent.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 OpenMarket Ltd
  3. # Copyright 2018 New Vector Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import ConfigParser
  17. import logging
  18. import logging.handlers
  19. import os
  20. import twisted.internet.reactor
  21. from twisted.python import log
  22. from db.sqlitedb import SqliteDatabase
  23. from netaddr import IPSet, IPNetwork
  24. from http.httpcommon import SslComponents
  25. from http.httpserver import (
  26. ClientApiHttpServer, ReplicationHttpsServer,
  27. InternalApiHttpServer,
  28. )
  29. from http.httpsclient import ReplicationHttpsClient
  30. from http.servlets.blindlysignstuffservlet import BlindlySignStuffServlet
  31. from http.servlets.pubkeyservlets import EphemeralPubkeyIsValidServlet, PubkeyIsValidServlet
  32. from validators.emailvalidator import EmailValidator
  33. from validators.msisdnvalidator import MsisdnValidator
  34. from hs_federation.verifier import Verifier
  35. from sign.ed25519 import SydentEd25519
  36. from http.servlets.emailservlet import EmailRequestCodeServlet, EmailValidateCodeServlet
  37. from http.servlets.msisdnservlet import MsisdnRequestCodeServlet, MsisdnValidateCodeServlet
  38. from http.servlets.lookupservlet import LookupServlet
  39. from http.servlets.bulklookupservlet import BulkLookupServlet
  40. from http.servlets.pubkeyservlets import Ed25519Servlet
  41. from http.servlets.threepidbindservlet import ThreePidBindServlet
  42. from http.servlets.threepidunbindservlet import ThreePidUnbindServlet
  43. from http.servlets.replication import ReplicationPushServlet
  44. from http.servlets.getvalidated3pidservlet import GetValidated3pidServlet
  45. from http.servlets.store_invite_servlet import StoreInviteServlet
  46. from http.servlets.infoservlet import InfoServlet
  47. from http.servlets.profilereplicationservlet import ProfileReplicationServlet
  48. from http.servlets.userdirectorysearchservlet import UserDirectorySearchServlet
  49. from http.servlets.v1_servlet import V1Servlet
  50. from threepid.bind import ThreepidBinder
  51. from replication.pusher import Pusher
  52. logger = logging.getLogger(__name__)
  53. def list_from_comma_sep_string(rawstr):
  54. if rawstr == '':
  55. return []
  56. return [x.strip() for x in rawstr.split(',')]
  57. CONFIG_DEFAULTS = {
  58. 'general': {
  59. 'server.name': '',
  60. 'log.path': '',
  61. 'pidfile.path': 'sydent.pid',
  62. 'shadow.hs.master': '',
  63. 'shadow.hs.slave': '',
  64. 'ips.nonshadow': '', # comma separated list of CIDR ranges which /info will return non-shadow HS to.
  65. },
  66. 'db': {
  67. 'db.file': 'sydent.db',
  68. },
  69. 'http': {
  70. 'clientapi.http.port': '8090',
  71. 'internalapi.http.port': '',
  72. 'replication.https.certfile': '',
  73. 'replication.https.cacert': '', # This should only be used for testing
  74. 'replication.https.port': '4434',
  75. 'obey_x_forwarded_for': 'False',
  76. },
  77. 'email': {
  78. 'email.template': 'res/email.template',
  79. 'email.from': 'Sydent Validation <noreply@{hostname}>',
  80. 'email.subject': 'Your Validation Token',
  81. 'email.invite.subject': '%(sender_display_name)s has invited you to chat',
  82. 'email.smtphost': 'localhost',
  83. 'email.smtpport': '25',
  84. 'email.smtpusername': '',
  85. 'email.smtppassword': '',
  86. 'email.hostname': '',
  87. 'email.tlsmode': '0',
  88. },
  89. 'sms': {
  90. 'bodyTemplate': 'Your code is {token}',
  91. },
  92. 'crypto': {
  93. 'ed25519.signingkey': '',
  94. },
  95. 'userdir': {
  96. 'userdir.allowed_homeservers': '',
  97. },
  98. }
  99. class Sydent:
  100. def __init__(self):
  101. self.config_file = os.environ.get('SYDENT_CONF', "sydent.conf")
  102. self.cfg = parse_config(self.config_file)
  103. log_format = (
  104. "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s"
  105. " - %(message)s"
  106. )
  107. formatter = logging.Formatter(log_format)
  108. logPath = self.cfg.get('general', "log.path")
  109. if logPath != '':
  110. handler = logging.handlers.TimedRotatingFileHandler(
  111. logPath, when='midnight', backupCount=365
  112. )
  113. handler.setFormatter(formatter)
  114. def sighup(signum, stack):
  115. logger.info("Closing log file due to SIGHUP")
  116. handler.doRollover()
  117. logger.info("Opened new log file due to SIGHUP")
  118. else:
  119. handler = logging.StreamHandler()
  120. handler.setFormatter(formatter)
  121. rootLogger = logging.getLogger('')
  122. rootLogger.setLevel(logging.INFO)
  123. rootLogger.addHandler(handler)
  124. logger.info("Starting Sydent server")
  125. self.pidfile = self.cfg.get('general', "pidfile.path");
  126. self.nonshadow_ips = None
  127. ips = self.cfg.get('general', "ips.nonshadow");
  128. if ips:
  129. self.nonshadow_ips = IPSet()
  130. ips = ips.split(',')
  131. for ip in ips:
  132. self.nonshadow_ips.add(IPNetwork(ip))
  133. observer = log.PythonLoggingObserver()
  134. observer.start()
  135. self.db = SqliteDatabase(self).db
  136. self.server_name = self.cfg.get('general', 'server.name')
  137. if self.server_name == '':
  138. self.server_name = os.uname()[1]
  139. logger.warn(("You had not specified a server name. I have guessed that this server is called '%s' "
  140. + " and saved this in the config file. If this is incorrect, you should edit server.name in "
  141. + "the config file.") % (self.server_name,))
  142. self.cfg.set('general', 'server.name', self.server_name)
  143. self.save_config()
  144. self.shadow_hs_master = self.cfg.get('general', 'shadow.hs.master')
  145. self.shadow_hs_slave = self.cfg.get('general', 'shadow.hs.slave')
  146. self.user_dir_allowed_hses = set(list_from_comma_sep_string(
  147. self.cfg.get('userdir', 'userdir.allowed_homeservers', '')
  148. ))
  149. self.validators = Validators()
  150. self.validators.email = EmailValidator(self)
  151. self.validators.msisdn = MsisdnValidator(self)
  152. self.keyring = Keyring()
  153. self.keyring.ed25519 = SydentEd25519(self).signing_key
  154. self.keyring.ed25519.alg = 'ed25519'
  155. self.sig_verifier = Verifier(self)
  156. self.servlets = Servlets()
  157. self.servlets.v1 = V1Servlet(self)
  158. self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
  159. self.servlets.emailValidate = EmailValidateCodeServlet(self)
  160. self.servlets.msisdnRequestCode = MsisdnRequestCodeServlet(self)
  161. self.servlets.msisdnValidate = MsisdnValidateCodeServlet(self)
  162. self.servlets.lookup = LookupServlet(self)
  163. self.servlets.bulk_lookup = BulkLookupServlet(self)
  164. self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
  165. self.servlets.pubkeyIsValid = PubkeyIsValidServlet(self)
  166. self.servlets.ephemeralPubkeyIsValid = EphemeralPubkeyIsValidServlet(self)
  167. self.servlets.threepidBind = ThreePidBindServlet(self)
  168. self.servlets.threepidUnbind = ThreePidUnbindServlet(self)
  169. self.servlets.replicationPush = ReplicationPushServlet(self)
  170. self.servlets.getValidated3pid = GetValidated3pidServlet(self)
  171. self.servlets.storeInviteServlet = StoreInviteServlet(self)
  172. self.servlets.blindlySignStuffServlet = BlindlySignStuffServlet(self)
  173. self.servlets.profileReplicationServlet = ProfileReplicationServlet(self)
  174. self.servlets.info = InfoServlet(self)
  175. self.servlets.userDirectorySearchServlet = UserDirectorySearchServlet(self)
  176. self.threepidBinder = ThreepidBinder(self)
  177. self.sslComponents = SslComponents(self)
  178. self.clientApiHttpServer = ClientApiHttpServer(self)
  179. self.replicationHttpsServer = ReplicationHttpsServer(self)
  180. self.replicationHttpsClient = ReplicationHttpsClient(self)
  181. self.pusher = Pusher(self)
  182. def save_config(self):
  183. fp = open(self.config_file, 'w')
  184. self.cfg.write(fp)
  185. fp.close()
  186. def run(self):
  187. self.clientApiHttpServer.setup()
  188. self.replicationHttpsServer.setup()
  189. self.pusher.setup()
  190. internalport = self.cfg.get('http', 'internalapi.http.port')
  191. if internalport:
  192. try:
  193. interface = self.cfg.get('http', 'internalapi.http.bind_address')
  194. except ConfigParser.NoOptionError:
  195. interface = '::1'
  196. self.internalApiHttpServer = InternalApiHttpServer(self)
  197. self.internalApiHttpServer.setup(interface, int(internalport))
  198. if self.pidfile:
  199. with open(self.pidfile, 'w') as pidfile:
  200. pidfile.write(str(os.getpid()) + "\n")
  201. twisted.internet.reactor.run()
  202. def ip_from_request(self, request):
  203. if (self.cfg.get('http', 'obey_x_forwarded_for') and
  204. request.requestHeaders.hasHeader("X-Forwarded-For")):
  205. return request.requestHeaders.getRawHeaders("X-Forwarded-For")[0]
  206. return request.getClientIP()
  207. class Validators:
  208. pass
  209. class Servlets:
  210. pass
  211. class Keyring:
  212. pass
  213. def parse_config(config_file):
  214. """Parse the given config file, populating missing items and sections
  215. Args:
  216. config_file (str): the file to be parsed
  217. """
  218. cfg = ConfigParser.SafeConfigParser()
  219. # if the config file doesn't exist, prepopulate the config object
  220. # with the defaults, in the right section.
  221. if not os.path.exists(config_file):
  222. for sect, entries in CONFIG_DEFAULTS.items():
  223. cfg.add_section(sect)
  224. for k, v in entries.items():
  225. cfg.set(sect, k, v)
  226. else:
  227. # otherwise, we have to put the defaults in the DEFAULT section,
  228. # to ensure that they don't override anyone's settings which are
  229. # in their config file in the default section (which is likely,
  230. # because sydent used to be braindead).
  231. for sect, entries in CONFIG_DEFAULTS.items():
  232. cfg.add_section(sect)
  233. for k, v in entries.items():
  234. cfg.set(ConfigParser.DEFAULTSECT, k, v)
  235. cfg.read(config_file)
  236. return cfg
  237. if __name__ == '__main__':
  238. syd = Sydent()
  239. syd.run()