def do_open(self, data:str="") -> None: """ open { socket { host port } | session | channel [ type ] | sftp } Usually, the order of opening is: 1. get a *socket* connection. 2. create an ssh *session*. 3. open a *channel* in the established transport layer. """ if not len(data): self.do_help('open') return data = data.strip().split() f = getattr(self, '_do_'+data[0], None) if f: f(data[1:]) return else: gkf.tombstone(red('no operation named {}'.format(data)))
def attach_IO(self, f, strip_comments=False) -> JSONReader: """ This function treats a string argument as a filename that needs to be opened, and treats a file as something that needs to be read. The function does not /parse/ the contents, but it does return self for ease of chaining. Raises URException if no file exists, or if it cannot be opened for read access, or if the argument is an incompatible type. """ try: x = open(f, 'r') self.s = x.read() x.close() if strip_comments: self.comment_stripper() except AttributeError as e: # This tests for a filename x = fname.Fname(f) if not x: raise Exception("No source named " + str(x) + " exists.") self.origin = str(x) self.attach_IO(self.origin, strip_comments) except IOError as e: gkf.tombstone(gkf.type_and_text(e)) raise e from None return self
def convert(self, source=None) -> object: """ Parse the self.s string as JSON. Returns the parsed object. Raises URException on failure to parse. NOTE: just because the parse is successful, the JSON message might still be meaningless. Thus, the name of the function is 'convert' not 'parse.' """ if source is not None: self.s = source o = None try: o = simplejson.loads(self.s) except simplejson.JSONDecodeError as e: start = max(0, e.pos - 20) stop = min(e.pos + 1, len(self.s)) t = ("Syntax error: " + e.msg + "\nin " + self.origin + " at offset " + str(e.pos) + ",\nnear the end of the phrase << " + self.s[start:stop] + ">> of the original input, " + "Line: " + str(e.lineno) + ", column:" + str(e.colno) + ". ") gkf.tombstone(t) raise ue.URException(self.origin + ' failed syntax check.') return o
def do_send(self, data:str="") -> None: """ send { file filename | string } Sends stuff over the channel. """ if not self.hop.channel: gkf.tombstone(red('channel not open.')) self.do_help('send') return if data.startswith('file'): try: _, filename = data.split() f = fname.Fname(filename) if f: data=f() except Exception as e: gkf.tombstone(red(gkf.type_and_text(e))) try: i = self.hop.channel.send(data) gkf.tombstone(blue('sent {} bytes.'.format(i))) except KeyboardInterrupt: gkf.tombstone(blue('aborting. Control-C pressed.')) except Exception as e: gkf.tombstone(red(gkf.type_and_text(e))) finally: self.hop.open_channel()
def do_debug(self, data:str="") -> None: """ debug [ value ] With no parameter, this function prints the current debug level (as if you cannot tell already). Otherwise, set the level. """ if not len(data): gkf.tombstone(blue('debug level is {}'.format(self.hop.debug_level()))) return logging_levels = { "CRITICAL":"50", "ERROR":"40", "WARNING":"30", "INFO":"20", "DEBUG":"10", "NOTSET":"0" } data = data.strip().upper() if data not in logging_levels.keys() and data not in logging_levels.values(): gkf.tombstone(red('not sure what this level means: {}'.format(data))) return try: level = int(data) except: level = int(logging_levels[data]) finally: self.hop.debug_level(level)
def do_error(self, data:str="") -> None: """ error [reset] [re]displays the error of the connection, and optionally resets it """ gkf.tombstone(blue(self.hop.error)) if 'reset'.startswith(data.strip().lower()): self.hop.error = None
def do_setpass(self, data:str="") -> None: """ setpass [password] sets, displays, or clears ('none') the password to be used. """ data = data.strip() if data.lower() == 'none': self.hop.password = None elif not data: gkf.tombstone(blue('password is set to {}'.format(self.hop.password))) else: self.hop.password = data
def do_save(self, data:str="") -> None: """ save [ additional-file-name ] Saves the current configuration, including information about the current host. If you supply a filename, that file will contain a duplicate copy of this data. The configuration is written as a JSON file, indented 4 spaces per level, with the host names sorted alphabetically. The kex-es and ciphers are listed in the order the host perfers them. """ try: with open(data, 'w') as f: json.dump(self.cfg, f, sort_keys=True, indent=4) gkf.tombstone('Duplicate config file written to {}'.format(data)) except: pass old_sec_info = self.cfg.get(self.hop.remote_host, {}) new_sec_info = self.hop.security if new_sec_info == {}: gkf.tombstone('No active connection / no data to update.') return if old_sec_info == new_sec_info: gkf.tombstone('Update not required for {}'.format(self.hop.remote_host)) return self.cfg[self.hop.remote_host] = new_sec_info with open(self.cfg_file, 'w') as f: json.dump(self.cfg, f, sort_keys=True, indent=4) gkf.tombstone('Update successful. Written to {}'.format(self.cfg_file))
def do_setsockdomain(self, data:str="") -> None: """ setsockdomain [{ af_inet | af_unix }] af_inet -- internet sockets af_unix -- a socket on local host that most people call a 'pipe' """ if not data: gkf.tombstone(blue('socket domain is {}'.format(self.hop.sock_domain))); return data = data.strip().lower() if data == 'af_inet': self.hop.sock_domain = socket.AF_INET elif data == 'af_unix': self.hop.sock_domain = socket.AF_UNIX else: gkf.tombstone(blue('unknown socket domain: {}'.format(data)))
def do_do(self, data:str="") -> None: """ do { something } attempt to exit a command by stuffing the text through the channel """ if not self.hop.channel or not data: self.do_help('do') return try: gkf.tombstone(blue('attempting remote command {}'.format(data))) in_, out_, err_ = self.hop.channel.exec_command(data) except KeyboardInterrupt as e: gkf.tombstone(blue('aborting. Control-C pressed.')) except Exception as e: gkf.tombstone(red(gkf.type_and_text(e))) else: out_.channel.recv_exit_status(); gkf.tombstone(blue(out_.readlines())) finally: self.hop.open_channel()
def do_settimeout(self, data:str="") -> None: """ settimeout [ { tcp | auth | banner } {seconds} ] Without parameters, settimeout will show the current socket timeout values. Otherwise, set it and don't forget it. """ if not data: gkf.tombstone('timeouts (tcp, auth, banner): ({}, {}, {})'.format( self.hop.tcp_timeout, self.hop.auth_timeout, self.hop.banner_timeout)) return data = data.strip().split() if len(data) < 2: gkf.tombstone(red('missing timeout value.')) self.do_help('settimeout') return try: setattr(self.hop, data[0]+'_timeout', float(data[1])) except AttributeError as e: gkf.tombstone(red('no timeout value for ' + data[0])) except ValueError as e: gkf.tombstone(red('bad value for timeout: {}' + data[1])) else: self.do_settimeout()
def _do_transport(self, data:str="") -> None: """ Creates a transport layer from an open/active ssh session. """ if not self.hop.client: gkf.tombstone('You must create an ssh session before you can create a transport layer atop it.') return gkf.tombstone(blue('attempting to create a transport layer')) start_time = time.time() OK = self.hop.open_transport() stop_time = time.time() if OK: gkf.tombstone(blue('success')) else: gkf.tombstone(red('failed '+self.hop.error_msg())) gkf.tombstone(blue('elapsed time: {}'.format(elapsed_time(start_time, stop_time))))
def open_session(self) -> bool: """ Attempt to create an SSH session with the remote host using the socket, transport, and channel that we [may] have already openend. """ global members self.error = None self.client = SSHClient() self.client.load_system_host_keys() self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy) try: username=self.ssh_info.get('user', getpass.getuser()) if not self.password: self.client.connect(self.ssh_info['hostname'], int(self.ssh_info['port']), username=username, key_filename=self.ssh_info['identityfile'], sock=self.sock) else: self.client.connect(self.ssh_info['hostname'], int(self.ssh_info['port']), username=username, password=self.password, sock=self.sock) except paramiko.ssh_exception.BadAuthenticationType as e: self.error = str(e) except TypeError as e: gkf.tombstone(red('Socket not open.')) self.error = -1 except Exception as e: self.error = gkf.type_and_text(e) else: self.open_transport() opts = self.transport.get_security_options() self.security = { k:list(getattr(opts, k, None)) for k in members } self.security['host_key'] = self.transport.get_remote_server_key().get_base64() self.security['version'] = self.transport.remote_version finally: return self.error is None
def do_setsocktype(self, data:str="") -> None: """ setsocktype [{ stream | dgram | raw }] stream -- ordinary TCP socket dgram -- ordinary UDP socket raw -- bare metal """ sock_types = {'stream':socket.SOCK_STREAM, 'dgram':socket.SOCK_DGRAM, 'raw':socket.SOCK_RAW } if not data: gkf.tombstone('socket type is {}'.format(self.hop.sock_type)); return try: self.hop.sock_type = sock_types[data.strip().lower()] except: gkf.tombstone(blue('unknown socket type: {}'.format(data)))
def do_connect(self, info: str = "") -> None: """ Usage: connect {IP|name} {port} """ info = info.strip().split() self.connection = beachhead.SocketConnection() try: self.connection.open_socket(info[0], int(info[1])) except Exception as e: gkf.tombstone(gkf.type_and_text(e)) return print('Connected to /something/ at {}:{}'.format(info[0], info[1])) print('Ready to talk. Sending TEST') self.connection.send('TEST') reply = self.read() print('Received {} as reply'.format(reply))
def _do_sftp(self, data:list=[]) -> None: """ Open an sftp connection to the remote host. """ if not self.hop.transport: gkf.tombstone(red('Transport layer is not open. You must create it first.')) return gkf.tombstone(blue('creating sftp client.')) start_time = time.time() OK = self.hop.open_sftp() stop_time = time.time() if OK: gkf.tombstone(blue('success')) else: gkf.tombstone(red('failure '+self.hop.error_msg())) gkf.tombstone(blue('elapsed time: {}'.format(elapsed_time(start_time, stop_time))))
def do_logging(self, data:str="") -> None: """ Usage: logging { on | off } turns logging (to $PWD/beachhead.log) on or off. No error is created when logging is on and you ask to turn it on, etc. If you would like to specify a different logfile, there are two solutions. [1] Symbolic links: rm -f $PWD/beachhead.log ln -s yourfile $PWD/beachhead.log [2] Use a different program. """ states = { "on":True, "off":False } if not data: self.do_help('logging') return try: state = states.get(data.lower(), None) if state is None: raise StopIteration from None except StopIteration as e: self.do_help('logging') return except Exception as e: gkf.tombstone(gkf.type_and_text(e)) return if state: logging.getLogger("paramiko").setLevel(logging.WARNING) paramiko.util.log_to_file("beachhead.log") else: logging.getLogger("paramiko").setLevel(logging.NOTSET) return
def comment_stripper(self) -> JSONReader: """ Remove (illegal) bash type comments from the source code. Build a list of lines that are really JSON, and join them back into a string. """ if __name__ == "__main__": gkf.tombstone("Strippin' the comments...") if self.s is None: return self comment_free_lines = [] for line in self.s.split("\n"): if len(line.strip()) == 0: continue elif line.strip()[0] == '#': gkf.tombstone(line) continue else: comment_free_lines.append(line) self.s = "\n".join(comment_free_lines) return self
def _do_channel(self, data:list=[]) -> None: """ channel [ session | forward | direct | x11 ] Acquire a channel of the desired type. "session" is the default. """ data = 'session' if not data else data[0].lower() channel_types = {"session":"session", "forward":"forwarded-tcpip", "direct":"direct-tcpip", "x":"x11"} if data not in channel_types.keys() and data not in channel_types.values(): gkf.tombstone(blue('unknown channel type: {}'.format(data))) return gkf.tombstone(blue('attempting to create a channel of type {}'.format(data))) start_time = time.time() OK = self.hop.transport.open_channel(data) stop_time = time.time() if OK: gkf.tombstone(blue('success')) else: gkf.tombstone(red('failed ' + self.hop.error_msg())) gkf.tombstone(blue('elapsed time: {}'.format(elapsed_time(start_time, stop_time))))
def do_get(self, data:str="") -> None: """ get a file from the remote host. Syntax: get filename """ if not self.hop.sftp: gkf.tombstone(red('sftp channel is not open.')) return if not data: self.do_help('get') return start_time = time.time() OK = self.hop.sftp.get(data, Fname(data).fname) stop_time = time.time() if OK: gkf.tombstone('success') else: gkf.tombstone('failure {}'.format(self.hop.error_msg())) gkf.tombstone('elapsed time: {}'.format(elapsed_time(stop_time, start_time)))
def _do_socket(self, data:list=[]) -> None: """ Attemps to open a new socket with the current parameters. """ if len(data) < 1: gkf.tombstone('nothing to do.') return elif len(data) == 1: data.append(None) start_time = time.time() OK = self.hop.open_socket(data[0], data[1]) stop_time = time.time() if OK: gkf.tombstone(blue('connected.')) else: gkf.tombstone(self.hop.error_msg()) return gkf.tombstone(blue('elapsed time: {}'.format(elapsed_time(start_time, stop_time))))
def do_probe(self, data:str="") -> None: """ Syntax: probe {host} [ host, [host] .. ] The 'probe' is nothing more than a convenience. It connects to a host with logging on and set to the debug level. The transaction is appended to the logfile for later inspection. Each probe is given a 9-digit random ID. In the logfile, you will find a BEGIN TRANSACTION and an END TRANSACTION containing the information that is gleaned from the probe. """ self.do_logging('on') self.do_debug('10') hostnames = data.strip().split() if not hostnames: self.do_help('probe') return if 'all' in hostnames: hostnames = sorted(list(gkf.get_ssh_host_info('all'))) hostnames.remove('*') transaction_log = open('beachhead.log', 'a') transaction_id = "{:0>9}".format(random.randrange(1000000000)) try: transaction_log.write('BEGIN TRANSACTION {}\n'.format(transaction_id)) transaction_log.flush() for _ in hostnames: gkf.tombstone('probing {}'.format(_)) transaction_log.write('probing {}\n'.format(_)) transaction_log.flush() try: self.do_open('socket {}'.format(_)) if not self.hop: continue self.do_open('session') self.do_close() except Exception as e: gkf.tombstone(gkf.type_and_text(e)) finally: transaction_log.write('END TRANSACTION {}\n'.format(transaction_id)) transaction_log.flush() transaction_log.close() gkf.tombstone("Written to logfile as transaction ID {}".format(transaction_id))
def _do_session(self, data:list=[]) -> None: """ session Attempt to create an SSH session with the remote host using the socket, transport, and channel that we [may] have already openend. """ self.hop.client = SSHClient() self.hop.client.load_system_host_keys() self.hop.client.set_missing_host_key_policy(paramiko.AutoAddPolicy) start_time = time.time() OK = self.hop.open_session() stop_time = time.time() if OK: gkf.tombstone(blue('ssh session established.')) else: gkf.tombstone(red('failed '+self.hop.error_msg())) gkf.tombstone(blue('elapsed time: {}'.format(elapsed_time(start_time, stop_time))))
def default(self, data: str = "") -> None: gkf.tombstone(beachhead.red('unknown command {}'.format(data))) self.do_help(data)
print('Connected to /something/ at {}:{}'.format(info[0], info[1])) print('Ready to talk. Sending TEST') self.connection.send('TEST') reply = self.read() print('Received {} as reply'.format(reply)) def do_exit(self, info: str = "") -> None: """ Usage: exit """ if self.connection: self.connection.close() sys.exit(os.EX_OK) if __name__ == "__main__": # subprocess.call('clear',shell=True) while True: try: Davis().cmdloop() except KeyboardInterrupt: gkf.tombstone("Exiting via control-C.") sys.exit(os.EX_OK) except Exception as e: gkf.tombstone(gkf.type_and_text(e)) gkf.tombstone(gkf.formatted_stack_trace(True)) sys.exit(1)
def _do_version(self) -> None: gkf.tombstone("This is the only version you will ever need.") gkf.tombstone("What difference does it make?") pass
def do_status(self, data:str="") -> None: """ status displays the current state of the connection. """ global members gkf.tombstone(blue("debug level: {}".format(self.hop.debug_level()))) if not self.hop.sock: gkf.tombstone('not connected.'); return gkf.tombstone(blue("local end: {}".format(self.hop.sock.getsockname()))) gkf.tombstone(blue("remote end: {}".format(self.hop.sock.getpeername()))) gkf.tombstone(blue("type/domain: {} / {}".format(self.hop.sock_type, self.hop.sock_domain))) gkf.tombstone(blue("ssh session: {}".format(self.hop.client))) gkf.tombstone(blue("transport: {}".format(self.hop.transport))) gkf.tombstone(blue("sftp layer: {}".format(self.hop.sftp))) gkf.tombstone(blue("channel: {}".format(self.hop.channel))) try: banner=self.hop.transport.get_banner().decode('utf-8') except: banner="no banner found" gkf.tombstone(blue("banner: {}".format(banner))) gkf.tombstone(blue("*** security info: ***")) for _ in self.hop.security.keys(): gkf.tombstone(blue("{} : {}").format(_,self.hop.security.get(_, None)))
def do_close(self, data="") -> None: """ Close the open socket connection (if any) """ if self.hop: self.hop.close() else: gkf.tombstone(blue('nothing to do'))
def do_hosts(self, data:str="") -> None: """ hosts: print a list of the available (known) hosts """ gkf.tombstone("\n"+blue("\n".join(sorted(list(gkf.get_ssh_host_info('all'))))))
def do_put(self, data:str="") -> None: """ put a file onto the remote host. Syntax: put filename NOTE: filename can be a wildcard spec. """ if not self.hop.sftp: gkf.tombstone(red('sftp channel is not open.')) return if not data: gkf.tombstone(red('you have to send something ...')) self.do_help('put') return files = glob.glob(data) if not files: gkf.tombstone(red('no file[s] named {}'.format(data))) return start_time = time.time() OK = None for f in files: try: OK = self.hop.sftp.put(f.fqn, f.fname) except Exception as e: gkf.tombstone(red(gkf.type_and_text(e))) stop_time = time.time() if OK: gkf.tombstone('success') else: gkf.tombstone('failure {}'.format(self.hop.error_msg())) gkf.tombstone('elapsed time: '.format(elapsed_time(stop_time, start_time)))