$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); } ?>