#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2017 cytopia <cytopia@everythingcli.org>

"""
vHost creator for Apache 2.2, Apache 2.4 and Nginx.
"""

# --------------------------------------------------------------------------------------------------
# Imports
# --------------------------------------------------------------------------------------------------
from __future__ import print_function
import os
import sys
import re
import getopt
import itertools
import yaml

if os.environ.get("MYPY_CHECK", False):
    from typing import Optional, List, Dict, Any, Tuple


# --------------------------------------------------------------------------------------------------
# Globals
# --------------------------------------------------------------------------------------------------
APPNAME = "vhost-gen"
APPREPO = "https://github.com/devilbox/vhost-gen"
VERSION = "1.0.7"
RELDATE = "2022-12-22"

# Default paths
CONFIG_PATH = "/etc/vhost-gen/conf.yml"
TEMPLATE_DIR = "/etc/vhost-gen/templates"

# stdout/stderr log paths
STDOUT_ACCESS = "/tmp/www-access.log"
STDERR_ERROR = "/tmp/www-error.log"

# Default configuration
DEFAULT_CONFIG = {
    "server": "nginx",
    "conf_dir": "/etc/nginx/conf.d",
    "custom": "",
    "vhost": {
        "port": "80",
        "ssl_port": "443",
        "name": {"prefix": "", "suffix": ""},
        "docroot": {"suffix": ""},
        "index": ["index.php", "index.html", "index.htm"],
        "ssl": {
            "http2": True,
            "dir_crt": "",
            "dir_key": "",
            "honor_cipher_order": "on",
            "ciphers": "HIGH:!aNULL:!MD5",
            "protocols": "TLSv1 TLSv1.1 TLSv1.2",
        },
        "log": {
            "access": {"prefix": "", "stdout": False},
            "error": {"prefix": "", "stderr": False},
            "dir": {"create": False, "path": "/var/log/nginx"},
        },
        "php_fpm": {"enable": False, "address": "", "port": 9000, "timeout": 180},
        "alias": [],
        "deny": [],
        "server_status": {"enable": False, "alias": "/server-status"},
    },
}

# Available templates
TEMPLATES = {"apache22": "apache22.yml", "apache24": "apache24.yml", "nginx": "nginx.yml"}


# --------------------------------------------------------------------------------------------------
# System Functions
# --------------------------------------------------------------------------------------------------
# Log info to stderr during execution based on verbosity
# level:
#  0: error
#  1: warning
#  2: info
#  3: debug
def log(level, message, verbosity):
    # type: (int, str, int) -> None
    """Log based on current verbosity."""

    # stime = time.strftime("%Y-%m-%d %H:%M:%S")

    if level == 0:
        print("vhost-gen: \x1b[31;21m[ERR]  %s\x1b[0m" % (message), file=sys.stderr)
    elif level == 1:
        print("vhost-gen: \x1b[33;21m[WARN] %s\x1b[0m" % (message), file=sys.stderr)
    elif level == 2 and verbosity >= 1:
        print("vhost-gen: [INFO] %s" % (message), file=sys.stderr)
    elif level == 3 and verbosity >= 2:
        print("vhost-gen: [DBG]  %s" % (message), file=sys.stderr)


def print_help():
    # type: () -> None
    """Show program help."""
    print(
        """
Usage: vhost-gen -p|r <str> -n <str> [-l <str> -c <str> -t <str> -o <str> -d -s -v]
       vhost-gen --help
       vhost-gen --version

vhost-gen will dynamically generate vhost configuration files
for Nginx, Apache 2.2 or Apache 2.4 depending on what you have set
in /etc/vhost-gen/conf.yml

Required arguments:
  -p|r <str>  You need to choose one of the mutually exclusive arguments.
              -p: Path to document root/
              -r: http(s)://Host:Port for reverse proxy.
              Depening on the choice, it will either generate a document serving
              vhost or a reverse proxy vhost.
              Note, when using -p, this can also have a suffix directory to be set
              in conf.yml
  -l <str>    Location path when using reverse proxy.
              Note, this is not required for normal document root server (-p)
  -n <str>    Name of vhost
              Note, this can also have a prefix and/or suffix to be set in conf.yml

Optional arguments:
  -m <str>    Vhost generation mode. Possible values are:
              -m plain: Only generate http version (default)
              -m ssl:   Only generate https version
              -m both:  Generate http and https version
              -m redir: Generate https version and make http redirect to https
  -c <str>    Path to global configuration file.
              If not set, the default location is /etc/vhost-gen/conf.yml
              If no config is found, a default is used with all features turned off.
  -t <str>    Path to global vhost template directory.
              If not set, the default location is /etc/vhost-gen/templates/
              If vhost template files are not found in this directory, the program will
              abort.
  -o <str>    Path to local override vhost template directory.
              This is used as a secondary template directory and definitions found here
              will be merged with the ones found in the global template directory.
              Note, definitions in local vhost teplate directory take precedence over
              the ones found in the global template directory.
  -d          Make this vhost the default virtual host.
              Note, this will also change the server_name directive of nginx to '_'
              as well as discarding any prefix or suffix specified for the name.
              Apache does not have any specialities, the first vhost takes precedence.
  -s          If specified, the generated vhost will be saved in the location found in
              conf.yml. If not specified, vhost will be printed to stdout.
  -v          Be verbose. Use twice for debug output.

Misc arguments:
  --help      Show this help.
  --version   Show version.
"""
    )


