FunctionalLeProxyServerTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <?php
  2. use LeProxy\LeProxy\LeProxyServer;
  3. use Psr\Http\Message\ServerRequestInterface;
  4. use React\Http\Server;
  5. use React\Socket\Server as Socket;
  6. use React\EventLoop\Factory;
  7. use React\Socket\Connector;
  8. use RingCentral\Psr7\Response;
  9. use React\Socket\ConnectionInterface;
  10. use Clue\React\Block;
  11. use React\Promise\Stream;
  12. use RingCentral\Psr7;
  13. use LeProxy\LeProxy\ConnectorFactory;
  14. class FunctionalLeProxyServerTest extends PHPUnit_Framework_TestCase
  15. {
  16. private $loop;
  17. private $socketProxy;
  18. private $socketOrigin;
  19. private $proxy;
  20. private $headers = array();
  21. public function setUp()
  22. {
  23. $this->loop = Factory::create();
  24. $this->socketOrigin = new Socket(8082, $this->loop);
  25. $origin = new Server(function (ServerRequestInterface $request) {
  26. return new Response(
  27. 200,
  28. $this->headers + array(
  29. 'X-Powered-By' => '',
  30. 'Date' => '',
  31. ),
  32. Psr7\str($request)
  33. );
  34. });
  35. $origin->listen($this->socketOrigin);
  36. $proxy = new LeProxyServer($this->loop);
  37. $this->socketProxy = $proxy->listen('127.0.0.1:8084', false);
  38. $this->proxy = $this->socketProxy->getAddress();
  39. }
  40. //private function setConnector()
  41. public function tearDown()
  42. {
  43. $this->socketOrigin->close();
  44. $this->socketProxy->close();
  45. }
  46. public function testPlainGetReturnsUpstreamResponseHeaders()
  47. {
  48. // connect to proxy and send absolute target URI
  49. $connector = new Connector($this->loop);
  50. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  51. $conn->write("GET http://127.0.0.1:8082/ HTTP/1.1\r\n\r\n");
  52. return Stream\buffer($conn);
  53. });
  54. $response = Block\await($promise, $this->loop, 0.1);
  55. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  56. $this->assertNotContains("Server:", $response);
  57. $this->assertNotContains("Date:", $response);
  58. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  59. }
  60. public function testPlainGetReturnsUpstreamResponseHeadersCustom()
  61. {
  62. $this->headers = array(
  63. 'Server' => 'React',
  64. 'Date' => 'Tue, 27 Jun 2017 12:52:16 GMT',
  65. 'X-Powered-By' => 'React'
  66. );
  67. // connect to proxy and send absolute target URI
  68. $connector = new Connector($this->loop);
  69. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  70. $conn->write("GET http://127.0.0.1:8082/ HTTP/1.1\r\n\r\n");
  71. return Stream\buffer($conn);
  72. });
  73. $response = Block\await($promise, $this->loop, 0.1);
  74. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  75. $this->assertContains("Server: React\r\n", $response);
  76. $this->assertContains("Date: Tue, 27 Jun 2017 12:52:16 GMT", $response);
  77. $this->assertContains("X-Powered-By: React\r\n", $response);
  78. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  79. $this->assertNotContains("User-Agent:", $response);
  80. }
  81. public function testPlainGetWithExplicitUserAgent()
  82. {
  83. // connect to proxy and send absolute target URI
  84. $connector = new Connector($this->loop);
  85. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  86. $conn->write("GET http://127.0.0.1:8082/ HTTP/1.1\r\nUser-Agent: custom\r\n\r\n");
  87. return Stream\buffer($conn);
  88. });
  89. $response = Block\await($promise, $this->loop, 0.1);
  90. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  91. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  92. $this->assertContains("User-Agent: custom\r\n", $response);
  93. }
  94. public function testPlainGetInvalidUriReturns502WithProxyResponseHeaders()
  95. {
  96. // connect to proxy and send absolute target URI
  97. $connector = new Connector($this->loop);
  98. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  99. $conn->write("GET http://127.0.0.1:2/ HTTP/1.1\r\n\r\n");
  100. return Stream\buffer($conn);
  101. });
  102. $response = Block\await($promise, $this->loop, 0.1);
  103. $this->assertStringStartsWith("HTTP/1.1 502 Bad Gateway\r\n", $response);
  104. $this->assertContains("Server: LeProxy\r\n", $response);
  105. $this->assertContains("\r\n\r\nUnable to request:", $response);
  106. }
  107. public function testPlainGetWithoutPathUsesRootPath()
  108. {
  109. // connect to proxy and send absolute target URI
  110. $connector = new Connector($this->loop);
  111. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  112. $conn->write("GET http://127.0.0.1:8082 HTTP/1.1\r\n\r\n");
  113. return Stream\buffer($conn);
  114. });
  115. $response = Block\await($promise, $this->loop, 0.1);
  116. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  117. $this->assertNotContains("Server:", $response);
  118. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  119. }
  120. public function testPlainGetOverUnixDomainSocketProxy()
  121. {
  122. // close TCP/IP socket and restart random Unix domain socket (UDS) path
  123. $this->socketProxy->close();
  124. $proxy = new LeProxyServer($this->loop);
  125. $path = tempnam(sys_get_temp_dir(), 'test');
  126. unlink($path);
  127. $this->socketProxy = $proxy->listen($path, false);
  128. $this->proxy = $this->socketProxy->getAddress();
  129. // connect to proxy and send absolute target URI
  130. $connector = new Connector($this->loop);
  131. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) use ($path) {
  132. unlink($path);
  133. $conn->write("GET http://127.0.0.1:8082/ HTTP/1.1\r\n\r\n");
  134. return Stream\buffer($conn);
  135. });
  136. $response = Block\await($promise, $this->loop, 0.1);
  137. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  138. $this->assertNotContains("Server:", $response);
  139. $this->assertNotContains("Date:", $response);
  140. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  141. }
  142. public function testPlainOptions()
  143. {
  144. // connect to proxy and send absolute target URI
  145. $connector = new Connector($this->loop);
  146. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  147. $conn->write("OPTIONS http://127.0.0.1:8082/ HTTP/1.1\r\n\r\n");
  148. return Stream\buffer($conn);
  149. });
  150. $response = Block\await($promise, $this->loop, 0.1);
  151. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  152. $this->assertNotContains("Server:", $response);
  153. $this->assertContains("\r\n\r\nOPTIONS / HTTP/1.1\r\n", $response);
  154. }
  155. public function testPlainOptionsWithoutPathUsesAsteriskForm()
  156. {
  157. // connect to proxy and send absolute target URI
  158. $connector = new Connector($this->loop);
  159. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  160. $conn->write("OPTIONS http://127.0.0.1:8082 HTTP/1.1\r\n\r\n");
  161. return Stream\buffer($conn);
  162. });
  163. $response = Block\await($promise, $this->loop, 0.1);
  164. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  165. $this->assertNotContains("Server:", $response);
  166. $this->assertContains("\r\n\r\nOPTIONS * HTTP/1.1\r\n", $response);
  167. }
  168. public function testConnectGet()
  169. {
  170. // connect to proxy and send CONNECT requets and then normal request
  171. $connector = new Connector($this->loop);
  172. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  173. $conn->write("CONNECT 127.0.0.1:8082 HTTP/1.1\r\n\r\n");
  174. $conn->once('data', function () use ($conn) {
  175. $conn->write("GET / HTTP/1.1\r\n\r\n");
  176. });
  177. return Stream\buffer($conn);
  178. });
  179. $response = Block\await($promise, $this->loop, 0.2);
  180. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  181. $this->assertContains("Server: LeProxy\r\n", $response);
  182. $this->assertContains("\r\n\r\nHTTP/1.1 200 OK\r\n", $response);
  183. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  184. }
  185. public function testConnectInvalidUriReturns502()
  186. {
  187. // connect to proxy and send CONNECT request
  188. $connector = new Connector($this->loop);
  189. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  190. $conn->write("CONNECT 127.0.0.1:2 HTTP/1.1\r\n\r\n");
  191. return Stream\buffer($conn);
  192. });
  193. $response = Block\await($promise, $this->loop, 0.1);
  194. $this->assertStringStartsWith("HTTP/1.1 502 Bad Gateway\r\n", $response);
  195. $this->assertContains("Server: LeProxy\r\n", $response);
  196. $this->assertContains("\r\n\r\nUnable to connect:", $response);
  197. }
  198. public function testConnectGetWithValidAuth()
  199. {
  200. $this->socketProxy->close();
  201. $proxy = new LeProxyServer($this->loop);
  202. $this->socketProxy = $proxy->listen('user:pass@127.0.0.1:8084', false);
  203. $this->proxy = $this->socketProxy->getAddress();
  204. // connect to proxy and send CONNECT requets and then normal request
  205. $connector = new Connector($this->loop);
  206. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  207. $conn->write("CONNECT 127.0.0.1:8082 HTTP/1.1\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n");
  208. $conn->once('data', function () use ($conn) {
  209. $conn->write("GET / HTTP/1.1\r\n\r\n");
  210. });
  211. return Stream\buffer($conn);
  212. });
  213. $response = Block\await($promise, $this->loop, 0.2);
  214. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  215. $this->assertContains("Server: LeProxy\r\n", $response);
  216. $this->assertContains("\r\n\r\nHTTP/1.1 200 OK\r\n", $response);
  217. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  218. }
  219. public function testConnectGetWithInvalidAuthFails()
  220. {
  221. $this->socketProxy->close();
  222. $proxy = new LeProxyServer($this->loop);
  223. $this->socketProxy = $proxy->listen('user:pass@127.0.0.1:8084', false);
  224. $this->proxy = $this->socketProxy->getAddress();
  225. // connect to proxy and send CONNECT requets and then normal request
  226. $connector = new Connector($this->loop);
  227. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  228. $conn->write("CONNECT 127.0.0.1:8082 HTTP/1.1\r\n\r\n");
  229. $conn->once('data', function () use ($conn) {
  230. $conn->write("GET / HTTP/1.1\r\n\r\n");
  231. });
  232. return Stream\buffer($conn);
  233. });
  234. $response = Block\await($promise, $this->loop, 0.2);
  235. $this->assertStringStartsWith("HTTP/1.1 407 Proxy Authentication Required\r\n", $response);
  236. $this->assertContains("Server: LeProxy\r\n", $response);
  237. }
  238. public function testSocksGet()
  239. {
  240. // connect to proxy and send CONNECT requets and then normal request
  241. $connector = new Connector($this->loop);
  242. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  243. $conn->write("\x05\x01\x00" . "\x05\x01\x00\x03\x09" . "localhost" . "\x1F\x92");
  244. $conn->once('data', function () use ($conn) {
  245. $conn->once('data', function () use ($conn) {
  246. $conn->write("GET / HTTP/1.1\r\n\r\n");
  247. });
  248. });
  249. return Stream\buffer($conn);
  250. });
  251. $response = Block\await($promise, $this->loop, 0.2);
  252. $this->assertStringStartsWith("\x05\x00" . "\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00", $response);
  253. $this->assertContains("HTTP/1.1 200 OK\r\n", $response);
  254. $this->assertContains("\r\n\r\nGET / HTTP/1.1\r\n", $response);
  255. }
  256. public function testSocksBlockedWillReturnRulesetError()
  257. {
  258. $base = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
  259. $base->expects($this->never())->method('connect');
  260. $blocker = ConnectorFactory::createBlockingConnector(array('*'), $base);
  261. $this->socketProxy->close();
  262. $proxy = new LeProxyServer($this->loop, $blocker);
  263. $this->socketProxy = $proxy->listen('127.0.0.1:8084', false);
  264. $this->proxy = $this->socketProxy->getAddress();
  265. // connect to proxy and send CONNECT requets and then normal request
  266. $connector = new Connector($this->loop);
  267. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  268. $conn->write("\x05\x01\x00" . "\x05\x01\x00\x03\x09" . "localhost" . "\x1F\x92");
  269. return Stream\buffer($conn);
  270. });
  271. $response = Block\await($promise, $this->loop, 0.2);
  272. $this->assertEquals("\x05\x00" . "\x05\x02\x00\x01\x00\x00\x00\x00\x00\x00", $response);
  273. }
  274. public function testPacDirect()
  275. {
  276. // connect to proxy and send direct request
  277. $connector = new Connector($this->loop);
  278. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  279. $conn->write("GET /pac HTTP/1.1\r\n\r\n");
  280. return Stream\buffer($conn);
  281. });
  282. $response = Block\await($promise, $this->loop, 0.1);
  283. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  284. $this->assertContains("Server: LeProxy\r\n", $response);
  285. $this->assertContains("PROXY", $response);
  286. }
  287. public function testPacInvalidMethod()
  288. {
  289. // connect to proxy and send direct request with non-GET method
  290. $connector = new Connector($this->loop);
  291. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  292. $conn->write("POST /pac HTTP/1.1\r\n\r\n");
  293. return Stream\buffer($conn);
  294. });
  295. $response = Block\await($promise, $this->loop, 0.1);
  296. $this->assertStringStartsWith("HTTP/1.1 405 Method Not Allowed\r\n", $response);
  297. }
  298. public function testPacPlain()
  299. {
  300. // connect to proxy and send plain request
  301. $connector = new Connector($this->loop);
  302. $uri = str_replace('tcp:', 'http:', $this->proxy);
  303. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) use ($uri) {
  304. $conn->write("GET $uri/pac HTTP/1.1\r\n\r\n");
  305. return Stream\buffer($conn);
  306. });
  307. $response = Block\await($promise, $this->loop, 0.1);
  308. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  309. $this->assertContains("PROXY", $response);
  310. }
  311. public function testPacPlainToInvalidHostWillReturnError()
  312. {
  313. // connect to proxy and send plain request to other host
  314. $connector = new Connector($this->loop);
  315. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  316. $conn->write("GET http://127.0.0.1:2/pac HTTP/1.1\r\n\r\n");
  317. return Stream\buffer($conn);
  318. });
  319. $response = Block\await($promise, $this->loop, 0.1);
  320. $this->assertStringStartsWith("HTTP/1.1 502 Bad Gateway\r\n", $response);
  321. $this->assertContains("\r\n\r\nUnable to request:", $response);
  322. }
  323. public function testPacConnect()
  324. {
  325. // connect to proxy and send CONNECT request
  326. $connector = new Connector($this->loop);
  327. $uri = str_replace('tcp://', '', $this->proxy);
  328. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) use ($uri) {
  329. $conn->write("CONNECT $uri HTTP/1.1\r\n\r\n");
  330. $conn->once('data', function () use ($conn) {
  331. $conn->write("GET /pac HTTP/1.1\r\n\r\n");
  332. });
  333. return Stream\buffer($conn);
  334. });
  335. $response = Block\await($promise, $this->loop, 0.2);
  336. $this->assertStringStartsWith("HTTP/1.1 200 OK\r\n", $response);
  337. $this->assertContains("\r\n\r\nHTTP/1.1 200 OK\r\n", $response);
  338. $this->assertContains("PROXY", $response);
  339. }
  340. public function testDirectInvalid()
  341. {
  342. // connect to proxy and send direct request
  343. $connector = new Connector($this->loop);
  344. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  345. $conn->write("GET /pac HTTP\r\n\r\n");
  346. return Stream\buffer($conn);
  347. });
  348. $response = Block\await($promise, $this->loop, 0.1);
  349. $this->assertStringStartsWith("HTTP/1.1 400 Bad Request\r\n", $response);
  350. //$this->assertContains("Server: LeProxy\r\n", $response);
  351. //$this->assertNotContains('X-Powered-By', $response);
  352. }
  353. public function testPlainPostWithChunkedTransferEncodingReturns411LengthRequired()
  354. {
  355. // connect to proxy and send request with (rare but valid) "Transfer-Encoding: chunked"
  356. $connector = new Connector($this->loop);
  357. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  358. $conn->write("POST http://127.0.0.1:8082/ HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n");
  359. return Stream\buffer($conn);
  360. });
  361. $response = Block\await($promise, $this->loop, 0.1);
  362. $this->assertStringStartsWith("HTTP/1.1 411 Length Required\r\n", $response);
  363. $this->assertContains("Server: LeProxy", $response);
  364. $this->assertContains("\r\n\r\nLeProxy HTTP/SOCKS proxy does not allow buffering chunked requests", $response);
  365. }
  366. public function testPlainPostWithUnknownTransferEncodingReturns501NotImplemented()
  367. {
  368. // connect to proxy and send request with unknown "Transfer-Encoding: foo"
  369. $connector = new Connector($this->loop);
  370. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  371. $conn->write("POST http://127.0.0.1:8082/ HTTP/1.1\r\nTransfer-Encoding: foo\r\n\r\n");
  372. return Stream\buffer($conn);
  373. });
  374. $response = Block\await($promise, $this->loop, 0.1);
  375. $this->assertStringStartsWith("HTTP/1.1 501 Not Implemented\r\n", $response);
  376. $this->assertNotContains("Server: LeProxy", $response);
  377. }
  378. public function testPlainPostWithChunkedTransferEncodingAndContentLengthReturns400BadRequest()
  379. {
  380. // connect to proxy and send invalid request with both "Transfer-Encoding: chunked" and "Content-Length"
  381. $connector = new Connector($this->loop);
  382. $promise = $connector->connect($this->proxy)->then(function (ConnectionInterface $conn) {
  383. $conn->write("POST http://127.0.0.1:8082/ HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 0\r\n\r\n");
  384. return Stream\buffer($conn);
  385. });
  386. $response = Block\await($promise, $this->loop, 0.1);
  387. $this->assertStringStartsWith("HTTP/1.1 400 Bad Request\r\n", $response);
  388. $this->assertNotContains("Server: LeProxy", $response);
  389. }
  390. }