$mainsocket, 'linkstate' => 0, 'type' => 1);
}
//注册退出执行函数
register_shutdown_function('shutdown');
while ($recvflag) {
//重排
array_filter($socklist);
sort($socklist);
//检测控制连接是否连接.
if ($mainsocket == false) {
$ip = dnsopen($seraddr);//解析dns
if (!$ip) {
ConsoleOut('连接ngrok服务器失败.');
sleep(1);
continue;
}
$mainsocket = connectremote($ip, $port);
if (!$mainsocket) {
ConsoleOut('连接ngrok服务器失败.');
sleep(10);
continue;
}
$socklist[] = array('sock' => $mainsocket, 'linkstate' => 0, 'type' => 1);
}
//如果非cli超过1小时自杀
if (is_cli() == false) {
if ($starttime+3600 < time()) {
fclose($mainsocket);
$recvflag = false;
break;
}
}
//发送心跳
if ($pingtime+25 < time() && $pingtime != 0) {
sendpack($mainsocket, Ping());
$pingtime = time();
}
//重新赋值
$readfds = array();
$writefds = array();
foreach ($socklist as $k => $z) {
if (is_resource($z['sock'])) {
$readfds[] = $z['sock'];
if ($z['linkstate'] == 0) {
$writefds[] = $z['sock'];
}
} else {
//close的时候不是资源。。移除
if ($z['type'] == 1) {
$mainsocket = false;
}
array_splice($socklist, $k, 1);
}
}
//查询
$res = stream_select($readfds, $writefds, $e, $t);
if ($res === false) {
ConsoleOut('sockerr', 'debug');
}
//有事件
if ($res > 0) {
foreach ($socklist as $k => $sockinfo) {
$sock = $sockinfo['sock'];
//可读
if (in_array($sock, $readfds)) {
$recvbut = fread($sock, 1024);
if ($recvbut == false || strlen($recvbut) == 0) {
//主连接关闭,关闭所有
if ($sockinfo['type'] == 1) {
$mainsocket = false;
}
if ($sockinfo['type'] == 3) {
fclose($sockinfo['tosock']);
}
unset($socklist[$k]);
continue;
}
if (strlen($recvbut) > 0) {
if (!isset($sockinfo['recvbuf'])) {
$sockinfo['recvbuf'] = $recvbut;
} else {
$sockinfo['recvbuf'] = $sockinfo['recvbuf'].$recvbut;
}
$socklist[$k] = $sockinfo;
}
//控制连接,或者远程未连接本地连接
if ($sockinfo['type'] == 1 || ($sockinfo['type'] == 2 && $sockinfo['linkstate'] == 1)) {
$allrecvbut = $sockinfo['recvbuf'];
//处理
$lenbuf = substr($allrecvbut, 0, 8);
$len = tolen1($lenbuf);
if (strlen($allrecvbut) >= (8+$len)) {
$json = substr($allrecvbut, 8, $len);
ConsoleOut($json, 'debug');
$js = json_decode($json, true);
//远程主连接
if ($sockinfo['type'] == 1) {
if ($js['Type'] == 'ReqProxy') {
$newsock = connectremote($seraddr, $port);
if ($newsock) {
$socklist[] = array('sock' => $newsock, 'linkstate' => 0, 'type' => 2);
}
}
if ($js['Type'] == 'AuthResp') {
$ClientId = $js['Payload']['ClientId'];
$pingtime = time();
sendpack($sock, Ping());
foreach ($Tunnels as $tunnelinfo) {
//注册端口
sendpack($sock, ReqTunnel($tunnelinfo['protocol'], $tunnelinfo['hostname'], $tunnelinfo['subdomain'], $tunnelinfo['httpauth'], $tunnelinfo['rport']));
}
}
if ($js['Type'] == 'NewTunnel') {
if ($js['Payload']['Error'] != null) {
ConsoleOut('隧道建立失败:'.$js['Payload']['Error']);
sleep(30);
} else {
ConsoleOut('隧道建立成功:'.$js['Payload']['Url']);
}
}
}
//远程代理连接
if ($sockinfo['type'] == 2) {
//未连接本地
if ($sockinfo['linkstate'] == 1) {
if ($js['Type'] == 'StartProxy') {
$loacladdr = getloacladdr($Tunnels, $js['Payload']['Url']);
$newsock = connectlocal($loacladdr['lhost'], $loacladdr['lport']);
if ($newsock) {
$socklist[] = array('sock' => $newsock, 'linkstate' => 0, 'type' => 3, 'tosock' => $sock);
//把本地连接覆盖上去
$sockinfo['tosock'] = $newsock;
$sockinfo['linkstate'] = 2;
} else {
$body = '
Web服务错误隧道 %s 无效
无法连接到%s. 此端口尚未提供Web服务
';
$html = sprintf($body, $js['Payload']['Url'], $loacladdr['lhost'].':'.$loacladdr['lport']);
$header = "HTTP/1.0 502 Bad Gateway"."\r\n";
$header .= "Content-Type: text/html"."\r\n";
$header .= "Content-Length: %d"."\r\n";
$header .= "\r\n"."%s";
$buf = sprintf($header, strlen($html), $html);
sendbuf($sock, $buf);
}
}
}
}
//edit buffer
if (strlen($allrecvbut) == (8+$len)) {
$sockinfo['recvbuf'] = '';
} else {
$sockinfo['recvbuf'] = substr($allrecvbut, 8+$len);
}
$socklist[$k] = $sockinfo;
}
}
//远程连接已连接本地跟本地连接,纯转发
if ($sockinfo['type'] == 3 || ($sockinfo['type'] == 2 && $sockinfo['linkstate'] == 2)) {
sendbuf($sockinfo['tosock'], $sockinfo['recvbuf']);
$sockinfo['recvbuf'] = '';
$socklist[$k] = $sockinfo;
}
}
//可写
if (in_array($sock, $writefds)) {
if ($sockinfo['linkstate'] == 0) {
if ($sockinfo['type'] == 1) {
sendpack($sock, NgrokAuth(), false);
$sockinfo['linkstate'] = 1;
$socklist[$k] = $sockinfo;
}
if ($sockinfo['type'] == 2) {
sendpack($sock, RegProxy($ClientId), false);
$sockinfo['linkstate'] = 1;
$socklist[$k] = $sockinfo;
}
if ($sockinfo['type'] == 3) {
$sockinfo['linkstate'] = 1;
$socklist[$k] = $sockinfo;
}
}
}
}
}
}
/* 域名解析 */
function dnsopen($host) {
$ip = gethostbyname($host);//解析dns
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
return $ip;
}
/* 连接到远程 */
function connectremote($seraddr, $port) {
global $is_verify_peer;
// 连接获取socket资源
$socket = stream_socket_client('tcp://'.$seraddr.':'.$port, $errno, $errstr, 30);
if (!$socket) {
return false;
}
//设置加密连接,默认是ssl,如果需要tls连接,可以查看php手册stream_socket_enable_crypto函数的解释
if ($is_verify_peer == false) {
stream_context_set_option($socket, 'ssl', 'verify_host', false);
stream_context_set_option($socket, 'ssl', 'verify_peer_name', false);
stream_context_set_option($socket, 'ssl', 'verify_peer', false);
stream_context_set_option($socket, 'ssl', 'allow_self_signed', false);
}
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
stream_set_blocking($socket, 0);//设置为非阻塞模式
return $socket;
}
/* 连接到本地 */
function connectlocal($localaddr, $localport) {
$socket = stream_socket_client('tcp://'.$localaddr.':'.$localport, $errno, $errstr, 30);
if (!$socket) {
return false;
}
stream_set_blocking($socket, 0);//设置为非阻塞模式
return $socket;
}
function getloacladdr($Tunnels, $url) {
$protocol = substr($url, 0, strpos($url, ':'));
$hostname = substr($url, strpos($url, '//')+2);
$subdomain = trim(substr($hostname, 0, strpos($hostname, '.')));
$rport = substr($url, strrpos($url, ':')+1);
// echo 'protocol:'.$protocol."\r\n";
// echo '$subdomain:'.$subdomain."\r\n";
// echo '$hostname:'.$hostname."\r\n";
// echo '$rport:'.$rport."\r\n";
foreach ($Tunnels as $k => $z) {
//
if ($protocol == $z['protocol']) {
if ($hostname == $z['hostname']) {
return $z;
}
if ($subdomain == $z['subdomain']) {
return $z;
}
}
if ($protocol == 'tcp') {
if ($rport == $z['rport']) {
return $z;
}
}
}
// array('protocol'=>$protocol,'hostname'=>'','subdomain'=>'','rport'=>0,'lhost'=>'','lport'=>80),
}
function NgrokAuth() {
$Payload = array(
'ClientId' => '',
'OS' => 'darwin',
'Arch' => 'amd64',
'Version' => '2',
'MmVersion' => '2.1',
'User' => 'user',
'Password' => '',
);
$json = array(
'Type' => 'Auth',
'Payload' => $Payload,
);
return json_encode($json);
}
function ReqTunnel($protocol, $HostName, $Subdomain, $HttpAuth, $RemotePort) {
$Payload = array(
'ReqId' => getRandChar(8),
'Protocol' => $protocol,
'Hostname' => $HostName,
'Subdomain' => $Subdomain,
'HttpAuth' => $HttpAuth,
'RemotePort' => $RemotePort,
);
$json = array(
'Type' => 'ReqTunnel',
'Payload' => $Payload,
);
return json_encode($json);
}
function RegProxy($ClientId) {
$Payload = array('ClientId' => $ClientId);
$json = array(
'Type' => 'RegProxy',
'Payload' => $Payload,
);
return json_encode($json);
}
function Ping() {
$Payload = (object) array();
$json = array(
'Type' => 'Ping',
'Payload' => $Payload,
);
return json_encode($json);
}
/* 网络字节序 (只支持整型范围) */
function lentobyte($len) {
$xx = pack("N", $len);
$xx1 = pack("C4", 0, 0, 0, 0);
return $xx1.$xx;
}
/* 机器字节序 (小端 只支持整型范围) */
function lentobyte1($len) {
$xx = pack("L", $len);
$xx1 = pack("C4", 0, 0, 0, 0);
return $xx.$xx1;
}
function sendpack($sock, $msg, $isblock = true) {
if ($isblock) {
stream_set_blocking($sock, 1);//设置为非阻塞模式
}
fwrite($sock, lentobyte1(strlen($msg)).$msg);
if ($isblock) {
stream_set_blocking($sock, 0);//设置为非阻塞模式
}
}
function sendbuf($sock, $buf, $isblock = true) {
if ($isblock) {
stream_set_blocking($sock, 1);//设置为非阻塞模式
}
fwrite($sock, $buf);
if ($isblock) {
stream_set_blocking($sock, 0);//设置为非阻塞模式
}
}
/* 网络字节序 (只支持整型范围) */
function tolen($v) {
$array = unpack("N", $v);
return $array[1];
}
/* 机器字节序 (小端) 只支持整型范围 */
function tolen1($v) {
$array = unpack("L", $v);
return $array[1];
}
//随机生成字符串
function getRandChar($length) {
$str = null;
$strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($strPol)-1;
for ($i = 0; $i < $length; $i++) {
$str .= $strPol[rand(0, $max)];
}
return $str;
}
//输出日记到命令行
function ConsoleOut($log, $level = 'info') {
global $isDebug;
if ($level == 'debug' and $isDebug == false) {
return;
}
//cli
if (is_cli()) {
if (DIRECTORY_SEPARATOR == "\\") {
$log = iconv('UTF-8', 'GB2312', $log);
}
echo $log."\r\n";
}
//web
else {
echo $log."
";
ob_flush();
flush();
// file_put_contents("ngrok.log", date("Y-m-d H:i:s:::") . $log . "\r\n", FILE_APPEND);
}
}
//判断是否命令行运行
function is_cli() {
return (php_sapi_name() === 'cli')?true:false;
}
//ngrok.cc 获取服务器设置
function ngrok_auth($clientid) {
global $is_verify_peer;
$host = 'www.ngrok.cc';
$port = 443;
$fp = stream_socket_client('tcp://'.$host.':'.$port, $errno, $errstr, 10);
if (!$fp) {
ConsoleOut('连接认证服务器: https://www.ngrok.cc 错误.');
sleep(10);
exit();
}
// 如果不校验证书把证书校验设置成false
if ($is_verify_peer == false) {
stream_context_set_option($fp, 'ssl', 'verify_host', false);
stream_context_set_option($fp, 'ssl', 'verify_peer_name', false);
stream_context_set_option($fp, 'ssl', 'verify_peer', false);
stream_context_set_option($fp, 'ssl', 'allow_self_signed', false);
}
stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
$header = "GET "."/api/clientid/clientid/%s"." HTTP/1.1"."\r\n";
$header .= "Host: %s"."\r\n";
$header .= "\r\n";
$buf = sprintf($header, $clientid, $host);
$write = fputs($fp, $buf);
$body = null;
while (!feof($fp)) {
$line = fgets($fp, 1024);//去除请求包的头只显示页面的返回数据
if ($line == "\n" || $line == "\r\n") {
$chunk_size = (integer) hexdec(fgets($fp, 1024));
if ($chunk_size > 0) {
$body = fread($fp, $chunk_size);
break;
}
}
}
fclose($fp);
$authData = json_decode($body, true);
if ($authData['status'] != 200) {
ConsoleOut('认证错误:'.$authData['msg'].' ErrorCode:'.$authData['status']);
sleep(10);
exit();
}
ConsoleOut('认证成功,正在连接服务器...');
//设置映射隧道,支持多渠道[客户端id]
ngrok_adds($authData['data']);
$proto = explode(':', $authData['server']);
return $proto;
}
//ngrok.cc 添加到渠道队列
function ngrok_adds($Tunnel) {
global $Tunnels;
foreach ($Tunnel as $tunnelinfo) {
if (isset($tunnelinfo['proto']['http'])) {
$protocol = 'http';
}
if (isset($tunnelinfo['proto']['https'])) {
$protocol = 'https';
}
if (isset($tunnelinfo['proto']['tcp'])) {
$protocol = 'tcp';
}
$proto = explode(':', $tunnelinfo['proto'][$protocol]);//127.0.0.1:80 拆分成数组
if ($proto[0] == '') {
$proto[0] = '127.0.0.1';
}
if ($proto[1] == '' || $proto[1] == 0) {
$proto[1] = 80;
}
$Tunnels[] = array(
'protocol' => $protocol,
'hostname' => $tunnelinfo['hostname'],
'subdomain' => $tunnelinfo['subdomain'],
'httpauth' => $tunnelinfo['httpauth'],
'rport' => $tunnelinfo['remoteport'],
'lhost' => $proto[0],
'lport' => $proto[1],
);
}
}
//注册退出执行函数
function shutdown() {
global $mainsocket;
sendpack($mainsocket, 'close');
fclose($mainsocket);
}
?>