PHP 实现简单的 Socks5 Server

Stella981
• 阅读 752

利用 Phalcon7 的异步功能实现,完整源码 https://github.com/dreamsxin/cphalcon7/blob/master/examples/async/Socks5Server.php。

<?php

class Pool
{
    private $channel;
    private $concurrency;
    private $count = 0;
    private $context;

    public function __construct(int $concurrency = 1, int $capacity = 0)
    {
        $this->concurrency = max(1, $concurrency);
        $this->channel = new \Phalcon\Async\Channel($capacity);
        $this->context = \Phalcon\Async\Context::background();
    }

    public function close(?\Throwable $e = null): void
    {
        $this->count = \PHP_INT_MAX;
        $this->channel->close($e);
    }

    public function submit(callable $work, $socket, ...$args): \Phalcon\Async\Awaitable
    {
        if ($this->count < $this->concurrency) {
            $this->count++;
            Socks5Server::info('Pool count '.$this->count);
            \Phalcon\Async\Task::asyncWithContext($this->context, static function (iterable $it) {
                try {
                    foreach ($it as list ($defer, $context, $work, $socket, $args)) {
                        try {
                            $defer->resolve($context->run($work, $socket, ...$args));
                        } catch (\Throwable $e) {
                            Socks5Server::err($e->getMessage());
                            $defer->fail($e);
                        } finally {
                        }
                    }
                } catch (\Throwable $e) {
                    Socks5Server::err($e->getMessage());
                } finally {
                    --$this->count;
                }
            }, $this->channel->getIterator());
        }

        $this->channel->send([
            $defer = new \Phalcon\Async\Deferred(),
            \Phalcon\Async\Context::current(),
            $work,
            $socket,
            $args
        ]);

        return $defer->awaitable();
    }
}

class Socks5Server
{
    static public $debug = false;
    private $server;

    private $host;
    private $port;

    private $clients;

    public function __construct($host, $port, callable $callback = NULL, int $concurrency = 1, int $capacity = 0)
    {
        $this->host = $host;
        $this->port = $port;
        $this->callback = $callback;
        $this->pool = new Pool($concurrency, $capacity);
    }

    public function start()
    {
        $callback = $this->callback;
        $ws = $this;
        $worker = static function ($socket) use ($ws, $callback) {
            $socket->status = Socks5::STATUS_INIT;
            $socket->is_closing = false;
            try {
                $buffer = '';
                while (!$socket->is_closing && null !== ($chunk = $socket->read())) {

                    $buffer .= $chunk;
                    switch ($socket->status) {
                        case Socks5::STATUS_INIT:
                            /**
                             * 协商版本以及验证方式
                             * +---------+-----------------+------------------+
                             * |协议版本 |支持的验证式数量 |验证方式          |
                             * +---------+-----------------+------------------+
                             * |1个字节  |1个字节          |1种式占一个字节   |
                             * +---------+-----------------+------------------+
                             * |0x05     |0x02             |0x00,0x02         |
                             * +---------+-----------------+------------------+
                             */
                            /**
                             * 0x00 无验证需求
                             * 0x01 通用安全服务应用程序接口(GSSAPI)
                             * 0x02 用户名/密码(USERNAME/PASSWORD)
                             * 0x03 至 X’7F’ IANA 分配(IANA ASSIGNED)
                             * 0x80 至 X’FE’ 私人方法保留(RESERVED FOR PRIVATE METHODS)
                             * 0xFF 无可接受方法(NO ACCEPTABLE METHODS)
                             */
                            $authtypes = Socks5::parseAuth($buffer);
                            if ($authtypes === false) {
                                $socket->is_closing = true;
                                break;
                            }
                            if ($authtypes === true)  { // continue
                                break;
                            }
                            $socket->status = Socks5::STATUS_ADDR;
                            $socket->write("\x05\x00"); // TODO: 暂时
                            $buffer = '';
                            break;
                        case Socks5::STATUS_AUTH:
                            
                            if ($callback && \is_callable($callback)) {
                                // TODO
                            }
                            break;
                        case Socks5::STATUS_ADDR:
                            self::info('Socks5::STATUS_ADDR');
                            /**
                             * 建立代理连接
                             * +----------+------------+---------+-----------+-----------------------+------------+
                             * |协议版本  |请求的类型  |保留字段 |地址类型   |地址数据               |地址端口    |
                             * +----------+------------+---------+-----------+-----------------------+------------+
                             * |1个字节   |1个字节     |1个字节  |1个字节    |变长                   |2个字节     |
                             * +----------+------------+---------+-----------+-----------------------+------------+
                             * |0x05      |0x01        |0x00     |0x01       |0x0a,0x00,0x01,0x0a    |0x00,0x50   |
                             * +----------+------------+---------+-----------+-----------------------+------------+
                             */
                            /**
                             * 请求类型
                             * CONNECT : 0x01, 建立代理连接
                             * BIND : 0x02,告诉代理服务器监听目标机器的连接,也就是让代理服务器创建socket监听来自目标机器的连接。FTP这类需要服务端主动联接客户端的的应用场景。
                             *     1. 只有在完成了connnect操作之后才能进行bind操作
                             *     2. bind操作之后,代理服务器会有两次响应, 第一次响应是在创建socket监听完成之后,第二次是在目标机器连接到代理服务器上之后。
                             * UDP ASSOCIATE : 0x03, udp 协议请求代理。
                             */
                            if (strlen($buffer) < 2) {
                                break;
                            }
                            $cmd = ord($buffer[1]);
                            if ($cmd != Socks5::CMD_CONNECT) {
                                self::err("Bad command ".$cmd);
                                $socket->is_closing = true;
                                break;
                            }
                            $headers = Socks5::parseAddr($buffer);
                            if (!$headers)  {
                                self::err('Error header');
                                $socket->is_closing = true;
                                break;
                            }
                            if ($headers === true)  { // continue
                                break;
                            }
                            /**
                             * 数据包转发
                             * +----+------+------+----------+----------+----------+
                             * |RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
                             * +----+------+------+----------+----------+----------+
                             * | 2  |  1   |  1   | Variable |    2     | Variable |
                             * +----+------+------+----------+----------+----------+
                             */
                            $buffer = substr($buffer, $headers[3]);
                            $socket->status  = Socks5::STATUS_CONNECTING;
                            if (!in_array($headers[0], [Socks5::TYPE_IPV4, Socks5::TYPE_HOST, Socks5::TYPE_IPV6])) {
                                self::err('Addr type error');
                                $socket->is_closing = true;
                                break;
                            }
                            $tls = NULL;
                            if ($headers[2] == 443) {
                                $tls = new \Phalcon\Async\Network\TlsClientEncryption();
                                $tls = $tls->withAllowSelfSigned(true);
                            }
                            $socket->client = \Phalcon\Async\Network\TcpSocket::connect($headers[1], $headers[2], $tls);
                            $socket->write(Socks5::REPLY_ADDR);
                            $socket->status  = Socks5::STATUS_STREAM;
    
                            \Phalcon\Async\Task::async(function ($client) use ($socket) {
                                while (!$socket->is_closing && null !== ($chunk = $client->read())) {
                                    $socket->write($chunk);
                                }
                            }, $socket->client);
                            break;
                        case Socks5::STATUS_STREAM:
                            self::info('Socks5::STATUS_STREAM');
                            $socket->client->write($buffer);
                            $buffer = '';
                            break;
                    }
                }
            } catch (\Throwable $e) {
                self::err($e->getMessage());
            } finally {
                $socket->close();
            }
        };

        try {
            $this->server = \Phalcon\Async\Network\TcpServer::listen($this->host, $this->port);
            echo Phalcon\Cli\Color::info('start server listen:'.$this->host.':'.$this->port).PHP_EOL;
            while (true) {
                $socket = $this->server->accept();
                if ($socket === false) {
                    continue;
                }
                $this->pool->submit($worker, $socket);
            }
        } catch (\Throwable $e) {
            self::err($e->getMessage());
        } finally {
            if ($this->server) {
                $this->server->close();
            }
        }

    }