def print_version():
    # type: () -> None
    """Show program version."""
    print("%s: Version v%s (%s) by cytopia" % (APPNAME, VERSION, RELDATE))
    print("<https://github.com/devilbox/vhost-gen>")
    print("<https://github.com/cytopia>")
    print("The MIT License (MIT)")


# --------------------------------------------------------------------------------------------------
# Wrapper Functions
# --------------------------------------------------------------------------------------------------
def str_replace(string, replacer):
    # type: (str, Dict[str, Any]) -> str
    """Generic string replace."""

    # Replace all 'keys' with 'values'
    for key, val in replacer.items():
        string = string.replace(key, val)

    return string


def str_indent(text, amount, char=" "):
    # type: (str, int, str) -> str
    """Indent every newline inside str by specified value."""
    padding = amount * char
    return "".join(padding + line for line in text.splitlines(True))


def to_str(string):
    # type: (Optional[str]) -> str
    """Dummy string retriever."""
    if string is None:
        return ""
    return str(string)


def load_yaml(path):
    # type: (str) -> Tuple[bool, Dict[str, Any], str]
    """Wrapper to load yaml file safely."""

    try:
        with open(path, "r") as stream:
            try:
                data = yaml.safe_load(stream)
                if data is None:
                    data = {}
                return (True, data, "")
            except yaml.YAMLError as err:
                return (False, {}, str(err))
    except IOError:
        return (False, {}, "File does not exist: " + path)


def merge_yaml(yaml1, yaml2):
    # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
    """Merge two yaml strings. The secondary takes precedence."""
    return dict(itertools.chain(yaml1.items(), yaml2.items()))


def symlink(src, dst, force=False):
    # type: (str, str, bool) -> Tuple[bool, Optional[str]]
    """
    Wrapper function to create a symlink with the addition of
    being able to overwrite an already existing file.
    """

    if os.path.isdir(dst):
        return (False, "[ERR] destination is a directory: " + dst)

    if force and os.path.exists(dst):
        try:
            os.remove(dst)
        except OSError as err:
            return (False, "[ERR] Cannot delete: " + dst + ": " + str(err))

    try:
        os.symlink(src, dst)
    except OSError as err:
        return (False, "[ERR] Cannot create link: " + str(err))

    return (True, None)


# --------------------------------------------------------------------------------------------------
# Argument Functions
# --------------------------------------------------------------------------------------------------
def parse_args(argv):
    # type: (List[str]) -> Tuple[Any, ...]
    """Parse command line arguments."""

    # Config location, can be overwritten with -c
    l_config_path = CONFIG_PATH
    l_template_dir = TEMPLATE_DIR
    o_template_dir = None
    save = None
    docroot = None
    name = ""
    proxy = None
    mode = None
    location = None
    default = False
    verbose = 0

    # Define command line options
    try:
        opts, argv = getopt.getopt(argv, "vm:c:p:r:l:n:t:o:ds", ["version", "help"])
    except getopt.GetoptError as err:
        log(0, str(err), 1)
        log(0, "Type --help for help", 1)
        sys.exit(2)

    # Get command line options
    for opt, arg in opts:
        if opt == "--version":
            print_version()
            sys.exit()
        elif opt == "--help":
            print_help()
            sys.exit()
        # Verbose
        elif opt == "-v":
            verbose = verbose + 1
        # Config file overwrite
        elif opt == "-c":
            l_config_path = arg
        # Vhost document root path
        elif opt == "-p":
            docroot = arg
        # Vhost reverse proxy (ADDR:PORT)
        elif opt == "-r":
            proxy = arg
        # Mode overwrite
        elif opt == "-m":
            mode = arg
        # Location for reverse proxy
        elif opt == "-l":
            location = arg
        # Vhost name
        elif opt == "-n":
            name = arg
        # Global template dir
        elif opt == "-t":
            l_template_dir = arg
        # Local template dir
        elif opt == "-o":
            o_template_dir = arg
        # Save?
        elif opt == "-d":
            default = True
        elif opt == "-s":
            save = True

    return (
        l_config_path,
        l_template_dir,
        o_template_dir,
        docroot,
        proxy,
        mode,
        location,
        name,
        default,
        save,
        verbose,
    )


