diff --git a/Makefile b/Makefile index 5048388..f3f054a 100644 --- a/Makefile +++ b/Makefile @@ -10,4 +10,4 @@ typecheck: mypy --ignore-missing-imports pantalaimon run-local: - python -m pantalaimon.main https://localhost:8448 --proxy http://localhost:8080 -k --log-level debug + python -m pantalaimon.main --log-level debug --config ./contrib/pantalaimon.conf diff --git a/pantalaimon/config.py b/pantalaimon/config.py new file mode 100644 index 0000000..ecb256d --- /dev/null +++ b/pantalaimon/config.py @@ -0,0 +1,150 @@ +import configparser +import os + +from typing import Union + +from ipaddress import ip_address, IPv4Address, IPv6Address +from urllib.parse import urlparse, ParseResult + +import logbook +import attr + + +class PanConfigParser(configparser.ConfigParser): + def __init__(self): + super().__init__( + default_section="Default", + defaults={ + "SSL": "True", + "ListenAddress": "localhost", + "ListenPort": "8009", + "LogLevel": "warnig", + }, + converters={ + "address": parse_address, + "url": parse_url, + "loglevel": parse_log_level, + } + ) + + +def parse_address(value): + # type: (str) -> Union[IPv4Address, IPv6Address] + if value == "localhost": + return ip_address("127.0.0.1") + + return ip_address(value) + + +def parse_url(value): + # type: (str) -> ParseResult + value = urlparse(value) + + if value.scheme not in ('http', 'https'): + raise ValueError(f"Invalid URL scheme {value.scheme}. " + f"Only HTTP(s) URLs are allowed") + value.port + + return value + +def parse_log_level(value): + # type: (str) -> logbook + value = value.lower() + + if value == "info": + return logbook.INFO + elif value == "warning": + return logbook.WARNING + elif value == "error": + return logbook.ERROR + elif value == "debug": + return logbook.DEBUG + + return logbook.WARNING + + +class PanConfigError(Exception): + """Pantalaimon configuration error.""" + + pass + + +@attr.s +class ServerConfig: + """Server configuration. + + Args: + homeserver (ParseResult): The URL of the Matrix homeserver that we want + to forward requests to. + listen_address (str): The local address where pantalaimon will listen + for connections. + listen_port (int): The port where pantalaimon will listen for + connections. + proxy (ParseResult): + A proxy that the daemon should use when making connections to the + homeserver. + ssl (bool): Enable or disable SSL for the connection between + pantalaimon and the homeserver. + """ + + homeserver = attr.ib() + listen_address = attr.ib(type=Union[IPv4Address, IPv6Address]) + listen_port = attr.ib(type=int) + proxy = attr.ib(type=str) + ssl = attr.ib(type=bool, default=True) + + +@attr.s +class PanConfig: + """Pantalaimon configuration. + + Args: + config_path (str): The path where we should search for a configuration + file. + filename (str): The name of the file that we should read. + """ + + config_file = attr.ib() + + log_level = attr.ib(default=None) + servers = attr.ib(init=False, default=attr.Factory(dict)) + + def read(self): + """Read the configuration file. + + Raises OSError if the file can't be read or PanConfigError if there is + a syntax error with the config file. + """ + config = PanConfigParser() + try: + config.read(os.path.abspath(self.config_file)) + except configparser.Error as e: + raise PanConfigError(e) + + if self.log_level is None: + self.log_level = config["Default"].getloglevel("LogLevel") + + try: + for section_name, section in config.items(): + + if section_name == "Default": + continue + + homeserver = section.geturl("Homeserver") + listen_address = section.getaddress("ListenAddress") + listen_port = section.getint("ListenPort") + ssl = section.getboolean("SSL") + proxy = section.geturl("Proxy") + + server_conf = ServerConfig( + homeserver, + listen_address, + listen_port, + proxy, + ssl + ) + + self.servers[section_name] = server_conf + + except ValueError as e: + raise PanConfigError(e) diff --git a/pantalaimon/main.py b/pantalaimon/main.py index ea3e830..34947f3 100644 --- a/pantalaimon/main.py +++ b/pantalaimon/main.py @@ -7,16 +7,28 @@ from urllib.parse import urlparse import click import janus -import logbook -from appdirs import user_data_dir +from appdirs import user_data_dir, user_config_dir from logbook import StderrHandler from aiohttp import web from pantalaimon.ui import GlibT -from pantalaimon.log import logger from pantalaimon.daemon import ProxyDaemon +from pantalaimon.config import PanConfig, PanConfigError, parse_log_level +from pantalaimon.log import logger + + +def create_dirs(data_dir, conf_dir): + try: + os.makedirs(data_dir) + except OSError: + pass + + try: + os.makedirs(conf_dir) + except OSError: + pass async def init(homeserver, http_proxy, ssl, send_queue, recv_queue): @@ -84,77 +96,55 @@ class ipaddress(click.ParamType): @click.command( help=("pantalaimon is a reverse proxy for matrix homeservers that " "transparently encrypts and decrypts messages for clients that " - "connect to pantalaimon.\n\n" - "HOMESERVER - the homeserver that the daemon should connect to.") + "connect to pantalaimon.") ) -@click.option( - "--proxy", - type=URL(), - default=None, - help="A proxy that will be used to connect to the homeserver." -) -@click.option( - "-k", - "--ssl-insecure/--no-ssl-insecure", - default=False, - help="Disable SSL verification for the homeserver connection." -) -@click.option( - "-l", - "--listen-address", - type=ipaddress(), - default=ip_address("127.0.0.1"), - help=("The listening address for incoming client connections " - "(default: 127.0.0.1)") -) -@click.option( - "-p", - "--listen-port", - type=int, - default=8009, - help="The listening port for incoming client connections (default: 8009)" -) @click.option("--log-level", type=click.Choice([ "error", "warning", "info", "debug" -]), default="error") -@click.argument( - "homeserver", - type=URL(), -) +]), default=None) +@click.option("-c", "--config", type=click.Path(exists=True)) +@click.pass_context def main( - proxy, - ssl_insecure, - listen_address, - listen_port, + context, log_level, - homeserver + config ): - ssl = None if ssl_insecure is False else False + conf_dir = user_config_dir("pantalaimon", "") + data_dir = user_data_dir("pantalaimon", "") + create_dirs(data_dir, conf_dir) - StderrHandler(level=log_level.upper()).push_application() + config = config or os.path.join(conf_dir, "pantalaimon.conf") - if log_level == "info": - logger.level = logbook.INFO - elif log_level == "warning": - logger.level = logbook.WARNING - elif log_level == "error": - logger.level = logbook.ERROR - elif log_level == "debug": - logger.level = logbook.DEBUG + if log_level: + log_level = parse_log_level(log_level) + + pan_conf = PanConfig(config, log_level) + + try: + pan_conf.read() + except (OSError, PanConfigError) as e: + context.fail(e) + + if not pan_conf.servers: + context.fail("Homeserver is not configured.") + + logger.level = pan_conf.log_level + StderrHandler().push_application() loop = asyncio.get_event_loop() - pan_queue = janus.Queue(loop=loop) ui_queue = janus.Queue(loop=loop) + # TODO start the other servers as well + server_conf = list(pan_conf.servers.values())[0] + proxy, app = loop.run_until_complete(init( - homeserver, - proxy.geturl() if proxy else None, - ssl, + server_conf.homeserver, + server_conf.proxy.geturl() if server_conf.proxy else None, + server_conf.ssl, pan_queue.async_q, ui_queue.async_q )) @@ -179,7 +169,11 @@ def main( home = os.path.expanduser("~") os.chdir(home) - web.run_app(app, host=str(listen_address), port=listen_port) + web.run_app( + app, + host=str(server_conf.listen_address), + port=server_conf.listen_port + ) if __name__ == "__main__":