summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@baturin.org>2019-09-04 15:39:38 +0200
committerDaniil Baturin <daniil@baturin.org>2019-09-04 15:39:38 +0200
commit4d50edfc9543f3d27eb83300dd27d598ffe63fe2 (patch)
tree6d403977484aa5231659d17efdd031ea01c1a768
parent6167fab1737ef59f223ee6c7fff0a493c76d9793 (diff)
downloadvyos-1x-4d50edfc9543f3d27eb83300dd27d598ffe63fe2.tar.gz
vyos-1x-4d50edfc9543f3d27eb83300dd27d598ffe63fe2.zip
T1443: backport the HTTP API to crux.
Implementation by Daniil Baturin and John Estabrook.
-rw-r--r--debian/control2
-rw-r--r--interface-definitions/https.xml123
-rw-r--r--python/vyos/config.py26
-rw-r--r--python/vyos/configsession.py160
-rw-r--r--python/vyos/defaults.py26
-rwxr-xr-xsrc/conf_mode/http-api.py98
-rwxr-xr-xsrc/conf_mode/https.py186
-rwxr-xr-xsrc/conf_mode/vyos_cert.py143
-rwxr-xr-xsrc/services/vyos-http-api-server247
-rw-r--r--src/systemd/vyos-http-api.service24
10 files changed, 1032 insertions, 3 deletions
diff --git a/debian/control b/debian/control
index 8ad0ff1df..28e60f30e 100644
--- a/debian/control
+++ b/debian/control
@@ -27,6 +27,7 @@ Depends: python3,
python3-isc-dhcp-leases,
python3-hurry.filesize,
python3-vici (>= 5.7.2),
+ python3-bottle,
python3-zmq,
ipaddrcheck,
tcpdump,
@@ -56,6 +57,7 @@ Depends: python3,
mdns-repeater,
udp-broadcast-relay,
pdns-recursor,
+ ssl-cert, nginx-light,
${shlibs:Depends},
${misc:Depends}
Description: VyOS configuration scripts and data
diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml
new file mode 100644
index 000000000..7a87133f3
--- /dev/null
+++ b/interface-definitions/https.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0"?>
+<!-- HTTPS configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="https" owner="${vyos_conf_scripts_dir}/https.py">
+ <properties>
+ <help>HTTPS configuration</help>
+ <priority>1001</priority>
+ </properties>
+ <children>
+ <tagNode name="listen-addresses">
+ <properties>
+ <help>Addresses to listen for HTTPS requests</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>HTTPS IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>HTTPS IPv6 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>'*'</format>
+ <description>any</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ <regex>^\\*$</regex>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="server-names">
+ <properties>
+ <help>Server names: exact, wildcard, regex, or '_' (any)</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="certificates">
+ <properties>
+ <help>TLS certificates</help>
+ </properties>
+ <children>
+ <node name="system-generated-certificate" owner="${vyos_conf_scripts_dir}/vyos_cert.py">
+ <properties>
+ <help>Use an automatically generated self-signed certificate</help>
+ <valueless/>
+ </properties>
+ <children>
+ <leafNode name="lifetime">
+ <properties>
+ <help>Lifetime in days; default is 365</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Number of days</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ <node name="api" owner="${vyos_conf_scripts_dir}/http-api.py">
+ <properties>
+ <help>VyOS HTTP API configuration</help>
+ <priority>1002</priority>
+ </properties>
+ <children>
+ <leafNode name="port">
+ <properties>
+ <help>Port for HTTP API service</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="keys">
+ <properties>
+ <help>HTTP API keys</help>
+ </properties>
+ <children>
+ <tagNode name="id">
+ <properties>
+ <help>HTTP API id</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>HTTP API plaintext key</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="strict">
+ <properties>
+ <help>Enforce strict path checking</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="debug">
+ <properties>
+ <help>Debug</help>
+ <valueless/>
+ <hidden/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
+
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 7483e3552..c9c73b971 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -87,9 +87,13 @@ class Config(object):
the only state it keeps is relative *config path* for convenient access to config
subtrees.
"""
- def __init__(self):
+ def __init__(self, session_env=None):
self._cli_shell_api = "/bin/cli-shell-api"
self._level = ""
+ if session_env:
+ self.__session_env = session_env
+ else:
+ self.__session_env = None
def _make_command(self, op, path):
args = path.split()
@@ -97,7 +101,10 @@ class Config(object):
return cmd
def _run(self, cmd):
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ if self.__session_env:
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env)
+ else:
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
out = p.stdout.read()
p.wait()
if p.returncode != 0:
@@ -169,6 +176,21 @@ class Config(object):
except VyOSError:
return False
+ def show_config(self, path='', default=None):
+ """
+ Args:
+ path (str): Configuration tree path, or empty
+ default (str): Default value to return
+
+ Returns:
+ str: working configuration
+ """
+ try:
+ out = self._run(self._make_command('showConfig', path))
+ return out
+ except VyOSError:
+ return(default)
+
def is_multi(self, path):
"""
Args:
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
new file mode 100644
index 000000000..acbdd3d5f
--- /dev/null
+++ b/python/vyos/configsession.py
@@ -0,0 +1,160 @@
+# configsession -- the write API for the VyOS running config
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This library is free software; you can redistribute it and/or modify it under the terms of
+# the GNU Lesser General Public License as published by the Free Software Foundation;
+# either version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along with this library;
+# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import re
+import sys
+import subprocess
+
+CLI_SHELL_API = '/bin/cli-shell-api'
+SET = '/opt/vyatta/sbin/my_set'
+DELETE = '/opt/vyatta/sbin/my_delete'
+COMMENT = '/opt/vyatta/sbin/my_comment'
+COMMIT = '/opt/vyatta/sbin/my_commit'
+DISCARD = '/opt/vyatta/sbin/my_discard'
+SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
+LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
+
+# Default "commit via" string
+APP = "vyos-http-api"
+
+# When started as a service rather than from a user shell,
+# the process lacks the VyOS-specific environment that comes
+# from bash configs, so we have to inject it
+# XXX: maybe it's better to do via a systemd environment file
+def inject_vyos_env(env):
+ env['VYATTA_CFG_GROUP_NAME'] = 'vyattacfg'
+ env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin'
+ env['vyatta_bindir']= '/opt/vyatta/bin'
+ env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyatta_configdir'] = '/opt/vyatta/config'
+ env['vyatta_datadir'] = '/opt/vyatta/share'
+ env['vyatta_datarootdir'] = '/opt/vyatta/share'
+ env['vyatta_libdir'] = '/opt/vyatta/lib'
+ env['vyatta_libexecdir'] = '/opt/vyatta/libexec'
+ env['vyatta_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyatta_prefix'] = '/opt/vyatta'
+ env['vyatta_sbindir'] = '/opt/vyatta/sbin'
+ env['vyatta_sysconfdir'] = '/opt/vyatta/etc'
+ env['vyos_bin_dir'] = '/usr/bin'
+ env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyos_completion_dir'] = '/usr/libexec/vyos/completion'
+ env['vyos_configdir'] = '/opt/vyatta/config'
+ env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode'
+ env['vyos_datadir'] = '/opt/vyatta/share'
+ env['vyos_datarootdir']= '/opt/vyatta/share'
+ env['vyos_libdir'] = '/opt/vyatta/lib'
+ env['vyos_libexec_dir'] = '/usr/libexec/vyos'
+ env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode'
+ env['vyos_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyos_prefix'] = '/opt/vyatta'
+ env['vyos_sbin_dir'] = '/usr/sbin'
+ env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
+
+ return env
+
+
+class ConfigSessionError(Exception):
+ pass
+
+
+class ConfigSession(object):
+ """
+ The write API of VyOS.
+ """
+ def __init__(self, session_id, app=APP):
+ """
+ Creates a new config session.
+
+ Args:
+ session_id (str): Session identifier
+ app (str): Application name, purely informational
+
+ Note:
+ The session identifier MUST be globally unique within the system.
+ The best practice is to only have one ConfigSession object per process
+ and used the PID for the session identifier.
+ """
+
+ env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)])
+ self.__session_id = session_id
+
+ # Extract actual variables from the chunk of shell it outputs
+ # XXX: it's better to extend cli-shell-api to provide easily readable output
+ env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode())
+
+ session_env = os.environ
+ session_env = inject_vyos_env(session_env)
+ for k, v in env_list:
+ session_env[k] = v
+
+ self.__session_env = session_env
+ self.__session_env["COMMIT_VIA"] = app
+
+ self.__run_command([CLI_SHELL_API, 'setupSession'])
+
+ def __del__(self):
+ try:
+ output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip()
+ if output:
+ print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr)
+ except Exception as e:
+ print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr)
+
+ def __run_command(self, cmd_list):
+ p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env)
+ result = p.wait()
+ output = p.stdout.read().decode()
+ if result != 0:
+ raise ConfigSessionError(output)
+ return output
+
+ def get_session_env(self):
+ return self.__session_env
+
+ def set(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([SET] + path + value)
+
+ def delete(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([DELETE] + path + value)
+
+ def comment(self, path, value=None):
+ if not value:
+ value = [""]
+ else:
+ value = [value]
+ self.__run_command([COMMENT] + path + value)
+
+ def commit(self):
+ self.__run_command([COMMIT])
+
+ def discard(self):
+ self.__run_command([DISCARD])
+
+ def show_config(self, path, format='raw'):
+ config_data = self.__run_command(SHOW_CONFIG + path)
+
+ if format == 'raw':
+ return config_data
+
+ def load_config(self, file_path):
+ self.__run_command(LOAD_CONFIG + [file_path])
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index c46723626..f51e4ddda 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -15,7 +15,11 @@
directories = {
- "data": "/usr/share/vyos/"
+ "data": "/usr/share/vyos/",
+ "conf_mode": "/usr/libexec/vyos/conf_mode",
+ "config": "/opt/vyatta/etc/config",
+ "current": "/opt/vyatta/etc/config-migrate/current",
+ "migrate": "/opt/vyatta/etc/config-migrate/migrate",
}
cfg_group = 'vyattacfg'
@@ -23,3 +27,23 @@ cfg_group = 'vyattacfg'
cfg_vintage = 'vyatta'
commit_lock = '/opt/vyatta/config/.lock'
+
+https_data = {
+ 'listen_addresses' : { '*': ['_'] }
+}
+
+api_data = {
+ 'listen_address' : '127.0.0.1',
+ 'port' : '8080',
+ 'strict' : 'false',
+ 'debug' : 'false',
+ 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
+}
+
+vyos_cert_data = {
+ "conf": "/etc/nginx/snippets/vyos-cert.conf",
+ "crt": "/etc/ssl/certs/vyos-selfsigned.crt",
+ "key": "/etc/ssl/private/vyos-selfsign",
+ "lifetime": "365",
+}
+
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
new file mode 100755
index 000000000..1f91ac582
--- /dev/null
+++ b/src/conf_mode/http-api.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 os
+import subprocess
+import json
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = '/etc/vyos/http-api.conf'
+
+vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def get_config():
+ http_api = vyos.defaults.api_data
+
+ conf = Config()
+ if not conf.exists('service https api'):
+ return None
+ else:
+ conf.set_level('service https api')
+
+ if conf.exists('strict'):
+ http_api['strict'] = 'true'
+
+ if conf.exists('debug'):
+ http_api['debug'] = 'true'
+
+ if conf.exists('port'):
+ port = conf.return_value('port')
+ http_api['port'] = port
+
+ if conf.exists('keys'):
+ for name in conf.list_nodes('keys id'):
+ if conf.exists('keys id {0} key'.format(name)):
+ key = conf.return_value('keys id {0} key'.format(name))
+ new_key = { 'id': name, 'key': key }
+ http_api['api_keys'].append(new_key)
+
+ return http_api
+
+def verify(http_api):
+ return None
+
+def generate(http_api):
+ if http_api is None:
+ return None
+
+ with open(config_file, 'w') as f:
+ json.dump(http_api, f, indent=2)
+
+ return None
+
+def apply(http_api):
+ if http_api is not None:
+ os.system('sudo systemctl restart vyos-http-api.service')
+ else:
+ os.system('sudo systemctl stop vyos-http-api.service')
+
+ for dep in dependencies:
+ cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ try:
+ subprocess.check_call(cmd, shell=True)
+ except subprocess.CalledProcessError as err:
+ raise ConfigError("{}.".format(err))
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
new file mode 100755
index 000000000..d5aa1f5b3
--- /dev/null
+++ b/src/conf_mode/https.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 os
+
+import jinja2
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = '/etc/nginx/sites-available/default'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+
+### Autogenerated by http-api.py ###
+# Default server configuration
+#
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+ server_name _;
+ return 302 https://$server_name$request_uri;
+}
+
+{% for addr, names in listen_addresses.items() %}
+server {
+
+ # SSL configuration
+ #
+{% if addr == '*' %}
+ listen 443 ssl default_server;
+ listen [::]:443 ssl default_server;
+{% else %}
+ listen {{ addr }}:443 ssl;
+{% endif %}
+
+{% for name in names %}
+ server_name {{ name }};
+{% endfor %}
+
+{% if vyos_cert %}
+ include {{ vyos_cert.conf }};
+{% else %}
+ #
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ #
+ include snippets/snakeoil.conf;
+{% endif %}
+
+ # proxy settings for HTTP API, if enabled; 503, if not
+ location ~ /(retrieve|configure) {
+{% if api %}
+ proxy_pass http://localhost:{{ api.port }};
+ proxy_buffering off;
+{% else %}
+ return 503;
+{% endif %}
+ }
+
+ error_page 501 502 503 =200 @50*_json;
+
+ location @50*_json {
+ default_type application/json;
+ return 200 '{"error": "Start service in configuration mode: set service https api"}';
+ }
+
+}
+{% else %}
+server {
+ # SSL configuration
+ #
+ listen 443 ssl default_server;
+ listen [::]:443 ssl default_server;
+
+ server_name _;
+
+{% if vyos_cert %}
+ include {{ vyos_cert.conf }};
+{% else %}
+ #
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ #
+ include snippets/snakeoil.conf;
+{% endif %}
+
+ # proxy settings for HTTP API, if enabled; 503, if not
+ location ~ /(retrieve|configure) {
+{% if api %}
+ proxy_pass http://localhost:{{ api.port }};
+ proxy_buffering off;
+{% else %}
+ return 503;
+{% endif %}
+ }
+
+ error_page 501 502 503 =200 @50*_json;
+
+ location @50*_json {
+ default_type application/json;
+ return 200 '{"error": "Start service in configuration mode: set service https api"}';
+ }
+
+}
+
+{% endfor %}
+"""
+
+def get_config():
+ https = vyos.defaults.https_data
+ conf = Config()
+ if not conf.exists('service https'):
+ return None
+ else:
+ conf.set_level('service https')
+
+ if conf.exists('listen-addresses'):
+ addrs = {}
+ for addr in conf.list_nodes('listen-addresses'):
+ addrs[addr] = ['_']
+ if conf.exists('listen-addresses {0} server-names'.format(addr)):
+ names = conf.return_values('listen-addresses {0} server-names'.format(addr))
+ addrs[addr] = names[:]
+ https['listen_addresses'] = addrs
+
+ if conf.exists('certificates'):
+ if conf.exists('certificates system-generated-certificate'):
+ https['vyos_cert'] = vyos.defaults.vyos_cert_data
+
+ if conf.exists('api'):
+ https['api'] = vyos.defaults.api_data
+
+ if conf.exists('api port'):
+ port = conf.return_value('api port')
+ https['api']['port'] = port
+
+ return https
+
+def verify(https):
+ return None
+
+def generate(https):
+ if https is None:
+ return None
+
+ tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
+ config_text = tmpl.render(https)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(https):
+ if https is not None:
+ os.system('sudo systemctl restart nginx.service')
+ else:
+ os.system('sudo systemctl stop nginx.service')
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
new file mode 100755
index 000000000..4a44573ca
--- /dev/null
+++ b/src/conf_mode/vyos_cert.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 os
+import subprocess
+import tempfile
+import pathlib
+import ssl
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def status_self_signed(cert_data):
+# check existence and expiration date
+ path = pathlib.Path(cert_data['conf'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['crt'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['key'])
+ if not path.is_file():
+ return False
+
+ # check if certificate is 1/2 past lifetime, with openssl -checkend
+ end_days = int(cert_data['lifetime'])
+ end_seconds = int(0.5*60*60*24*end_days)
+ checkend_cmd = ('openssl x509 -checkend {end} -noout -in {crt}'
+ ''.format(end=end_seconds, **cert_data))
+ try:
+ subprocess.check_call(checkend_cmd, shell=True)
+ return True
+ except subprocess.CalledProcessError as err:
+ if err.returncode == 1:
+ return False
+ else:
+ print("Called process error: {}.".format(err))
+
+def generate_self_signed(cert_data):
+ san_config = None
+
+ if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0):
+ san_config = tempfile.NamedTemporaryFile()
+ with open(san_config.name, 'w') as fd:
+ fd.write('[req]\n')
+ fd.write('distinguished_name=req\n')
+ fd.write('[san]\n')
+ fd.write('subjectAltName=DNS:vyos\n')
+
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-extensions san -config {san_conf}'
+ ''.format(san_conf=san_config.name,
+ **cert_data))
+
+ else:
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-addext "subjectAltName=DNS:vyos"'
+ ''.format(**cert_data))
+
+ try:
+ subprocess.check_call(openssl_req_cmd, shell=True)
+ except subprocess.CalledProcessError as err:
+ print("Called process error: {}.".format(err))
+
+ os.chmod('{key}'.format(**cert_data), 0o400)
+
+ with open('{conf}'.format(**cert_data), 'w') as f:
+ f.write('ssl_certificate {crt};\n'.format(**cert_data))
+ f.write('ssl_certificate_key {key};\n'.format(**cert_data))
+
+ if san_config:
+ san_config.close()
+
+def get_config():
+ vyos_cert = vyos.defaults.vyos_cert_data
+
+ conf = Config()
+ if not conf.exists('service https certificates system-generated-certificate'):
+ return None
+ else:
+ conf.set_level('service https certificates system-generated-certificate')
+
+ if conf.exists('lifetime'):
+ lifetime = conf.return_value('lifetime')
+ vyos_cert['lifetime'] = lifetime
+
+ return vyos_cert
+
+def verify(vyos_cert):
+ return None
+
+def generate(vyos_cert):
+ if vyos_cert is None:
+ return None
+
+ if not status_self_signed(vyos_cert):
+ generate_self_signed(vyos_cert)
+
+def apply(vyos_cert):
+ for dep in dependencies:
+ cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ try:
+ subprocess.check_call(cmd, shell=True)
+ except subprocess.CalledProcessError as err:
+ raise ConfigError("{}.".format(err))
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
new file mode 100755
index 000000000..afab9be70
--- /dev/null
+++ b/src/services/vyos-http-api-server
@@ -0,0 +1,247 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 os
+import sys
+import grp
+import json
+import traceback
+import threading
+
+import vyos.config
+
+import bottle
+
+from vyos.configsession import ConfigSession, ConfigSessionError
+from vyos.config import VyOSError
+
+
+DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
+
+CFG_GROUP = 'vyattacfg'
+
+app = bottle.default_app()
+
+# Giant lock!
+lock = threading.Lock()
+
+def load_server_config():
+ with open(DEFAULT_CONFIG_FILE) as f:
+ config = json.load(f)
+ return config
+
+def check_auth(key_list, key):
+ id = None
+ for k in key_list:
+ if k['key'] == key:
+ id = k['id']
+ return id
+
+def error(code, msg):
+ bottle.response.status = code
+ resp = {"success": False, "error": msg, "data": None}
+ return json.dumps(resp)
+
+def success(data):
+ resp = {"success": True, "data": data, "error": None}
+ return json.dumps(resp)
+
+@app.route('/configure', method='POST')
+def configure():
+ session = app.config['vyos_session']
+ config = app.config['vyos_config']
+ api_keys = app.config['vyos_keys']
+
+ key = bottle.request.forms.get("key")
+ id = check_auth(api_keys, key)
+ if not id:
+ return error(401, "Valid API key is required")
+
+ strict_field = bottle.request.forms.get("strict")
+ if strict_field == "true":
+ strict = True
+ else:
+ strict = False
+
+ commands = bottle.request.forms.get("data")
+ if not commands:
+ return error(400, "Non-empty data field is required")
+ else:
+ try:
+ commands = json.loads(commands)
+ except Exception as e:
+ return error(400, "Failed to parse JSON: {0}".format(e))
+
+ # Allow users to pass just one command
+ if not isinstance(commands, list):
+ commands = [commands]
+
+ # We don't want multiple people/apps to be able to commit at once,
+ # or modify the shared session while someone else is doing the same,
+ # so the lock is really global
+ lock.acquire()
+
+ status = 200
+ error_msg = None
+ try:
+ for c in commands:
+ # What we've got may not even be a dict
+ if not isinstance(c, dict):
+ raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c)))
+
+ # Missing op or path is a show stopper
+ if not ('op' in c):
+ raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c)))
+ if not ('path' in c):
+ raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c)))
+
+ # Missing value is fine, substitute for empty string
+ if 'value' in c:
+ value = c['value']
+ else:
+ value = ""
+
+ op = c['op']
+ path = c['path']
+
+ if not path:
+ raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c)))
+
+ # Type checking
+ if not isinstance(path, list):
+ raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c)))
+
+ if not isinstance(value, str):
+ raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c)))
+
+ # Account for the case when value field is present and set to null
+ if not value:
+ value = ""
+
+ # For vyos.configsessios calls that have no separate value arguments,
+ # and for type checking too
+ try:
+ cfg_path = " ".join(path + [value]).strip()
+ except TypeError:
+ raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c)))
+
+ if op == 'set':
+ # XXX: it would be nice to do a strict check for "path already exists",
+ # but there's probably no way to do that
+ session.set(path, value=value)
+ elif op == 'delete':
+ if strict and not config.exists(cfg_path):
+ raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path))
+ session.delete(path, value=value)
+ elif op == 'comment':
+ session.comment(path, value=value)
+ else:
+ raise ConfigSessionError("\"{0}\" is not a valid operation".format(op))
+ # end for
+ session.commit()
+ print("Configuration modified via HTTP API using key \"{0}\"".format(id))
+ except ConfigSessionError as e:
+ session.discard()
+ status = 400
+ if app.config['vyos_debug']:
+ print(traceback.format_exc(), file=sys.stderr)
+ error_msg = str(e)
+ except Exception as e:
+ session.discard()
+ print(traceback.format_exc(), file=sys.stderr)
+ status = 500
+
+ # Don't give the details away to the outer world
+ error_msg = "An internal error occured. Check the logs for details."
+ finally:
+ lock.release()
+
+ if status != 200:
+ return error(status, error_msg)
+ else:
+ return success(None)
+
+@app.route('/retrieve', method='POST')
+def get_value():
+ config = app.config['vyos_config']
+ session = app.config['vyos_session']
+
+ api_keys = app.config['vyos_keys']
+
+ key = bottle.request.forms.get("key")
+ id = check_auth(api_keys, key)
+ if not id:
+ return error(401, "Valid API key is required")
+
+ command = bottle.request.forms.get("data")
+ command = json.loads(command)
+
+ try:
+ op = command['op']
+ path = " ".join(command['path'])
+ except KeyError:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ try:
+ if op == 'returnValue':
+ res = config.return_value(path)
+ elif op == 'returnValues':
+ res = config.return_values(path)
+ elif op == 'exists':
+ res = config.exists(path)
+ elif op == 'showConfig':
+ config_format = 'raw'
+ if 'configFormat' in command:
+ config_format = command['configFormat']
+
+ res = session.show_config(command['path'], format=config_format)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except VyOSError as e:
+ return error(400, str(e))
+ except Exception as e:
+ print(traceback.format_exc(), file=sys.stderr)
+ return error(500, "An internal error occured. Check the logs for details.")
+
+ return success(res)
+
+if __name__ == '__main__':
+ # systemd's user and group options don't work, do it by hand here,
+ # else no one else will be able to commit
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ # Need to set file permissions to 775 too so that every vyattacfg group member
+ # has write access to the running config
+ os.umask(0o002)
+
+ try:
+ server_config = load_server_config()
+ except Exception as e:
+ print("Failed to load the HTTP API server config: {0}".format(e))
+
+ session = ConfigSession(os.getpid())
+ env = session.get_session_env()
+ config = vyos.config.Config(session_env=env)
+
+ app.config['vyos_session'] = session
+ app.config['vyos_config'] = config
+ app.config['vyos_keys'] = server_config['api_keys']
+ app.config['vyos_debug'] = server_config['debug']
+
+ bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True)
diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service
new file mode 100644
index 000000000..4fa68b4ff
--- /dev/null
+++ b/src/systemd/vyos-http-api.service
@@ -0,0 +1,24 @@
+[Unit]
+Description=VyOS HTTP API service
+After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service
+Requires=vyos-router.service
+
+[Service]
+ExecStartPre=/usr/libexec/vyos/init/vyos-config
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server
+Type=idle
+KillMode=process
+
+SyslogIdentifier=vyos-http-api
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+# Installing in a earlier target leaves ExecStartPre waiting
+WantedBy=getty.target
+