diff options
Diffstat (limited to 'vhosts/t/phpcon-kagawa-2025/src/Http/Server.php')
| -rw-r--r-- | vhosts/t/phpcon-kagawa-2025/src/Http/Server.php | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php b/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php new file mode 100644 index 0000000..cf13dcf --- /dev/null +++ b/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php @@ -0,0 +1,169 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\TinyPhpHttpd\Http; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Server\RequestHandlerInterface; +use RuntimeException; + +final readonly class Server implements ResponseFactoryInterface, StreamFactoryInterface +{ + public function __construct( + private string $host, + private int $port + ) { + } + + public function run(RequestHandlerInterface $handler): void + { + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($socket === false) { + throw new RuntimeException('socket_create() failed: ' . socket_strerror(socket_last_error())); + } + + socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); + + if (socket_bind($socket, $this->host, $this->port) === false) { + throw new RuntimeException('socket_bind() failed: ' . socket_strerror(socket_last_error($socket))); + } + + if (socket_listen($socket, 5) === false) { + throw new RuntimeException('socket_listen() failed: ' . socket_strerror(socket_last_error($socket))); + } + + echo "HTTP server started on http://{$this->host}:{$this->port}\n"; + echo "Press Ctrl+C to stop\n\n"; + + for (; ;) { + $sock = socket_accept($socket); + if ($sock === false) { + echo 'socket_accept() failed: ' . socket_strerror(socket_last_error($socket)) . "\n"; + continue; + } + + $rawRequest = socket_read($sock, 8192); + + if ($rawRequest) { + $request = $this->parseRequest($rawRequest); + + echo 'Request: ' . $request->getMethod() . ' ' . $request->getRequestTarget() . "\n"; + + $response = $handler->handle($request); + + if (! $response->hasHeader('Connection')) { + $response = $response->withHeader('Connection', 'close'); + } + + $responseString = $this->responseToString($response); + socket_write($sock, $responseString, strlen($responseString)); + } + + socket_close($sock); + } + + // @phpstan-ignore deadCode.unreachable + socket_close($socket); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + return new Response($code, [], '', $reasonPhrase); + } + + public function createStream(string $content = ''): StreamInterface + { + return new Stream($content); + } + + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + throw new RuntimeException('Not implemented'); + } + + public function createStreamFromResource($resource): StreamInterface + { + throw new RuntimeException('Not implemented'); + } + + private function parseRequest(string $rawRequest): ServerRequest + { + $lines = explode("\r\n", $rawRequest); + $requestLine = trim($lines[0]); + + $parts = explode(' ', $requestLine); + $method = $parts[0]; + $path = $parts[1] ?? '/'; + + $headers = []; + $bodyStart = 0; + for ($i = 1; $i < count($lines); $i++) { + if ($lines[$i] === '') { + $bodyStart = $i + 1; + break; + } + $headerParts = explode(':', $lines[$i], 2); + if (count($headerParts) === 2) { + $headers[strtolower(trim($headerParts[0]))] = trim($headerParts[1]); + } + } + + $requestBody = ''; + if ($bodyStart > 0 && $bodyStart < count($lines)) { + $requestBody = implode("\r\n", array_slice($lines, $bodyStart)); + } + + $cookies = []; + if (isset($headers['cookie'])) { + $cookiePairs = explode(';', $headers['cookie']); + foreach ($cookiePairs as $pair) { + $kv = explode('=', trim($pair), 2); + if (count($kv) === 2) { + $cookies[$kv[0]] = urldecode($kv[1]); + } + } + } + + return new ServerRequest( + $method, + $path, + $headers, + $requestBody, + [], + $cookies, + ); + } + + private function responseToString(ResponseInterface $response): string + { + if (! $response->hasHeader('Content-Length')) { + $size = $response->getBody()->getSize(); + if ($size === null) { + throw new RuntimeException('Cannot determine body size for Content-Length header'); + } + $response = $response->withHeader('Content-Length', (string) $size); + } + + $result = sprintf( + "HTTP/%s %d %s\r\n", + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + $result .= "{$name}: {$value}\r\n"; + } + } + + $result .= "\r\n"; + $result .= (string) $response->getBody(); + + return $result; + } +} |
