appservice.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. from typing import Dict
  16. from six import string_types
  17. from six.moves.urllib import parse as urlparse
  18. import yaml
  19. from netaddr import IPSet
  20. from synapse.appservice import ApplicationService
  21. from synapse.types import UserID
  22. from ._base import Config, ConfigError
  23. logger = logging.getLogger(__name__)
  24. class AppServiceConfig(Config):
  25. section = "appservice"
  26. def read_config(self, config, **kwargs):
  27. self.app_service_config_files = config.get("app_service_config_files", [])
  28. self.notify_appservices = config.get("notify_appservices", True)
  29. self.track_appservice_user_ips = config.get("track_appservice_user_ips", False)
  30. def generate_config_section(cls, **kwargs):
  31. return """\
  32. # A list of application service config files to use
  33. #
  34. #app_service_config_files:
  35. # - app_service_1.yaml
  36. # - app_service_2.yaml
  37. # Uncomment to enable tracking of application service IP addresses. Implicitly
  38. # enables MAU tracking for application service users.
  39. #
  40. #track_appservice_user_ips: true
  41. """
  42. def load_appservices(hostname, config_files):
  43. """Returns a list of Application Services from the config files."""
  44. if not isinstance(config_files, list):
  45. logger.warning("Expected %s to be a list of AS config files.", config_files)
  46. return []
  47. # Dicts of value -> filename
  48. seen_as_tokens = {} # type: Dict[str, str]
  49. seen_ids = {} # type: Dict[str, str]
  50. appservices = []
  51. for config_file in config_files:
  52. try:
  53. with open(config_file, "r") as f:
  54. appservice = _load_appservice(hostname, yaml.safe_load(f), config_file)
  55. if appservice.id in seen_ids:
  56. raise ConfigError(
  57. "Cannot reuse ID across application services: "
  58. "%s (files: %s, %s)"
  59. % (appservice.id, config_file, seen_ids[appservice.id])
  60. )
  61. seen_ids[appservice.id] = config_file
  62. if appservice.token in seen_as_tokens:
  63. raise ConfigError(
  64. "Cannot reuse as_token across application services: "
  65. "%s (files: %s, %s)"
  66. % (
  67. appservice.token,
  68. config_file,
  69. seen_as_tokens[appservice.token],
  70. )
  71. )
  72. seen_as_tokens[appservice.token] = config_file
  73. logger.info("Loaded application service: %s", appservice)
  74. appservices.append(appservice)
  75. except Exception as e:
  76. logger.error("Failed to load appservice from '%s'", config_file)
  77. logger.exception(e)
  78. raise
  79. return appservices
  80. def _load_appservice(hostname, as_info, config_filename):
  81. required_string_fields = ["id", "as_token", "hs_token", "sender_localpart"]
  82. for field in required_string_fields:
  83. if not isinstance(as_info.get(field), string_types):
  84. raise KeyError(
  85. "Required string field: '%s' (%s)" % (field, config_filename)
  86. )
  87. # 'url' must either be a string or explicitly null, not missing
  88. # to avoid accidentally turning off push for ASes.
  89. if (
  90. not isinstance(as_info.get("url"), string_types)
  91. and as_info.get("url", "") is not None
  92. ):
  93. raise KeyError(
  94. "Required string field or explicit null: 'url' (%s)" % (config_filename,)
  95. )
  96. localpart = as_info["sender_localpart"]
  97. if urlparse.quote(localpart) != localpart:
  98. raise ValueError("sender_localpart needs characters which are not URL encoded.")
  99. user = UserID(localpart, hostname)
  100. user_id = user.to_string()
  101. # Rate limiting for users of this AS is on by default (excludes sender)
  102. rate_limited = True
  103. if isinstance(as_info.get("rate_limited"), bool):
  104. rate_limited = as_info.get("rate_limited")
  105. # namespace checks
  106. if not isinstance(as_info.get("namespaces"), dict):
  107. raise KeyError("Requires 'namespaces' object.")
  108. for ns in ApplicationService.NS_LIST:
  109. # specific namespaces are optional
  110. if ns in as_info["namespaces"]:
  111. # expect a list of dicts with exclusive and regex keys
  112. for regex_obj in as_info["namespaces"][ns]:
  113. if not isinstance(regex_obj, dict):
  114. raise ValueError(
  115. "Expected namespace entry in %s to be an object, but got %s",
  116. ns,
  117. regex_obj,
  118. )
  119. if not isinstance(regex_obj.get("regex"), string_types):
  120. raise ValueError("Missing/bad type 'regex' key in %s", regex_obj)
  121. if not isinstance(regex_obj.get("exclusive"), bool):
  122. raise ValueError(
  123. "Missing/bad type 'exclusive' key in %s", regex_obj
  124. )
  125. # protocols check
  126. protocols = as_info.get("protocols")
  127. if protocols:
  128. # Because strings are lists in python
  129. if isinstance(protocols, str) or not isinstance(protocols, list):
  130. raise KeyError("Optional 'protocols' must be a list if present.")
  131. for p in protocols:
  132. if not isinstance(p, str):
  133. raise KeyError("Bad value for 'protocols' item")
  134. if as_info["url"] is None:
  135. logger.info(
  136. "(%s) Explicitly empty 'url' provided. This application service"
  137. " will not receive events or queries.",
  138. config_filename,
  139. )
  140. ip_range_whitelist = None
  141. if as_info.get("ip_range_whitelist"):
  142. ip_range_whitelist = IPSet(as_info.get("ip_range_whitelist"))
  143. return ApplicationService(
  144. token=as_info["as_token"],
  145. hostname=hostname,
  146. url=as_info["url"],
  147. namespaces=as_info["namespaces"],
  148. hs_token=as_info["hs_token"],
  149. sender=user_id,
  150. id=as_info["id"],
  151. protocols=protocols,
  152. rate_limited=rate_limited,
  153. ip_range_whitelist=ip_range_whitelist,
  154. )