Http.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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;
  16. use Throwable;
  17. use Workerman\Connection\TcpConnection;
  18. use Workerman\Protocols\Http\Request;
  19. use Workerman\Protocols\Http\Response;
  20. use function clearstatcache;
  21. use function count;
  22. use function explode;
  23. use function filesize;
  24. use function fopen;
  25. use function fread;
  26. use function fseek;
  27. use function ftell;
  28. use function in_array;
  29. use function ini_get;
  30. use function is_array;
  31. use function is_object;
  32. use function key;
  33. use function preg_match;
  34. use function strlen;
  35. use function strpos;
  36. use function strstr;
  37. use function substr;
  38. use function sys_get_temp_dir;
  39. /**
  40. * Class Http.
  41. * @package Workerman\Protocols
  42. */
  43. class Http
  44. {
  45. /**
  46. * Request class name.
  47. *
  48. * @var string
  49. */
  50. protected static string $requestClass = Request::class;
  51. /**
  52. * Upload tmp dir.
  53. *
  54. * @var string
  55. */
  56. protected static string $uploadTmpDir = '';
  57. /**
  58. * Cache.
  59. *
  60. * @var bool.
  61. */
  62. protected static bool $enableCache = true;
  63. /**
  64. * Get or set the request class name.
  65. *
  66. * @param string|null $className
  67. * @return string
  68. */
  69. public static function requestClass(string $className = null): string
  70. {
  71. if ($className) {
  72. static::$requestClass = $className;
  73. }
  74. return static::$requestClass;
  75. }
  76. /**
  77. * Enable or disable Cache.
  78. *
  79. * @param bool $value
  80. */
  81. public static function enableCache(bool $value)
  82. {
  83. static::$enableCache = $value;
  84. }
  85. /**
  86. * Check the integrity of the package.
  87. *
  88. * @param string $buffer
  89. * @param TcpConnection $connection
  90. * @return int
  91. * @throws Throwable
  92. */
  93. public static function input(string $buffer, TcpConnection $connection): int
  94. {
  95. static $input = [];
  96. if (!isset($buffer[512]) && isset($input[$buffer])) {
  97. return $input[$buffer];
  98. }
  99. $crlfPos = strpos($buffer, "\r\n\r\n");
  100. if (false === $crlfPos) {
  101. // Judge whether the package length exceeds the limit.
  102. if (strlen($buffer) >= 16384) {
  103. $connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true);
  104. }
  105. return 0;
  106. }
  107. $length = $crlfPos + 4;
  108. $firstLine = explode(" ", strstr($buffer, "\r\n", true), 3);
  109. if (!in_array($firstLine[0], ['GET', 'POST', 'OPTIONS', 'HEAD', 'DELETE', 'PUT', 'PATCH'])) {
  110. $connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true);
  111. return 0;
  112. }
  113. $header = substr($buffer, 0, $crlfPos);
  114. $hostHeaderPosition = stripos($header, "\r\nHost: ");
  115. if (false === $hostHeaderPosition && $firstLine[2] === "HTTP/1.1") {
  116. $connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true);
  117. return 0;
  118. }
  119. if ($pos = stripos($header, "\r\nContent-Length: ")) {
  120. $length += (int)substr($header, $pos + 18, 10);
  121. $hasContentLength = true;
  122. } else if (preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) {
  123. $length += (int)$match[1];
  124. $hasContentLength = true;
  125. } else {
  126. $hasContentLength = false;
  127. if (false !== stripos($header, "\r\nTransfer-Encoding:")) {
  128. $connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true);
  129. return 0;
  130. }
  131. }
  132. if ($hasContentLength && $length > $connection->maxPackageSize) {
  133. $connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true);
  134. return 0;
  135. }
  136. if (!isset($buffer[512])) {
  137. $input[$buffer] = $length;
  138. if (count($input) > 512) {
  139. unset($input[key($input)]);
  140. }
  141. }
  142. return $length;
  143. }
  144. /**
  145. * Http decode.
  146. *
  147. * @param string $buffer
  148. * @param TcpConnection $connection
  149. * @return Request
  150. */
  151. public static function decode(string $buffer, TcpConnection $connection): Request
  152. {
  153. static $requests = [];
  154. $cacheable = static::$enableCache && !isset($buffer[512]);
  155. if (true === $cacheable && isset($requests[$buffer])) {
  156. $request = clone $requests[$buffer];
  157. $request->connection = $connection;
  158. $connection->request = $request;
  159. $request->properties = [];
  160. return $request;
  161. }
  162. $request = new static::$requestClass($buffer);
  163. $request->connection = $connection;
  164. $connection->request = $request;
  165. if (true === $cacheable) {
  166. $requests[$buffer] = $request;
  167. if (count($requests) > 512) {
  168. unset($requests[key($requests)]);
  169. }
  170. }
  171. return $request;
  172. }
  173. /**
  174. * Http encode.
  175. *
  176. * @param string|Response $response
  177. * @param TcpConnection $connection
  178. * @return string
  179. * @throws Throwable
  180. */
  181. public static function encode(mixed $response, TcpConnection $connection): string
  182. {
  183. if (isset($connection->request)) {
  184. $request = $connection->request;
  185. $request->session = $request->connection = $connection->request = null;
  186. }
  187. if (!is_object($response)) {
  188. $extHeader = '';
  189. if ($connection->headers) {
  190. foreach ($connection->headers as $name => $value) {
  191. if (is_array($value)) {
  192. foreach ($value as $item) {
  193. $extHeader .= "$name: $item\r\n";
  194. }
  195. } else {
  196. $extHeader .= "$name: $value\r\n";
  197. }
  198. }
  199. $connection->headers = [];
  200. }
  201. $response = (string)$response;
  202. $bodyLen = strlen($response);
  203. return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$extHeader}Connection: keep-alive\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $bodyLen\r\n\r\n$response";
  204. }
  205. if ($connection->headers) {
  206. $response->withHeaders($connection->headers);
  207. $connection->headers = [];
  208. }
  209. if (isset($response->file)) {
  210. $file = $response->file['file'];
  211. $offset = $response->file['offset'];
  212. $length = $response->file['length'];
  213. clearstatcache();
  214. $fileSize = (int)filesize($file);
  215. $bodyLen = $length > 0 ? $length : $fileSize - $offset;
  216. $response->withHeaders([
  217. 'Content-Length' => $bodyLen,
  218. 'Accept-Ranges' => 'bytes',
  219. ]);
  220. if ($offset || $length) {
  221. $offsetEnd = $offset + $bodyLen - 1;
  222. $response->header('Content-Range', "bytes $offset-$offsetEnd/$fileSize");
  223. }
  224. if ($bodyLen < 2 * 1024 * 1024) {
  225. $connection->send($response . file_get_contents($file, false, null, $offset, $bodyLen), true);
  226. return '';
  227. }
  228. $handler = fopen($file, 'r');
  229. if (false === $handler) {
  230. $connection->close(new Response(403, null, '403 Forbidden'));
  231. return '';
  232. }
  233. $connection->send((string)$response, true);
  234. static::sendStream($connection, $handler, $offset, $length);
  235. return '';
  236. }
  237. return (string)$response;
  238. }
  239. /**
  240. * Send remainder of a stream to client.
  241. *
  242. * @param TcpConnection $connection
  243. * @param resource $handler
  244. * @param int $offset
  245. * @param int $length
  246. * @throws Throwable
  247. */
  248. protected static function sendStream(TcpConnection $connection, $handler, int $offset = 0, int $length = 0): void
  249. {
  250. $connection->context->bufferFull = false;
  251. $connection->context->streamSending = true;
  252. if ($offset !== 0) {
  253. fseek($handler, $offset);
  254. }
  255. $offsetEnd = $offset + $length;
  256. // Read file content from disk piece by piece and send to client.
  257. $doWrite = function () use ($connection, $handler, $length, $offsetEnd) {
  258. // Send buffer not full.
  259. while ($connection->context->bufferFull === false) {
  260. // Read from disk.
  261. $size = 1024 * 1024;
  262. if ($length !== 0) {
  263. $tell = ftell($handler);
  264. $remainSize = $offsetEnd - $tell;
  265. if ($remainSize <= 0) {
  266. fclose($handler);
  267. $connection->onBufferDrain = null;
  268. return;
  269. }
  270. $size = min($remainSize, $size);
  271. }
  272. $buffer = fread($handler, $size);
  273. // Read eof.
  274. if ($buffer === '' || $buffer === false) {
  275. fclose($handler);
  276. $connection->onBufferDrain = null;
  277. $connection->context->streamSending = false;
  278. return;
  279. }
  280. $connection->send($buffer, true);
  281. }
  282. };
  283. // Send buffer full.
  284. $connection->onBufferFull = function ($connection) {
  285. $connection->context->bufferFull = true;
  286. };
  287. // Send buffer drain.
  288. $connection->onBufferDrain = function ($connection) use ($doWrite) {
  289. $connection->context->bufferFull = false;
  290. $doWrite();
  291. };
  292. $doWrite();
  293. }
  294. /**
  295. * Set or get uploadTmpDir.
  296. *
  297. * @param string|null $dir
  298. * @return string
  299. */
  300. public static function uploadTmpDir(string|null $dir = null): string
  301. {
  302. if (null !== $dir) {
  303. static::$uploadTmpDir = $dir;
  304. }
  305. if (static::$uploadTmpDir === '') {
  306. if ($uploadTmpDir = ini_get('upload_tmp_dir')) {
  307. static::$uploadTmpDir = $uploadTmpDir;
  308. } else if ($uploadTmpDir = sys_get_temp_dir()) {
  309. static::$uploadTmpDir = $uploadTmpDir;
  310. }
  311. }
  312. return static::$uploadTmpDir;
  313. }
  314. }