支持 Laravel.io 的持续发展 →

Laravel Zero - 让我们一起构建一个 TCP 服务器

2024年3月2日 阅读时间:9分钟

你好 👋

几周前,我开始为我的团队“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 类。通过这样做,您可以挂钩到各种事件。我们将使用 StartConnectReceiveClose。您可以在这里找到所有事件。

在底层,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服务器,端口号从80008004

这就完成了!要更有趣,您可以将Nginx配置为反向代理以添加速率限制。虽然我们将在本文中不涉及这一点,但文档将是您最好的朋友。

结论

Laravel Zero是一个强大的包,用于启动控制台应用程序。我们只是触及了皮毛,因为它提供了更多。您可以构建交互式菜单,安排任务,发送桌面通知,使用内置的HTTP客户端消耗API,缓存数据,甚至可以将应用程序构建为独立的可执行文件。所以,下次您在处理这样的应用程序时,考虑使用Laravel Zero,它可能正是您所需要的!✨

最后更新于4个月前。

driesvints, mzotelli, 0akiev0 点赞了这篇文章

3
喜欢这篇文章吗?告诉作者并对他们给予支持!
oussamamater (Oussama Mater) 我是一名软件工程师和CTF玩家。我使用Laravel和Vue.js将想法变为应用程序 🚀

你可能喜欢以下文章

2024年3月11日

如何使用 Larastan 将你的 Laravel 应用从0到9进行构建

在 Laravel 应用执行前发现错误是可能的,多亏了 Larastan,这是...

阅读文章
2024年7月19日

无需 traits 标准化 API 响应

我发现大多数用于 API 响应的库都使用 traits 实现,...

阅读文章
2024年7月17日

在 Laravel 项目中通过 Discord 通知收集反馈

如何在 Laravel 项目中创建反馈模块,并在收到消息时收到 Discord 通知...

阅读文章

我们想感谢这些 惊人的公司 支持

你的标志在这里?

Laravel.io

问题解决、知识分享和社区建设的 Laravel 门户网站。

© 2024 Laravel.io - 版权所有。