RequestParserコンポーネント - ha1t/php-gm-server GitHub Wiki
RequestParser は Gemini プロトコルクライアントから送信されるリクエスト文字列を解析するコアコンポーネントです。本ページでは、RequestParser クラスの実装、リクエストパース処理、スキーム検証、ホスト名抽出、パス解析、およびセキュリティ上の制限事項を詳細に解説します。
RequestParser はシステム全体の最初の検証ポイントとして機能し、クライアントから受信した生のリクエスト文字列を構造化されたホスト名とパス情報に変換します。Gemini プロトコル仕様に準拠した形式のみを受け入れ、不正なリクエストは null を返して拒否します。
このコンポーネントは GeminiServerコンポーネント によって呼び出され、リクエスト処理フロー全体の第2段階を担当します。詳細な設計背景については Geminiプロトコル仕様実装 を参照してください。
RequestParser は静的メソッド parse() を公開する単一責任クラスです。
<?php
declare(strict_types=1);
namespace GeminiServer;
class RequestParser
{
private const MAX_REQUEST_LENGTH = 1024;
/**
* @return array{host: string, path: string}|null
*/
public static function parse(string $raw): ?array主要な特性:
- 静的メソッドのみで構成(インスタンス化不要)
- 最大リクエスト長: 1024 バイト(Gemini プロトコル仕様)
- 戻り値: パース成功時は
['host' => '...', 'path' => '...']の配列、失敗時はnull
出典: php-gm-server/src/RequestParser.php:1-15
flowchart TD
A["生リクエスト受信<br/>raw string"] --> B["CR LFまでを<br/>第1行として抽出"]
B --> C["長さチェック<br/>0-1024バイト"]
C -->|失敗| D["null を返却"]
C -->|成功| E["parse_url で<br/>URLパース"]
E -->|失敗| D
E -->|成功| F["スキーム検証<br/>gemini のみ"]
F -->|失敗| D
F -->|成功| G["ホスト名抽出"]
G -->|空文字列| D
G -->|存在| H["パス抽出<br/>デフォルト /"]
H --> I["結果を<br/>配列で返却"]
RequestParser は受信したリクエストから最初の行(CR LF = \r\n までの部分)を抽出します。
$line = explode("\r\n", $raw, 2)[0];Gemini プロトコルでは、クライアントリクエストは <URL>\r\n の形式です。explode("\r\n", ..., 2) で第1行のみを取得し、その後のデータ(存在する場合)は破棄します。
出典: php-gm-server/src/RequestParser.php:16
if ($line === '' || strlen($line) > self::MAX_REQUEST_LENGTH) {
return null;
}Gemini プロトコル仕様により、リクエストは最大 1024 バイトに制限されています。これはセキュリティ上の考慮(バッファオーバーフロー防止)と、シンプルなプロトコル設計の原則に基づいています。詳細は セキュリティ設計 を参照してください。
- 空文字列の場合:
nullを返却 - 1024 バイト超の場合:
nullを返却
出典: php-gm-server/src/RequestParser.php:18-20
$parts = parse_url($line);
if ($parts === false) {
return null;
}PHP の組み込み関数 parse_url() を使用して URL 構造を解析します。戻り値は ['scheme' => '...', 'host' => '...', 'path' => '...'] の連想配列です。パース失敗時(不正な URL 構文)は false を返し、RequestParser は null を返却します。
出典: php-gm-server/src/RequestParser.php:22-26
$scheme = $parts['scheme'] ?? '';
if ($scheme !== 'gemini') {
return null;
}Gemini プロトコルは gemini:// スキームのみをサポートします。https://、http://、その他のスキームはすべて拒否されます。スキーム情報が欠落している場合も(?? で空文字列にデフォルト化)拒否されます。
出典: php-gm-server/src/RequestParser.php:28-31
$host = $parts['host'] ?? '';
if ($host === '') {
return null;
}ホスト名(例: example.com)を抽出します。ホスト名が存在しない、または空文字列の場合は拒否されます。
重要: RequestParser はホスト名の妥当性(例: FQDN 形式、有効なドメイン名)を詳細には検証しません。ホスト名文字列が存在するかどうかのみをチェックします。理由は、Gemini プロトコル仕様がホスト名形式を厳密に定義していないためです。
出典: php-gm-server/src/RequestParser.php:33-36
$path = $parts['path'] ?? '/';
if ($path === '') {
$path = '/';
}
return [
'host' => $host,
'path' => $path,
];パス情報を抽出します。以下のルールが適用されます:
- パスが指定されていない場合: デフォルト値
/を使用 - パスが空文字列の場合:
/に変換 - パスが指定されている場合: そのまま使用
例:
-
gemini://example.com→ path =/ -
gemini://example.com/→ path =/ -
gemini://example.com/hello.gmi→ path =/hello.gmi
出典: php-gm-server/src/RequestParser.php:38-46
| 検証項目 | ルール | 失敗時の動作 |
|---|---|---|
| リクエスト行抽出 | CR LF で第1行を確定 | 部分リクエストは許容 |
| 空チェック | 空文字列を拒否 |
null 返却 |
| 長さチェック | 0-1024 バイト |
null 返却 |
| URL パース | 構文が有効な URL |
null 返却 |
| スキーム |
gemini のみ許容 |
null 返却 |
| ホスト名 | 空でない文字列 |
null 返却 |
| パス | 任意(デフォルト /) |
常に有効 |
RequestParser は GeminiServer の data イベントハンドラー内で呼び出されます。
$request = RequestParser::parse($buffer);
if ($request === null) {
$conn->write(ResponseBuilder::badRequest('Invalid request'));
$conn->end();
return;
}
echo "Request: {$request['host']}{$request['path']}\n";
$result = $this->fileHandler->handle($request['path']);処理フロー:
- クライアントから受信したデータはバッファに蓄積される
-
\r\nが検出されたらリクエスト完全(キャリッジリターンまで受信)と判断 - RequestParser によってリクエストをパース
- パース成功時: ホスト名とパスを取得して処理継続
- パース失敗時: ステータスコード 59(不正リクエスト)でレスポンス
出典: php-gm-server/src/GeminiServer.php:62-68
RequestParserTest は以下の6つのテストケースでバリデーション規則を検証します:
// 1. 正常なリクエスト
testParseValidRequest(): "gemini://example.com/hello.gmi\r\n"
// 結果: host = 'example.com', path = '/hello.gmi'
// 2. ルートパスの標準化
testParseRootPath(): "gemini://example.com/\r\n"
// 結果: host = 'example.com', path = '/'
// 3. パス省略時の補完
testParseNoTrailingSlash(): "gemini://example.com\r\n"
// 結果: host = 'example.com', path = '/'
// 4. 非 gemini スキームの拒否
testRejectNonGeminiScheme(): "https://example.com/\r\n"
// 結果: null
// 5. 1024 バイト超過の拒否
testRejectTooLongRequest(): "gemini://example.com/" + 1024文字 + "\r\n"
// 結果: null
// 6. 空リクエストの拒否
testRejectEmptyRequest(): ""
// 結果: null出典: php-gm-server/tests/RequestParserTest.php:12-50
最大 1024 バイトという厳密な制限により、以下のセキュリティリスクを軽減します:
- バッファオーバーフロー: メモリバッファの溢出を防止
- メモリ枯渇: 無制限に大量のデータを蓄積されることを防止
- 処理時間の予測: パース処理の最大実行時間が保証される
詳細は セキュリティ設計 を参照してください。
gemini:// 以外のスキーム(http://, https://, ftp:// など)を明示的に拒否することで、プロトコル混同(protocol confusion)攻撃を防止します。
重要: RequestParser はパストラバーサル(../ 攻撃)の検査を行いません。パスの正規化と安全性検証は StaticFileHandlerコンポーネント で実装されます。RequestParser は URL 構文の妥当性のみをチェックします。
sequenceDiagram
participant Client
participant GeminiServer
participant RequestParser
participant StaticFileHandler
participant ResponseBuilder
Client->>GeminiServer: データ送信<br/>gemini://example.com/hello.gmi\r\n
GeminiServer->>GeminiServer: バッファに蓄積
GeminiServer->>GeminiServer: \r\n を検出
GeminiServer->>RequestParser: parse(buffer)
RequestParser->>RequestParser: URL パース
RequestParser->>RequestParser: スキーム検証
RequestParser->>RequestParser: ホスト名抽出
RequestParser->>RequestParser: パス抽出
RequestParser-->>GeminiServer: {host, path}
GeminiServer->>StaticFileHandler: handle(path)
StaticFileHandler-->>GeminiServer: {mime, body}
GeminiServer->>ResponseBuilder: success(mime, body)
ResponseBuilder-->>GeminiServer: Gemini形式レスポンス
GeminiServer-->>Client: レスポンス送信<br/>20 text/gemini\r\n...
RequestParser が null を返した場合、GeminiServer は以下のいずれかの対応を実施します:
| エラーシナリオ | ステータスコード | META 情報 |
|---|---|---|
| リクエスト長超過 | 59 | Request too long |
| パース失敗 | 59 | Invalid request |
| リソース未検出(RequestParser 後) | 51 | Not found |
詳細は エラーハンドリング戦略 を参照してください。
RequestParser は状態を持たないため、インスタンス化の必要がありません。静的メソッドの採用により:
- インスタンス生成のオーバーヘッド排除
- 使用箇所で簡潔な記述(
RequestParser::parse(...)) - テスト時のモック化は不要(実装が単純で副作用なし)
URL パースに PHP の組み込み関数 parse_url() を使用します。理由:
- 標準的な URL 構文解析ロジック(RFC に準拠)
- メンテナンス負荷の低減(自前実装のバグリスク排除)
- パフォーマンス(C 言語で実装された組み込み関数)
RequestParser はホスト名の形式(FQDN、IP アドレス、ポート番号の有無など)を詳細には検証しません。理由:
- Gemini プロトコル仕様がホスト名形式を厳密に定義していない
- 検証の責務分離(構文チェックのみに集中)
- 拡張性(異なるホスト名形式の未来対応)
パスが省略されたリクエスト(gemini://example.com など)は、パス '/' にデフォルト化されます。理由:
- HTTP プロトコルとの一貫性
- ルートリソースアクセスの自然な表現
- ユーザー利便性(省略可能な形式をサポート)
RequestParser は以下の特性を持ちます:
- 時間計算量: O(n)、ここで n はリクエスト長(最大 1024 バイト)
- 空間計算量: O(1)(固定サイズの出力配列)
- 最大実行時間: 数ミリ秒以下(1024 バイトの URL パース)
Gemini プロトコルの短い最大リクエスト長により、パフォーマンスは実運用で問題になることはありません。
RequestParser のテストを実行するには:
cd /home/ha1t/src/php-gm-server
vendor/bin/phpunit tests/RequestParserTest.php出典: php-gm-server/tests/RequestParserTest.php
- リクエスト処理フロー — リクエスト受信からレスポンス送信までの完全フロー
- GeminiServerコンポーネント — RequestParser の呼び出し元
- ResponseBuilderコンポーネント — エラーレスポンス生成
- StaticFileHandlerコンポーネント — パストラバーサル防止の実装者
- Geminiプロトコル仕様実装 — プロトコル仕様の詳細
- セキュリティ設計 — リクエスト長制限とその根拠
- エラーハンドリング戦略 — エラーレスポンスの設計