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

Separate config parsing from rest of the code #383

Closed
wants to merge 13 commits into from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.vscode/
*.iml
_trial_temp
_trial_temp.lock
*.egg
*.egg-info
.python-version
Expand All @@ -12,3 +13,4 @@ _trial_temp
/sydent.conf
/sydent.db
/matrix_is_test/sydent.stderr
sydent.pid
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions changelog.d/383.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Seperated the config parsing from the rest of the code.
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 18 additions & 12 deletions scripts/casefold_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
import os
import sqlite3
import sys
from typing import Any, Dict, List, Tuple
from typing import TYPE_CHECKING, Any, Dict, List, Tuple

import signedjson.sign

from sydent.sydent import Sydent, parse_config_file
from sydent.config.server import SydentConfig
from sydent.util import json_decoder
from sydent.util.emailutils import sendEmail
from sydent.util.hash import sha256_and_url_safe_base64
from tests.utils import ResolvingMemoryReactorClock

if TYPE_CHECKING:
from sydent.sydent import Sydent


def calculate_lookup_hash(sydent, address):
cur = sydent.db.cursor()
Expand All @@ -39,7 +42,7 @@ def calculate_lookup_hash(sydent, address):


def update_local_associations(
sydent, db: sqlite3.Connection, send_email: bool, dry_run: bool
sydent: "Sydent", db: sqlite3.Connection, send_email: bool, dry_run: bool
):
"""Update the DB table local_threepid_associations so that all stored
emails are casefolded, and any duplicate mxid's associated with the
Expand Down Expand Up @@ -104,11 +107,13 @@ def update_local_associations(
if mxid in processed_mxids:
continue
else:
templateFile = sydent.get_branded_template(
None,
"migration_template.eml",
("email", "email.template"),
)
if sydent.config.email.template is None:
templateFile = sydent.get_branded_template(
None,
"migration_template.eml",
)
else:
templateFile = sydent.config.email.template

sendEmail(
sydent,
Expand Down Expand Up @@ -139,7 +144,7 @@ def update_local_associations(


def update_global_associations(
sydent, db: sqlite3.Connection, send_email: bool, dry_run: bool
sydent: "Sydent", db: sqlite3.Connection, send_email: bool, dry_run: bool
):
"""Update the DB table global_threepid_associations so that all stored
emails are casefolded, the signed association is re-signed and any duplicate
Expand All @@ -149,7 +154,7 @@ def update_global_associations(
"""

# get every row where the local server is origin server and medium is email
origin_server = sydent.server_name
origin_server = sydent.config.general.server_name
medium = "email"

cur = db.cursor()
Expand All @@ -176,7 +181,7 @@ def update_global_associations(
sg_assoc["address"] = address.casefold()
sg_assoc = json.dumps(
signedjson.sign.sign_json(
sg_assoc, sydent.server_name, sydent.keyring.ed25519
sg_assoc, sydent.config.general.server_name, sydent.keyring.ed25519
)
)

Expand Down Expand Up @@ -249,7 +254,8 @@ 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)
config = SydentConfig()
config.parse_config_file(args.config_path)

reactor = ResolvingMemoryReactorClock()
sydent = Sydent(config, reactor, False)
Expand Down
2 changes: 2 additions & 0 deletions sydent/config/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class BaseConfig:
pass
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved
60 changes: 60 additions & 0 deletions sydent/config/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging
from configparser import ConfigParser

import nacl
import signedjson.key

from sydent.config._base import BaseConfig

logger = logging.getLogger(__name__)


class CryptoConfig(BaseConfig):
def parse_config(self, cfg: ConfigParser):
"""
Parse the crypto section of the config

Args:
cfg (ConfigParser): the configuration to be parsed
"""

signing_key_str = cfg.get("crypto", "ed25519.signingkey")
signing_key_parts = signing_key_str.split(" ")

save_key = False

if signing_key_str == "":
logger.info(
"This server does not yet have an ed25519 signing key. "
+ "Creating one and saving it in the config file."
)

self.signing_key = signedjson.key.generate_signing_key("0")

save_key = True
elif len(signing_key_parts) == 1:
# old format key
logger.info("Updating signing key format: brace yourselves")

self.signing_key = nacl.signing.SigningKey(
signing_key_str, encoder=nacl.encoding.HexEncoder
)
self.signing_key.version = "0"
self.signing_key.alg = signedjson.key.NACL_ED25519

save_key = True
else:
self.signing_key = signedjson.key.decode_signing_key_base64(
signing_key_parts[0], signing_key_parts[1], signing_key_parts[2]
)

if save_key:
signing_key_str = "%s %s %s" % (
self.signing_key.alg,
self.signing_key.version,
signedjson.key.encode_signing_key_base64(self.signing_key),
)
cfg.set("crypto", "ed25519.signingkey", signing_key_str)

# This will trigger the updated cfg to be saved to file
self.update_cfg = True
14 changes: 14 additions & 0 deletions sydent/config/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from configparser import ConfigParser

from sydent.config._base import BaseConfig


class DatabaseConfig(BaseConfig):
def parse_config(self, cfg: ConfigParser):
"""
Parse the database section of the config

Args:
cfg (ConfigParser): the configuration to be parsed
"""
self.database_path = cfg.get("db", "db.file")
52 changes: 52 additions & 0 deletions sydent/config/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import socket
from configparser import ConfigParser

from sydent.config._base import BaseConfig


class EmailConfig(BaseConfig):
def parse_config(self, cfg: ConfigParser):
"""
Parse the email section of the config

Args:
cfg (ConfigParser): the configuration to be parsed
"""

self.template = None
if cfg.has_option("email", "email.template"):
self.template = cfg.get("email", "email.template")

self.invite_template = None
if cfg.has_option("email", "email.invite_template"):
self.invite_template = cfg.get("email", "email.invite_template")

self.sender = cfg.get("email", "email.from")

self.host_name = cfg.get("email", "email.hostname")
if self.host_name == "":
self.host_name = socket.getfqdn()

self.validation_subject = cfg.get("email", "email.subject")
self.invite_subject = cfg.get("email", "email.invite.subject", raw=True)
self.invite_subject_space = cfg.get(
"email", "email.invite.subject_space", raw=True
)

self.smtp_server = cfg.get("email", "email.smtphost")
self.smtp_port = cfg.get("email", "email.smtpport")
self.smtp_username = cfg.get("email", "email.smtpusername")
self.smtp_password = cfg.get("email", "email.smtppassword")
self.tls_mode = cfg.get("email", "email.tlsmode")

self.default_web_client_location = cfg.get(
"email", "email.default_web_client_location"
)

self.username_obfuscate_characters = cfg.getint(
"email", "email.third_party_invite_username_obfuscate_characters"
)

self.domain_obfuscate_characters = cfg.getint(
"email", "email.third_party_invite_domain_obfuscate_characters"
)
122 changes: 122 additions & 0 deletions sydent/config/general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import logging
import os
from configparser import ConfigParser
from typing import Set

from jinja2.environment import Environment
from jinja2.loaders import FileSystemLoader

from sydent.config._base import BaseConfig
from sydent.util.ip_range import DEFAULT_IP_RANGE_BLACKLIST, generate_ip_set

logger = logging.getLogger(__name__)


class GeneralConfig(BaseConfig):
def parse_config(self, cfg: ConfigParser):
"""
Parse the 'general' section of the config

Args:
cfg (ConfigParser): the configuration to be parsed
"""
self.server_name = cfg.get("general", "server.name")
if self.server_name == "":
self.server_name = os.uname()[1]
logger.warning(
(
"You had not specified a server name. I have guessed that this server is called '%s' "
+ "If this is incorrect, you should edit 'general.server.name' in the config file."
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved
)
% (self.server_name,)
)

self.pidfile = cfg.get("general", "pidfile.path")

self.terms_path = cfg.get("general", "terms.path")

self.address_lookup_limit = cfg.getint("general", "address_lookup_limit")

# Get the possible brands by looking at directories under the
# templates.path directory.
self.templates_path = cfg.get("general", "templates.path")
if os.path.exists(self.templates_path):
self.valid_brands = {
p
for p in os.listdir(self.templates_path)
if os.path.isdir(os.path.join(self.templates_path, p))
}
else:
logging.warning(
"The path specified by 'general.templates.path' does not exist."
)
# This is a legacy code-path and assumes that verify_response_template,
# email.template, and email.invite_template are defined.
self.valid_brands = set()

self.template_environment = Environment(
loader=FileSystemLoader(cfg.get("general", "templates.path")),
autoescape=True,
)

self.default_brand = cfg.get("general", "brand.default")

if cfg.has_option("general", "prometheus_port"):
prometheus_port = cfg.getint("general", "prometheus_port")
prometheus_addr = cfg.get("general", "prometheus_addr")

import prometheus_client

prometheus_client.start_http_server(
port=prometheus_port,
addr=prometheus_addr,
)
Azrenbeth marked this conversation as resolved.
Show resolved Hide resolved

if cfg.has_option("general", "sentry_dsn"):
sentry_dsn = cfg.get("general", "sentry_dsn")

import sentry_sdk

sentry_sdk.init(dsn=sentry_dsn)
with sentry_sdk.configure_scope() as scope:
scope.set_tag("sydent_server_name", self.server_name)

self.enable_v1_associations = parse_cfg_bool(
cfg.get("general", "enable_v1_associations")
)

self.delete_tokens_on_bind = parse_cfg_bool(
cfg.get("general", "delete_tokens_on_bind")
)

ip_blacklist = set_from_comma_sep_string(cfg.get("general", "ip.blacklist"))
if not ip_blacklist:
ip_blacklist = DEFAULT_IP_RANGE_BLACKLIST

ip_whitelist = set_from_comma_sep_string(cfg.get("general", "ip.whitelist"))

self.ip_blacklist = generate_ip_set(ip_blacklist)
self.ip_whitelist = generate_ip_set(ip_whitelist)


def set_from_comma_sep_string(rawstr: str) -> Set[str]:
"""
Parse the a comma seperated string into a set

Args:
rawstr (str): the string to be parsed
"""
if rawstr == "":
return set()
return {x.strip() for x in rawstr.split(",")}


def parse_cfg_bool(value: str):
"""
Parse a string config option into a boolean
This method ignores capitalisation

Args:
value (str): the string to be parsed
"""
return value.lower() == "true"
Loading