def validate_args_req(name, docroot, proxy, mode, location, verbose):
    # type: (Optional[str], Optional[str], Optional[str], str, Optional[str], int) -> None
    """Validate required arguments."""
    # Validate required command line options are set
    if docroot is None and proxy is None:
        log(0, "-p or -r is required", verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)
    if docroot is not None and proxy is not None:
        log(0, "-p and -r are mutually exclusive", verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    # Check proxy string
    if proxy is not None:
        if location is None:
            log(0, "When specifying -r, -l is also required.", verbose)
            log(0, "Type --help for help", verbose)
            sys.exit(1)

        # Regex: HOSTNAME/IP:PORT
        regex = re.compile("(^http(s)?://[-_.a-zA-Z0-9]+:[0-9]+$)", re.IGNORECASE)
        if not regex.match(proxy):
            log(
                0,
                "Invalid proxy argument string: '{}', should be {} or {}.".format(
                    proxy, "http(s)://HOST:PORT", "http(s)://IP:PORT"
                ),
                verbose,
            )
            log(0, "Type --help for help", verbose)
            sys.exit(1)

        port = int(re.sub("^.*:", "", proxy))
        if port < 1 or port > 65535:
            log(
                0,
                "Invalid reverse proxy range: '{}', should be between 1 and 65535".format(port),
                verbose,
            )
            log(0, "Type --help for help", verbose)
            sys.exit(1)

    # Check mode string
    if mode is not None:
        if mode not in ("plain", "ssl", "both", "redir"):
            log(
                0,
                "Invalid -m mode string: '{}', should be: {}, {}, {} or {}".format(
                    mode, "plain", "ssl", "both", "redir"
                ),
                verbose,
            )
            log(0, "Type --help for help", verbose)
            sys.exit(1)

    # Check normal server settings
    if docroot is not None:
        if location is not None:
            log(1, "-l is ignored when using normal vhost (-p)", verbose)

    if name is None:
        log(0, "-n is required", verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    regex = re.compile("(^[-_.a-zA-Z0-9]+$)", re.IGNORECASE)
    if not regex.match(name):
        log(0, "Invalid name: {}".format(name), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)


def validate_args_opt(config_path, tpl_dir, verbose):
    # type: (str, str, int) -> None
    """Validate optional arguments."""

    if not os.path.isfile(config_path):
        log(1, "Config file not found: {}".format(config_path), verbose)

    if not os.path.isdir(tpl_dir):
        log(0, "Template path does not exist: {}".format(tpl_dir), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    # Validate global templates
    tpl_file = os.path.join(tpl_dir, TEMPLATES["apache22"])
    if not os.path.isfile(tpl_file):
        log(0, "Apache 2.2 template file does not exist: {}".format(tpl_file), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    tpl_file = os.path.join(tpl_dir, TEMPLATES["apache24"])
    if not os.path.isfile(tpl_file):
        log(0, "Apache 2.4 template file does not exist: {}".format(tpl_file), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    tpl_file = os.path.join(tpl_dir, TEMPLATES["nginx"])
    if not os.path.isfile(tpl_file):
        log(0, "Nginx template file does not exist: {}".format(tpl_file), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)


# --------------------------------------------------------------------------------------------------
# Config File Functions
# --------------------------------------------------------------------------------------------------
def validate_config(config, verbose):
    # type: (Dict[str, Any], bool) -> None
    """Validate some important keys in config dict."""

    # Validate server type
    valid_hosts = list(TEMPLATES.keys())
    if config["server"] not in valid_hosts:
        log(0, "httpd.server must be 'apache22', 'apache24' or 'nginx'", verbose)
        log(0, "Your configuration is: " + config["server"], verbose)
        sys.exit(1)


# --------------------------------------------------------------------------------------------------
# Get vHost Skeleton placeholders
# --------------------------------------------------------------------------------------------------
def vhost_get_port(config, ssl):
    # type: (Dict[str, Any], bool) -> str
    """Get listen port."""
    if ssl:
        if config["server"] == "nginx":
            return to_str(config["vhost"]["ssl_port"]) + " ssl"
        return to_str(config["vhost"]["ssl_port"])

    return to_str(config["vhost"]["port"])


def vhost_get_http_proto(config, ssl):
    # type: (Dict[str, Any], bool) -> str
    """Get HTTP protocol. Only relevant for Apache 2.4/Nginx and SSL."""

    if config["server"] == "apache24":
        if ssl and config["vhost"]["ssl"]["http2"]:
            return "h2 http/1.1"
        return "http/1.1"

    if config["server"] == "nginx":
        if ssl and config["vhost"]["ssl"]["http2"]:
            return " http2"

    return ""


def vhost_get_default_server(config, default):
    # type: (Dict[str, Any], bool) -> str
    """
    Get vhost default directive which makes it the default vhost.

    :param dict config: Configuration dictionary
    :param bool default: Default vhost
    """
    if default:
        if config["server"] == "nginx":
            # The leading space is required here for the template to
            # separate it from the port directive left to it.
            return " default_server"

        if config["server"] in ("apache22", "apache24"):
            return "_default_"

    else:
        if config["server"] in ("apache22", "apache24"):
            return "*"

    return ""


def vhost_get_server_name(config, server_name, default):
    # type: (Dict[str, Any], str, bool) -> str
    """Get server name."""

    # Nginx uses: "server_name _;" as the default
    if default and config["server"] == "nginx":
        return "_"

    # Apache does not have any specialities. The first one takes precedence.
    # The name will be the same as with every other vhost.
    prefix = to_str(config["vhost"]["name"]["prefix"])
    suffix = to_str(config["vhost"]["name"]["suffix"])
    return prefix + server_name + suffix


def vhost_get_access_log(config, server_name):
    # type: (Dict[str, Any], str) -> str
    """Get access log directive."""
    if config["vhost"]["log"]["access"]["stdout"]:
        return STDOUT_ACCESS

    prefix = to_str(config["vhost"]["log"]["access"]["prefix"])
    name = prefix + server_name + "-access.log"
    path = os.path.join(config["vhost"]["log"]["dir"]["path"], name)
    return path


def vhost_get_error_log(config, server_name):
    # type: (Dict[str, Any], str) -> str
    """Get error log directive."""
    if config["vhost"]["log"]["error"]["stderr"]:
        return STDERR_ERROR

    prefix = to_str(config["vhost"]["log"]["error"]["prefix"])
    name = prefix + server_name + "-error.log"
    path = os.path.join(config["vhost"]["log"]["dir"]["path"], name)
    return path


# --------------------------------------------------------------------------------------------------
# Get vHost Type (normal or reverse proxy
# --------------------------------------------------------------------------------------------------
def vhost_get_vhost_docroot(config, template, docroot, proxy, verbose):
    # type: (Dict[str, Any], Dict[str, Any], Optional[str], Optional[str], int) -> str
    """Get document root directive."""
    if proxy is not None:
        return ""

    return str_replace(
        template["vhost_type"]["docroot"],
        {
            "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy, verbose),
            "__INDEX__": vhost_get_index(config, verbose),
        },
    )


def vhost_get_vhost_rproxy(template, proxy, location, verbose):
    # type: (Dict[str, Any], Optional[str], Optional[str], int) -> str
    """Get reverse proxy definition."""
    if proxy is not None:
        match = re.search("^.*://(.+):[0-9]+", str(proxy))
        if not match:
            log(0, "Wrong proxy format", verbose)
            log(0, "Type --help for help", verbose)
            sys.exit(1)

        proxy_addr = match.group(1)
        return str_replace(
            template["vhost_type"]["rproxy"],
            {
                "__LOCATION__": location,
                "__PROXY_PROTO__": re.sub("://.*$", "", proxy),
                "__PROXY_ADDR__": proxy_addr,
                "__PROXY_PORT__": re.sub("^.*:", "", proxy),
            },
        )
    return ""


# --------------------------------------------------------------------------------------------------
# Get vHost Features
# --------------------------------------------------------------------------------------------------
def vhost_get_vhost_ssl(config, template, server_name, verbose):
    # type: (Dict[str, Any], Dict[str, Any], str, int) -> str
    """Get ssl definition."""
    return str_replace(
        template["features"]["ssl"],
        {
            "__SSL_PATH_CRT__": to_str(vhost_get_ssl_crt_path(config, server_name, verbose)),
            "__SSL_PATH_KEY__": to_str(vhost_get_ssl_key_path(config, server_name, verbose)),
            "__SSL_PROTOCOLS__": to_str(config["vhost"]["ssl"]["protocols"]),
            "__SSL_HONOR_CIPHER_ORDER__": to_str(config["vhost"]["ssl"]["honor_cipher_order"]),
            "__SSL_CIPHERS__": to_str(config["vhost"]["ssl"]["ciphers"]),
        },
    )


def vhost_get_vhost_redir(config, template, server_name):
    # type: (Dict[str, Any], Dict[str, Any], str) -> str
    """Get redirect to ssl definition."""
    return str_replace(
        template["features"]["redirect"],
        {
            "__VHOST_NAME__": vhost_get_server_name(config, server_name, False),
            "__SSL_PORT__": to_str(config["vhost"]["ssl_port"]),
        },
    )


def vhost_get_ssl_crt_path(config, server_name, verbose):
    # type: (Dict[str, Any], str, int) -> str
    """Get ssl crt path"""
    prefix = to_str(config["vhost"]["name"]["prefix"])
    suffix = to_str(config["vhost"]["name"]["suffix"])
    name = prefix + server_name + suffix + ".crt"
    log(3, "[ssl-crt]   Retrieving cfg 'ssl' name: {}".format(name), verbose)

    path = to_str(config["vhost"]["ssl"]["dir_crt"])
    log(3, "[ssl-crt]  Retrieving cfg 'ssl' path: {}".format(path), verbose)

    output = os.path.join(path, name)
    log(3, "[ssl-crt]  Returning: {}".format(output), verbose)
    return output


def vhost_get_ssl_key_path(config, server_name, verbose):
    # type: (Dict[str, Any], str, int) -> str
    """Get ssl key path"""
    prefix = to_str(config["vhost"]["name"]["prefix"])
    suffix = to_str(config["vhost"]["name"]["suffix"])
    name = prefix + server_name + suffix + ".key"
    log(3, "[ssl-key]  Retrieving cfg 'ssl' name: {}".format(name), verbose)

    path = to_str(config["vhost"]["ssl"]["dir_crt"])
    log(3, "[ssl-key]  Retrieving cfg 'ssl' path: {}".format(path), verbose)

    output = os.path.join(path, name)
    log(3, "[ssl-key]  Returning: {}".format(output), verbose)
    return output


def vhost_get_docroot_path(config, docroot, proxy, verbose):
    # type: (Dict[str, Any], Optional[str], Optional[str], int) -> str
    """Get path of document root."""
    if proxy is not None:
        log(3, "[docroot] Retrieving cfg 'docroot': None, we're using Proxy", verbose)
        return ""

    suffix = to_str(config["vhost"]["docroot"]["suffix"])
    output = os.path.join(str(docroot), suffix)
    log(3, "[docroot] Retrieving cfg 'docroot': {}".format(output), verbose)
    log(3, "[docroot] Returning: {}".format(output), verbose)
    return output


def vhost_get_index(config, verbose):
    # type: (Dict[str, Any], int) -> str
    """Get index."""
    if "index" in config["vhost"] and config["vhost"]["index"]:
        elem = config["vhost"]["index"]
        log(3, "[index]   Retrieving cfg 'index': {}".format(elem), verbose)
    else:
        elem = DEFAULT_CONFIG["vhost"]["index"]  # type: ignore
        log(3, "[index]   Retrieving default 'index': {}".format(elem), verbose)

    output = " ".join(elem)
    log(3, "[index]   Returning: {}".format(output), verbose)
    return output


def vhost_get_php_fpm(config, template, docroot, proxy, verbose):
    # type: (Dict[str, Any], Dict[str, Any], Optional[str], Optional[str], int) -> str
    """Get PHP FPM directive. If using reverse proxy, PHP-FPM will be disabled."""
    if proxy is not None:
        log(3, "[php_fpm]  Retrieving cfg 'php_fpm': None, we're using Proxy", verbose)
        return ""

    # Get PHP-FPM
    php_fpm = ""
    if config["vhost"]["php_fpm"]["enable"]:
        log(
            3,
            "[php_fpm] Retrieving cfg 'php_fpm': {}:{}".format(
                config["vhost"]["php_fpm"]["address"], config["vhost"]["php_fpm"]["port"]
            ),
            verbose,
        )
        php_fpm = str_replace(
            template["features"]["php_fpm"],
            {
                "__PHP_ADDR__": to_str(config["vhost"]["php_fpm"]["address"]),
                "__PHP_PORT__": to_str(config["vhost"]["php_fpm"]["port"]),
                "__PHP_TIMEOUT__": to_str(config["vhost"]["php_fpm"]["timeout"]),
                "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy, verbose),
            },
        )
    log(3, "[php_fpm] Returning: {}".format(php_fpm), verbose)
    return php_fpm


def vhost_get_aliases(config, template, verbose):
    # type: (Dict[str, Any], Dict[str, Any], int) -> str
    """Get virtual host alias directives."""
    aliases = []
    for item in config["vhost"]["alias"]:
        log(3, "[alias]   Retrieving cfg 'alias': {}".format(item["alias"]), verbose)
        # Add optional xdomain request if enabled
        xdomain_request = ""
        if "xdomain_request" in item:
            if item["xdomain_request"]["enable"]:
                xdomain_request = str_replace(
                    template["features"]["xdomain_request"],
                    {"__REGEX__": to_str(item["xdomain_request"]["origin"])},
                )
        # Replace everything
        aliases.append(
            str_replace(
                template["features"]["alias"],
                {
                    "__ALIAS__": to_str(item["alias"]),
                    "__PATH__": to_str(item["path"]),
                    "__XDOMAIN_REQ__": str_indent(xdomain_request, 4).rstrip(),
                },
            )
        )
    # Join by OS independent newlines
    output = os.linesep.join(aliases)
    log(3, "[alias]   Returning: {}".format(output), verbose)
    return output


def vhost_get_denies(config, template, verbose):
    # type: (Dict[str, Any], Dict[str, Any], int) -> str
    """Get virtual host deny alias directives."""
    denies = []
    for item in config["vhost"]["deny"]:
        log(3, "[denies]  Retrieving cfg 'deny': {}".format(item["alias"]), verbose)
        denies.append(
            str_replace(template["features"]["deny"], {"__REGEX__": to_str(item["alias"])})
        )
    # Join by OS independent newlines
    output = os.linesep.join(denies)
    log(3, "[denies]  Returning: {}".format(output), verbose)
    return output


def vhost_get_server_status(config, template, verbose):
    # type: (Dict[str, Any], Dict[str, Any], int) -> str
    """Get virtual host server status directivs."""
    status = ""
    enable = config["vhost"]["server_status"]["enable"]

    log(3, "[status]  Retrieving cfg 'enable': {}".format(enable), verbose)
    if enable:
        status = template["features"]["server_status"]
        log(3, "[status]  Retrieving tpl 'server_status': {}".format(status), verbose)

    output = str_replace(status, {"__REGEX__": to_str(config["vhost"]["server_status"]["alias"])})
    log(3, "[status]  Returning: {}".format(output), verbose)
    return output


def vhost_get_custom_section(config, template, verbose):
    # type: (Dict[str, Any], Dict[str, Any], int) -> str
    """Get virtual host custom directives."""
    output = to_str(config["custom"])
    log(3, "[custom]  Retrieving cfg 'custom': {}".format(output), verbose)
    if output:
        output += "\n"

    if "custom" in template:
        output += to_str(template["custom"])

    log(3, "[custom]  Returning': {}".format(output), verbose)
    return output


# --------------------------------------------------------------------------------------------------
# vHost create
# --------------------------------------------------------------------------------------------------
def get_vhost_plain(
    config,  # type: Dict[str, Any]
    tpl,  # type: Dict[str, Any]
    docroot,  # type: Optional[str]
    proxy,  # type: str
    location,  # type: Optional[str]
    server_name,  # type: str
    default,  # type: bool
    verbose,  # type: int
):
    # type: (...) -> str
    """Get plain vhost"""
    return str_replace(
        tpl["vhost"],
        {
            "__PORT__": vhost_get_port(config, False),
            "__HTTP_PROTO__": vhost_get_http_proto(config, False),
            "__DEFAULT_VHOST__": vhost_get_default_server(config, default),
            "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy, verbose),
            "__VHOST_NAME__": vhost_get_server_name(config, server_name, default),
            "__VHOST_DOCROOT__": str_indent(
                vhost_get_vhost_docroot(config, tpl, docroot, proxy, verbose), 4
            ),
            "__VHOST_RPROXY__": str_indent(
                vhost_get_vhost_rproxy(tpl, proxy, location, verbose), 4
            ),
            "__REDIRECT__": "",
            "__SSL__": "",
            "__INDEX__": vhost_get_index(config, verbose),
            "__ACCESS_LOG__": vhost_get_access_log(config, server_name),
            "__ERROR_LOG__": vhost_get_error_log(config, server_name),
            "__PHP_FPM__": str_indent(vhost_get_php_fpm(config, tpl, docroot, proxy, verbose), 4),
            "__ALIASES__": str_indent(vhost_get_aliases(config, tpl, verbose), 4),
            "__DENIES__": str_indent(vhost_get_denies(config, tpl, verbose), 4),
            "__SERVER_STATUS__": str_indent(vhost_get_server_status(config, tpl, verbose), 4),
            "__CUSTOM__": str_indent(vhost_get_custom_section(config, tpl, verbose), 4),
        },
    )


def get_vhost_ssl(
    config,  # type: Dict[str, Any]
    tpl,  # type: Dict[str, Any]
    docroot,  # type: Optional[str]
    proxy,  # type: str
    location,  # type: Optional[str]
    server_name,  # type: str
    default,  # type: bool
    verbose,  # type: int
):
    # type: (...) -> str
    """Get ssl vhost"""
    return str_replace(
        tpl["vhost"],
        {
            "__PORT__": vhost_get_port(config, True),
            "__HTTP_PROTO__": vhost_get_http_proto(config, True),
            "__DEFAULT_VHOST__": vhost_get_default_server(config, default),
            "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy, verbose),
            "__VHOST_NAME__": vhost_get_server_name(config, server_name, default),
            "__VHOST_DOCROOT__": str_indent(
                vhost_get_vhost_docroot(config, tpl, docroot, proxy, verbose), 4
            ),
            "__VHOST_RPROXY__": str_indent(
                vhost_get_vhost_rproxy(tpl, proxy, location, verbose), 4
            ),
            "__REDIRECT__": "",
            "__SSL__": str_indent(vhost_get_vhost_ssl(config, tpl, server_name, verbose), 4),
            "__INDEX__": vhost_get_index(config, verbose),
            "__ACCESS_LOG__": vhost_get_access_log(config, server_name + "_ssl"),
            "__ERROR_LOG__": vhost_get_error_log(config, server_name + "_ssl"),
            "__PHP_FPM__": str_indent(vhost_get_php_fpm(config, tpl, docroot, proxy, verbose), 4),
            "__ALIASES__": str_indent(vhost_get_aliases(config, tpl, verbose), 4),
            "__DENIES__": str_indent(vhost_get_denies(config, tpl, verbose), 4),
            "__SERVER_STATUS__": str_indent(vhost_get_server_status(config, tpl, verbose), 4),
            "__CUSTOM__": str_indent(vhost_get_custom_section(config, tpl, verbose), 4),
        },
    )


def get_vhost_redir(
    config,  # type: Dict[str, Any]
    tpl,  # type: Dict[str, Any]
    docroot,  # type: Optional[str]
    proxy,  # type: str
    server_name,  # type: str
    default,  # type: bool
    verbose,  # type: int
):
    # type: (...) -> str
    """Get redirect to ssl vhost"""
    return str_replace(
        tpl["vhost"],
        {
            "__PORT__": vhost_get_port(config, False),
            "__HTTP_PROTO__": vhost_get_http_proto(config, False),
            "__DEFAULT_VHOST__": vhost_get_default_server(config, default),
            "__DOCUMENT_ROOT__": vhost_get_docroot_path(config, docroot, proxy, verbose),
            "__VHOST_NAME__": vhost_get_server_name(config, server_name, default),
            "__VHOST_DOCROOT__": "",
            "__VHOST_RPROXY__": "",
            "__REDIRECT__": str_indent(vhost_get_vhost_redir(config, tpl, server_name), 4),
            "__SSL__": "",
            "__INDEX__": "",
            "__ACCESS_LOG__": vhost_get_access_log(config, server_name),
            "__ERROR_LOG__": vhost_get_error_log(config, server_name),
            "__PHP_FPM__": "",
            "__ALIASES__": "",
            "__DENIES__": "",
            "__SERVER_STATUS__": "",
            "__CUSTOM__": "",
        },
    )


def get_vhost(
    config,  # type: Dict[str, Any]
    tpl,  # type: Dict[str, Any]
    docroot,  # type: Optional[str]
    proxy,  # type: str
    mode,  # type: str
    location,  # type: Optional[str]
    server_name,  # type: str
    default,  # type: bool
    verbose,  # type: int
):
    # type: (...) -> str
    """Create the vhost."""

    if mode == "ssl":
        log(2, "Creating vhost type: https-only (ssl)", verbose)
        return get_vhost_ssl(config, tpl, docroot, proxy, location, server_name, default, verbose)
    if mode == "both":
        log(2, "Creating vhost type: https and http (both)", verbose)
        return get_vhost_ssl(
            config, tpl, docroot, proxy, location, server_name, default, verbose
        ) + get_vhost_plain(config, tpl, docroot, proxy, location, server_name, default, verbose)

    if mode == "redir":
        log(2, "Creating vhost type: https and redirect http to https (redir)", verbose)
        return get_vhost_ssl(
            config, tpl, docroot, proxy, location, server_name, default, verbose
        ) + get_vhost_redir(config, tpl, docroot, proxy, server_name, default, verbose)

    log(2, "Creating vhost type: http-only (plain)", verbose)
    return get_vhost_plain(config, tpl, docroot, proxy, location, server_name, default, verbose)


# --------------------------------------------------------------------------------------------------
# Load configs and templates
# --------------------------------------------------------------------------------------------------
def load_config(config_path, verbose):
    # type: (str, int) -> Tuple[bool, Dict[str, Any], str]
    """Load config and merge with defaults in case not found or something is missing."""

    # Load configuration file
    log(2, "Loading configuration file        (-c): {}".format(config_path), verbose)
    if os.path.isfile(config_path):
        succ, config, err = load_yaml(config_path)
        if not succ:
            return (False, {}, err)
    else:
        log(1, "Config file not found in: {}".format(config_path), verbose)
        log(1, "Using default config settings", verbose)
        config = {}

    # Merge config settings with program defaults (config takes precedence over defaults)
    config = merge_yaml(DEFAULT_CONFIG, config)

    return (True, config, "")


def load_template(template_dir, o_template_dir, server, verbose):
    # type: (str, str, str, int) -> Tuple[bool, Dict[str, Any], str]
    """Load global and optional template file and merge them."""

    # Load global template file
    log(
        2,
        "Loading vhost template (global)   (-t): {}".format(
            os.path.join(template_dir, TEMPLATES[server])
        ),
        verbose,
    )
    succ, template, err = load_yaml(os.path.join(template_dir, TEMPLATES[server]))
    if not succ:
        return (
            False,
            {},
            "(global vhost template: "
            + os.path.join(template_dir, TEMPLATES[server])
            + "): "
            + err,
        )

    # Load optional template file (if specified file and merge it)
    if o_template_dir is not None:
        log(
            2,
            "Loading vhost template (override) (-o): {}".format(
                os.path.join(o_template_dir, TEMPLATES[server])
            ),
            verbose,
        )
        # Template dir exists, but no file was added, give the user a warning, but do not abort
        if not os.path.isfile(os.path.join(o_template_dir, TEMPLATES[server])):
            log(
                2,
                "Override Vhost template not found: {}".format(
                    os.path.join(o_template_dir, TEMPLATES[server])
                ),
                verbose,
            )
        else:
            succ, template2, err = load_yaml(os.path.join(o_template_dir, TEMPLATES[server]))
            if not succ:
                return (
                    False,
                    {},
                    "(override vhost template: "
                    + os.path.join(o_template_dir, TEMPLATES[server])
                    + "): "
                    + err,
                )
            template = merge_yaml(template, template2)

    return (True, template, "")


# --------------------------------------------------------------------------------------------------
# Post actions
# --------------------------------------------------------------------------------------------------
def apply_log_settings(config, verbose):
    # type: (Dict[str, Any], int) -> Tuple[bool, Optional[str]]
    """
    This function will apply various settings for the log defines, including
    creating the directory itself as well as handling log file output (access
    and error) to stderr/stdout.
    """
    # Symlink stdout to access logfile
    if config["vhost"]["log"]["access"]["stdout"]:
        log(2, "Log setting: stdout -> /dev/stdout", verbose)
        succ, err = symlink("/dev/stdout", STDOUT_ACCESS, force=True)
        if not succ:
            return (False, err)

    # Symlink stderr to error logfile
    if config["vhost"]["log"]["error"]["stderr"]:
        log(2, "Log setting: stderr -> /dev/stderr", verbose)
        succ, err = symlink("/dev/stderr", STDERR_ERROR, force=True)
        if not succ:
            return (False, err)

    # Create log dir
    if config["vhost"]["log"]["dir"]["create"]:
        log(
            2,
            "Log setting: dir -> {}".format(os.path.isdir(config["vhost"]["log"]["dir"]["path"])),
            verbose,
        )
        if not os.path.isdir(config["vhost"]["log"]["dir"]["path"]):
            try:
                os.makedirs(config["vhost"]["log"]["dir"]["path"])
            except OSError as err:
                return (False, "[ERR] Cannot create directory: " + str(err))

    log(2, "Log setting: Not specified", verbose)
    return (True, None)


# --------------------------------------------------------------------------------------------------
# Main Function
# --------------------------------------------------------------------------------------------------
def main(argv):
    # type: (List[Any]) -> None
    """Main entrypoint."""

    # Get command line arguments
    (
        config_path,
        tpl_dir,
        o_tpl_dir,
        docroot,
        proxy,
        mode,
        location,
        name,
        default,
        save,
        verbose,
    ) = parse_args(argv)

    # Validate command line arguments This will abort the program on error
    # This will abort the program on error
    validate_args_req(name, docroot, proxy, mode, location, verbose)
    validate_args_opt(config_path, tpl_dir, verbose)

    # Load config
    succ, config, err_config = load_config(config_path, verbose)
    if not succ:
        log(0, "Error loading config: {}".format(err_config), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    # Load template
    succ, template, err_tpl = load_template(tpl_dir, o_tpl_dir, config["server"], verbose)
    if not succ:
        log(0, "Error loading tempalte: {}".format(err_tpl), verbose)
        log(0, "Type --help for help", verbose)
        sys.exit(1)

    # Validate configuration file
    # This will abort the program on error
    validate_config(config, verbose)

    # Retrieve fully build vhost
    vhost = get_vhost(config, template, docroot, proxy, mode, location, name, default, verbose)

    log(
        2,
        "Using vhost name: {}".format(
            to_str(config["vhost"]["name"]["prefix"])
            + name
            + to_str(config["vhost"]["name"]["suffix"])
        ),
        verbose,
    )

    if save:
        if not os.path.isdir(config["conf_dir"]):
            log(0, "conf_dir does not exist: " + config["conf_dir"], verbose)
            sys.exit(1)
        if not os.access(config["conf_dir"], os.W_OK):
            log(0, "conf_dir does not have write permissions: " + config["conf_dir"], verbose)
            sys.exit(1)

        vhost_path = os.path.join(config["conf_dir"], name + ".conf")
        with open(vhost_path, "w") as outfile:
            outfile.write(vhost)

        # Apply settings for logging (symlinks, mkdir) only in save mode
        succ, err_apply = apply_log_settings(config, verbose)
        if not succ:
            log(0, str(err_apply), verbose)
            sys.exit(1)
        log(2, "Vhost config written to: " + vhost_path, verbose)
    else:
        print(vhost)


# --------------------------------------------------------------------------------------------------
# Main Entry Point
# --------------------------------------------------------------------------------------------------
if __name__ == "__main__":
    main(sys.argv[1:])
