#!/usr/bin/env python3
#
# Copyright (C) 2018 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#

import sys
import re
import os
import jinja2
import syslog as sl
import time

import vyos.config
import vyos.defaults

from vyos import ConfigError


ra_conn_name = "remote-access"
charon_conf_file = "/etc/strongswan.d/charon.conf"
ipsec_secrets_flie = "/etc/ipsec.secrets"
ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
ipsec_conf_flie = "/etc/ipsec.conf"
ca_cert_path = "/etc/ipsec.d/cacerts"
server_cert_path = "/etc/ipsec.d/certs"
server_key_path = "/etc/ipsec.d/private"
delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
charon_pidfile = "/var/run/charon.pid"

l2pt_ipsec_conf = '''
{{delim_ipsec_l2tp_begin}}
include {{ipsec_ra_conn_file}}
{{delim_ipsec_l2tp_end}}
'''

l2pt_ipsec_secrets_conf = '''
{{delim_ipsec_l2tp_begin}}
{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}"
{% elif ipsec_l2tp_auth_mode == 'x509' %}
: RSA {{server_key_file_copied}}
{% endif%}
{{delim_ipsec_l2tp_end}}
'''

l2tp_ipsec_ra_conn_conf = '''
{{delim_ipsec_l2tp_begin}}
conn {{ra_conn_name}}
  type=transport
  left={{outside_addr}}
  leftsubnet=%dynamic[/1701]
  rightsubnet=%dynamic
  mark_in=%unique
  auto=add
  ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024!
  dpddelay=15
  dpdtimeout=45
  dpdaction=clear
  esp=aes256-sha1,3des-sha1!
  rekey=no
{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
  authby=secret
  leftauth=psk
  rightauth=psk
{% elif ipsec_l2tp_auth_mode == 'x509' %}
  authby=rsasig
  leftrsasigkey=%cert
  rightrsasigkey=%cert
  rightca=%same
  leftcert={{server_cert_file_copied}}
{% endif %}
  ikelifetime={{ipsec_l2tp_ike_lifetime}}
  keylife={{ipsec_l2tp_lifetime}}
{{delim_ipsec_l2tp_end}}
'''

def get_config():
    config = vyos.config.Config()
    data = {"install_routes": "yes"}

    if config.exists("vpn ipsec options disable-route-autoinstall"):
        data["install_routes"] = "no"

    if config.exists("vpn ipsec ipsec-interfaces interface"):
        data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface")

    # Init config variables
    data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin
    data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end
    data["ipsec_ra_conn_file"] = ipsec_ra_conn_file
    data["ra_conn_name"] = ra_conn_name
    # Get l2tp ipsec settings
    data["ipsec_l2tp"] = False
    conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful
    if config.exists(conf_ipsec_command):
        data["ipsec_l2tp"] = True

        # Authentication params
        if config.exists(conf_ipsec_command + "authentication mode"):
            data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode")
        if config.exists(conf_ipsec_command + "authentication pre-shared-secret"):
            data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret")

        # mode x509
        if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"):
            data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file")
        if config.exists(conf_ipsec_command + "authentication x509 crl-file"):
            data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file")
        if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"):
            data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")
            data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0)
        if config.exists(conf_ipsec_command + "authentication x509 server-key-file"):
            data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file")
            data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0)
        if config.exists(conf_ipsec_command + "authentication x509 server-key-password"):
            data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password")

        # Common l2tp ipsec params
        if config.exists(conf_ipsec_command + "ike-lifetime"):
            data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime")
        else:
            data["ipsec_l2tp_ike_lifetime"] = "3600"

        if config.exists(conf_ipsec_command + "lifetime"):
            data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime")
        else:
            data["ipsec_l2tp_lifetime"] = "3600"

    if config.exists("vpn l2tp remote-access outside-address"):
        data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address')

    return data

### ipsec secret l2tp
def write_ipsec_secrets(c):
    tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True)
    l2pt_ipsec_secrets_txt = tmpl.render(c)
    old_umask = os.umask(0o077)
    open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt)
    os.umask(old_umask)
    sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written')

