123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- #!@PYTHON@
- # Copyright (C) 2010 Internet Systems Consortium.
- #
- # Permission to use, copy, modify, and distribute this software for any
- # purpose with or without fee is hereby granted, provided that the above
- # copyright notice and this permission notice appear in all copies.
- #
- # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
- # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
- # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
- # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
- # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
- # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
- # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- ''' cmdctl module is the configuration entry point for all commands from bindctl
- or some other web tools client of bind10. cmdctl is pure https server which provi-
- des RESTful API. When command client connecting with cmdctl, it should first login
- with legal username and password.
- When cmdctl starting up, it will collect command specification and
- configuration specification/data of other available modules from configmanager, then
- wait for receiving request from client, parse the request and resend the request to
- the proper module. When getting the request result from the module, send back the
- resut to client.
- '''
- import sys; sys.path.append ('@@PYTHONPATH@@')
- import os
- import socketserver
- import http.server
- import urllib.parse
- import json
- import re
- import ssl, socket
- import isc
- import pprint
- import select
- import csv
- import random
- import time
- import signal
- from isc.config import ccsession
- import isc.util.process
- import isc.net.parse
- from optparse import OptionParser, OptionValueError
- from hashlib import sha1
- from isc.util import socketserver_mixin
- from isc.log_messages.cmdctl_messages import *
- isc.log.init("b10-cmdctl", buffer=True)
- logger = isc.log.Logger("cmdctl")
- # Debug level for communication with BIND10
- DBG_CMDCTL_MESSAGING = logger.DBGLVL_COMMAND
- try:
- import threading
- except ImportError:
- import dummy_threading as threading
- isc.util.process.rename()
- __version__ = 'BIND10'
- URL_PATTERN = re.compile('/([\w]+)(?:/([\w]+))?/?')
- CONFIG_DATA_URL = 'config_data'
- MODULE_SPEC_URL = 'module_spec'
- # If B10_FROM_BUILD is set in the environment, we use data files
- # from a directory relative to that, otherwise we use the ones
- # installed on the system
- if "B10_FROM_BUILD" in os.environ:
- SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/cmdctl"
- else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
- SPECFILE_LOCATION = SPECFILE_PATH + os.sep + "cmdctl.spec"
- class CmdctlException(Exception):
- pass
- class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
- '''https connection request handler.
- Currently only GET and POST are supported. '''
- def do_GET(self):
- '''The client should send its session id in header with
- the name 'cookie'
- '''
- self.session_id = self.headers.get('cookie')
- rcode, reply = http.client.OK, []
- if self._is_session_valid():
- if self._is_user_logged_in():
- rcode, reply = self._handle_get_request()
- else:
- rcode, reply = http.client.UNAUTHORIZED, ["please login"]
- else:
- rcode = http.client.BAD_REQUEST
- self.send_response(rcode)
- self.end_headers()
- self.wfile.write(json.dumps(reply).encode())
- def _handle_get_request(self):
- '''Currently only support the following three url GET request '''
- id, module = self._parse_request_path()
- return self.server.get_reply_data_for_GET(id, module)
- def _is_session_valid(self):
- return self.session_id
- def _is_user_logged_in(self):
- login_time = self.server.user_sessions.get(self.session_id)
- if not login_time:
- return False
- idle_time = time.time() - login_time
- if idle_time > self.server.idle_timeout:
- return False
- # Update idle time
- self.server.user_sessions[self.session_id] = time.time()
- return True
- def _parse_request_path(self):
- '''Parse the url, the legal url should like /ldh or /ldh/ldh '''
- groups = URL_PATTERN.match(self.path)
- if not groups:
- return (None, None)
- else:
- return (groups.group(1), groups.group(2))
- def do_POST(self):
- '''Process POST request. '''
- '''Process user login and send command to proper module
- The client should send its session id in header with
- the name 'cookie'
- '''
- self.session_id = self.headers.get('cookie')
- rcode, reply = http.client.OK, []
- if self._is_session_valid():
- if self.path == '/login':
- rcode, reply = self._handle_login()
- elif self._is_user_logged_in():
- rcode, reply = self._handle_post_request()
- else:
- rcode, reply = http.client.UNAUTHORIZED, ["please login"]
- else:
- rcode, reply = http.client.BAD_REQUEST, ["session isn't valid"]
- self.send_response(rcode)
- self.end_headers()
- self.wfile.write(json.dumps(reply).encode())
- def _handle_login(self):
- if self._is_user_logged_in():
- return http.client.OK, ["user has already login"]
- is_user_valid, error_info = self._check_user_name_and_pwd()
- if is_user_valid:
- self.server.save_user_session_id(self.session_id)
- return http.client.OK, ["login success "]
- else:
- return http.client.UNAUTHORIZED, error_info
- def _check_user_name_and_pwd(self):
- '''Check user name and its password '''
- length = self.headers.get('Content-Length')
- if not length:
- return False, ["invalid username or password"]
- try:
- user_info = json.loads((self.rfile.read(int(length))).decode())
- except:
- return False, ["invalid username or password"]
- user_name = user_info.get('username')
- if not user_name:
- return False, ["need user name"]
- if not self.server.get_user_info(user_name):
- logger.info(CMDCTL_NO_SUCH_USER, user_name)
- return False, ["username or password error"]
- user_pwd = user_info.get('password')
- if not user_pwd:
- return False, ["need password"]
- local_info = self.server.get_user_info(user_name)
- pwd_hashval = sha1((user_pwd + local_info[1]).encode())
- if pwd_hashval.hexdigest() != local_info[0]:
- logger.info(CMDCTL_BAD_PASSWORD, user_name)
- return False, ["username or password error"]
- return True, None
- def _handle_post_request(self):
- '''Handle all the post request from client. '''
- mod, cmd = self._parse_request_path()
- if (not mod) or (not cmd):
- return http.client.BAD_REQUEST, ['malformed url']
- param = None
- len = self.headers.get('Content-Length')
- if len:
- try:
- post_str = str(self.rfile.read(int(len)).decode())
- param = json.loads(post_str)
- except:
- pass
- rcode, reply = self.server.send_command_to_module(mod, cmd, param)
- ret = http.client.OK
- if rcode != 0:
- ret = http.client.BAD_REQUEST
- return ret, reply
- def log_request(self, code='-', size='-'):
- '''Rewrite the log request function, log nothing.'''
- pass
- class CommandControl():
- '''Get all modules' config data/specification from configmanager.
- receive command from client and resend it to proper module.
- '''
- def __init__(self, httpserver, verbose = False):
- ''' httpserver: the http server which use the object of
- CommandControl to communicate with other modules. '''
- self._verbose = verbose
- self._httpserver = httpserver
- self._lock = threading.Lock()
- self._setup_session()
- self.modules_spec = self._get_modules_specification()
- self._config_data = self._get_config_data_from_config_manager()
- self._serving = True
- self._start_msg_handle_thread()
- def _setup_session(self):
- '''Setup the session for receving the commands
- sent from other modules. There are two sessions
- for cmdctl, one(self.module_cc) is used for receiving
- commands sent from other modules, another one (self._cc)
- is used to send the command from Bindctl or other tools
- to proper modules.'''
- self._cc = isc.cc.Session()
- self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
- self.config_handler,
- self.command_handler)
- self._module_name = self._module_cc.get_module_spec().get_module_name()
- self._cmdctl_config_data = self._module_cc.get_full_config()
- self._module_cc.start()
- def _accounts_file_check(self, filepath):
- ''' Check whether the accounts file is valid, each row
- should be a list with 3 items.'''
- csvfile = None
- errstr = None
- try:
- csvfile = open(filepath)
- reader = csv.reader(csvfile)
- for row in reader:
- a = (row[0], row[1], row[2])
- except (IOError, IndexError) as e:
- errstr = 'Invalid accounts file: ' + str(e)
- finally:
- if csvfile:
- csvfile.close()
- return errstr
- def _config_data_check(self, new_config):
- ''' Check whether the new config data is valid or
- not. '''
- errstr = None
- for key in new_config:
- if key == 'version':
- continue
- elif key in ['key_file', 'cert_file']:
- #TODO, only check whether the file exist,
- # further check need to be done: eg. whether
- # the private/certificate is valid.
- path = new_config[key]
- if not os.path.exists(path):
- errstr = "the file doesn't exist: " + path
- elif key == 'accounts_file':
- errstr = self._accounts_file_check(new_config[key])
- else:
- errstr = 'unknown config item: ' + key
- if errstr != None:
- logger.error(CMDCTL_BAD_CONFIG_DATA, errstr);
- return ccsession.create_answer(1, errstr)
- return ccsession.create_answer(0)
- def config_handler(self, new_config):
- answer = self._config_data_check(new_config)
- rcode, val = ccsession.parse_answer(answer)
- if rcode != 0:
- return answer
- with self._lock:
- for key in new_config:
- if key in self._cmdctl_config_data:
- self._cmdctl_config_data[key] = new_config[key]
- return answer
- def command_handler(self, command, args):
- answer = ccsession.create_answer(0)
- if command == ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
- # The 'value' of a specification update can be either
- # a specification, or None. In the first case, simply
- # set it. If it is None, delete the module if it is
- # known.
- with self._lock:
- if args[1] is None:
- if args[0] in self.modules_spec:
- del self.modules_spec[args[0]]
- else:
- answer = ccsession.create_answer(1,
- 'No such module: ' +
- args[0])
- else:
- self.modules_spec[args[0]] = args[1]
- elif command == ccsession.COMMAND_SHUTDOWN:
- #When cmdctl get 'shutdown' command from boss,
- #shutdown the outer httpserver.
- self._module_cc.send_stopping()
- self._httpserver.shutdown()
- self._serving = False
- elif command == 'print_settings':
- answer = ccsession.create_answer(0, self._cmdctl_config_data)
- else:
- answer = ccsession.create_answer(1, 'unknown command: ' + command)
- return answer
- def _start_msg_handle_thread(self):
- ''' Start one thread to handle received message from msgq.'''
- td = threading.Thread(target=self._handle_msg_from_msgq)
- td.daemon = True
- td.start()
- def _handle_msg_from_msgq(self):
- '''Process all the received commands with module session. '''
- while self._serving:
- self._module_cc.check_command(False)
- def _parse_command_result(self, rcode, reply):
- '''Ignore the error reason when command rcode isn't 0, '''
- if rcode != 0:
- return {}
- return reply
- def _get_config_data_from_config_manager(self):
- '''Get config data for all modules from configmanager '''
- rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_CONFIG)
- return self._parse_command_result(rcode, reply)
- def _update_config_data(self, module_name, command_name):
- '''Get lastest config data for all modules from configmanager '''
- if module_name == 'ConfigManager' and command_name == ccsession.COMMAND_SET_CONFIG:
- data = self._get_config_data_from_config_manager()
- with self._lock:
- self._config_data = data
- def get_config_data(self):
- with self._lock:
- data = self._config_data
- return data
- def get_modules_spec(self):
- with self._lock:
- spec = self.modules_spec
- return spec
- def _get_modules_specification(self):
- '''Get all the modules' specification files. '''
- rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_MODULE_SPEC)
- return self._parse_command_result(rcode, reply)
- def send_command_with_check(self, module_name, command_name, params = None):
- '''Before send the command to modules, check if module_name, command_name
- parameters are legal according the spec file of the module.
- Return rcode, dict. TODO, the rcode should be defined properly.
- rcode = 0: dict is the correct returned value.
- rcode > 0: dict is : { 'error' : 'error reason' }
- '''
- # core module ConfigManager does not have a specification file
- if module_name == 'ConfigManager':
- return self.send_command(module_name, command_name, params)
- specs = self.get_modules_spec()
- if module_name not in specs.keys():
- return 1, {'error' : 'unknown module'}
- spec_obj = isc.config.module_spec.ModuleSpec(specs[module_name], False)
- errors = []
- if not spec_obj.validate_command(command_name, params, errors):
- return 1, {'error': errors[0]}
- return self.send_command(module_name, command_name, params)
- def send_command(self, module_name, command_name, params = None):
- '''Send the command from bindctl to proper module. '''
- errstr = 'unknown error'
- answer = None
- logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_SEND_COMMAND,
- command_name, module_name)
- if module_name == self._module_name:
- # Process the command sent to cmdctl directly.
- answer = self.command_handler(command_name, params)
- else:
- msg = ccsession.create_command(command_name, params)
- seq = self._cc.group_sendmsg(msg, module_name)
- logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_COMMAND_SENT,
- command_name, module_name)
- #TODO, it may be blocked, msqg need to add a new interface waiting in timeout.
- try:
- answer, env = self._cc.group_recvmsg(False, seq)
- except isc.cc.session.SessionTimeout:
- errstr = "Module '%s' not responding" % module_name
- if answer:
- try:
- rcode, arg = ccsession.parse_answer(answer)
- if rcode == 0:
- self._update_config_data(module_name, command_name)
- if arg != None:
- return rcode, arg
- else:
- return rcode, {}
- else:
- errstr = str(answer['result'][1])
- except ccsession.ModuleCCSessionError as mcse:
- errstr = str("Error in ccsession answer:") + str(mcse)
- logger.error(CMDCTL_COMMAND_ERROR, command_name, module_name, errstr)
- return 1, {'error': errstr}
- def get_cmdctl_config_data(self):
- ''' If running in source code tree, use keyfile, certificate
- and user accounts file in source code. '''
- if "B10_FROM_SOURCE" in os.environ:
- sysconf_path = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl/"
- accountsfile = sysconf_path + "cmdctl-accounts.csv"
- keyfile = sysconf_path + "cmdctl-keyfile.pem"
- certfile = sysconf_path + "cmdctl-certfile.pem"
- return (keyfile, certfile, accountsfile)
- with self._lock:
- keyfile = self._cmdctl_config_data.get('key_file')
- certfile = self._cmdctl_config_data.get('cert_file')
- accountsfile = self._cmdctl_config_data.get('accounts_file')
- return (keyfile, certfile, accountsfile)
- class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
- socketserver.ThreadingMixIn,
- http.server.HTTPServer):
- '''Make the server address can be reused.'''
- allow_reuse_address = True
- def __init__(self, server_address, RequestHandlerClass,
- CommandControlClass,
- idle_timeout = 1200, verbose = False):
- '''idle_timeout: the max idle time for login'''
- socketserver_mixin.NoPollMixIn.__init__(self)
- try:
- http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
- logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_STARTED,
- server_address[0], server_address[1])
- except socket.error as err:
- raise CmdctlException("Error creating server, because: %s \n" % str(err))
- self.user_sessions = {}
- self.idle_timeout = idle_timeout
- self.cmdctl = CommandControlClass(self, verbose)
- self._verbose = verbose
- self._lock = threading.Lock()
- self._user_infos = {}
- self._accounts_file = None
- def _create_user_info(self, accounts_file):
- '''Read all user's name and its' salt, hashed password
- from accounts file.'''
- if (self._accounts_file == accounts_file) and (len(self._user_infos) > 0):
- return
- with self._lock:
- self._user_infos = {}
- csvfile = None
- try:
- csvfile = open(accounts_file)
- reader = csv.reader(csvfile)
- for row in reader:
- self._user_infos[row[0]] = [row[1], row[2]]
- except (IOError, IndexError) as e:
- logger.error(CMDCTL_USER_DATABASE_READ_ERROR,
- accounts_file, e)
- finally:
- if csvfile:
- csvfile.close()
- self._accounts_file = accounts_file
- if len(self._user_infos) == 0:
- logger.error(CMDCTL_NO_USER_ENTRIES_READ)
- def get_user_info(self, username):
- '''Get user's salt and hashed string. If the user
- doesn't exist, return None, or else, the list
- [salt, hashed password] will be returned.'''
- with self._lock:
- info = self._user_infos.get(username)
- return info
- def save_user_session_id(self, session_id):
- ''' Record user's id and login time. '''
- self.user_sessions[session_id] = time.time()
- def _check_key_and_cert(self, key, cert):
- # TODO, check the content of key/certificate file
- if not os.path.exists(key):
- raise CmdctlException("key file '%s' doesn't exist " % key)
- if not os.path.exists(cert):
- raise CmdctlException("certificate file '%s' doesn't exist " % cert)
- def _wrap_socket_in_ssl_context(self, sock, key, cert):
- try:
- self._check_key_and_cert(key, cert)
- ssl_sock = ssl.wrap_socket(sock,
- server_side = True,
- certfile = cert,
- keyfile = key,
- ssl_version = ssl.PROTOCOL_SSLv23)
- return ssl_sock
- except (ssl.SSLError, CmdctlException) as err :
- logger.error(CMDCTL_SSL_SETUP_FAILURE_USER_DENIED, err)
- self.close_request(sock)
- # raise socket error to finish the request
- raise socket.error
- def get_request(self):
- '''Get client request socket and wrap it in SSL context. '''
- key, cert, account_file = self.cmdctl.get_cmdctl_config_data()
- self._create_user_info(account_file)
- newsocket, fromaddr = self.socket.accept()
- ssl_sock = self._wrap_socket_in_ssl_context(newsocket, key, cert)
- return (ssl_sock, fromaddr)
- def get_reply_data_for_GET(self, id, module):
- '''Currently only support the following three url GET request '''
- rcode, reply = http.client.NO_CONTENT, []
- if not module:
- if id == CONFIG_DATA_URL:
- rcode, reply = http.client.OK, self.cmdctl.get_config_data()
- elif id == MODULE_SPEC_URL:
- rcode, reply = http.client.OK, self.cmdctl.get_modules_spec()
- return rcode, reply
- def send_command_to_module(self, module_name, command_name, params):
- return self.cmdctl.send_command_with_check(module_name, command_name, params)
- httpd = None
- def signal_handler(signal, frame):
- if httpd:
- httpd.shutdown()
- sys.exit(0)
- def set_signal_handler():
- signal.signal(signal.SIGTERM, signal_handler)
- signal.signal(signal.SIGINT, signal_handler)
- def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
- ''' Start cmdctl as one https server. '''
- httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler,
- CommandControl, idle_timeout, verbose)
- httpd.serve_forever()
- def check_port(option, opt_str, value, parser):
- try:
- parser.values.port = isc.net.parse.port_parse(value)
- except ValueError as e:
- raise OptionValueError(str(e))
- def check_addr(option, opt_str, value, parser):
- try:
- isc.net.parse.addr_parse(value)
- parser.values.addr = value
- except ValueError as e:
- raise OptionValueError(str(e))
- def set_cmd_options(parser):
- parser.add_option('-p', '--port', dest = 'port', type = 'int',
- action = 'callback', callback=check_port,
- default = '8080', help = 'port cmdctl will use')
- parser.add_option('-a', '--address', dest = 'addr', type = 'string',
- action = 'callback', callback=check_addr,
- default = '127.0.0.1', help = 'IP address cmdctl will use')
- parser.add_option('-i', '--idle-timeout', dest = 'idle_timeout', type = 'int',
- default = '1200', help = 'login idle time out')
- parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
- help="display more about what is going on")
- if __name__ == '__main__':
- set_signal_handler()
- parser = OptionParser(version = __version__)
- set_cmd_options(parser)
- (options, args) = parser.parse_args()
- result = 1 # in case of failure
- try:
- if options.verbose:
- logger.set_severity("DEBUG", 99)
- run(options.addr, options.port, options.idle_timeout, options.verbose)
- result = 0
- except isc.cc.SessionError as err:
- logger.fatal(CMDCTL_CC_SESSION_ERROR, err)
- except isc.cc.SessionTimeout:
- logger.fatal(CMDCTL_CC_SESSION_TIMEOUT)
- except KeyboardInterrupt:
- logger.info(CMDCTL_STOPPED_BY_KEYBOARD)
- except CmdctlException as err:
- logger.fatal(CMDCTL_UNCAUGHT_EXCEPTION, err);
- if httpd:
- httpd.shutdown()
- sys.exit(result)
|