你好 👋
几周前,我开始为我的团队“Securinets ISI”举办的一年一度的 CTF 比赛开发一个 TCP 服务器。目标是让玩家可以通过类似以下命令快速提交标志
echo "flag" | nc 127.0.0.1 8000
为了这个任务,我需要开发一个控制台应用程序。不同于独立拉取包,比如从 Laravel 中拉取 DB 和视图组件,从 Symfony 中拉取控制台组件,以及 Phinx 用于数据库迁移,我发现 Laravel Zero 是一个完美的选择。我们将用它来构建服务器!
Laravel Zero 是一个轻量级和模块化的微框架,用于快速开发强大的控制台应用程序,基于 Laravel 组件构建。
关于服务器的一些信息
如果你不熟悉 TCP 服务器是什么,别担心,你的一生都在使用它。Web 服务器是 TCP 服务器,只是多了一层抽象。这个服务器在更低的层次上运行。服务器会监听“服务器套接字”,而套接字就是一个绑定到 PORT 的 IP,表示为 IP:PORT。这个套接字等待客户端发出请求。因此,每次通信都涉及两个套接字;一个用于服务器,一个用于客户端。
客户端在连接到服务器时,会接收到操作系统即时分配的随机端口。是的,单个服务器套接字可以处理多个客户端,但是不是同时进行。每次客户端连接时,它就会被推送到一个接受队列中,等待轮到它。
真正了解服务器是如何工作的是一件很令人感兴趣的事情。我已经为你提供了一些链接让你阅读;祝你在探索中享受乐趣。而且,是的,我很快就会写关于这些的内容。
构建服务器
毫不拖延,让我们先开始安装 Laravel Zero
composer create-project --prefer-dist laravel-zero/laravel-zero securinets
现在,您可以重命名应用程序为您喜欢的任何名字
php application app:rename securinets
接下来,我想能够使用环境变量。我的应用程序需要数据库,我还将需要 Blade 引擎。Laravel Zero 使安装这些组件变得容易
php securinets app:install dotenv
php securinets app:install database
php securinets app:install view
我们可以通过运行以下命令来开始使用我们的控制台应用程序
php securinets [command]
对于我们的案例,我们需要构建一个 TCP 服务器,积极地监听传入的连接。我们有几种选择,比如 RoadRunner 或 Swoole,而我选择后者。
Swoole 是一个 PHP 扩展,允许您做很多事情,比如异步编程、事件循环、协程等等。
对于 Linux 系统,安装很简单
sudo apt update && sudo apt install -y php8.2-openswoole
我正在使用 PHP 8.2,请根据需要调整版本。
对于 VSCode 用户,您可以安装以下包以帮助 IDE
composer require openswoole/ide-helper
然后,在您的 settings.json
"intelephense.environment.includePaths": [
"vendor/openswoole/ide-helper"
],
现在,我们可以使用 Laravel Zero 生成一个新的命令来启动服务器
php securinets make:command ServeCommand
在 ServeCommand
中,我们可以利用 Swoole。它提供了一个 Server
类,它期望一个主机和一个端口。好消息是您仍然可以定义环境变量并在配置中引用它们,就像您在 Laravel 应用程序中做的那样。所有这些都由 Laravel Zero 处理,因为如您所回忆的,我们使用 app:install
命令安装了这些组件,否则您必须手动安装。
我欣赏 Laravel serve
命令日志的整洁性,所以也许我们可以创建类似的功能?我知道 Laravel 通过将 echo
语句与 str_repeat
结合来构建日志,所有这一切都在命令本身中完成。然而,我觉得这种方法有点嘈杂。所以,让我们利用 Blade 引擎来提供更干净解决方案。
在您的 resources/views
中创建一个 log.blade.php
视图
<div class="flex">
<span class="mr-1 ml-2 text-gray-600">{{ $date }}</span>
<span class="mr-1">{{ $hour }}</span>
<span class="mr-1 font-bold">[{{ $client }}]</span>
<span class="mr-2 text-gray-600">
{{ str_repeat('.', max(\Termwind\terminal()->width() - 80 - mb_strlen($client), 0)) }}
</span>
<span @class([
'px-1 font-bold',
'text-red' => !$connected,
'text-green' => $connected,
])">{{ $connected ? 'CONNECTED' : 'DISCONNECTED' }}</span>
</div>
这就是我们将在日志中看到的内容。现在,对于我们将发送给客户端的响应,我们希望它们也很酷。所以,创建一个 response.blade.php
。
<div class="py-1 ml-2">
<div @class([
'px-1 text-white',
'bg-red' => !$correct,
'bg-green-600' => $correct,
])>
{{ $correct ? 'SUCCESS' : 'ERROR' }}
</div>
<span class="ml-1">
{{ $message }}
</span>
</div>
现在,我们的视图已经准备好了,您可能想知道:“这不是 Tailwind CSS 吗?”嗯,是的!Laravel Zero 附带了一个 Termwind,这是一个功能强大的包,它允许您使用 Tailwind CSS 类来为控制台应用程序提供样式。
现在,我们可以使用 Swoole。但在那之前,让我们先了解一下基础知识。要创建一个服务器,您需要实例化 OpenSwoole\Server
类。通过这样做,您可以挂钩到各种事件。我们将使用 Start
、Connect
、Receive
和 Close
。您可以在这里找到所有事件。
在底层,Swoole 运行一个事件循环。事件循环是 Nginx 革命性的原因,并且是解决 C10k 问题的关键。好奇吗?我们很快就会回到 2004 年讨论这些问题👀
现在,根据我们已经学到的,命令看起来会是这样(代码已注释)
<?php
namespace App\Commands;
use OpenSwoole\Server;
use Termwind\HtmlRenderer;
use LaravelZero\Framework\Commands\Command;
use function Termwind\render;
class ServeCommand extends Command
{
private string $host;
private int $port;
public function __construct()
{
parent::__construct();
$this->host = config('server.host', '127.0.0.1');
$this->port = config('server.port', 9001);
}
/**
* The signature of the command.
*
* @var string
*/
protected $signature = 'serve';
/**
* The description of the command.
*
* @var string
*/
protected $description = 'Start the flag submission server.';
public function handle(): mixed
{
// Create a server object
$server = new Server($this->host, $this->port);
// Hook into the Start event
$server->on('Start', function () {
/** @var \Illuminate\Contracts\Support\Renderable $render */
$render = view('start', [
'host' => $this->host,
'port' => $this->port,
]);
// Yes, you can use the Blade engine to return the HTML as a rendered string,
// which can then be rendered by Termwind
render($render->render());
});
$server->on('Connect', function (Server $server, int $fd) {
$this->log($server->getClientInfo($fd), true);
});
$server->on('Receive', function (Server $server, int $fd, int $reactor_id, string $data) {
// I am only simulating the response; you should execute the business logic.
$response = view('response', [
'message' => 'Correct submission, keep it up.',
'correct' => true,
])->render();
$response = (new HtmlRenderer())->parse($data)->toString();
// This is important; if you want the client to see a correctly rendered output,
// You need to format it, so the result is an escaped ANSI sequence
$response = $this->output->getFormatter()->format($response);
$server->send($fd, $response . PHP_EOL);
$server->close($fd);
});
$server->on('Close', function (Server $server, int $fd) {
$this->log($server->getClientInfo($fd), false);
});
// This will start the TCP server
$server->start();
return Command::SUCCESS;
}
/**
* @param array<string>|bool $infos
*/
private function log(array|bool $infos, bool $connected): void
{
if (is_array($infos)) {
/** @var \Illuminate\Contracts\Support\Renderable $render */
$render = view('log', [
'date' => date('Y-m-d'),
'hour' => date('H:i:s'),
'client' => $infos['remote_ip'],
'connected' => $connected,
]);
render($render->render());
}
}
}
您可以在这里找到服务器对象上的所有方法。
我想引起您注意的一件事是接收事件。在这个例子中,我们正在返回客户端发送的内容。在那里实现逻辑是自由的;例如,在我的情况下,我会将提交的标志与某些标准进行验证。此外,请注意,为了以您预期的格式打印输出,您需要获取控制台命令背后使用的格式化程序并发送其结果,它是一个ANSI序列。这样,它就能在客户端的终端上正确渲染。
现在,我们可以通过执行以下命令来运行我们的服务器
php securinets serve
您将收到提示
我是否是唯一一个对这件事感到兴奋的人,我的意思是这有多酷?
还有其他什么吗?
现在我们有了我们的控制台应用程序,我们想要确保这个命令始终运行。由于一个或多个原因,它可能会崩溃并停止。当这种情况发生时,我们希望能够立即重新启动它。这就是为什么我会使用Supervisor来做这件事。
对于Linux用户,您可以使用以下命令
sudo apt update && sudo apt install supervisor
现在,创建一个配置文件
sudo nano /etc/supervisor/conf.d/securinets.conf
粘贴以下配置
[program:securinets]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php8.2 /path/to/console/application/securinets serve
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=always-a-low-privileged-user
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/securinets.log
stopwaitsecs=3600
了解更多关于配置的信息在这里。
通过运行以下命令启动Supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start securinets:*
现在,您的TCP服务器正在运行。如果您想运行多个实例,将numprocs
设置为5,例如。在您的应用程序中,检查端口是否被占用,并移动到下一个端口。这样,如果您从端口8000
开始,您将有5个TCP服务器,端口号从8000
到8004
。
这就完成了!要更有趣,您可以将Nginx配置为反向代理以添加速率限制。虽然我们将在本文中不涉及这一点,但文档将是您最好的朋友。
结论
Laravel Zero是一个强大的包,用于启动控制台应用程序。我们只是触及了皮毛,因为它提供了更多。您可以构建交互式菜单,安排任务,发送桌面通知,使用内置的HTTP客户端消耗API,缓存数据,甚至可以将应用程序构建为独立的可执行文件。所以,下次您在处理这样的应用程序时,考虑使用Laravel Zero,它可能正是您所需要的!✨
driesvints, mzotelli, 0akiev0 点赞了这篇文章