Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move the configuration file handling code into a separate module #385

Merged
merged 28 commits into from Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cf17843
Add SydentConfig class and use when calling Sydent constructor
Sep 7, 2021
2985b37
Move database config handling over to SydentConfig
Sep 7, 2021
3a1a400
Move crypto config handling over to SydentConfig
Sep 7, 2021
91fdc6b
Move sms config handling over to SydentConfig
Sep 7, 2021
4b0a900
Move deprecated email template config over to SydentConfig
Sep 7, 2021
21b030a
Move email config handled by synapse.py over to SynapseConfig
Sep 7, 2021
11f8711
Move rest of email config over to SydenConfig
Sep 7, 2021
f09779b
Move deprecated http template config over to SydentConfig
Sep 7, 2021
25aac48
Move rest of http config handling over to SynapseConfig
Sep 7, 2021
1492c1c
Move server name config handling to SydentConfig
Sep 7, 2021
bb02656
Move 'general' template config handling over to SydentConfig
Sep 7, 2021
2d460ee
Move rest of 'general' config handling over to SydentConfig
Sep 7, 2021
4b0eb67
Remove deprecated template argument from get_branded_template
Sep 8, 2021
37b928b
Remove old cfg argument from Sydent constructor
Sep 8, 2021
a9415f4
Add changelog
Sep 8, 2021
c0b7b03
Merge remote-tracking branch 'origin/main' into azren/move_config_cod…
Sep 9, 2021
cf58f3a
Add file that got lost while fixing merge conflicts
Sep 9, 2021
85bd376
Apply suggestions from code review
Sep 10, 2021
6cf0090
Run linters
Sep 10, 2021
b0ad7d0
Make initial read of internalapi.http.port a local variable
Sep 10, 2021
760963e
Standardize it being one empty line after licence
Sep 10, 2021
d2c24c3
Document more clearly that parse_config_file sets up logging
Sep 10, 2021
e4b0bcb
Merge remote-tracking branch 'origin/main' into azren/move_config_cod…
Sep 10, 2021
64d3d34
Readd lines between licence and code to make linters happy
Sep 10, 2021
e57fa59
Apply suggestions from code review
Sep 10, 2021
d12d283
Add missing file from last commit
Sep 10, 2021
e4ce136
Fix type issues in config code
Sep 13, 2021
98bc803
Merge remote-tracking branch 'origin/main' into azren/move_config_cod…
Sep 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions scripts/casefold_db.py
Expand Up @@ -23,7 +23,7 @@
import signedjson.sign

