from utils import * from getpass import getuser import cmd import datetime, os, re CMDS = [ "ls", "cd", "pwd", "mkdir", "rm", "cp", "mv", "touch", "cat", "less", "more", "head", "tail", "find", "grep", "ssh", "ifconfig", "netstat", "whoami", "hostname", "uname", "df", "free", "ps", "kill", "killall", "shutdown", "reboot", "date", "history", "alias", "unalias", "echo", "printf", "env", "set", "help", "rsync", "ping", "curl", "wget", 'll', 'la', 'ifconfig', 'uptime', 'reset', 'history', 'exec', 'sshd', 'iptables', 'route', 'netstat', 'iftop', 'tcpdump', 'traceroute', 'whois', 'dig', 'sh' ] class FakeShell(cmd.Cmd): def __init__(self, config, shell_config, username = 'ubuntu'): super().__init__() self.shell_config = shell_config self.config = config self.username = username # 从配置获取用户名 if self.username == 'root': self.home_path = f"vdisk/root" self.restricted_dirs = ['/sys', '/tmp', '/usr', '/var', '/dev'] else: self.home_path = f"vdisk/home/{self.username}" self.restricted_dirs = ['/sys', '/tmp', '/usr', '/var', '/dev', '/root'] self.path = self.home_path # 当前路径 self.vdisk_root = "vdisk" self.intro = self.motd() self.use_rawinput = False self.case_sensitive_path = True def onecmd(self, line): """拦截 '?' 并阻止其调用 do_help""" line = custom_strip(line) command = line.split() if command == "?": return self.error_msg("?") # 返回 "?: command not found" return super().onecmd(line) def get_prompt(self): dir_display = self._get_display_path(show_home_as_tilde=True) # 获取主机名 HOSTNAME = self.config['settings']['hostname'] # ANSI颜色代码 COLOR_USER = '\033[1;32m' # 绿色加粗(用户名) COLOR_PATH = '\033[1;34m' # 蓝色加粗(路径) RESET = '\033[0m' # if self.username == 'root': return self.shell_config['messages']['root_prompt'].format( username=self.username, endl='', path=dir_display, space=' ', hostname=HOSTNAME, ) return self.shell_config['messages']['prompt'].format( username=self.username, endl='', path=dir_display, space=' ', hostname=HOSTNAME, COLOR_USER=COLOR_USER, COLOR_PATH=COLOR_PATH, RESET=RESET ) def motd(self): return self.shell_config['messages']['motd'].format(endl='\r\n') def error_msg(self, command): return f"{command}: command not found\r\n" def fake_command(self, command): return "This command is not available.\r\n" def _normalize_path(self, path): """规范化路径,处理相对路径和绝对路径""" # 处理~表示home目录 if path.startswith('~'): path = self.home_path elif path.startswith('/'): # 处理绝对路径 path = os.path.join(self.vdisk_root, path.lstrip('/')) else: # 处理相对路径 path = os.path.join(self.path, path) # 规范化路径,解析..和. normalized = os.path.normpath(path) # 确保路径仍在虚拟磁盘内 abs_vdisk = os.path.abspath(self.vdisk_root) if not os.path.abspath(normalized).startswith(abs_vdisk): normalized = self.vdisk_root # 退回到虚拟根目录 # 如果是大小写敏感的,检查路径是否存在(精确匹配大小写) if self.case_sensitive_path: dirname, basename = os.path.split(normalized) if dirname and os.path.exists(dirname): # 检查目录中的文件名是否大小写匹配 try: actual_names = os.listdir(dirname) found = False for name in actual_names: if name == basename: # 精确匹配大小写 found = True break if not found and basename: # 如果没找到且basename不为空 return normalized # 仍然返回原始路径,但后续操作会失败 except OSError: pass return normalized def _is_safe_path(self, path): """检查路径是否在允许的虚拟磁盘范围内""" try: abs_path = os.path.abspath(path) abs_vdisk = os.path.abspath(self.vdisk_root) # 检查是否在虚拟磁盘范围内 if not abs_path.startswith(abs_vdisk): return False # 如果是大小写敏感的,检查路径是否存在(精确匹配大小写) if self.case_sensitive_path: dirname, basename = os.path.split(abs_path) if dirname and os.path.exists(dirname): try: actual_names = os.listdir(dirname) if basename and basename not in actual_names: return False # 大小写不匹配 except OSError: pass # 检查是否试图访问限制目录 rel_path = os.path.relpath(abs_path, abs_vdisk) rel_path_parts = rel_path.split(os.sep) for restricted_dir in self.restricted_dirs: # 检查路径的任何部分是否匹配限制目录 if restricted_dir.lstrip('/') in rel_path_parts: return False return True except: return False def _get_display_path(self, path=None, show_home_as_tilde=True): """获取用于显示的路径 :param path: 要显示的路径,None表示当前路径 :param show_home_as_tilde: 是否将home目录显示为~ """ if path is None: path = self.path # 获取home目录路径 if self.username == 'root': home_path = os.path.join(self.vdisk_root, "root") else: home_path = os.path.join(self.vdisk_root, "home", self.username) abs_home = os.path.abspath(home_path) abs_path = os.path.abspath(path) # 如果是home目录且需要显示为~ if show_home_as_tilde and abs_path == abs_home: return "~" # 如果是home目录的子目录且需要显示为~ if show_home_as_tilde and abs_path.startswith(abs_home): rel_path = os.path.relpath(abs_path, abs_home) return f"~/{rel_path}" if rel_path != "." else "~" # 处理普通路径 rel_path = os.path.relpath(path, self.vdisk_root) # 处理根目录情况 if rel_path == ".": return "/" return "/" + rel_path.replace('\\', '/') def _replace_escape(self, match): """替换转义字符""" esc = match.group(1) escapes = { '\\': '\\', 'a': '\a', 'b': '\b', 'c': '', # -c 标志会忽略剩余输出 'e': '\x1b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v', '0': '\0', '"': '"' } return escapes.get(esc, esc) def default(self, line): """处理未知命令""" line = custom_strip(line) command = line.split()[0] if command in CMDS: return "\r\n" return self.error_msg(command) def do_apt(self, arg): """处理apt命令""" if arg and custom_strip(arg)[0] == '-get': return 'apt-get : Permission denied\r\n' return 'apt: Permission denied: /usr/bin/apt\r\n' def do_cat(self, arg): """输出文件内容,如果是目录则显示错误""" args = arg.split() result = "" if not args: # 如果没有参数,从标准输入读取(这里不支持) return "\r\n" for a in args: file_path = self._normalize_path(a) if os.path.isdir(file_path): display_path = self._get_display_path(file_path, False) result += f"\ncat: {display_path}: Is a directory" continue elif not os.path.exists(file_path): display_path = self._get_display_path(file_path, False) result += f"\ncat: {display_path}: No such file or directory" continue # 检查是否试图访问限制目录 if not self._is_safe_path(file_path): display_path = self._get_display_path(file_path, False) result += f"\ncat: {display_path}: Permission denied" continue else: try: with open(file_path, "r") as f: content = f.read() result += '\n' +content except UnicodeDecodeError: display_path = self._get_display_path(file_path) result += f"\ncat: {display_path}: Is a binary file" except PermissionError: display_path = self._get_display_path(file_path) result += f"\ncat: {display_path}: Permission denied" except: display_path = self._get_display_path(file_path) result += f"\ncat: {display_path}: Error reading file" if result.startswith('\n'): # 移除第一个换行符 result = result[1:] return result + '\r\n' def do_cd(self, arg): """改变命令位置""" args = arg.split() if len(args) > 1: return "bash: cd: too many arguments\r\n" try: # 处理无参数情况(回到home目录) if not args: target = "~" else: target = args[0] # 处理home目录情况 if target == "~": target = f"~/{target[1:]}" if len(target) > 1 else "~" # 处理根目录情况 if target == "/": self.path = os.path.join(self.vdisk_root) return "" # 处理特殊路径 if target == "-": return "bash: cd: OLDPWD not set\r\n" # 规范化路径 new_path = self._normalize_path(target) # 确保路径在虚拟磁盘内 abs_vdisk = os.path.abspath(self.vdisk_root) if not os.path.abspath(new_path).startswith(abs_vdisk): new_path = self.vdisk_root # 检查路径存在性和类型 if not os.path.exists(new_path): return f"bash: cd: {self._get_display_path(new_path)}: No such file or directory\r\n" # 如果是大小写敏感的,检查精确匹配 if self.case_sensitive_path and os.path.basename(new_path): dirname = os.path.dirname(new_path) basename = os.path.basename(new_path) if os.path.exists(dirname): actual_names = os.listdir(dirname) if basename not in actual_names: return f"bash: cd: {self._get_display_path(new_path)}: No such file or directory\r\n" if not os.path.isdir(new_path): return f"bash: cd: {self._get_display_path(new_path)}: Not a directory\r\n" # 检查路径安全性 if not self._is_safe_path(new_path): display_path = self._get_display_path(new_path) return f"bash: cd: {display_path}: Permission denied\r\n" # 更新当前路径 self.path = new_path return "" except Exception as e: return f"cd: {str(e)}\r\n" def do_clear(self, arg): """处理clear命令""" return '\033[H\033[2J' def do_echo(self, arg): """实现echo命令,支持引号字符串和转义字符""" # 使用正则表达式解析参数(支持单双引号) pattern = re.compile(r''' ([^\s"']+ # 非空白非引号字符 |"(?:[^"\\]|\\.)*" # 双引号字符串 |'(?:[^'\\]|\\.)*') ''', re.VERBOSE) tokens = [] for match in pattern.finditer(arg): token = match.group(1) # 处理引号包裹的字符串 if len(token) >= 2: if token.startswith('"') and token.endswith('"'): token = token[1:-1].replace('\\"', '"') elif token.startswith("'") and token.endswith("'"): token = token[1:-1].replace("\\'", "'") tokens.append(token) # 解析参数选项 output = [] interpret_escapes = False no_newline = False i = 0 while i < len(tokens): token = tokens[i] if token.startswith('-'): if token == '-n': no_newline = True elif token == '-e': interpret_escapes = True elif token == '-E': interpret_escapes = False else: # 未知选项结束解析 output.extend(tokens[i:]) break i += 1 else: output.extend(tokens[i:]) break # 处理转义字符(仅双引号字符串需要处理) processed = [] for token in output: if interpret_escapes: token = re.sub(r'\\([\\abcefnrtv0"])', self._replace_escape, token) processed.append(token) result = ' '.join(processed) # 处理空字符串情况 if not result and (arg.strip().startswith('"') or arg.strip().startswith("'")): result = '' # 允许输出空字符串 # 添加换行符 if no_newline: return result return result + '\r\n' def do_exit(self, arg): """处理exit命令""" return True # 返回True表示退出shell def do_help(self, arg): """处理help命令""" return 'help: Permission denied: command is not available\r\n' def do_la(self, arg): """列出目录内容(长格式,显示隐藏文件)""" return self.do_ls(f"-la {arg}") def do_ll(self, arg): """列出目录内容(长格式)""" return self.do_ls(f"-l {arg}") def do_logout(self, arg): """处理logout命令""" return True # 返回True表示退出shell def do_ls(self, arg): """列出目录内容""" args = arg.split() path = self.path show_all = False # -a 显示隐藏文件 show_dirs = False # -A 显示隐藏文件,不包括 . 和 .. long_format = False # -l 长格式 color = True # 默认启用颜色 # 颜色定义(文件夹名称蓝色,/符号不染色) COLORS = { 'dir': '\033[1;34m', # 蓝色加粗(目录名称) 'exec': '\033[1;32m', # 绿色加粗(可执行文件) 'link': '\033[1;36m', # 青色加粗(链接) 'reset': '\033[0m' # 重置颜色 } # 解析参数 for a in args: if a.startswith("-"): if "a" in a: show_all = True show_dirs = True if "A" in a: show_all = True show_dirs = False if "l" in a: long_format = True elif a not in (".", ".."): path = self._normalize_path(a) try: # 安全路径检查 if not self._is_safe_path(path): display_path = self._get_display_path(path) return f"ls: cannot access '{display_path}': Permission denied\r\n" # 获取目录内容(手动添加.和..) items = [] if show_dirs: items.extend([".", ".."]) items.extend(os.listdir(path)) if not show_all: items = [i for i in items if not i.startswith(".")] # 处理显示项目 items_display = [] for item in items: item_path = os.path.join(path, item) try: is_dir = os.path.isdir(item_path) is_exec = is_linux_executable(item_path) and not is_dir is_link = os.path.islink(item_path) # 处理显示名称(文件夹名称和/符号分开染色) display_name = item suffix = "" if is_dir: suffix = "/" if color: if is_dir: display_name = f"{COLORS['dir']}{display_name}{COLORS['reset']}{suffix}" elif is_exec: display_name = f"{COLORS['exec']}{display_name}*{COLORS['reset']}" elif is_link: display_name = f"{COLORS['link']}{display_name}@{COLORS['reset']}" else: display_name = f"{display_name}{suffix}" items_display.append(display_name) except: items_display.append(item) # 出错时显示原始名称 # 长格式处理 if long_format: result = [] total_blocks = 0 # 改用累加文件大小计算total for item in items: item_path = os.path.join(path, item) try: stat = os.stat(item_path) total_blocks += stat.st_size / 512 # 模拟block计算 # 文件类型和权限 mode = stat.st_mode permissions = ( 'd' if os.path.isdir(item_path) else 'l' if os.path.islink(item_path) else '-' ) permissions += ''.join([ 'r' if mode & 0o400 else '-', 'w' if mode & 0o200 else '-', 'x' if mode & 0o100 else '-', 'r' if mode & 0o040 else '-', 'w' if mode & 0o020 else '-', 'x' if mode & 0o010 else '-', 'r' if mode & 0o004 else '-', 'w' if mode & 0o002 else '-', 'x' if mode & 0o001 else '-', ]) # 时间格式 mtime = datetime.datetime.fromtimestamp(stat.st_mtime) now = datetime.datetime.now() if (now - mtime).days > 180: # 超过6个月显示年份 time_str = mtime.strftime('%b %d %Y') else: time_str = mtime.strftime('%b %d %H:%M') # 文件名处理(名称和符号分开染色) name_part = item suffix = "" if os.path.isdir(item_path): suffix = "/" elif is_linux_executable(item_path): suffix = "*" elif os.path.islink(item_path): suffix = "@" if color: if os.path.isdir(item_path): name_part = f"{COLORS['dir']}{name_part}{COLORS['reset']}" elif is_linux_executable(item_path): name_part = f"{COLORS['exec']}{name_part}{COLORS['reset']}" elif os.path.islink(item_path): name_part = f"{COLORS['link']}{name_part}{COLORS['reset']}" line = ( f"{permissions} " f"{stat.st_nlink:>2} " f"{stat.st_uid:<8} " f"{stat.st_gid:<8} " f"{stat.st_size:>8} " f"{time_str} " f"{name_part}{suffix}" ) result.append(line) except: result.append(f"?????????? ? ? ? ? ? ? {item}") # 添加total行(使用计算得到的blocks) if result: result.insert(0, f"total {int(total_blocks)}") return "\n".join(result) + '\r\n' else: return "" else: # 普通格式输出(80字符宽) if not items_display: return "" term_width = 80 # 固定宽度 max_len = max(len(item) for item in items_display) + 2 col_width = min(max_len, term_width) num_cols = max(1, term_width // col_width) num_rows = (len(items_display) + num_cols - 1) // num_cols # 格式化输出 output = [] for row in range(num_rows): line = [] for col in range(num_cols): idx = row + col * num_rows if idx < len(items_display): item = items_display[idx] line.append(f"{item:<{col_width}}") output.append(''.join(line)) return "\n".join(output) + '\r\n' except FileNotFoundError: display_path = self._get_display_path(path) return f"ls: cannot access '{display_path}': No such file or directory\r\n" except PermissionError: display_path = self._get_display_path(path) return f"ls: cannot open directory '{display_path}': Permission denied\r\n" def do_passwd(self, arg): """处理passwd命令""" return "passwd: Authentication token manipulation error.\r\n" def do_pwd(self, arg): """打印当前工作目录""" return self._get_display_path(show_home_as_tilde=False) + '\r\n' def do_su(self, arg): """处理su命令""" return "su: faild to execute /bin/bash: Permission denied.\r\n" def do_sudo(self, arg): """处理sudo命令""" return f"{self.username} is not in the sudoers file. This incident will be reported.\r\n" def do_whoami(self, arg): """返回当前用户名""" return self.username + '\r\n'