| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- <?php
- /**
- * ngrok.cc 内网穿透服务 PHP 版
- *
- * 本程序仅适用于ngrok.cc 使用前请先在 https://ngrok.cc 注册账号.
- * 机器只要装有php(无须web服务)即可运行本程序,可用于路由器等OpenWRT操作系统
- * 命令行模式执行 php sunny.php --authtoken=xxxxxx 即可运行
- *
- * 感谢 dosgo 提供的 ngrok-php 原版程序
- *
- */
- ConsoleOut("欢迎使用内网穿透 sunny-php v1.38\r\nCtrl+C 退出");
- set_time_limit(0);//设置执行时间
- ignore_user_abort(true);
- error_reporting(E_ALL^E_NOTICE^E_WARNING);
- //检测大小端
- define('BIG_ENDIAN', pack('L', 1) === pack('N', 1));
- // 获取传入的参数
- $options = getopt("",
- array(
- "clientid::",
- )
- );
- //
- if ($options['clientid'] == '') {
- ConsoleOut("\r\n使用说明\r\n在命令行模式运行\r\nphp sunny.php --clientid=xxxxxx\r\n如果是多个隧道换成 php sunny.php --clientid=xxxxxx,xxxxxx \r\n请登录 ngrok.cc获取clientid\r\n");
- ConsoleOut("\r\n请输入隧道id:");
- $clientid = trim(fgets(fopen('php://stdin', 'r')));
- if ($clientid == "") {
- sleep(10);
- die();
- }
- } else {
- $clientid = $options['clientid'];
- }
- // 隧道数组
- $Tunnels = array();
- // 根据隧道id请求接口获取隧道信息
- $serverArr = ngrok_auth($clientid);
- $seraddr = $serverArr[0];//服务器地址
- $port = $serverArr[1];//端口
- $is_verify_peer = false;//是否校验证书
- $isDebug = false;//调试开关
- //定义变量
- $readfds = array();
- $writefds = array();
- $e = null;
- $t = 1;
- $socklist = array();
- $ClientId = '';
- $recvflag = true;
- $starttime = time();//启动时间
- $pingtime = 0;
- //建立隧道协议
- $mainsocket = connectremote($seraddr, $port);
- if ($mainsocket) {
- $socklist[] = array('sock' => $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 = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Web服务错误</title><meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><style>html,body{height:100%%}body{margin:0;padding:0;width:100%%;display:table;font-weight:100;font-family:"Microsoft YaHei",Arial,Helvetica,sans-serif}.container{text-align:center;display:table-cell;vertical-align:middle}.content{border:1px solid #ebccd1;text-align:center;display:inline-block;background-color:#f2dede;color:#a94442;padding:30px}.title{font-size:18px}.copyright{margin-top:30px;text-align:right;color:#000}</style></head><body><div class="container"><div class="content"><div class="title">隧道 %s 无效<br>无法连接到<strong>%s</strong>. 此端口尚未提供Web服务</div></div></div></body></html>';
- $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."<br/>";
- 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);
- }
- ?>
|