from sydent.config import SydentConfig
from sydent.sydent import Sydent, parse_config_file
from sydent.sydent import Sydent
from sydent.util import json_decoder
from sydent.util.emailutils import sendEmail
from sydent.util.hash import sha256_and_url_safe_base64
Expand Down Expand Up @@ -252,13 +252,11 @@ def update_global_associations(
print(f"The config file '{args.config_path}' does not exist.")
sys.exit(1)

config = parse_config_file(args.config_path)

sydent_config = SydentConfig()
sydent_config.parse_from_config_parser(config)
sydent_config.parse_config_file(args.config_path)

reactor = ResolvingMemoryReactorClock()
sydent = Sydent(config, sydent_config, reactor, False)
sydent = Sydent(sydent_config, reactor, False)

update_global_associations(sydent, sydent.db, not args.no_email, args.dry_run)
update_local_associations(sydent, sydent.db, not args.no_email, args.dry_run)
228 changes: 227 additions & 1 deletion sydent/config/__init__.py
Expand Up @@ -12,7 +12,13 @@
# See the License for the specific language governing permissions and
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved
# limitations under the License.

from configparser import ConfigParser
import copy
import logging
import os
from configparser import DEFAULTSECT, ConfigParser
from typing import Dict

from twisted.python import log

from sydent.config.crypto import CryptoConfig
from sydent.config.database import DatabaseConfig
Expand All @@ -21,6 +27,128 @@
from sydent.config.http import HTTPConfig
from sydent.config.sms import SMSConfig

logger = logging.getLogger(__name__)

CONFIG_DEFAULTS = {
"general": {
"server.name": os.environ.get("SYDENT_SERVER_NAME", ""),
"log.path": "",
"log.level": "INFO",
"pidfile.path": os.environ.get("SYDENT_PID_FILE", "sydent.pid"),
"terms.path": "",
"address_lookup_limit": "10000", # Maximum amount of addresses in a single /lookup request
# The root path to use for load templates. This should contain branded
# directories. Each directory should contain the following templates:
#
# * invite_template.eml
# * verification_template.eml
# * verify_response_template.html
"templates.path": "res",
# The brand directory to use if no brand hint (or an invalid brand hint)
# is provided by the request.
"brand.default": "matrix-org",
# The following can be added to your local config file to enable prometheus
# support.
# 'prometheus_port': '8080', # The port to serve metrics on
# 'prometheus_addr': '', # The address to bind to. Empty string means bind to all.
# The following can be added to your local config file to enable sentry support.
# 'sentry_dsn': 'https://...' # The DSN has configured in the sentry instance project.
# Whether clients and homeservers can register an association using v1 endpoints.
"enable_v1_associations": "true",
"delete_tokens_on_bind": "true",
# Prevent outgoing requests from being sent to the following blacklisted
# IP address CIDR ranges. If this option is not specified or empty then
# it defaults to private IP address ranges.
#
# The blacklist applies to all outbound requests except replication
# requests.
#
# (0.0.0.0 and :: are always blacklisted, whether or not they are
# explicitly listed here, since they correspond to unroutable
# addresses.)
"ip.blacklist": "",
# List of IP address CIDR ranges that should be allowed for outbound
# requests. This is useful for specifying exceptions to wide-ranging
# blacklisted target IP ranges.
#
# This whitelist overrides `ip.blacklist` and defaults to an empty
# list.
"ip.whitelist": "",
},
"db": {
"db.file": os.environ.get("SYDENT_DB_PATH", "sydent.db"),
},
"http": {
"clientapi.http.bind_address": "::",
"clientapi.http.port": "8090",
"internalapi.http.bind_address": "::1",
"internalapi.http.port": "",
"replication.https.certfile": "",
"replication.https.cacert": "", # This should only be used for testing
"replication.https.bind_address": "::",
"replication.https.port": "4434",
"obey_x_forwarded_for": "False",
"federation.verifycerts": "True",
# verify_response_template is deprecated, but still used if defined. Define
# templates.path and brand.default under general instead.
#
# 'verify_response_template': 'res/verify_response_page_template',
"client_http_base": "",
},
"email": {
# email.template and email.invite_template are deprecated, but still used
# if defined. Define templates.path and brand.default under general instead.
#
# 'email.template': 'res/verification_template.eml',
# 'email.invite_template': 'res/invite_template.eml',
"email.from": "Sydent Validation <noreply@{hostname}>",
"email.subject": "Your Validation Token",
"email.invite.subject": "%(sender_display_name)s has invited you to chat",
"email.invite.subject_space": "%(sender_display_name)s has invited you to a space",
"email.smtphost": "localhost",
"email.smtpport": "25",
"email.smtpusername": "",
"email.smtppassword": "",
"email.hostname": "",
"email.tlsmode": "0",
# The web client location which will be used if it is not provided by
# the homeserver.
#
# This should be the scheme and hostname only, see res/invite_template.eml
# for the full URL that gets generated.
"email.default_web_client_location": "https://app.element.io",
# When a user is invited to a room via their email address, that invite is
# displayed in the room list using an obfuscated version of the user's email
# address. These config options determine how much of the email address to
# obfuscate. Note that the '@' sign is always included.
#
# If the string is longer than a configured limit below, it is truncated to that limit
# with '...' added. Otherwise:
#
# * If the string is longer than 5 characters, it is truncated to 3 characters + '...'
# * If the string is longer than 1 character, it is truncated to 1 character + '...'
# * If the string is 1 character long, it is converted to '...'
#
# This ensures that a full email address is never shown, even if it is extremely
# short.
#
# The number of characters from the beginning to reveal of the email's username
# portion (left of the '@' sign)
"email.third_party_invite_username_obfuscate_characters": "3",
# The number of characters from the beginning to reveal of the email's domain
# portion (right of the '@' sign)
"email.third_party_invite_domain_obfuscate_characters": "3",
},
"sms": {
"bodyTemplate": "Your code is {token}",
"username": "",
"password": "",
},
"crypto": {
"ed25519.signingkey": "",
},
}


class ConfigError(Exception):
pass
Expand All @@ -30,6 +158,10 @@ class SydentConfig:
"""This is the class in charge of handling Sydent's configuration.
Handling of each individual section is delegated to other classes
stored in a `config_sections` list.

To use this class, create a new object and then call one of
`parse_config_file` or `parse_config_dict` before creating the
Sydent object that uses it.
"""

def __init__(self):
Expand Down Expand Up @@ -73,3 +205,97 @@ def parse_from_config_parser(self, cfg: ConfigParser) -> bool:
# user has asked for this specifially (e.g. on first
# run only, or when specify --generate-config)
return self.crypto.save_key
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved

def parse_config_file(self, config_file: str) -> None:
"""
Parse the given config from a filepath, populating missing items and
sections

:param config_file: the file to be parsed
"""
# If the config file doesn't exist, prepopulate the config object
# with the defaults, in the right section.
#
# Otherwise, we have to put the defaults in the DEFAULT section,
# to ensure that they don't override anyone's settings which are
# in their config file in the default section (which is likely,
# because sydent used to be braindead).
use_defaults = not os.path.exists(config_file)

cfg = ConfigParser()
for sect, entries in CONFIG_DEFAULTS.items():
cfg.add_section(sect)
for k, v in entries.items():
cfg.set(DEFAULTSECT if use_defaults else sect, k, v)

cfg.read(config_file)

# Logging is configured in cfg, but these options must be parsed first
# so that we can log while parsing the rest
setup_logging(cfg)
richvdh marked this conversation as resolved.
Show resolved Hide resolved

needs_saving = self.parse_from_config_parser(cfg)

if needs_saving:
fp = open(config_file, "w")
cfg.write(fp)
fp.close()

def parse_config_dict(self, config_dict: Dict) -> None:
"""
Parse the given config from a dictionary, populating missing items and sections

:param config_dict: the configuration dictionary to be parsed
"""
# Build a config dictionary from the defaults merged with the given dictionary
config = copy.deepcopy(CONFIG_DEFAULTS)
for section, section_dict in config_dict.items():
if section not in config:
config[section] = {}
for option in section_dict.keys():
config[section][option] = config_dict[section][option]

# Build a ConfigParser from the merged dictionary
cfg = ConfigParser()
for section, section_dict in config.items():
cfg.add_section(section)
for option, value in section_dict.items():
cfg.set(section, option, value)

# This is only ever called by tests so don't configure logging
# as tests do this themselves

self.parse_from_config_parser(cfg)


def setup_logging(cfg: ConfigParser) -> None:
"""
Setup logging using the options selected in the config

:param cfg: the configuration
"""
log_format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s" " - %(message)s"
formatter = logging.Formatter(log_format)

logPath = cfg.get("general", "log.path")
if logPath != "":
handler = logging.handlers.TimedRotatingFileHandler(
logPath, when="midnight", backupCount=365
)
handler.setFormatter(formatter)

def sighup(signum, stack):
logger.info("Closing log file due to SIGHUP")
handler.doRollover()
logger.info("Opened new log file due to SIGHUP")

else:
handler = logging.StreamHandler()

handler.setFormatter(formatter)
rootLogger = logging.getLogger("")
rootLogger.setLevel(cfg.get("general", "log.level"))
rootLogger.addHandler(handler)

observer = log.PythonLoggingObserver()
observer.start()