From fc2cc0f9b660408d5fc0cffcaffc33bfbc8ca5f2 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 16 Jun 2019 21:26:38 +0200 Subject: T1431: initial implementation of the HTTP API. --- src/services/vyos-http-api-server | 203 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100755 src/services/vyos-http-api-server (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server new file mode 100755 index 000000000..32f8adc73 --- /dev/null +++ b/src/services/vyos-http-api-server @@ -0,0 +1,203 @@ +#!/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 . +# +# + +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)) + + # 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: + op = c['op'] + path = c['path'] + value = c['value'] + + # Account for null values + if not value: + value = "" + + # For vyos.config calls + cfg_path = " ".join(path + [value]).strip() + + 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." + + 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'] + + 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) + + op = command['op'] + path = " ".join(command['path']) + + try: + if op == 'returnValue': + res = config.return_value(path) + elif op == 'returnValues': + res = config.return_values(path) + elif op == 'exists': + res = config.exists(path) + 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) -- cgit v1.2.3 From 8d70134d1adba4d787476ded970ee40ab18d1622 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 14:37:09 +0200 Subject: T1431: release the lock even if discard() caused an exception. It may be better to crash the process in that situation. --- src/services/vyos-http-api-server | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 32f8adc73..7b9e3d671 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -135,8 +135,9 @@ def configure(): # Don't give the details away to the outer world error_msg = "An internal error occured. Check the logs for details." + finally: + lock.release() - lock.release() if status != 200: return error(status, error_msg) else: -- cgit v1.2.3 From ac2b3fbede362348b94c3e759ff90a15f0fef62a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 19:59:16 +0200 Subject: [HTTP API] T1431: make the value field optional and add better validation. --- src/services/vyos-http-api-server | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 7b9e3d671..45723010a 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -96,16 +96,36 @@ def configure(): error_msg = None try: for c in commands: + # 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 not ('value' in c): + value = "" + op = c['op'] path = c['path'] value = c['value'] - # Account for null values + # 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.config calls - cfg_path = " ".join(path + [value]).strip() + # 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", -- cgit v1.2.3 From 7f06879361999e3b3aab6f66bb267841d958bfdb Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 20:12:24 +0200 Subject: [HTTP API] T1431: allow sending a single command, and make sure commands are dicts. --- src/services/vyos-http-api-server | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 45723010a..301c083a1 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -87,6 +87,10 @@ def configure(): 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 @@ -96,6 +100,10 @@ def configure(): 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))) -- cgit v1.2.3 From 1d40561bbd3aac552c8585d09d8436884aabdee7 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 20:19:21 +0200 Subject: [HTTP API] T1431: make the value field optional. --- src/services/vyos-http-api-server | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 301c083a1..834c06b4d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -109,13 +109,15 @@ def configure(): 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 not ('value' in c): + if 'value' in c: + value = c['value'] + else: value = "" op = c['op'] path = c['path'] - value = c['value'] # Type checking if not isinstance(path, list): -- cgit v1.2.3 From 73021645d1d1fa0e851bae7e003982f9ee491e84 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 17 Jun 2019 21:07:41 +0200 Subject: [HTTP API] T1431: disallow empty config paths. --- src/services/vyos-http-api-server | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 834c06b4d..e11eb6d52 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -119,6 +119,9 @@ def configure(): 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))) -- cgit v1.2.3