Request.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. <?php
  2. /**
  3. * This file is part of workerman.
  4. *
  5. * Licensed under The MIT License
  6. * For full copyright and license information, please see the MIT-LICENSE.txt
  7. * Redistributions of files must retain the above copyright notice.
  8. *
  9. * @author walkor<walkor@workerman.net>
  10. * @copyright walkor<walkor@workerman.net>
  11. * @link http://www.workerman.net/
  12. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  13. */
  14. declare(strict_types=1);
  15. namespace Workerman\Protocols\Http;
  16. use Exception;
  17. use RuntimeException;
  18. use Workerman\Connection\TcpConnection;
  19. use Workerman\Protocols\Http;
  20. use function array_walk_recursive;
  21. use function bin2hex;
  22. use function clearstatcache;
  23. use function count;
  24. use function explode;
  25. use function file_put_contents;
  26. use function is_file;
  27. use function json_decode;
  28. use function ltrim;
  29. use function microtime;
  30. use function pack;
  31. use function parse_str;
  32. use function parse_url;
  33. use function preg_match;
  34. use function preg_replace;
  35. use function strlen;
  36. use function strpos;
  37. use function strstr;
  38. use function strtolower;
  39. use function substr;
  40. use function tempnam;
  41. use function trim;
  42. use function unlink;
  43. use function urlencode;
  44. /**
  45. * Class Request
  46. * @package Workerman\Protocols\Http
  47. */
  48. class Request
  49. {
  50. /**
  51. * Connection.
  52. *
  53. * @var ?TcpConnection
  54. */
  55. public ?TcpConnection $connection = null;
  56. /**
  57. * Session instance.
  58. *
  59. * @var ?Session
  60. */
  61. public ?Session $session = null;
  62. /**
  63. * @var int
  64. */
  65. public static int $maxFileUploads = 1024;
  66. /**
  67. * Properties.
  68. *
  69. * @var array
  70. */
  71. public array $properties = [];
  72. /**
  73. * Http buffer.
  74. *
  75. * @var string
  76. */
  77. protected string $buffer;
  78. /**
  79. * Request data.
  80. *
  81. * @var array
  82. */
  83. protected array $data = [];
  84. /**
  85. * Enable cache.
  86. *
  87. * @var bool
  88. */
  89. protected static bool $enableCache = true;
  90. /**
  91. * Request constructor.
  92. *
  93. * @param string $buffer
  94. */
  95. public function __construct(string $buffer)
  96. {
  97. $this->buffer = $buffer;
  98. }
  99. /**
  100. * Get query.
  101. *
  102. * @param string|null $name
  103. * @param mixed|null $default
  104. * @return mixed
  105. */
  106. public function get(string $name = null, mixed $default = null): mixed
  107. {
  108. if (!isset($this->data['get'])) {
  109. $this->parseGet();
  110. }
  111. if (null === $name) {
  112. return $this->data['get'];
  113. }
  114. return $this->data['get'][$name] ?? $default;
  115. }
  116. /**
  117. * Get post.
  118. *
  119. * @param string|null $name
  120. * @param mixed|null $default
  121. * @return mixed
  122. */
  123. public function post(string $name = null, mixed $default = null): mixed
  124. {
  125. if (!isset($this->data['post'])) {
  126. $this->parsePost();
  127. }
  128. if (null === $name) {
  129. return $this->data['post'];
  130. }
  131. return $this->data['post'][$name] ?? $default;
  132. }
  133. /**
  134. * Get header item by name.
  135. *
  136. * @param string|null $name
  137. * @param mixed|null $default
  138. * @return mixed
  139. */
  140. public function header(string $name = null, mixed $default = null): mixed
  141. {
  142. if (!isset($this->data['headers'])) {
  143. $this->parseHeaders();
  144. }
  145. if (null === $name) {
  146. return $this->data['headers'];
  147. }
  148. $name = strtolower($name);
  149. return $this->data['headers'][$name] ?? $default;
  150. }
  151. /**
  152. * Get cookie item by name.
  153. *
  154. * @param string|null $name
  155. * @param mixed|null $default
  156. * @return mixed
  157. */
  158. public function cookie(string $name = null, mixed $default = null): mixed
  159. {
  160. if (!isset($this->data['cookie'])) {
  161. $this->data['cookie'] = [];
  162. parse_str(preg_replace('/; ?/', '&', $this->header('cookie', '')), $this->data['cookie']);
  163. }
  164. if ($name === null) {
  165. return $this->data['cookie'];
  166. }
  167. return $this->data['cookie'][$name] ?? $default;
  168. }
  169. /**
  170. * Get upload files.
  171. *
  172. * @param string|null $name
  173. * @return array|null
  174. */
  175. public function file(string $name = null)
  176. {
  177. if (!isset($this->data['files'])) {
  178. $this->parsePost();
  179. }
  180. if (null === $name) {
  181. return $this->data['files'];
  182. }
  183. return $this->data['files'][$name] ?? null;
  184. }
  185. /**
  186. * Get method.
  187. *
  188. * @return string
  189. */
  190. public function method(): string
  191. {
  192. if (!isset($this->data['method'])) {
  193. $this->parseHeadFirstLine();
  194. }
  195. return $this->data['method'];
  196. }
  197. /**
  198. * Get http protocol version.
  199. *
  200. * @return string
  201. */
  202. public function protocolVersion(): string
  203. {
  204. if (!isset($this->data['protocolVersion'])) {
  205. $this->parseProtocolVersion();
  206. }
  207. return $this->data['protocolVersion'];
  208. }
  209. /**
  210. * Get host.
  211. *
  212. * @param bool $withoutPort
  213. * @return string|null
  214. */
  215. public function host(bool $withoutPort = false): ?string
  216. {
  217. $host = $this->header('host');
  218. if ($host && $withoutPort && $pos = strpos($host, ':')) {
  219. return substr($host, 0, $pos);
  220. }
  221. return $host;
  222. }
  223. /**
  224. * Get uri.
  225. *
  226. * @return string
  227. */
  228. public function uri(): string
  229. {
  230. if (!isset($this->data['uri'])) {
  231. $this->parseHeadFirstLine();
  232. }
  233. return $this->data['uri'];
  234. }
  235. /**
  236. * Get path.
  237. *
  238. * @return string
  239. */
  240. public function path(): string
  241. {
  242. if (!isset($this->data['path'])) {
  243. $this->data['path'] = (string)parse_url($this->uri(), PHP_URL_PATH);
  244. }
  245. return $this->data['path'];
  246. }
  247. /**
  248. * Get query string.
  249. *
  250. * @return string
  251. */
  252. public function queryString(): string
  253. {
  254. if (!isset($this->data['query_string'])) {
  255. $this->data['query_string'] = (string)parse_url($this->uri(), PHP_URL_QUERY);
  256. }
  257. return $this->data['query_string'];
  258. }
  259. /**
  260. * Get session.
  261. *
  262. * @return Session
  263. * @throws Exception
  264. */
  265. public function session(): Session
  266. {
  267. if ($this->session === null) {
  268. $this->session = new Session($this->sessionId());
  269. }
  270. return $this->session;
  271. }
  272. /**
  273. * Get/Set session id.
  274. *
  275. * @param null $sessionId
  276. * @return string
  277. * @throws Exception
  278. */
  279. public function sessionId($sessionId = null): string
  280. {
  281. if ($sessionId) {
  282. unset($this->sid);
  283. }
  284. if (!isset($this->sid)) {
  285. $sessionName = Session::$name;
  286. $sid = $sessionId ? '' : $this->cookie($sessionName);
  287. if ($sid === '' || $sid === null) {
  288. if (!$this->connection) {
  289. throw new RuntimeException('Request->session() fail, header already send');
  290. }
  291. $sid = $sessionId ?: static::createSessionId();
  292. $cookieParams = Session::getCookieParams();
  293. $this->setSidCookie($sessionName, $sid, $cookieParams);
  294. }
  295. $this->sid = $sid;
  296. }
  297. return $this->sid;
  298. }
  299. /**
  300. * Session regenerate id.
  301. *
  302. * @param bool $deleteOldSession
  303. * @return string
  304. * @throws Exception
  305. */
  306. public function sessionRegenerateId(bool $deleteOldSession = false): string
  307. {
  308. $session = $this->session();
  309. $sessionData = $session->all();
  310. if ($deleteOldSession) {
  311. $session->flush();
  312. }
  313. $newSid = static::createSessionId();
  314. $session = new Session($newSid);
  315. $session->put($sessionData);
  316. $cookieParams = Session::getCookieParams();
  317. $sessionName = Session::$name;
  318. $this->setSidCookie($sessionName, $newSid, $cookieParams);
  319. return $newSid;
  320. }
  321. /**
  322. * Get http raw head.
  323. *
  324. * @return string
  325. */
  326. public function rawHead(): string
  327. {
  328. if (!isset($this->data['head'])) {
  329. $this->data['head'] = strstr($this->buffer, "\r\n\r\n", true);
  330. }
  331. return $this->data['head'];
  332. }
  333. /**
  334. * Get http raw body.
  335. *
  336. * @return string
  337. */
  338. public function rawBody(): string
  339. {
  340. return substr($this->buffer, strpos($this->buffer, "\r\n\r\n") + 4);
  341. }
  342. /**
  343. * Get raw buffer.
  344. *
  345. * @return string
  346. */
  347. public function rawBuffer(): string
  348. {
  349. return $this->buffer;
  350. }
  351. /**
  352. * Enable or disable cache.
  353. *
  354. * @param bool $value
  355. */
  356. public static function enableCache(bool $value): void
  357. {
  358. static::$enableCache = $value;
  359. }
  360. /**
  361. * Parse first line of http header buffer.
  362. *
  363. * @return void
  364. */
  365. protected function parseHeadFirstLine(): void
  366. {
  367. $firstLine = strstr($this->buffer, "\r\n", true);
  368. $tmp = explode(' ', $firstLine, 3);
  369. $this->data['method'] = $tmp[0];
  370. $this->data['uri'] = $tmp[1] ?? '/';
  371. }
  372. /**
  373. * Parse protocol version.
  374. *
  375. * @return void
  376. */
  377. protected function parseProtocolVersion(): void
  378. {
  379. $firstLine = strstr($this->buffer, "\r\n", true);
  380. $protocolVersion = substr(strstr($firstLine, 'HTTP/'), 5);
  381. $this->data['protocolVersion'] = $protocolVersion ?: '1.0';
  382. }
  383. /**
  384. * Parse headers.
  385. *
  386. * @return void
  387. */
  388. protected function parseHeaders(): void
  389. {
  390. static $cache = [];
  391. $this->data['headers'] = [];
  392. $rawHead = $this->rawHead();
  393. $endLinePosition = strpos($rawHead, "\r\n");
  394. if ($endLinePosition === false) {
  395. return;
  396. }
  397. $headBuffer = substr($rawHead, $endLinePosition + 2);
  398. $cacheable = static::$enableCache && !isset($headBuffer[4096]);
  399. if ($cacheable && isset($cache[$headBuffer])) {
  400. $this->data['headers'] = $cache[$headBuffer];
  401. return;
  402. }
  403. $headData = explode("\r\n", $headBuffer);
  404. foreach ($headData as $content) {
  405. if (str_contains($content, ':')) {
  406. [$key, $value] = explode(':', $content, 2);
  407. $key = strtolower($key);
  408. $value = ltrim($value);
  409. } else {
  410. $key = strtolower($content);
  411. $value = '';
  412. }
  413. if (isset($this->data['headers'][$key])) {
  414. $this->data['headers'][$key] = "{$this->data['headers'][$key]},$value";
  415. } else {
  416. $this->data['headers'][$key] = $value;
  417. }
  418. }
  419. if ($cacheable) {
  420. $cache[$headBuffer] = $this->data['headers'];
  421. if (count($cache) > 128) {
  422. unset($cache[key($cache)]);
  423. }
  424. }
  425. }
  426. /**
  427. * Parse head.
  428. *
  429. * @return void
  430. */
  431. protected function parseGet(): void
  432. {
  433. static $cache = [];
  434. $queryString = $this->queryString();
  435. $this->data['get'] = [];
  436. if ($queryString === '') {
  437. return;
  438. }
  439. $cacheable = static::$enableCache && !isset($queryString[1024]);
  440. if ($cacheable && isset($cache[$queryString])) {
  441. $this->data['get'] = $cache[$queryString];
  442. return;
  443. }
  444. parse_str($queryString, $this->data['get']);
  445. if ($cacheable) {
  446. $cache[$queryString] = $this->data['get'];
  447. if (count($cache) > 256) {
  448. unset($cache[key($cache)]);
  449. }
  450. }
  451. }
  452. /**
  453. * Parse post.
  454. *
  455. * @return void
  456. */
  457. protected function parsePost(): void
  458. {
  459. static $cache = [];
  460. $this->data['post'] = $this->data['files'] = [];
  461. $contentType = $this->header('content-type', '');
  462. if (preg_match('/boundary="?(\S+)"?/', $contentType, $match)) {
  463. $httpPostBoundary = '--' . $match[1];
  464. $this->parseUploadFiles($httpPostBoundary);
  465. return;
  466. }
  467. $bodyBuffer = $this->rawBody();
  468. if ($bodyBuffer === '') {
  469. return;
  470. }
  471. $cacheable = static::$enableCache && !isset($bodyBuffer[1024]);
  472. if ($cacheable && isset($cache[$bodyBuffer])) {
  473. $this->data['post'] = $cache[$bodyBuffer];
  474. return;
  475. }
  476. if (preg_match('/\bjson\b/i', $contentType)) {
  477. $this->data['post'] = (array)json_decode($bodyBuffer, true);
  478. } else {
  479. parse_str($bodyBuffer, $this->data['post']);
  480. }
  481. if ($cacheable) {
  482. $cache[$bodyBuffer] = $this->data['post'];
  483. if (count($cache) > 256) {
  484. unset($cache[key($cache)]);
  485. }
  486. }
  487. }
  488. /**
  489. * Parse upload files.
  490. *
  491. * @param string $httpPostBoundary
  492. * @return void
  493. */
  494. protected function parseUploadFiles(string $httpPostBoundary): void
  495. {
  496. $httpPostBoundary = trim($httpPostBoundary, '"');
  497. $buffer = $this->buffer;
  498. $postEncodeString = '';
  499. $filesEncodeString = '';
  500. $files = [];
  501. $bodyPosition = strpos($buffer, "\r\n\r\n") + 4;
  502. $offset = $bodyPosition + strlen($httpPostBoundary) + 2;
  503. $maxCount = static::$maxFileUploads;
  504. while ($maxCount-- > 0 && $offset) {
  505. $offset = $this->parseUploadFile($httpPostBoundary, $offset, $postEncodeString, $filesEncodeString, $files);
  506. }
  507. if ($postEncodeString) {
  508. parse_str($postEncodeString, $this->data['post']);
  509. }
  510. if ($filesEncodeString) {
  511. parse_str($filesEncodeString, $this->data['files']);
  512. array_walk_recursive($this->data['files'], function (&$value) use ($files) {
  513. $value = $files[$value];
  514. });
  515. }
  516. }
  517. /**
  518. * Parse upload file.
  519. *
  520. * @param $boundary
  521. * @param $sectionStartOffset
  522. * @param $postEncodeString
  523. * @param $filesEncodeStr
  524. * @param $files
  525. * @return int
  526. */
  527. protected function parseUploadFile($boundary, $sectionStartOffset, &$postEncodeString, &$filesEncodeStr, &$files): int
  528. {
  529. $file = [];
  530. $boundary = "\r\n$boundary";
  531. if (strlen($this->buffer) < $sectionStartOffset) {
  532. return 0;
  533. }
  534. $sectionEndOffset = strpos($this->buffer, $boundary, $sectionStartOffset);
  535. if (!$sectionEndOffset) {
  536. return 0;
  537. }
  538. $contentLinesEndOffset = strpos($this->buffer, "\r\n\r\n", $sectionStartOffset);
  539. if (!$contentLinesEndOffset || $contentLinesEndOffset + 4 > $sectionEndOffset) {
  540. return 0;
  541. }
  542. $contentLinesStr = substr($this->buffer, $sectionStartOffset, $contentLinesEndOffset - $sectionStartOffset);
  543. $contentLines = explode("\r\n", trim($contentLinesStr . "\r\n"));
  544. $boundaryValue = substr($this->buffer, $contentLinesEndOffset + 4, $sectionEndOffset - $contentLinesEndOffset - 4);
  545. $uploadKey = false;
  546. foreach ($contentLines as $contentLine) {
  547. if (!strpos($contentLine, ': ')) {
  548. return 0;
  549. }
  550. [$key, $value] = explode(': ', $contentLine);
  551. switch (strtolower($key)) {
  552. case "content-disposition":
  553. // Is file data.
  554. if (preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) {
  555. $error = 0;
  556. $tmpFile = '';
  557. $fileName = $match[1];
  558. $size = strlen($boundaryValue);
  559. $tmpUploadDir = HTTP::uploadTmpDir();
  560. if (!$tmpUploadDir) {
  561. $error = UPLOAD_ERR_NO_TMP_DIR;
  562. } else if ($boundaryValue === '' && $fileName === '') {
  563. $error = UPLOAD_ERR_NO_FILE;
  564. } else {
  565. $tmpFile = tempnam($tmpUploadDir, 'workerman.upload.');
  566. if ($tmpFile === false || false === file_put_contents($tmpFile, $boundaryValue)) {
  567. $error = UPLOAD_ERR_CANT_WRITE;
  568. }
  569. }
  570. $uploadKey = $fileName;
  571. // Parse upload files.
  572. $file = [...$file, 'name' => $match[2], 'tmp_name' => $tmpFile, 'size' => $size, 'error' => $error];
  573. if (!isset($file['type'])) {
  574. $file['type'] = '';
  575. }
  576. break;
  577. }
  578. // Is post field.
  579. // Parse $POST.
  580. if (preg_match('/name="(.*?)"$/', $value, $match)) {
  581. $k = $match[1];
  582. $postEncodeString .= urlencode($k) . "=" . urlencode($boundaryValue) . '&';
  583. }
  584. return $sectionEndOffset + strlen($boundary) + 2;
  585. case "content-type":
  586. $file['type'] = trim($value);
  587. break;
  588. }
  589. }
  590. if ($uploadKey === false) {
  591. return 0;
  592. }
  593. $filesEncodeStr .= urlencode($uploadKey) . '=' . count($files) . '&';
  594. $files[] = $file;
  595. return $sectionEndOffset + strlen($boundary) + 2;
  596. }
  597. /**
  598. * Create session id.
  599. *
  600. * @return string
  601. * @throws Exception
  602. */
  603. public static function createSessionId(): string
  604. {
  605. return bin2hex(pack('d', microtime(true)) . random_bytes(8));
  606. }
  607. /**
  608. * @param string $sessionName
  609. * @param string $sid
  610. * @param array $cookieParams
  611. * @return void
  612. */
  613. protected function setSidCookie(string $sessionName, string $sid, array $cookieParams): void
  614. {
  615. if (!$this->connection) {
  616. throw new RuntimeException('Request->setSidCookie() fail, header already send');
  617. }
  618. $this->connection->headers['Set-Cookie'] = [$sessionName . '=' . $sid
  619. . (empty($cookieParams['domain']) ? '' : '; Domain=' . $cookieParams['domain'])
  620. . (empty($cookieParams['lifetime']) ? '' : '; Max-Age=' . $cookieParams['lifetime'])
  621. . (empty($cookieParams['path']) ? '' : '; Path=' . $cookieParams['path'])
  622. . (empty($cookieParams['samesite']) ? '' : '; SameSite=' . $cookieParams['samesite'])
  623. . (!$cookieParams['secure'] ? '' : '; Secure')
  624. . (!$cookieParams['httponly'] ? '' : '; HttpOnly')];
  625. }
  626. /**
  627. * __toString.
  628. */
  629. public function __toString()
  630. {
  631. return $this->buffer;
  632. }
  633. /**
  634. * Setter.
  635. *
  636. * @param string $name
  637. * @param mixed $value
  638. * @return void
  639. */
  640. public function __set(string $name, mixed $value)
  641. {
  642. $this->properties[$name] = $value;
  643. }
  644. /**
  645. * Getter.
  646. *
  647. * @param string $name
  648. * @return mixed|null
  649. */
  650. public function __get(string $name)
  651. {
  652. return $this->properties[$name] ?? null;
  653. }
  654. /**
  655. * Isset.
  656. *
  657. * @param string $name
  658. * @return bool
  659. */
  660. public function __isset(string $name)
  661. {
  662. return isset($this->properties[$name]);
  663. }
  664. /**
  665. * Unset.
  666. *
  667. * @param string $name
  668. * @return void
  669. */
  670. public function __unset(string $name)
  671. {
  672. unset($this->properties[$name]);
  673. }
  674. /**
  675. * __destruct.
  676. *
  677. * @return void
  678. */
  679. public function __destruct()
  680. {
  681. if (isset($this->data['files'])) {
  682. clearstatcache();
  683. array_walk_recursive($this->data['files'], function ($value, $key) {
  684. if ($key === 'tmp_name' && is_file($value)) {
  685. unlink($value);
  686. }
  687. });
  688. }
  689. }
  690. }