RequestParserコンポーネント - ha1t/php-gm-server GitHub Wiki

RequestParserコンポーネント

RequestParser は Gemini プロトコルクライアントから送信されるリクエスト文字列を解析するコアコンポーネントです。本ページでは、RequestParser クラスの実装、リクエストパース処理、スキーム検証、ホスト名抽出、パス解析、およびセキュリティ上の制限事項を詳細に解説します。

概要

RequestParser はシステム全体の最初の検証ポイントとして機能し、クライアントから受信した生のリクエスト文字列を構造化されたホスト名とパス情報に変換します。Gemini プロトコル仕様に準拠した形式のみを受け入れ、不正なリクエストは null を返して拒否します。

このコンポーネントは GeminiServerコンポーネント によって呼び出され、リクエスト処理フロー全体の第2段階を担当します。詳細な設計背景については Geminiプロトコル仕様実装 を参照してください。

RequestParser クラスの構造

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/>配列で返却"]
Loading

詳細な検証ステップ

1. リクエスト行の抽出

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

2. 長さ検証(MAX_REQUEST_LENGTH)

if ($line === '' || strlen($line) > self::MAX_REQUEST_LENGTH) {
    return null;
}

Gemini プロトコル仕様により、リクエストは最大 1024 バイトに制限されています。これはセキュリティ上の考慮(バッファオーバーフロー防止)と、シンプルなプロトコル設計の原則に基づいています。詳細は セキュリティ設計 を参照してください。

  • 空文字列の場合: null を返却
  • 1024 バイト超の場合: null を返却

出典: php-gm-server/src/RequestParser.php:18-20

3. URL パース(parse_url)

$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

4. スキーム検証(gemini のみ受け入れ)

$scheme = $parts['scheme'] ?? '';
if ($scheme !== 'gemini') {
    return null;
}

Gemini プロトコルは gemini:// スキームのみをサポートします。https://http://、その他のスキームはすべて拒否されます。スキーム情報が欠落している場合も(?? で空文字列にデフォルト化)拒否されます。

出典: php-gm-server/src/RequestParser.php:28-31

5. ホスト名抽出と検証

$host = $parts['host'] ?? '';
if ($host === '') {
    return null;
}

ホスト名(例: example.com)を抽出します。ホスト名が存在しない、または空文字列の場合は拒否されます。

重要: RequestParser はホスト名の妥当性(例: FQDN 形式、有効なドメイン名)を詳細には検証しません。ホスト名文字列が存在するかどうかのみをチェックします。理由は、Gemini プロトコル仕様がホスト名形式を厳密に定義していないためです。

出典: php-gm-server/src/RequestParser.php:33-36

6. パス抽出と標準化

$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 返却
パス 任意(デフォルト / 常に有効

GeminiServer との統合

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']);

処理フロー:

  1. クライアントから受信したデータはバッファに蓄積される
  2. \r\n が検出されたらリクエスト完全(キャリッジリターンまで受信)と判断
  3. RequestParser によってリクエストをパース
  4. パース成功時: ホスト名とパスを取得して処理継続
  5. パース失敗時: ステータスコード 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

セキュリティ考慮事項

リクエスト長制限(DoS 防止)

最大 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...
Loading

エラーハンドリング戦略

RequestParser が null を返した場合、GeminiServer は以下のいずれかの対応を実施します:

エラーシナリオ ステータスコード META 情報
リクエスト長超過 59 Request too long
パース失敗 59 Invalid request
リソース未検出(RequestParser 後) 51 Not found

詳細は エラーハンドリング戦略 を参照してください。

実装上の設計判断

1. 静的メソッドの採用

RequestParser は状態を持たないため、インスタンス化の必要がありません。静的メソッドの採用により:

  • インスタンス生成のオーバーヘッド排除
  • 使用箇所で簡潔な記述(RequestParser::parse(...)
  • テスト時のモック化は不要(実装が単純で副作用なし)

2. parse_url の活用

URL パースに PHP の組み込み関数 parse_url() を使用します。理由:

  • 標準的な URL 構文解析ロジック(RFC に準拠)
  • メンテナンス負荷の低減(自前実装のバグリスク排除)
  • パフォーマンス(C 言語で実装された組み込み関数)

3. ホスト名の形式検証なし

RequestParser はホスト名の形式(FQDN、IP アドレス、ポート番号の有無など)を詳細には検証しません。理由:

  • Gemini プロトコル仕様がホスト名形式を厳密に定義していない
  • 検証の責務分離(構文チェックのみに集中)
  • 拡張性(異なるホスト名形式の未来対応)

4. デフォルトパスの '/' 設定

パスが省略されたリクエスト(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

関連ページ

⚠️ **GitHub.com Fallback** ⚠️