Request.php 20 KB

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