Files
2026-07-28 03:21:34 +08:00

619 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'