diff --git a/api/__init__.py b/api/__init__.py
index 42971ab..4de9342 100644
--- a/api/__init__.py
+++ b/api/__init__.py
@@ -1,5 +1,7 @@
# -*- encoding: utf-8 -*-
+from distutils.util import strtobool
import os
# TODO: create config module and put this constant there.
BASE_URI = os.environ.get("BOTZ_BASE_URI", "http://localhost")
+DEBUG = strtobool(os.environ.get("BOTZ_DEBUG", "False"))
diff --git a/api/app.py b/api/app.py
index 1912600..51203a4 100644
--- a/api/app.py
+++ b/api/app.py
@@ -8,6 +8,7 @@ from aiohttp import web
import base64
from cryptography import fernet
import logging
+import logging.handlers
import os
import typing as T
@@ -17,15 +18,19 @@ from aiohttp_session.cookie_storage import EncryptedCookieStorage
from bot_z.async_operator import AsyncOperator
from api.rest import routes
+from api.conf import read_conf
-def setup_log() -> logging.Logger:
+def setup_log(level: str, syslog: bool) -> logging.Logger:
alog = logging.getLogger("api")
- alog.setLevel(os.environ.get("BOTZ_LOGLEVEL", logging.INFO))
+ alog.setLevel(os.environ.get("BOTZ_LOGLEVEL", level))
h = logging.StreamHandler()
f = logging.Formatter("%(levelname)s [%(name)s] -> %(message)s")
h.setFormatter(f)
alog.addHandler(h)
+ if syslog:
+ sh = logging.handlers.SysLogHandler()
+ alog.addHandler(sh)
return alog
@@ -34,28 +39,63 @@ def init_secret() -> bytes:
return base64.urlsafe_b64decode(fernet_key)
-def setup_session(app: web.Application):
+def setup_session(app: web.Application, secure: bool):
secret = init_secret()
- setup(app, EncryptedCookieStorage(secret))
+ setup(
+ app,
+ EncryptedCookieStorage(
+ secret_key=secret, cookie_name="BOTZ_SESSION", httponly=False, secure=secure
+ ),
+ )
-def run(address: T.Text, port: int) -> None:
+def run(
+ address: T.Optional[T.Text], port: T.Optional[int], conf_path: T.Optional[T.Text]
+) -> None:
"""Application entrypoint."""
- alog = setup_log()
+ conf = read_conf(conf_path)
+
+ # This closure is needed to intercept the to-be-prepared response
+ # and add the right CORS headers
+ async def on_prepare_cors(request, response):
+ response.headers["Access-Control-Allow-Origin"] = conf["http"].get("cors_allow")
+ response.headers["Access-Control-Allow-Credentials"] = "true"
+
+ alog = setup_log(conf["log"]["level"], conf["log"]["syslog"])
+ alog.debug("conf %s", conf)
app = web.Application(logger=alog)
- setup_session(app)
+ app["base_uri"] = conf["base_uri"]
+ app["debug"] = conf["debug"]
+ app["headless"] = conf["headless"]
+ if conf["http"].get("cors_allow"):
+ app.on_response_prepare.append(on_prepare_cors)
+ setup_session(app, conf["http"]["cookie_secure"])
app.add_routes(routes)
- web.run_app(app, host=address, port=port)
+ addr = []
+ if address is not None:
+ addr.append(address)
+ if conf["http"].get("bind_addr"):
+ addr.extend(conf["http"]["bind_addr"])
+ if port is None:
+ port = conf["http"]["port"]
+ alog.debug("Starting app with: addr -> %s, port -> %d", addr, port)
+ web.run_app(app, host=addr, port=port)
@click.command()
@click.option(
- "-a",
- "--address",
- type=click.STRING,
- help="Address to bind the server to.",
- default="127.0.0.1",
+ "-a", "--address", type=click.STRING, help="Address to bind the server to."
)
-@click.option("-p", "--port", type=click.INT, help="Port to bind to", default=3003)
-def cli(address: T.Text, port: int) -> None:
- run(address, port)
+@click.option("-p", "--port", type=click.INT, help="Port to bind to.")
+@click.option(
+ "-c",
+ "--conf",
+ type=click.Path(exists=False),
+ help="A path to a configuration file.",
+)
+def cli(
+ address: T.Optional[T.Text],
+ port: T.Optional[int] = None,
+ conf: T.Optional[T.Text] = None,
+) -> None:
+ run(address, port, conf)
diff --git a/api/conf.py b/api/conf.py
new file mode 100644
index 0000000..497771c
--- /dev/null
+++ b/api/conf.py
@@ -0,0 +1,93 @@
+# -*- encoding: utf-8 -*-
+import os
+from pprint import pprint
+import typing as T
+
+import yaml
+
+from api import DEBUG, BASE_URI
+
+
+def read_conf(path: T.Optional[T.Text]) -> T.Dict:
+ """
+ Read the configuration from the provided path.
+ Such configuration may provide the following information:
+ ---
+ base_uri:
+ debug:
+ headless:
+ log:
+ level:
+ syslog:
+ http:
+ bind_addr:
+ port:
+ cookie_name:
+ cookie_secure:
+ cors_allow:
+ """
+ if path is None:
+ path = seek_path()
+ if path is not None:
+ with open(path) as f:
+ conf = yaml.safe_load(f)
+ else:
+ conf = {}
+ if "base_uri" not in conf:
+ if BASE_URI is None:
+ raise RuntimeError("Missing base_uri")
+ conf["base_uri"] = BASE_URI
+ if "debug" not in conf:
+ conf["debug"] = DEBUG
+ if "headless" not in conf:
+ conf["headless"] = True
+ conf = validate_log_conf(conf)
+ conf = validate_http_log(conf)
+ pprint(conf)
+ return conf
+
+
+def seek_path() -> T.Optional[T.Text]:
+ """
+ Seeks the path to a config file, in the following order:
+ - $BOTZ_CONFIG
+ - $PWD/.botz.yaml
+ - ~/.botz.yaml
+ - /etc/botz/conf.yaml
+ """
+ paths = [
+ os.path.join(os.path.curdir, ".botz.yaml"),
+ os.path.expanduser("~/.botz.yaml"),
+ "/etc/botz/conf.yaml",
+ ]
+ env = os.environ.get("BOTZ_CONFIG")
+ if env is not None:
+ paths.insert(0, env)
+ for path in paths:
+ if os.path.exists(path):
+ return path
+ return None
+
+
+def validate_log_conf(conf: T.Dict[T.Text, T.Any]) -> T.Dict[T.Text, T.Any]:
+ if "log" not in conf:
+ conf["log"] = {}
+ if conf["log"].get("level") is None:
+ conf["log"]["level"] = "INFO"
+ if conf["log"].get("syslog") is None:
+ conf["log"]["syslog"] = False
+ return conf
+
+
+def validate_http_log(conf: T.Dict[T.Text, T.Any]) -> T.Dict[T.Text, T.Any]:
+ if "http" not in conf:
+ conf["http"] = {}
+ if conf["http"].get("bind_addr") is None:
+ conf["http"]["bind_addr"] = ["127.0.0.1"]
+ if conf["http"].get("port") is None:
+ conf["http"]["port"] = 3003
+ if conf["http"].get("cookie_name") is None:
+ conf["http"]["cookie_name"] = "BOTZ_SESSION"
+ if conf["http"].get("cookie_secure") is None:
+ conf["http"]["cookie_secure"] = True
+ return conf
diff --git a/setup.py b/setup.py
index 53e26ef..f75fa90 100644
--- a/setup.py
+++ b/setup.py
@@ -40,6 +40,7 @@ requirements = [
"cchardet",
"aiohttp-session==2.7.0",
"cryptography==2.7",
+ "PyYAML==5.1.2",
]
setup_requirements = [] # type: T.List[str]