前序
最近各种折腾 S@cks5
的代理,有项目使用 s@cks 代理做爬虫,也有使用代理做流量中转,这反倒是让我对这协议产生了兴趣。
曾有过一个念头出现在脑海里,V2r@y
项目既然属于开源作品,那我便可以使用 Golang
对它魔改或者封装,以解决一些特殊场景的需求;不过后来因为一些原因 (懒癌),迟迟未对 Golang
开始学习,也就一直拖着...
最近又想到 Swoole
和 Golang
不也很 “类似” 吗,那我不如干脆先用 Swoole,实现一个简单的 Demo?说干就干,于是乎便有了此篇文章和代码。
过程心得
说实话,敲代码这么久,也是第一次纯手动的使用 TCP,按照规范实现一种标准协议(HTTP 这种就不算哈),有一些特别的心得。 例如:
传输层三元组,客户端、服务端和目标端,IP 和端口需特别留意。
底层数据进制转换,二进制、十进制和十六进制,平常应用层更多注意的是编码。
规范、规范、规范,由为提现了规范的重要性,而不是那么任意所为。
其它的...
使用细节
利用 Swoole 的 TCP 客户端实现,不支持 UDP。
启动之后的代理支持免验证和账号密码验证。
PHP 版本 7.3,Swoole4+,使用的需提前安装。
代码里面写了很详细的注释,欢迎交流学习。
服务端效果:
客户端效果:
实现代码
Ps:这份代码完全是为了写 Demo 而写的 Demo,里面很多代码规范不符合 Swoole
规范,大家不要学我。
服务端:
<?php
use Swoole\Coroutine\Client;
$server = new Swoole\Server('0.0.0.0', 6688);
$server->on('Connect', function ($server, $fd) {
echo "\n\n ----- 连接成功 ----- \n";
$info = $server->getClientInfo($fd);
$remote_ip = $info['remote_ip'];
$remote_port = $info['remote_port'];
echo "TCP成功连接: $remote_ip:$remote_port\n";
});
$pool = [];
$conn = [];
$isAuth = false;
$user = '123234';
$pass = 'abcd1234';
$server->on('Receive', function ($server, $fd, $reactor_id, $raw) {
$info = $server->getClientInfo($fd);
$remote_ip = $info['remote_ip'];
$remote_port = $info['remote_port'];
$src = md5($remote_port . $remote_ip);
echo "[" . date('Y-m-d H:i:s') . "]收到来自客户端:";
$data = bin2hex($raw);
$len = mb_strlen($data);
echo "($len) ";
echo $data;
echo "\n";
global $isAuth;
global $conn;
if ($conn[$src] === true) {
echo " ----- 传输数据 ----- \n";
global $pool;
$client = $pool[$src];
echo $raw;
echo "\n";
$client->send($raw);
echo " ----- 返回数据 ----- \n";
while ($recv = $client->recv()) {
if (!$recv) {
echo $client->errCode;
echo "\n";
$server->close($fd, true);
}
echo $recv;
$server->send($fd, $recv);
}
return;
}
if ($isAuth) {
if ($conn[$src] === null) {
echo " ----- 首次请求 ----- \n";
$ver = mb_substr($data, 0, 2);
$n_mth = mb_substr($data, 2, 2);
$mths = mb_substr($data, 4);
echo "版本:$ver\n";
echo "方法数目:$n_mth\n";
echo "可选方法:$mths\n";
$msg = '0502';
$conn[$src] = 1;
$server->send($fd, hex2bin($msg));
return;
}
if ($conn[$src] === 1) {
echo " ----- 开始验证授权 ----- \n";
$ver = mb_substr($data, 0, 2);
$user_len = hexdec(mb_substr($data, 2, 2)) * 2;
$username = hex2bin(mb_substr($data, 4, $user_len));
$pass_len = hexdec(mb_substr($data, 4 + $user_len, 2)) * 2;
$password = hex2bin(mb_substr($data, 4 + $user_len + 2, $pass_len));
echo "协议版本:$ver \n";
echo "账号长度:$user_len\n";
echo "验证账号:$username \n";
echo "密码长度:$pass_len \n";
echo "验证密码:$password \n";
global $user, $pass;
if ($user == $username && $pass == $password) {
echo "验证结果:账号密码正确\n";
$conn[$src] = 2;
$reply = ['VER' => $ver, 'STATUS' => '00',];
} else {
echo "验证结果:账号密码错误\n";
$conn[$src] = null;
$reply = ['VER' => $ver, 'STATUS' => '01',];
}
$server->send($fd, hex2bin(implode('', $reply)));
return;
}
if ($conn[$src] === 2) {
$ver = mb_substr($data, 0, 2);
$cmd = mb_substr($data, 2, 2);
$rsv = mb_substr($data, 4, 2);
$atyp = mb_substr($data, 6, 2);
$dst_addr = long2ip(hexdec(mb_substr($data, 8, 8)));
$dst_port = hexdec(mb_substr($data, 16));
echo " ----- 建立目标连接 ----- \n";
echo "版本:$ver\n";
echo "命令:$cmd\n";
echo "保留:$rsv\n";
echo "地址类型:$atyp\n";
echo "目标地址:$dst_addr\n";
echo "目标端口:$dst_port\n";
global $pool;
$s = microtime(true);
$client = new Client(SWOOLE_SOCK_TCP);
if (!$client->connect($dst_addr, $dst_port, 60)) {
echo "connect failed. Error: {$client->errCode}\n";
}
$info = $client->getsockname();
$ip = dechex(ip2long($info['address']));
$port = dechex($info['port']);
if (mb_strlen($ip) % 2 != 0) {
$ip = '0' . $ip;
}
if (mb_strlen($port) % 2 != 0) {
$port = '0' . $port;
}
echo "连接耗时:" . round(microtime(true) - $s, 2);
echo "\n";
$pool[$src] = $client;
$reply = [
'VER' => '05',
'REP' => '00',
'RSV' => '00',
'ATYP' => '01',
'BND.ADDR' => $ip,
'BND.PORT' => $port,
];
$conn[$src] = true;
$server->send($fd, hex2bin(implode('', $reply)));
return;
}
return;
}
if ($conn[$src] === null) {
echo " ----- 首次请求 ----- \n";
$msg = '0500';
$server->send($fd, hex2bin($msg));
$conn[$src] = 1;
return;
}
if ($conn[$src] === 1) {
$ver = mb_substr($data, 0, 2);
$cmd = mb_substr($data, 2, 2);
$rsv = mb_substr($data, 4, 2);
$atyp = mb_substr($data, 6, 2);
$dst_addr = long2ip(hexdec(mb_substr($data, 8, 8)));
$dst_port = hexdec(mb_substr($data, 16));
echo " ----- 建立目标连接 ----- \n";
echo "版本:$ver\n";
echo "命令:$cmd\n";
echo "保留:$rsv\n";
echo "地址类型:$atyp\n";
echo "目标地址:$dst_addr\n";
echo "目标端口:$dst_port\n";
global $pool;
$s = microtime(true);
$client = new Client(SWOOLE_SOCK_TCP);
if (!$client->connect($dst_addr, $dst_port, 60)) {
echo "connect failed. Error: {$client->errCode}\n";
}
$info = $client->getsockname();
$ip = dechex(ip2long($info['address']));
$port = dechex($info['port']);
if (mb_strlen($ip) % 2 != 0) {
$ip = '0' . $ip;
}
if (mb_strlen($port) % 2 != 0) {
$port = '0' . $port;
}
echo "连接耗时:" . round(microtime(true) - $s, 2);
echo "\n";
$pool[$src] = $client;
$reply = [
'VER' => '05',
'REP' => '00',
'RSV' => '00',
'ATYP' => '01',
'BND.ADDR' => $ip,
'BND.PORT' => $port,
];
$conn[$src] = true;
$server->send($fd, hex2bin(implode('', $reply)));
return;
}
});
$server->on('Close', function ($server, $fd) {
$info = $server->getClientInfo($fd);
$remote_ip = $info['remote_ip'];
$remote_port = $info['remote_port'];
$src = md5($remote_port . $remote_ip);
global $pool;
if ($client = $pool[$src]) {
$client->close();
unset($pool[$src]);
}
echo "TCP成功断开: $remote_ip:$remote_port\n";
echo " ----- 连接断开 ----- \n\n";
});
$server->start();
客户端:
此部分是次日追加的,仅实现了非授权的客户端请求实例,演示的是:使用 Socks5 代理 请求 myip.ipip.net
的过程。
<?php
use Swoole\Coroutine\Client;
use function Swoole\Coroutine\run;
run(function () {
$client = new Client(SWOOLE_SOCK_TCP);
$socks_ip = '127.0.0.1';
$socks_port = 1080;
if (!$client->connect($socks_ip, $socks_port, 60)) {
echo "Socks5连接失败:{$client->errCode}\n";
return;
}
echo "\n ----- 首次请求 ----- \n";
$msg = [
'VER' => '05',
'NMETHODS' => '01',
'METHODS' => '00',
];
$client->send(hex2bin(implode('', $msg)));
$res = bin2hex($client->recv());
echo "收到响应:$res \n";
echo "\n ----- 建立连接 ----- \n";
$dst_port = dechex(80);
$msg = [
'ver' => '05',
'cmd' => '01',
'rsv' => '00',
'type' => '01',
'dst_ip' => dechex(ip2long(gethostbyname('myip.ipip.net'))),
'dst_port' => str_pad($dst_port, 4, '0', STR_PAD_LEFT)
];
$client->send(hex2bin(implode('', $msg)));
$res = bin2hex($client->recv());
echo "收到响应:$res \n";
$ver = mb_substr($res, 0, 2);
$cmd = mb_substr($res, 2, 2);
$type = mb_substr($res, 6, 2);
$ip = long2ip(hexdec(mb_substr($res, 8, -4)));
$port = hexdec(mb_substr($res, -4));
echo "协议版本:$ver\n";
echo "响应命令:$cmd\n";
echo "地址类型:$type\n";
echo "IP 地址:$ip\n";
echo "IP 端口:$port\n";
if ($cmd !== '00') {
echo "代理服务器连接目标服务器失败\n";
return;
}
echo "代理服务器连接目标服务器成功\n";
echo "\n ----- 发送数据 ----- \n";
$msg = "GET / HTTP/1.1\r\n";
$msg .= "Host: myip.ipip.net\r\n";
$msg .= "Accept: */*\r\n";
$msg .=
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36\r\n";
$msg .= "Accept-Encoding: gzip, deflate\r\n";
$msg .= "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n\r\n";
echo $msg;
$client->send($msg);
$isRecvHead = false;
echo "收到响应:\n";
while (true) {
$res = $client->recv();
if (mb_strlen($res) > 0) {
if ($isRecvHead === false && mb_strpos($res, "\r\n\r\n") !== false) {
[$head, $content] = explode("\r\n\r\n", $res);
$isRecvHead = true;
echo $head;
echo "\r\n\r\n";
echo $content;
preg_match("/Content-Length: (\d+?)\r\n/", $head, $match);
if ($match) {
$len = $match[1];
$hex = bin2hex($content);
if (mb_strlen($hex) == 2 * $len) {
$client->close();
break;
} else {
continue;
}
}
}
echo $res;
} else {
$client->close();
var_dump($res);
var_dump($client->errCode);
break;
}
}
});
推荐本站淘宝优惠价购买喜欢的宝贝:
本文链接:https://hqyman.cn/post/3777.html 非本站原创文章欢迎转载,原创文章需保留本站地址!
打赏微信支付宝扫一扫,打赏作者吧~
休息一下~~