def exec_remote_cmd(self, cmd, probe_cmd=None): """ Execute command(cmd or probe-command) on remote host :param cmd: Command to execute :param probe_cmd: Probe command to execute before 'cmd' :return: None """ minion_id = self.get_value('minion', arg_type='query') LOG.debug(f'[exec_remote_cmd] Minion ID: {minion_id}') minion = MINIONS.get(minion_id) args = minion['args'] LOG.debug(f'[exec_remote_cmd] Minion args: {args}') self.ssh_transport_client = self.create_ssh_client(args) # Use probe_cmd to detect file's existence if probe_cmd: chan = self.ssh_transport_client.get_transport().open_session() chan.exec_command(probe_cmd) ext = (chan.recv_exit_status()) if ext: raise tornado.web.HTTPError(404, "Not found") transport = self.ssh_transport_client.get_transport() self.channel = transport.open_channel(kind='session') self.channel.exec_command(cmd)
def open(self): self.src_addr = self.get_client_endpoint() LOG.info('Connected from {}:{}'.format(*self.src_addr)) try: # Get id from query argument from minion_id = self.get_value('id') LOG.debug(f"############ minion id: {minion_id}") minion = MINIONS.get(minion_id) if not minion: self.close(reason='Websocket failed.') return minion_obj = minion.get('minion', None) if minion_obj: # minions[minion_id]["minion"] = None self.set_nodelay(True) minion_obj.set_handler(self) self.minion_ref = weakref.ref(minion_obj) self.loop.add_handler(minion_obj.fd, minion_obj, IOLoop.READ) else: self.close(reason='Websocket authentication failed.') except (tornado.web.MissingArgumentError, InvalidValueError) as err: self.close(reason=str(err))
async def post(self): args = self.get_args() try: self.ssh_term_client = self.create_ssh_client(args) minion = await run_async_func(self.create_minion, args) except InvalidValueError as err: # Catch error in self.get_args() raise tornado.web.HTTPError(400, str(err)) except (ValueError, paramiko.SSHException, paramiko.ssh_exception.SSHException, paramiko.ssh_exception.AuthenticationException, socket.timeout) as err: LOG.error("====================") LOG.error(err) # Delete dangling cache if str(err).lower().startswith( "unable to") and conf.mode != "term": delete_cache(str(args[1])) self.result.update(status=str(err)) else: # if not minions: # GRU[ip] = minions # minion.src_addr = (ip, port) MINIONS[minion.id] = {"minion": minion, "args": args} self.loop.call_later(2, recycle_minion, minion) self.result.update(id=minion.id, encoding=minion.encoding) # self.set_secure_cookie("minion", minion.id) self.write(self.result)
def on_close(self): LOG.info('Disconnected from {}:{}'.format(*self.src_addr)) if not self.close_reason: self.close_reason = 'client disconnected' minion = self.minion_ref() if self.minion_ref else None if minion: minion.close(reason=self.close_reason)
def _extract_filename(data: bytes) -> str: LOG.debug(data) ptn = re.compile(b'filename="(.*)"') m = ptn.search(data) if m: name = m.group(1).decode() else: name = "untitled" # Replace spaces with underscore return re.sub(r'\s+', '_', name)
def create_minion(self, args): ssh_endpoint = args[:2] LOG.info('Connecting to {}:{}'.format(*ssh_endpoint)) term = self.get_argument('term', '') or 'xterm' shell_channel = self.ssh_term_client.invoke_shell(term=term) shell_channel.setblocking(0) minion = Minion(self.loop, self.ssh_term_client, shell_channel, ssh_endpoint) minion.encoding = conf.encoding if conf.encoding else self.get_server_encoding( self.ssh_term_client) return minion
def get_server_encoding(self, ssh): try: _, stdout, _ = ssh.exec_command("locale charmap") except paramiko.SSHException as err: LOG.error(str(err)) else: result = stdout.read().decode().strip() if result: return result LOG.warning('!!! Unable to detect default encoding') return 'utf-8'
def get_args(self): data = json_decode(self.request.body) LOG.debug(data) # Minion login won't pass hostname in form data hostname = data.get("hostname", "localhost") username = data["username"] password = data["password"] port = int(data["port"]) args = (hostname, port, username, password) LOG.debug(f"Args for SSH: {args}") return args
def main(): LOG.info(f'Gru mode: {conf.mode}') loop = tornado.ioloop.IOLoop.current() app = Term1nal(loop=loop) ssl_ctx = get_ssl_context(conf) server_settings = dict( xheaders=True, max_body_size=6000 * 1024 * 1024, # 6G ) app.listen(conf.port, conf.address, **server_settings) if ssl_ctx: server_settings.update(ssl_options=ssl_ctx) app.listen(conf.ssl_port, conf.host, **server_settings) loop.start()
async def data_received(self, data): # A simple multipart/form-data # b'------WebKitFormBoundarysiqXYmhALsFpsMuh\r\nContent-Disposition: form-data; name="upload"; # filename="hello.txt"\r\nContent-Type: text/plain\r\n\r\n # hello\r\n\r\nworld\r\n\r\n------WebKitFormBoundarysiqXYmhALsFpsMuh--\r\n' """ :param data: :return: None """ if not self.boundary: self.boundary = self._get_boundary() LOG.debug(f"multipart/form-data boundary: {self.boundary}") # Split data with multipart/form-data boundary sep = f'--{self.boundary}' chunks = data.split(sep.encode('ISO-8859-1')) chunks_len = len(chunks) # DEBUG # print("=====================================") # print(f"Stream idx: {self.stream_idx}") # print(f"CHUNKS length: {len(chunks)}") # Data is small enough in one stream if chunks_len == 3: form_data_info, raw = self._partition_chunk(chunks[1]) self.filename = self._extract_filename(form_data_info) await run_async_func(self.exec_remote_cmd, f'cat > /tmp/{self.filename}') await run_async_func(self._write_chunk, raw) await run_async_func(self.ssh_transport_client.close) else: if self.stream_idx == 0: form_data_info, raw = self._partition_chunk(chunks[1]) self.filename = self._extract_filename(form_data_info) await run_async_func(self.exec_remote_cmd, f'cat > /tmp/{self.filename}') await run_async_func(self._write_chunk, raw) else: # Form data in the middle data stream if chunks_len == 1: await run_async_func(self._write_chunk, chunks[0]) else: # 'chunks_len' == 2, the LAST stream await run_async_func(self._write_chunk, chunks[0]) await run_async_func(self.ssh_transport_client.close) self.stream_idx += 1
def create_ssh_client(args) -> paramiko.SSHClient: print(f"[create_ssh_client]args: {args}") ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.client.MissingHostKeyPolicy) try: ssh.connect(*args, allow_agent=False, look_for_keys=False, timeout=conf.timeout) except socket.error: print(args[:2]) raise ValueError('Unable to connect to {}:{}'.format(*args[:2])) except (paramiko.AuthenticationException, paramiko.ssh_exception.AuthenticationException): raise ValueError('Authentication failed.') except EOFError: LOG.error("Got EOFError, retry") ssh.connect(*args, allow_agent=False, look_for_keys=False, timeout=conf.timeout) return ssh
def on_message(self, message): LOG.debug(f'{message} from {self.src_addr}') minion = self.minion_ref() try: msg = json.loads(message) except JSONDecodeError: return if not isinstance(msg, dict): return resize = msg.get('resize') if resize and len(resize) == 2: try: minion.chan.resize_pty(*resize) except (TypeError, struct.error, paramiko.SSHException): pass data = msg.get('data') if data and isinstance(data, str): minion.data_to_dst.append(data) minion.do_write()
def do_read(self): LOG.debug('minion {} on read'.format(self.id)) try: data = self.chan.recv(self.BUFFER_SIZE) except (OSError, IOError) as e: LOG.error(e) if errno_from_exception(e) in _ERRNO_CONNRESET: self.close(reason='CHAN ERROR DOING READ ') else: LOG.debug(f'{data} from {self.dst_addr}') if not data: self.close(reason='BYE ~') return LOG.debug(f'{data} to {self.handler.src_addr}') try: self.handler.write_message(data, binary=True) except tornado.websocket.WebSocketClosedError: self.close(reason='WEBSOCKET CLOSED')
async def get(self): chunk_size = 1024 * 1024 * 1 # 1 MiB remote_file_path = self.get_value("filepath", arg_type="query") filename = os.path.basename(remote_file_path) LOG.debug(remote_file_path) try: self.exec_remote_cmd(cmd=f'cat {remote_file_path}', probe_cmd=f'ls {remote_file_path}') except tornado.web.HTTPError: self.write(f'Not found: {remote_file_path}') await self.finish() return self.set_header("Content-Type", "application/octet-stream") self.set_header("Accept-Ranges", "bytes") self.set_header("Content-Disposition", f"attachment; filename={filename}") while True: chunk = self.channel.recv(chunk_size) if not chunk: break try: # Write the chunk to response self.write(chunk) # Send the chunk to client await self.flush() except tornado.iostream.StreamClosedError: break finally: del chunk await tornado.web.gen.sleep(0.000000001) # 1 nanosecond self.ssh_transport_client.close() try: await self.finish() except tornado.iostream.StreamClosedError as err: LOG.error(err) LOG.debug("Maybe user cancelled download") LOG.info(f"Download ended: {remote_file_path}")
def close(self, reason=None): if self.closed: return self.closed = True LOG.info(f'Closing minion {self.id}: {reason}') if self.handler: self.loop.remove_handler(self.fd) self.handler.close(reason=reason) self.chan.close() self.ssh.close() LOG.info('Connection to {}:{} lost'.format(*self.dst_addr)) clear_minion(self) LOG.debug(MINIONS)
def do_write(self): LOG.debug('minion {} on write'.format(self.id)) if not self.data_to_dst: return data = ''.join(self.data_to_dst) LOG.debug(f'{data} to {self.dst_addr}') try: sent = self.chan.send(data) except (OSError, IOError) as e: LOG.error(e) if errno_from_exception(e) in _ERRNO_CONNRESET: self.close(reason='chan error on writing') else: self.update_handler(IOLoop.WRITE) else: self.data_to_dst = [] data = data[sent:] if data: self.data_to_dst.append(data) self.update_handler(IOLoop.WRITE) else: self.update_handler(IOLoop.READ)
def recycle_minion(minion): if minion.handler: return LOG.warning('Recycling minion {}'.format(minion.id)) minion.close(reason='minion recycled')
def get(self): LOG.debug(f"MINIONS: {MINIONS}") self.render('index.html', mode=conf.mode)
def prepare(self): LOG.info("In NotFoundHandler") raise tornado.web.HTTPError(status_code=404, reason="Oops!")