summaryrefslogtreecommitdiffhomepage
path: root/vhosts/t/phpcon-kagawa-2025/src/Http/Server.php
diff options
context:
space:
mode:
Diffstat (limited to 'vhosts/t/phpcon-kagawa-2025/src/Http/Server.php')
-rw-r--r--vhosts/t/phpcon-kagawa-2025/src/Http/Server.php169
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;
+ }
+}