123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- #!@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 optparse import OptionParser, OptionValueError
- from hashlib import sha1
- try:
- import threading
- except ImportError:
- import dummy_threading as threading
- __version__ = 'BIND10'
- URL_PATTERN = re.compile('/([\w]+)(?:/([\w]+))?/?')
- # If B10_FROM_SOURCE 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_SOURCE" in os.environ:
- SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl"
- SYSCONF_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl"
- else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
- SYSCONF_PATH = "@sysconfdir@/@PACKAGE@".replace("${prefix}", PREFIX)
- SPECFILE_LOCATION = SPECFILE_PATH + "/cmdctl.spec"
- USER_INFO_FILE = SYSCONF_PATH + "/cmdctl-accounts.csv"
- PRIVATE_KEY_FILE = SYSCONF_PATH + "/cmdctl-keyfile.pem"
- CERTIFICATE_FILE = SYSCONF_PATH + "/cmdctl-certfile.pem"
-
- 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.user_infos.get(user_name):
- return False, ["user doesn't exist"]
- user_pwd = user_info.get('password')
- if not user_pwd:
- return False, ["need password"]
- local_info = self.server.user_infos.get(user_name)
- pwd_hashval = sha1((user_pwd + local_info[1]).encode())
- if pwd_hashval.hexdigest() != local_info[0]:
- return False, ["password doesn't match"]
- 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)
- print('b10-cmdctl finish send message \'%s\' to module %s' % (cmd, mod))
- 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):
- self.cc = isc.cc.Session()
- self.cc.group_subscribe('Cmd-Ctrld')
- #self.cc.group_subscribe('Boss', 'Cmd-Ctrld')
- self.command_spec = self.get_cmd_specification()
- self.config_spec = self.get_data_specification()
- self.config_data = self.get_config_data()
- 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_cmd_specification(self):
- rcode, reply = self.send_command('ConfigManager', isc.config.ccsession.COMMAND_GET_COMMANDS_SPEC)
- return self._parse_command_result(rcode, reply)
- def get_config_data(self):
- '''Get config data for all modules from configmanager '''
- rcode, reply = self.send_command('ConfigManager', isc.config.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 == isc.config.ccsession.COMMAND_SET_CONFIG:
- self.config_data = self.get_config_data()
- def get_data_specification(self):
- rcode, reply = self.send_command('ConfigManager', isc.config.ccsession.COMMAND_GET_MODULE_SPEC)
- return self._parse_command_result(rcode, reply)
- def handle_recv_msg(self):
- '''Handle received message, if 'shutdown' is received, return False'''
- (message, env) = self.cc.group_recvmsg(True)
- command, arg = isc.config.ccsession.parse_command(message)
- while command:
- if command == isc.config.ccsession.COMMAND_COMMANDS_UPDATE:
- self.command_spec[arg[0]] = arg[1]
- elif command == isc.config.ccsession.COMMAND_SPECIFICATION_UPDATE:
- self.config_spec[arg[0]] = arg[1]
- elif command == "shutdown":
- return False
- (message, env) = self.cc.group_recvmsg(True)
- command, arg = isc.config.ccsession.parse_command(message)
-
- return True
-
- 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.
- rcode = 0: dict is the correct returned value.
- rcode > 0: dict is : { 'error' : 'error reason' }
- TODO. add check for parameters.
- '''
- # core module ConfigManager does not have a specification file
- if module_name == 'ConfigManager':
- return self.send_command(module_name, command_name, params)
- if module_name not in self.command_spec.keys():
- return 1, {'error' : 'unknown module'}
- cmd_valid = False
- commands = self.command_spec[module_name]
- for cmd in commands:
- if cmd['command_name'] == command_name:
- cmd_valid = True
- break
- if not cmd_valid:
- return 1, {'error' : 'unknown command'}
- 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 = 'no error'
- print('b10-cmdctl send command \'%s\' to %s' %(command_name, module_name))
- try:
- msg = isc.config.ccsession.create_command(command_name, params)
- seq = self.cc.group_sendmsg(msg, module_name)
- #TODO, it may be blocked, msqg need to add a new interface waiting in timeout.
- answer, env = self.cc.group_recvmsg(False, seq)
- if answer:
- try:
- rcode, arg = isc.config.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:
- # todo: exception
- errstr = str(answer['result'][1])
- except isc.config.ccsession.ModuleCCSessionError as mcse:
- errstr = str("Error in ccsession answer:") + str(mcse)
- print(answer)
- except Exception as e:
- errstr = str(e)
- print(e, ':b10-cmdctl fail send command \'%s\' to %s' % (command_name, module_name))
-
- return 1, {'error': errstr}
- class SecureHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
- '''Make the server address can be reused.'''
- allow_reuse_address = True
- def __init__(self, server_address, RequestHandlerClass, idle_timeout = 1200):
- '''idle_timeout: the max idle time for login'''
- http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
- self.user_sessions = {}
- self.idle_timeout = idle_timeout
- self.cmdctrl = CommandControl()
- self.__is_shut_down = threading.Event()
- self.__serving = False
- self.user_infos = {}
- self._read_user_info()
- def _read_user_info(self):
- '''Read all user's name and its' password from csv file.'''
- csvfile = None
- try:
- csvfile = open(USER_INFO_FILE)
- reader = csv.reader(csvfile)
- for row in reader:
- self.user_infos[row[0]] = [row[1], row[2]]
- except Exception as e:
- print("Fail to read user information ", e)
- finally:
- if csvfile:
- csvfile.close()
-
- def save_user_session_id(self, session_id):
- # Record user's id and login time.
- self.user_sessions[session_id] = time.time()
-
- def get_request(self):
- '''Get client request socket and wrap it in SSL context. '''
- newsocket, fromaddr = self.socket.accept()
- try:
- connstream = ssl.wrap_socket(newsocket,
- server_side = True,
- certfile = CERTIFICATE_FILE,
- keyfile = PRIVATE_KEY_FILE,
- ssl_version = ssl.PROTOCOL_SSLv23)
- return (connstream, fromaddr)
- except ssl.SSLError as e :
- print("cmdctl: deny client's invalid connection", e)
- self.close_request(newsocket)
- # raise socket error to finish the request
- raise socket.error
-
- 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 == 'command_spec':
- rcode, reply = http.client.OK, self.cmdctrl.command_spec
- elif id == 'config_data':
- rcode, reply = http.client.OK, self.cmdctrl.config_data
- elif id == 'config_spec':
- rcode, reply = http.client.OK, self.cmdctrl.config_spec
-
- return rcode, reply
-
- def serve_forever(self, poll_interval = 0.5):
- '''Start cmdctl as one tcp server. '''
- self.__serving = True
- self.__is_shut_down.clear()
- while self.__serving:
- if not self.cmdctrl.handle_recv_msg():
- break
- r, w, e = select.select([self], [], [], poll_interval)
- if r:
- self._handle_request_noblock()
- self.__is_shut_down.set()
-
- def shutdown(self):
- self.__serving = False
- self.__is_shut_down.wait()
- def send_command_to_module(self, module_name, command_name, params):
- return self.cmdctrl.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):
- ''' Start cmdctl as one https server. '''
- print("b10-cmdctl module is starting on :%s port:%d" %(addr, port))
- httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler, idle_timeout)
- httpd.serve_forever()
- def check_port(option, opt_str, value, parser):
- if (value < 0) or (value > 65535):
- raise OptionValueError('%s requires a port number (0-65535)' % opt_str)
- parser.values.port = value
- def check_addr(option, opt_str, value, parser):
- ipstr = value
- ip_family = socket.AF_INET
- if (ipstr.find(':') != -1):
- ip_family = socket.AF_INET6
- try:
- socket.inet_pton(ip_family, ipstr)
- except:
- raise OptionValueError("%s invalid ip address" % ipstr)
- parser.values.addr = value
- 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')
- if __name__ == '__main__':
- try:
- parser = OptionParser(version = __version__)
- set_cmd_options(parser)
- (options, args) = parser.parse_args()
- set_signal_handler()
- run(options.addr, options.port, options.idle_timeout)
- except isc.cc.SessionError as se:
- print("[b10-cmdctl] Error creating b10-cmdctl, "
- "is the command channel daemon running?")
- except KeyboardInterrupt:
- print("exit http server")
- if httpd:
- httpd.shutdown()
|