リクエスト処理フロー - ha1t/php-gm-server GitHub Wiki
クライアント接続からレスポンス送信までの完全なリクエスト処理フローを詳細に説明します。TLS接続確立、バッファ管理、リクエストパース、ファイルハンドリング、レスポンス構築の各段階を網羅します。このページは GeminiServerコンポーネント の具体的な実装メカニズムに焦点を当てており、アーキテクチャ設計 で示されるイベント駆動型設計がどのように動作するかを明らかにします。
php-gm-server のリクエスト処理は、サーバー起動時の初期化から始まります。
// bin/server.php からの抜粋
$certGenerator = new CertificateGenerator($certDir);
$certPath = $certGenerator->generate($hostname);
$fileHandler = new StaticFileHandler($docRoot);
$server = new GeminiServer($host, $port, $fileHandler, $certPath);
$server->run();初期化の流れ php-gm-server/bin/server.php:17-23:
- CertificateGenerator により TLS証明書を生成(または既存証明書を再利用)
- StaticFileHandler がドキュメントルートのパスを正規化
- GeminiServer が TCP/TLS リスナーを起動
GeminiServer.run() メソッドにて、ReactPHP の TcpServer と SecureServer が初期化されます。
$loop = Loop::get();
$tcp = new TcpServer("{$this->host}:{$this->port}", $loop);
$server = new SecureServer($tcp, $loop, [
'local_cert' => $this->certPath,
'allow_self_signed' => true,
'verify_peer' => false,
]);接続ハンドラーの登録 php-gm-server/src/GeminiServer.php:35-40:
- TcpServer で指定されたホスト・ポートでリッスン
- SecureServer が TLS/SSL レイヤーを追加
- 自己署名証明書を受け入れ(TOFU モデル)
- ピア認証を無効化(サーバー認証のみ)
クライアントが接続するたびに 'connection' イベントが発火し、ConnectionInterface インスタンスが渡されます。
$server->on('connection', function (ConnectionInterface $conn) {
$buffer = '';
$conn->on('error', function (\Throwable $e) {
echo "Connection error: {$e->getMessage()}\n";
});
$conn->on('data', function (string $data) use ($conn, &$buffer) {
// リクエスト処理ロジック
});
});接続時の準備 php-gm-server/src/GeminiServer.php:44-50:
- バッファ変数
$bufferを初期化(リクエストデータを蓄積) - 接続固有のエラーハンドラーを登録
- 'data' イベントリスナーを設定
クライアントから送られたデータは、'data' イベントハンドラーで受信され、バッファに蓄積されます。
$conn->on('data', function (string $data) use ($conn, &$buffer) {
$buffer .= $data;
if (!str_contains($buffer, "\r\n")) {
if (strlen($buffer) > 1024) {
$conn->write(ResponseBuilder::badRequest('Request too long'));
$conn->end();
}
return;
}
// リクエストが完全(\r\n を含む)
// 以降の処理へ進行
});バッファリングロジック php-gm-server/src/GeminiServer.php:51-60:
- 受信したデータを既存バッファに追加
-
完全性チェック:バッファに
\r\nが含まれるかを確認 -
\r\nがない場合、さらなるデータ到着を待機 - バッファが 1024 バイトを超える場合、"Request too long" エラーで接続終了
Gemini プロトコル仕様により、リクエストは最大 1024 バイトに制限されます。
| パラメータ | 値 |
|---|---|
| MAX_REQUEST_LENGTH | 1024 |
| チェック時点 | バッファ蓄積中 |
| 超過時の処理 | ステータス 59 エラー、接続終了 |
php-gm-server/src/GeminiServer.php:55-58
リクエストの完全性が確認(\r\n 検出)されたら、RequestParser で Gemini URL 形式をパースします。
$request = RequestParser::parse($buffer);
if ($request === null) {
$conn->write(ResponseBuilder::badRequest('Invalid request'));
$conn->end();
return;
}
echo "Request: {$request['host']}{$request['path']}\n";パース処理の詳細 php-gm-server/src/RequestParser.php:14-47:
-
行の抽出:バッファから最初の
\r\nまでを抽出$line = explode("\r\n", $raw, 2)[0];
-
長さ検証:抽出した行が 1024 バイト以内か確認
-
URL パース:PHP の
parse_url()関数で URL を分解$parts = parse_url($line);
-
スキーム検証:スキームが
geminiのみを受け入れif ($scheme !== 'gemini') { return null; }
-
ホスト名抽出:
hostフィールドが空でないか確認 -
パス解析:
pathフィールドを抽出(デフォルト/)$path = $parts['path'] ?? '/';
成功時は以下の連想配列を返却:
[
'host' => 'example.com',
'path' => '/path/to/file.gmi'
]失敗時(スキーム不正、ホスト未指定など)は null を返却。
パースされたパスに基づき、StaticFileHandler がドキュメントルート内のファイルを検索・取得します。
$result = $this->fileHandler->handle($request['path']);
if ($result === null) {
$conn->write(ResponseBuilder::notFound());
$conn->end();
return;
}ファイルハンドリングのフロー php-gm-server/src/StaticFileHandler.php:35-68:
$filePath = $this->docRoot . $path;ドキュメントルート(例:/var/www/content)と リクエストパス(例:/index.gmi)を連結。
if (is_dir($filePath)) {
$filePath = rtrim($filePath, '/') . '/index.gmi';
}ディレクトリへのアクセス時は自動的に index.gmi を検索。
$realPath = realpath($filePath);
if ($realPath === false) {
return null;
}
if ($realPath !== $this->docRoot && !str_starts_with($realPath, $this->docRoot . DIRECTORY_SEPARATOR)) {
return null;
}セキュリティメカニズム php-gm-server/src/StaticFileHandler.php:43-50:
- realpath() でシンボリックリンクを解決し、正規化されたパスを取得
- ドキュメントルートチェック:正規化後のパスがドキュメントルート以下か確認
- パストラバーサル攻撃(
../など)を防止
if (!is_file($realPath)) {
return null;
}
$body = file_get_contents($realPath);
if ($body === false) {
return null;
}$ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
$mime = self::MIME_MAP[$ext] ?? 'application/octet-stream';
return [
'mime' => $mime,
'body' => $body,
];サポートされた MIME タイプ php-gm-server/src/StaticFileHandler.php:11-25:
| 拡張子 | MIME タイプ |
|---|---|
| gmi, gemini | text/gemini |
| txt | text/plain |
| html | text/html |
| css | text/css |
| js | text/javascript |
| json | application/json |
| png | image/png |
| jpg, jpeg | image/jpeg |
| gif | image/gif |
| svg | image/svg+xml |
| application/pdf | |
| その他 | application/octet-stream |
ファイルハンドラーから返された MIME タイプとボディに基づき、ResponseBuilder が Gemini プロトコル形式のレスポンスを構築します。
$conn->write(ResponseBuilder::success($result['mime'], $result['body']));
$conn->end();public static function success(string $mime, string $body): string
{
return "20 {$mime}\r\n{$body}";
}形式:20 {MIME}\r\n{BODY}
例:
20 text/gemini
# ページタイトル
このはファイルの内容です。
ソース php-gm-server/src/ResponseBuilder.php:9-12
public static function notFound(): string
{
return "51 Not found\r\n";
}形式:51 Not found\r\n
ソース php-gm-server/src/ResponseBuilder.php:14-17
public static function badRequest(string $reason = 'Bad request'): string
{
return "59 {$reason}\r\n";
}形式:59 {理由}\r\n
ソース php-gm-server/src/ResponseBuilder.php:19-22
リクエスト処理の各段階でエラーが発生する可能性があります。
バッファが 1024 バイトを超えた場合:
59 Request too long
RequestParser::parse() が null を返した場合:
59 Invalid request
StaticFileHandler::handle() が null を返した場合:
51 Not found
接続中に例外が発生した場合、コンソール出力:
Connection error: {例外メッセージ}
php-gm-server/src/GeminiServer.php:45-47
接続は 'error' イベント後も自動的に終了されます。
flowchart TD
A["クライアント接続"] --> B["TLS接続確立"]
B --> C["'connection' イベント"]
C --> D["バッファ初期化"]
D --> E["'data' イベント受信"]
E --> F["バッファにデータ追加"]
F --> G{"\r\n を<br/>検出?"}
G -->|いいえ| H{"バッファ > <br/>1024byte?"}
H -->|はい| I["59 Request too long"]
I --> J["接続終了"]
H -->|いいえ| K["次のデータ待機"]
K --> E
G -->|はい| L["RequestParser::parse"]
L --> M{"パース<br/>成功?"}
M -->|失敗| N["59 Invalid request"]
N --> J
M -->|成功| O["host と path を抽出"]
O --> P["StaticFileHandler::handle"]
P --> Q{"ファイル<br/>存在?"}
Q -->|いいえ| R["51 Not found"]
R --> J
Q -->|はい| S["MIME タイプ判定"]
S --> T["ResponseBuilder::success"]
T --> U["レスポンス送信"]
U --> J
sequenceDiagram
participant Client as クライアント
participant SS as SecureServer
participant GS as GeminiServer
participant RP as RequestParser
participant SFH as StaticFileHandler
participant RB as ResponseBuilder
Client ->> SS: TLS Handshake
SS ->> GS: connection イベント
Client ->> SS: データ送信 (part1)
SS ->> GS: data イベント
GS ->> GS: バッファに追加
Client ->> SS: データ送信 (part2 + \r\n)
SS ->> GS: data イベント
GS ->> GS: \r\n 検出
GS ->> RP: parse(buffer)
RP ->> RP: URL パース
RP ->> GS: {host, path}
GS ->> SFH: handle(path)
SFH ->> SFH: ファイル検索
SFH ->> SFH: realpath()
SFH ->> SFH: パストラバーサル防止
SFH ->> GS: {mime, body}
GS ->> RB: success(mime, body)
RB ->> GS: レスポンス文字列
GS ->> SS: write(response)
SS ->> Client: レスポンス送信
GS ->> SS: end()
SS ->> Client: 接続終了
flowchart LR
T1["時点1: 初期状態<br/>buffer = ''"]
T2["時点2: データ1受信<br/>buffer = 'gemini://.../'"]
T3["時点3: データ2受信<br/>buffer = 'gemini://...../r/n'"]
T4["時点4: \r\n 検出<br/>パース開始"]
T5["時点5: パース完了<br/>ファイル処理"]
T6["時点6: レスポンス送信<br/>接続終了"]
T1 --> T2
T2 --> T3
T3 --> T4
T4 --> T5
T5 --> T6
graph TD
E["エラー発生"]
E -->|リクエスト検証| E1["リクエスト長超過<br/>形式不正<br/>スキーム不正"]
E1 --> R1["59 Bad Request"]
E -->|リソース検索| E2["ファイル未検出<br/>ディレクトリ不正"]
E2 --> R2["51 Not Found"]
E -->|システムエラー| E3["接続エラー<br/>ファイル読み込み失敗"]
E3 --> R3["接続終了"]
- ストリーミング受信:データはバッファに蓄積され、完全なリクエストまで待機
- 早期終了:1024 バイト超過時は即座に接続を終了(リソース節約)
- realpath() のオーバーヘッド:パストラバーサル防止のため全リクエストで実行
- MIME タイプ判定:拡張子に基づくハッシュテーブル参照(高速)
- TLS ハンドシェイク:ReactPHP により非同期で処理
- 自動接続終了:レスポンス送信後、conn->end() で接続を閉鎖
- リクエスト長制限:1024 バイト(Gemini 仕様)
- スキーム検証:gemini のみを許可
- ホスト名必須チェック
- パストラバーサル防止:realpath() による正規化とドキュメントルート内チェック
- シンボリックリンク解決:realpath() で物理パスに変換
- 自己署名証明書の使用(TOFU モデル)
- ピア認証を無効化(サーバー認証のみ)
詳細は セキュリティ設計 を参照してください。
クライアントが 1024 バイトを超えるリクエストを送信している可能性があります。Gemini プロトコル仕様に準拠した クライアントを使用してください。
- リクエストが Gemini URL 形式でない(
gemini://で始まらない) - ホスト名が指定されていない
- URL パースに失敗している
- ファイルがドキュメントルート内に存在しない
- ディレクトリの場合、
index.gmiが存在しない - パストラバーサル防止機構により、ドキュメントルート外へのアクセスがブロックされている
- GeminiServerコンポーネント — GeminiServer クラスの詳細実装
- RequestParserコンポーネント — リクエストパース処理
- ResponseBuilderコンポーネント — レスポンス構築
- StaticFileHandlerコンポーネント — ファイルハンドリング
- Geminiプロトコル仕様実装 — プロトコル仕様詳細
- セキュリティ設計 — セキュリティ機構の詳細
- アーキテクチャ設計 — イベント駆動型設計