    static public function info($message)
    {
        if (self::$debug) {
            echo Phalcon\Cli\Color::info($message).PHP_EOL;
        }
    }

    static public function err($message)
    {
        echo Phalcon\Cli\Color::error($message).PHP_EOL;
    }
}



$opts = new \Phalcon\Cli\Options('Websocket CLI');
$opts->add([
    'type' => \Phalcon\Cli\Options::TYPE_STRING,
    'name' => 'server',
    'shortName' => 's',
    'required' => false, // 可选,需要用=号赋值
    'help' => "address"
]);
$opts->add([
    'type' => \Phalcon\Cli\Options::TYPE_INT,
    'name' => 'port',
    'shortName' => 'p',
    'required' => false,
    'help' => "port"
]);
$opts->add([
    'type' => \Phalcon\Cli\Options::TYPE_BOOLEAN,
    'name' => 'concurrency',
    'shortName' => 'c',
    'required' => false
]);
$opts->add([
    'type' => \Phalcon\Cli\Options::TYPE_BOOLEAN,
    'name' => 'capacity',
    'shortName' => 'C',
    'required' => false
]);
$opts->add([
    'type' => \Phalcon\Cli\Options::TYPE_BOOLEAN,
    'name' => 'debug',
    'shortName' => 'v',
    'required' => false,
    'help' => "enable debug"
]);
$vals = $opts->parse();
if ($vals === false ) {
    exit;
}
/**
 * 运行 php websocket-server.php
 */
if (isset($vals['debug'])) {
    Socks5Server::$debug = true;
    echo Phalcon\Cli\Color::info('Use debug mode').PHP_EOL;
}
$sserver = new Socks5Server(\Phalcon\Arr::get($vals, 'server', '0.0.0.0'), \Phalcon\Arr::get($vals, 'port', 10002), function($socket, $status, $data) {
    // TODO
}, \Phalcon\Arr::get($vals, 'concurrency', 500), \Phalcon\Arr::get($vals, 'capacity', 1));
$sserver->start();
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解2016年09月02日00:00:36 \牧野(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fme.csdn.net%2Fdcrmg) 阅读数:59593
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Google地球出现“无法连接到登录服务器(错误代码:c00a0194)”解决方法
Google地球出现“无法连接到登录服务器(错误代码:c00a0194)”解决方法参考文章:(1)Google地球出现“无法连接到登录服务器(错误代码:c00a0194)”解决方法(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.codeprj.com%2Fblo
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这