123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- #!@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 http.server
- import urllib.parse
- import json
- import re
- import ssl, socket
- import isc
- import pprint
- import select
- import csv
- import random
- from hashlib import sha1
- try:
- import threading
- except ImportError:
- import dummy_threading as threading
- 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"
- else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
- SPECFILE_LOCATION = SPECFILE_PATH + "/cmdctl.spec"
- USER_INFO_FILE = SPECFILE_PATH + "/passwd.csv"
- CERTIFICATE_FILE = SPECFILE_PATH + "/b10-cmdctl.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):
- return self.session_id in self.server.user_sessions
- 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 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()
- else:
- rcode, reply = self._handle_post_request()
- 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.user_sessions.append(self.session_id)
- return http.client.OK, ["login success "]
- else:
- return http.client.UNAUTHORIZED, error_info
- def _check_user_name_and_pwd(self):
- length = self.headers.get('Content-Length')
- if not length:
- return False, ["invalid username or password"]
- user_info = json.loads((self.rfile.read(int(length))).decode())
- if not user_info:
- 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):
- mod, cmd = self._parse_request_path()
- param = None
- len = self.headers.get('Content-Length')
- rcode = http.client.OK
- reply = None
- if len:
- post_str = str(self.rfile.read(int(len)).decode())
- print("command parameter:%s" % post_str)
- param = json.loads(post_str)
- # TODO, need return some proper return code.
- # currently always OK.
- reply = self.server.send_command_to_module(mod, cmd, param)
- print('b10-cmdctl finish send message \'%s\' to module %s' % (cmd, mod))
- return rcode, reply
-
-
- 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 get_cmd_specification(self):
- return self.send_command('ConfigManager', 'get_commands_spec')
- def get_config_data(self):
- return self.send_command('ConfigManager', 'get_config')
- def update_config_data(self, module_name, command_name):
- if module_name == 'ConfigManager' and command_name == 'set_config':
- self.config_data = self.get_config_data()
- def get_data_specification(self):
- return self.send_command('ConfigManager', 'get_module_spec')
- def handle_recv_msg(self):
- # Handle received message, if 'shutdown' is received, return False
- (message, env) = self.cc.group_recvmsg(True)
- while message:
- if 'commands_update' in message:
- self.command_spec[message['commands_update'][0]] = message['commands_update'][1]
- elif 'specification_update' in message:
- msgvalue = message['specification_update']
- self.config_spec[msgvalue[0]] = msgvalue[1]
- elif 'command' in message and message['command'][0] == 'shutdown':
- return False;
- (message, env) = self.cc.group_recvmsg(True)
-
- return True
-
- def send_command(self, module_name, command_name, params = None):
- content = [command_name]
- if params:
- content.append(params)
- msg = {'command' : content}
- print('b10-cmdctl send command \'%s\' to %s' %(command_name, module_name))
- try:
- self.cc.group_sendmsg(msg, module_name)
- #TODO, it may be blocked, msqg need to add a new interface
- # wait in timeout.
- answer, env = self.cc.group_recvmsg(False)
- if answer and 'result' in answer.keys() and type(answer['result']) == list:
- # TODO: with the new cc implementation, replace "1" by 1
- if answer['result'][0] == 1:
- # todo: exception
- print("Error: " + str(answer['result'][1]))
- return {}
- else:
- self.update_config_data(module_name, command_name)
- if len(answer['result']) > 1:
- return answer['result'][1]
- return {}
- else:
- print("Error: unexpected answer from %s" % module_name)
- print(answer)
- except Exception as e:
- print(e)
- print('b10-cmdctl fail send command \'%s\' to %s' % (command_name, module_name))
- return {}
- class SecureHTTPServer(http.server.HTTPServer):
- '''Make the server address can be reused.'''
- allow_reuse_address = True
- def __init__(self, server_address, RequestHandlerClass):
- http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
- self.user_sessions = []
- self.cmdctrl = CommandControl()
- self.__is_shut_down = threading.Event()
- self.__serving = False
- self.user_infos = {}
- self._read_user_info()
- def _read_user_info(self):
- # Get all username and password information
- 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)
- exit(1)
- finally:
- if csvfile:
- csvfile.close()
-
-
- def get_request(self):
- newsocket, fromaddr = self.socket.accept()
- try:
- connstream = ssl.wrap_socket(newsocket,
- server_side = True,
- certfile = CERTIFICATE_FILE,
- keyfile = CERTIFICATE_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:
- rcode = http.client.OK
- if id == 'command_spec':
- reply = self.cmdctrl.command_spec
- elif id == 'config_data':
- reply = self.cmdctrl.config_data
- elif id == 'config_spec':
- reply = self.cmdctrl.config_spec
-
- return rcode, reply
-
- def serve_forever(self, poll_interval = 0.5):
- 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(module_name, command_name, params)
- def run(server_class = SecureHTTPServer, addr = 'localhost', port = 8080):
- ''' Start cmdctl as one https server. '''
- print("b10-cmdctl module is starting on :%s port:%d" %(addr, port))
- httpd = server_class((addr, port), SecureHTTPRequestHandler)
- httpd.serve_forever()
- if __name__ == '__main__':
- try:
- run()
- 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")
-
|