sunny.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <?php
  2. /**
  3. * ngrok.cc 内网穿透服务 PHP 版
  4. *
  5. * 本程序仅适用于ngrok.cc 使用前请先在 https://ngrok.cc 注册账号.
  6. * 机器只要装有php(无须web服务)即可运行本程序,可用于路由器等OpenWRT操作系统
  7. * 命令行模式执行 php sunny.php --authtoken=xxxxxx 即可运行
  8. *
  9. * 感谢 dosgo 提供的 ngrok-php 原版程序
  10. *
  11. */
  12. ConsoleOut("欢迎使用内网穿透 sunny-php v1.38\r\nCtrl+C 退出");
  13. set_time_limit(0);//设置执行时间
  14. ignore_user_abort(true);
  15. error_reporting(E_ALL^E_NOTICE^E_WARNING);
  16. //检测大小端
  17. define('BIG_ENDIAN', pack('L', 1) === pack('N', 1));
  18. // 获取传入的参数
  19. $options = getopt("",
  20. array(
  21. "clientid::",
  22. )
  23. );
  24. //
  25. if ($options['clientid'] == '') {
  26. 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");
  27. ConsoleOut("\r\n请输入隧道id:");
  28. $clientid = trim(fgets(fopen('php://stdin', 'r')));
  29. if ($clientid == "") {
  30. sleep(10);
  31. die();
  32. }
  33. } else {
  34. $clientid = $options['clientid'];
  35. }
  36. // 隧道数组
  37. $Tunnels = array();
  38. // 根据隧道id请求接口获取隧道信息
  39. $serverArr = ngrok_auth($clientid);
  40. $seraddr = $serverArr[0];//服务器地址
  41. $port = $serverArr[1];//端口
  42. $is_verify_peer = false;//是否校验证书
  43. $isDebug = false;//调试开关
  44. //定义变量
  45. $readfds = array();
  46. $writefds = array();
  47. $e = null;
  48. $t = 1;
  49. $socklist = array();
  50. $ClientId = '';
  51. $recvflag = true;
  52. $starttime = time();//启动时间
  53. $pingtime = 0;
  54. //建立隧道协议
  55. $mainsocket = connectremote($seraddr, $port);
  56. if ($mainsocket) {
  57. $socklist[] = array('sock' => $mainsocket, 'linkstate' => 0, 'type' => 1);
  58. }
  59. //注册退出执行函数
  60. register_shutdown_function('shutdown');
  61. while ($recvflag) {
  62. //重排
  63. array_filter($socklist);
  64. sort($socklist);
  65. //检测控制连接是否连接.
  66. if ($mainsocket == false) {
  67. $ip = dnsopen($seraddr);//解析dns
  68. if (!$ip) {
  69. ConsoleOut('连接ngrok服务器失败.');
  70. sleep(1);
  71. continue;
  72. }
  73. $mainsocket = connectremote($ip, $port);
  74. if (!$mainsocket) {
  75. ConsoleOut('连接ngrok服务器失败.');
  76. sleep(10);
  77. continue;
  78. }
  79. $socklist[] = array('sock' => $mainsocket, 'linkstate' => 0, 'type' => 1);
  80. }
  81. //如果非cli超过1小时自杀
  82. if (is_cli() == false) {
  83. if ($starttime+3600 < time()) {
  84. fclose($mainsocket);
  85. $recvflag = false;
  86. break;
  87. }
  88. }
  89. //发送心跳
  90. if ($pingtime+25 < time() && $pingtime != 0) {
  91. sendpack($mainsocket, Ping());
  92. $pingtime = time();
  93. }
  94. //重新赋值
  95. $readfds = array();
  96. $writefds = array();
  97. foreach ($socklist as $k => $z) {
  98. if (is_resource($z['sock'])) {
  99. $readfds[] = $z['sock'];
  100. if ($z['linkstate'] == 0) {
  101. $writefds[] = $z['sock'];
  102. }
  103. } else {
  104. //close的时候不是资源。。移除
  105. if ($z['type'] == 1) {
  106. $mainsocket = false;
  107. }
  108. array_splice($socklist, $k, 1);
  109. }
  110. }
  111. //查询
  112. $res = stream_select($readfds, $writefds, $e, $t);
  113. if ($res === false) {
  114. ConsoleOut('sockerr', 'debug');
  115. }
  116. //有事件
  117. if ($res > 0) {
  118. foreach ($socklist as $k => $sockinfo) {
  119. $sock = $sockinfo['sock'];
  120. //可读
  121. if (in_array($sock, $readfds)) {
  122. $recvbut = fread($sock, 1024);
  123. if ($recvbut == false || strlen($recvbut) == 0) {
  124. //主连接关闭,关闭所有
  125. if ($sockinfo['type'] == 1) {
  126. $mainsocket = false;
  127. }
  128. if ($sockinfo['type'] == 3) {
  129. fclose($sockinfo['tosock']);
  130. }
  131. unset($socklist[$k]);
  132. continue;
  133. }
  134. if (strlen($recvbut) > 0) {
  135. if (!isset($sockinfo['recvbuf'])) {
  136. $sockinfo['recvbuf'] = $recvbut;
  137. } else {
  138. $sockinfo['recvbuf'] = $sockinfo['recvbuf'].$recvbut;
  139. }
  140. $socklist[$k] = $sockinfo;
  141. }
  142. //控制连接,或者远程未连接本地连接
  143. if ($sockinfo['type'] == 1 || ($sockinfo['type'] == 2 && $sockinfo['linkstate'] == 1)) {
  144. $allrecvbut = $sockinfo['recvbuf'];
  145. //处理
  146. $lenbuf = substr($allrecvbut, 0, 8);
  147. $len = tolen1($lenbuf);
  148. if (strlen($allrecvbut) >= (8+$len)) {
  149. $json = substr($allrecvbut, 8, $len);
  150. ConsoleOut($json, 'debug');
  151. $js = json_decode($json, true);
  152. //远程主连接
  153. if ($sockinfo['type'] == 1) {
  154. if ($js['Type'] == 'ReqProxy') {
  155. $newsock = connectremote($seraddr, $port);
  156. if ($newsock) {
  157. $socklist[] = array('sock' => $newsock, 'linkstate' => 0, 'type' => 2);
  158. }
  159. }
  160. if ($js['Type'] == 'AuthResp') {
  161. $ClientId = $js['Payload']['ClientId'];
  162. $pingtime = time();
  163. sendpack($sock, Ping());
  164. foreach ($Tunnels as $tunnelinfo) {
  165. //注册端口
  166. sendpack($sock, ReqTunnel($tunnelinfo['protocol'], $tunnelinfo['hostname'], $tunnelinfo['subdomain'], $tunnelinfo['httpauth'], $tunnelinfo['rport']));
  167. }
  168. }
  169. if ($js['Type'] == 'NewTunnel') {
  170. if ($js['Payload']['Error'] != null) {
  171. ConsoleOut('隧道建立失败:'.$js['Payload']['Error']);
  172. sleep(30);
  173. } else {
  174. ConsoleOut('隧道建立成功:'.$js['Payload']['Url']);
  175. }
  176. }
  177. }
  178. //远程代理连接
  179. if ($sockinfo['type'] == 2) {
  180. //未连接本地
  181. if ($sockinfo['linkstate'] == 1) {
  182. if ($js['Type'] == 'StartProxy') {
  183. $loacladdr = getloacladdr($Tunnels, $js['Payload']['Url']);
  184. $newsock = connectlocal($loacladdr['lhost'], $loacladdr['lport']);
  185. if ($newsock) {
  186. $socklist[] = array('sock' => $newsock, 'linkstate' => 0, 'type' => 3, 'tosock' => $sock);
  187. //把本地连接覆盖上去
  188. $sockinfo['tosock'] = $newsock;
  189. $sockinfo['linkstate'] = 2;
  190. } else {
  191. $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>';
  192. $html = sprintf($body, $js['Payload']['Url'], $loacladdr['lhost'].':'.$loacladdr['lport']);
  193. $header = "HTTP/1.0 502 Bad Gateway"."\r\n";
  194. $header .= "Content-Type: text/html"."\r\n";
  195. $header .= "Content-Length: %d"."\r\n";
  196. $header .= "\r\n"."%s";
  197. $buf = sprintf($header, strlen($html), $html);
  198. sendbuf($sock, $buf);
  199. }
  200. }
  201. }
  202. }
  203. //edit buffer
  204. if (strlen($allrecvbut) == (8+$len)) {
  205. $sockinfo['recvbuf'] = '';
  206. } else {
  207. $sockinfo['recvbuf'] = substr($allrecvbut, 8+$len);
  208. }
  209. $socklist[$k] = $sockinfo;
  210. }
  211. }
  212. //远程连接已连接本地跟本地连接,纯转发
  213. if ($sockinfo['type'] == 3 || ($sockinfo['type'] == 2 && $sockinfo['linkstate'] == 2)) {
  214. sendbuf($sockinfo['tosock'], $sockinfo['recvbuf']);
  215. $sockinfo['recvbuf'] = '';
  216. $socklist[$k] = $sockinfo;
  217. }
  218. }
  219. //可写
  220. if (in_array($sock, $writefds)) {
  221. if ($sockinfo['linkstate'] == 0) {
  222. if ($sockinfo['type'] == 1) {
  223. sendpack($sock, NgrokAuth(), false);
  224. $sockinfo['linkstate'] = 1;
  225. $socklist[$k] = $sockinfo;
  226. }
  227. if ($sockinfo['type'] == 2) {
  228. sendpack($sock, RegProxy($ClientId), false);
  229. $sockinfo['linkstate'] = 1;
  230. $socklist[$k] = $sockinfo;
  231. }
  232. if ($sockinfo['type'] == 3) {
  233. $sockinfo['linkstate'] = 1;
  234. $socklist[$k] = $sockinfo;
  235. }
  236. }
  237. }
  238. }
  239. }
  240. }
  241. /* 域名解析 */
  242. function dnsopen($host) {
  243. $ip = gethostbyname($host);//解析dns
  244. if (!filter_var($ip, FILTER_VALIDATE_IP)) {
  245. return false;
  246. }
  247. return $ip;
  248. }
  249. /* 连接到远程 */
  250. function connectremote($seraddr, $port) {
  251. global $is_verify_peer;
  252. // 连接获取socket资源
  253. $socket = stream_socket_client('tcp://'.$seraddr.':'.$port, $errno, $errstr, 30);
  254. if (!$socket) {
  255. return false;
  256. }
  257. //设置加密连接,默认是ssl,如果需要tls连接,可以查看php手册stream_socket_enable_crypto函数的解释
  258. if ($is_verify_peer == false) {
  259. stream_context_set_option($socket, 'ssl', 'verify_host', false);
  260. stream_context_set_option($socket, 'ssl', 'verify_peer_name', false);
  261. stream_context_set_option($socket, 'ssl', 'verify_peer', false);
  262. stream_context_set_option($socket, 'ssl', 'allow_self_signed', false);
  263. }
  264. stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
  265. stream_set_blocking($socket, 0);//设置为非阻塞模式
  266. return $socket;
  267. }
  268. /* 连接到本地 */
  269. function connectlocal($localaddr, $localport) {
  270. $socket = stream_socket_client('tcp://'.$localaddr.':'.$localport, $errno, $errstr, 30);
  271. if (!$socket) {
  272. return false;
  273. }
  274. stream_set_blocking($socket, 0);//设置为非阻塞模式
  275. return $socket;
  276. }
  277. function getloacladdr($Tunnels, $url) {
  278. $protocol = substr($url, 0, strpos($url, ':'));
  279. $hostname = substr($url, strpos($url, '//')+2);
  280. $subdomain = trim(substr($hostname, 0, strpos($hostname, '.')));
  281. $rport = substr($url, strrpos($url, ':')+1);
  282. // echo 'protocol:'.$protocol."\r\n";
  283. // echo '$subdomain:'.$subdomain."\r\n";
  284. // echo '$hostname:'.$hostname."\r\n";
  285. // echo '$rport:'.$rport."\r\n";
  286. foreach ($Tunnels as $k => $z) {
  287. //
  288. if ($protocol == $z['protocol']) {
  289. if ($hostname == $z['hostname']) {
  290. return $z;
  291. }
  292. if ($subdomain == $z['subdomain']) {
  293. return $z;
  294. }
  295. }
  296. if ($protocol == 'tcp') {
  297. if ($rport == $z['rport']) {
  298. return $z;
  299. }
  300. }
  301. }
  302. // array('protocol'=>$protocol,'hostname'=>'','subdomain'=>'','rport'=>0,'lhost'=>'','lport'=>80),
  303. }
  304. function NgrokAuth() {
  305. $Payload = array(
  306. 'ClientId' => '',
  307. 'OS' => 'darwin',
  308. 'Arch' => 'amd64',
  309. 'Version' => '2',
  310. 'MmVersion' => '2.1',
  311. 'User' => 'user',
  312. 'Password' => '',
  313. );
  314. $json = array(
  315. 'Type' => 'Auth',
  316. 'Payload' => $Payload,
  317. );
  318. return json_encode($json);
  319. }
  320. function ReqTunnel($protocol, $HostName, $Subdomain, $HttpAuth, $RemotePort) {
  321. $Payload = array(
  322. 'ReqId' => getRandChar(8),
  323. 'Protocol' => $protocol,
  324. 'Hostname' => $HostName,
  325. 'Subdomain' => $Subdomain,
  326. 'HttpAuth' => $HttpAuth,
  327. 'RemotePort' => $RemotePort,
  328. );
  329. $json = array(
  330. 'Type' => 'ReqTunnel',
  331. 'Payload' => $Payload,
  332. );
  333. return json_encode($json);
  334. }
  335. function RegProxy($ClientId) {
  336. $Payload = array('ClientId' => $ClientId);
  337. $json = array(
  338. 'Type' => 'RegProxy',
  339. 'Payload' => $Payload,
  340. );
  341. return json_encode($json);
  342. }
  343. function Ping() {
  344. $Payload = (object) array();
  345. $json = array(
  346. 'Type' => 'Ping',
  347. 'Payload' => $Payload,
  348. );
  349. return json_encode($json);
  350. }
  351. /* 网络字节序 (只支持整型范围) */
  352. function lentobyte($len) {
  353. $xx = pack("N", $len);
  354. $xx1 = pack("C4", 0, 0, 0, 0);
  355. return $xx1.$xx;
  356. }
  357. /* 机器字节序 (小端 只支持整型范围) */
  358. function lentobyte1($len) {
  359. $xx = pack("L", $len);
  360. $xx1 = pack("C4", 0, 0, 0, 0);
  361. return $xx.$xx1;
  362. }
  363. function sendpack($sock, $msg, $isblock = true) {
  364. if ($isblock) {
  365. stream_set_blocking($sock, 1);//设置为非阻塞模式
  366. }
  367. fwrite($sock, lentobyte1(strlen($msg)).$msg);
  368. if ($isblock) {
  369. stream_set_blocking($sock, 0);//设置为非阻塞模式
  370. }
  371. }
  372. function sendbuf($sock, $buf, $isblock = true) {
  373. if ($isblock) {
  374. stream_set_blocking($sock, 1);//设置为非阻塞模式
  375. }
  376. fwrite($sock, $buf);
  377. if ($isblock) {
  378. stream_set_blocking($sock, 0);//设置为非阻塞模式
  379. }
  380. }
  381. /* 网络字节序 (只支持整型范围) */
  382. function tolen($v) {
  383. $array = unpack("N", $v);
  384. return $array[1];
  385. }
  386. /* 机器字节序 (小端) 只支持整型范围 */
  387. function tolen1($v) {
  388. $array = unpack("L", $v);
  389. return $array[1];
  390. }
  391. //随机生成字符串
  392. function getRandChar($length) {
  393. $str = null;
  394. $strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
  395. $max = strlen($strPol)-1;
  396. for ($i = 0; $i < $length; $i++) {
  397. $str .= $strPol[rand(0, $max)];
  398. }
  399. return $str;
  400. }
  401. //输出日记到命令行
  402. function ConsoleOut($log, $level = 'info') {
  403. global $isDebug;
  404. if ($level == 'debug' and $isDebug == false) {
  405. return;
  406. }
  407. //cli
  408. if (is_cli()) {
  409. if (DIRECTORY_SEPARATOR == "\\") {
  410. $log = iconv('UTF-8', 'GB2312', $log);
  411. }
  412. echo $log."\r\n";
  413. }
  414. //web
  415. else {
  416. echo $log."<br/>";
  417. ob_flush();
  418. flush();
  419. // file_put_contents("ngrok.log", date("Y-m-d H:i:s:::") . $log . "\r\n", FILE_APPEND);
  420. }
  421. }
  422. //判断是否命令行运行
  423. function is_cli() {
  424. return (php_sapi_name() === 'cli')?true:false;
  425. }
  426. //ngrok.cc 获取服务器设置
  427. function ngrok_auth($clientid) {
  428. global $is_verify_peer;
  429. $host = 'www.ngrok.cc';
  430. $port = 443;
  431. $fp = stream_socket_client('tcp://'.$host.':'.$port, $errno, $errstr, 10);
  432. if (!$fp) {
  433. ConsoleOut('连接认证服务器: https://www.ngrok.cc 错误.');
  434. sleep(10);
  435. exit();
  436. }
  437. // 如果不校验证书把证书校验设置成false
  438. if ($is_verify_peer == false) {
  439. stream_context_set_option($fp, 'ssl', 'verify_host', false);
  440. stream_context_set_option($fp, 'ssl', 'verify_peer_name', false);
  441. stream_context_set_option($fp, 'ssl', 'verify_peer', false);
  442. stream_context_set_option($fp, 'ssl', 'allow_self_signed', false);
  443. }
  444. stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
  445. $header = "GET "."/api/clientid/clientid/%s"." HTTP/1.1"."\r\n";
  446. $header .= "Host: %s"."\r\n";
  447. $header .= "\r\n";
  448. $buf = sprintf($header, $clientid, $host);
  449. $write = fputs($fp, $buf);
  450. $body = null;
  451. while (!feof($fp)) {
  452. $line = fgets($fp, 1024);//去除请求包的头只显示页面的返回数据
  453. if ($line == "\n" || $line == "\r\n") {
  454. $chunk_size = (integer) hexdec(fgets($fp, 1024));
  455. if ($chunk_size > 0) {
  456. $body = fread($fp, $chunk_size);
  457. break;
  458. }
  459. }
  460. }
  461. fclose($fp);
  462. $authData = json_decode($body, true);
  463. if ($authData['status'] != 200) {
  464. ConsoleOut('认证错误:'.$authData['msg'].' ErrorCode:'.$authData['status']);
  465. sleep(10);
  466. exit();
  467. }
  468. ConsoleOut('认证成功,正在连接服务器...');
  469. //设置映射隧道,支持多渠道[客户端id]
  470. ngrok_adds($authData['data']);
  471. $proto = explode(':', $authData['server']);
  472. return $proto;
  473. }
  474. //ngrok.cc 添加到渠道队列
  475. function ngrok_adds($Tunnel) {
  476. global $Tunnels;
  477. foreach ($Tunnel as $tunnelinfo) {
  478. if (isset($tunnelinfo['proto']['http'])) {
  479. $protocol = 'http';
  480. }
  481. if (isset($tunnelinfo['proto']['https'])) {
  482. $protocol = 'https';
  483. }
  484. if (isset($tunnelinfo['proto']['tcp'])) {
  485. $protocol = 'tcp';
  486. }
  487. $proto = explode(':', $tunnelinfo['proto'][$protocol]);//127.0.0.1:80 拆分成数组
  488. if ($proto[0] == '') {
  489. $proto[0] = '127.0.0.1';
  490. }
  491. if ($proto[1] == '' || $proto[1] == 0) {
  492. $proto[1] = 80;
  493. }
  494. $Tunnels[] = array(
  495. 'protocol' => $protocol,
  496. 'hostname' => $tunnelinfo['hostname'],
  497. 'subdomain' => $tunnelinfo['subdomain'],
  498. 'httpauth' => $tunnelinfo['httpauth'],
  499. 'rport' => $tunnelinfo['remoteport'],
  500. 'lhost' => $proto[0],
  501. 'lport' => $proto[1],
  502. );
  503. }
  504. }
  505. //注册退出执行函数
  506. function shutdown() {
  507. global $mainsocket;
  508. sendpack($mainsocket, 'close');
  509. fclose($mainsocket);
  510. }
  511. ?>