### ipsec remote access connection config
def write_ipsec_ra_conn(c):
    tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True)
    ipsec_ra_conn_txt = tmpl.render(c)
    old_umask = os.umask(0o077)

    # Create tunnels directory if does not exist
    if not os.path.exists(ipsec_ra_conn_dir):
        os.makedirs(ipsec_ra_conn_dir)
        sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_dir  + " created")

    open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt)
    os.umask(old_umask)
    sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written')

### Remove config from file by delimiter
def remove_confs(delim_begin, delim_end, conf_file):
    os.system("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file)


### Append "include /path/to/ra_conn" to ipsec conf file
def append_ipsec_conf(c):
    tmpl = jinja2.Template(l2pt_ipsec_conf, trim_blocks=True)
    l2pt_ipsec_conf_txt = tmpl.render(c)
    old_umask = os.umask(0o077)
    open(ipsec_conf_flie,'a').write(l2pt_ipsec_conf_txt)
    os.umask(old_umask)
    sl.syslog(sl.LOG_NOTICE, ipsec_conf_flie + ' written')

### Checking certificate storage and notice if certificate not in /config directory
def check_cert_file_store(cert_name, file_path, dts_path):
    if not re.search('^\/config\/.+', file_path):
        print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.")
    #Checking file existence
    if not os.path.isfile(file_path):
      raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"")
    else:
      ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/
      # todo make check
      ret = os.system('cp -f '+file_path+' '+dts_path)
      if ret:
         raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path)
      else:
        sl.syslog(sl.LOG_NOTICE, file_path + ' copied to '+dts_path)

def verify(data):
    # l2tp ipsec check
    if data["ipsec_l2tp"]:
        # Checking dependecies for "authentication mode pre-shared-secret"
        if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
            if not data.get("ipsec_l2tp_secret"):
                raise ConfigError("pre-shared-secret required")
            if not data.get("outside_addr"):
                raise ConfigError("outside-address not defined")

        # Checking dependecies for "authentication mode x509"
        if data.get("ipsec_l2tp_auth_mode") == "x509":
            if not data.get("ipsec_l2tp_x509_server_key_file"):
                raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.")
            else:
                check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path)

            if not data.get("ipsec_l2tp_x509_server_cert_file"):
                raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.")
            else:
                check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path)

            if not data.get("ipsec_l2tp_x509_ca_cert_file"):
                raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509")
            else:
                check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path)

        if not data.get('ipsec_interfaces'):
           raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.")

def generate(data):
    tmpl_path = os.path.join(vyos.defaults.directories["data"], "templates", "ipsec")
    fs_loader = jinja2.FileSystemLoader(tmpl_path)
    env = jinja2.Environment(loader=fs_loader)


    charon_conf_tmpl = env.get_template("charon.tmpl")
    charon_conf = charon_conf_tmpl.render(data)

    with open(charon_conf_file, 'w') as f:
        f.write(charon_conf)

    if data["ipsec_l2tp"]:
        remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie)
        write_ipsec_secrets(data)
        write_ipsec_ra_conn(data)
        append_ipsec_conf(data)
    else:
        if os.path.exists(ipsec_ra_conn_file):
            remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file)
        remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_flie)
        remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie)

def restart_ipsec():
    os.system("ipsec restart >&/dev/null")
    # counter for apply swanctl config
    counter = 10
    while counter <= 10:
        if os.path.exists(charon_pidfile):
            os.system("swanctl -q >&/dev/null")
            break
        counter -=1
        time.sleep(1)
        if counter == 0:
            raise ConfigError('VPN configuration error: IPSec is not running.')

def apply(data):
    # Restart IPSec daemon
    restart_ipsec()

if __name__ == '__main__':
    try:
        c = get_config()
        verify(c)
        generate(c)
        apply(c)
    except ConfigError as e:
        print(e)
        sys.exit(1)