アーキテクチャ設計 - ha1t/php-gm-server GitHub Wiki

アーキテクチャ設計

このページでは、PHP GM Serverの全体的なシステムアーキテクチャ、コンポーネント構成、データフロー、および設計パターンについて説明します。本システムはReactPHPのイベントループを中心とした非同期I/O駆動アーキテクチャを採用しており、複数のクライアント接続を効率的に処理できる設計になっています。

システム概要

PHP GM Serverは、Geminiプロトコルをサポートするサーバーアプリケーションであり、ReactPHPのイベントループ駆動型アーキテクチャに基づいています。TLS/SSL対応のセキュアなソケット通信により、Geminiプロトコル仕様で要求される暗号化通信を実現しています。

graph TB
    Client["Geminiクライアント"]
    Network["TLS/SSLネットワーク"]
    
    ReactLoop["React EventLoop<br/>イベントループ"]
    TcpServer["TcpServer<br/>TCP接続層"]
    SecureServer["SecureServer<br/>TLS層"]
    
    GeminiServer["GeminiServer<br/>コア処理"]
    RequestParser["RequestParser<br/>リクエスト解析"]
    StaticFileHandler["StaticFileHandler<br/>ファイル配信"]
    ResponseBuilder["ResponseBuilder<br/>レスポンス構築"]
    CertificateGenerator["CertificateGenerator<br/>TLS証明書管理"]
    
    FileSystem["ファイルシステム<br/>ドキュメントルート"]
    Certs["証明書ストレージ<br/>certs/"]
    
    Client -->|"gemini://host/path\r\n"| Network
    Network --> TcpServer
    TcpServer --> SecureServer
    SecureServer -->|"接続イベント"| GeminiServer
    
    GeminiServer -->|"dataイベント<br/>バッファ処理"| RequestParser
    RequestParser -->|"パース結果"| GeminiServer
    GeminiServer -->|"ファイル要求"| StaticFileHandler
    StaticFileHandler -->|"MIME型 + 本体"| GeminiServer
    GeminiServer -->|"レスポンス生成"| ResponseBuilder
    ResponseBuilder -->|"STATUS SPACE META\r\nBODY"| GeminiServer
    
    GeminiServer -->|"データ送信"| SecureServer
    SecureServer -->|"TLS暗号化"| Network
    Network -->|"レスポンス"| Client
    
    GeminiServer -->|"eventLoop駆動"| ReactLoop
    ReactLoop -->|"非同期処理"| GeminiServer
    
    StaticFileHandler -->|"ファイル読み込み"| FileSystem
    CertificateGenerator -->|"証明書生成・読み込み"| Certs
    GeminiServer -->|"証明書パス"| SecureServer
    
    style ReactLoop fill:#e1f5ff
    style GeminiServer fill:#f3e5f5
    style RequestParser fill:#f1f8e9
    style StaticFileHandler fill:#f1f8e9
    style ResponseBuilder fill:#f1f8e9
    style CertificateGenerator fill:#fce4ec
    style TcpServer fill:#fff3e0
    style SecureServer fill:#fff3e0
Loading

Sources: php-gm-server/src/GeminiServer.php:31-90, php-gm-server/composer.json:5-8

イベントループ駆動アーキテクチャ

React PHPのイベントループは、本システムの核となる処理エンジンです。すべてのI/O操作が非同期で実行され、複数のクライアント接続を同時に処理できます。

イベントループの役割

GeminiServerのrun()メソッド内で、React\EventLoop\Loop::get()によってグローバルイベントループを取得します。このループは以下の役割を担います:

  • TcpServer・SecureServerの駆動:ネットワークI/O操作(接続受け入れ、データ受信、データ送信)をイベント駆動で処理
  • タイムアウト管理:接続ごとのタイムアウト設定が可能(必要に応じて)
  • 複数接続の並行処理:複数クライアントからのリクエストをブロッキングなく処理
sequenceDiagram
    actor Client1
    participant Loop as "Event Loop"
    participant Server as "GeminiServer"
    participant Handler as "Handler"
    
    activate Loop
    Loop->>Server: loop.run()実行開始
    
    Client1->>Loop: TCP接続要求
    Loop->>Server: connectionイベント発火
    activate Server
    Server->>Handler: 接続ハンドラー登録<br/>dataイベント・errorイベント
    deactivate Server
    
    Client1->>Loop: Geminiリクエスト送信
    Loop->>Handler: dataイベント発火
    activate Handler
    Handler->>Handler: バッファに蓄積
    Handler->>Handler: \\r\\nで分割チェック
    Handler->>Handler: RequestParser.parse()実行
    Handler->>Handler: StaticFileHandler.handle()実行
    Handler->>Handler: ResponseBuilder.success()実行
    Handler->>Handler: conn.write()レスポンス送信
    Handler->>Handler: conn.end()接続終了
    deactivate Handler
    
    deactivate Loop
Loading

Sources: php-gm-server/src/GeminiServer.php:31-34

コンポーネント構成

1. GeminiServer(メインコンポーネント)

GeminiServerクラスは、Geminiプロトコルサーバーの中核を担当します。React PHPのTcpServerSecureServerを組み合わせ、TLS/SSL対応のセキュアな通信を実現します。

主な責務

  • TcpServer・SecureServerの初期化と起動
  • クライアント接続の受け入れ
  • バッファを用いたリクエストデータの蓄積
  • 各コンポーネントの協調処理(RequestParser → StaticFileHandler → ResponseBuilder)
  • エラーハンドリング
// GeminiServer.php:31-90の抜粋
public function run(): void
{
    $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,
    ]);

    // 接続イベントハンドラー
    $server->on('connection', function (ConnectionInterface $conn) {
        // ... 接続処理
    });
    
    $loop->run();
}

Sources: php-gm-server/src/GeminiServer.php:12-91

リクエスト受信フロー

GeminiServerは接続ごとに以下の処理を実行します:

  1. 接続初期化:各接続に対してバッファを初期化
  2. データ蓄積dataイベントでデータをバッファに追加
  3. リクエスト完全性判定\r\nの検出でリクエスト完全と判断
  4. 長さ検証:1024バイト超過時は即座にエラー応答
  5. 下流処理:RequestParser → StaticFileHandler → ResponseBuilder
graph TD
    A["接続イベント発火"] --> B["バッファ初期化<br/>buffer = ''"]
    B --> C["dataイベント<br/>データ受信"]
    C --> D["バッファに蓄積<br/>buffer += data"]
    D --> E{"\\r\\n検出?"}
    E -->|No| F{"長さ > 1024?"}
    F -->|Yes| G["59 Request too long<br/>接続終了"]
    F -->|No| C
    E -->|Yes| H["RequestParser.parse<br/>リクエスト解析"]
    H --> I{"パース成功?"}
    I -->|失敗| J["59 Invalid request<br/>接続終了"]
    I -->|成功| K["StaticFileHandler.handle<br/>ファイル解決"]
    K --> L{"ファイル存在?"}
    L -->|No| M["51 Not found<br/>接続終了"]
    L -->|Yes| N["ResponseBuilder.success<br/>レスポンス構築"]
    N --> O["conn.write<br/>レスポンス送信"]
    O --> P["conn.end<br/>接続終了"]
    
    style A fill:#e3f2fd
    style G fill:#ffebee
    style J fill:#ffebee
    style M fill:#ffebee
    style P fill:#c8e6c9
Loading

Sources: php-gm-server/src/GeminiServer.php:44-82

2. RequestParser(リクエスト解析)

RequestParserクラスは、受信したGeminiプロトコルリクエストを解析し、ホスト名とパスを抽出します。

解析ルール

  • Gemini URIスキーム(gemini://)の確認
  • 最大長チェック(1024バイト)
  • ホスト名の抽出
  • パスの正規化(デフォルト値:/
// RequestParser.php:14-47の抜粋
public static function parse(string $raw): ?array
{
    // リクエスト行を\r\nで分割
    $line = explode("\r\n", $raw, 2)[0];

    // 長さチェック
    if ($line === '' || strlen($line) > self::MAX_REQUEST_LENGTH) {
        return null;
    }

    // URL解析
    $parts = parse_url($line);
    
    // スキーム検証
    $scheme = $parts['scheme'] ?? '';
    if ($scheme !== 'gemini') {
        return null;
    }

    // ホスト・パス抽出
    $host = $parts['host'] ?? '';
    $path = $parts['path'] ?? '/';
    
    return ['host' => $host, 'path' => $path];
}

Sources: php-gm-server/src/RequestParser.php:7-48

検証項目 条件 失敗時の処理
リクエスト長 ≤ 1024バイト null返却
スキーム gemini のみ null返却
ホスト名 空でない null返却
パス デフォルト / 正規化

3. StaticFileHandler(ファイル配信)

StaticFileHandlerクラスは、リクエストパスに対応するファイルをドキュメントルートから解決し、MIME型を判定します。パストラバーサル攻撃を防ぐため、realpath()による正規化とドキュメントルート内の検証を行います。

ファイル解決プロセス

// StaticFileHandler.php:35-68の抜粋
public function handle(string $path): ?array
{
    // ファイルパスの結合
    $filePath = $this->docRoot . $path;

    // ディレクトリの場合、index.gmiを追加
    if (is_dir($filePath)) {
        $filePath = rtrim($filePath, '/') . '/index.gmi';
    }

    // realpath()による正規化
    $realPath = realpath($filePath);
    if ($realPath === false) {
        return null;
    }

    // パストラバーサル攻撃防止:ドキュメントルート内チェック
    if ($realPath !== $this->docRoot && !str_starts_with($realPath, $this->docRoot . DIRECTORY_SEPARATOR)) {
        return null;
    }

    // ファイル存在と読み込み
    if (!is_file($realPath)) {
        return null;
    }

    $body = file_get_contents($realPath);

    // 拡張子からMIME型を判定
    $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
    $mime = self::MIME_MAP[$ext] ?? 'application/octet-stream';

    return ['mime' => $mime, 'body' => $body];
}

Sources: php-gm-server/src/StaticFileHandler.php:35-68

graph TD
    A["パス受信<br/>例: /document.gmi"] --> B["ドキュメントルートと結合<br/>docRoot + path"]
    B --> C{"ディレクトリ?"}
    C -->|Yes| D["index.gmiを追加<br/>path/index.gmi"]
    C -->|No| E["ファイルパス確定"]
    D --> E
    E --> F["realpath()実行<br/>正規化・シンボリック解決"]
    F --> G{"存在?"}
    G -->|No| H["null返却"]
    G -->|Yes| I{"ドキュメント<br/>ルート内?"}
    I -->|No| H
    I -->|Yes| J{"ファイル?"}
    J -->|No| H
    J -->|Yes| K["file_get_contents<br/>ファイル読み込み"]
    K --> L["拡張子判定<br/>MIME型決定"]
    L --> M["mime + body<br/>返却"]
    
    style M fill:#c8e6c9
    style H fill:#ffebee
Loading

Sources: php-gm-server/src/StaticFileHandler.php:7-69

MIME型マッピング

StaticFileHandlerは拡張子に基づいてMIME型を判定します。対応する拡張子とMIME型は以下の通りです:

拡張子 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
pdf application/pdf
その他 application/octet-stream

Sources: php-gm-server/src/StaticFileHandler.php:11-25

4. ResponseBuilder(レスポンス構築)

ResponseBuilderクラスは、Geminiプロトコル仕様に従ったレスポンスを構築します。ステータスコード、META情報、ボディを含むレスポンス文字列を生成します。

// ResponseBuilder.php:1-23の抜粋
class ResponseBuilder
{
    public static function success(string $mime, string $body): string
    {
        return "20 {$mime}\r\n{$body}";
    }

    public static function notFound(): string
    {
        return "51 Not found\r\n";
    }

    public static function badRequest(string $reason = 'Bad request'): string
    {
        return "59 {$reason}\r\n";
    }
}

Sources: php-gm-server/src/ResponseBuilder.php:1-23

メソッド ステータス 用途
success() 20 ファイル配信成功
notFound() 51 ファイル未検出
badRequest() 59 リクエスト形式不正

レスポンス形式

Geminiプロトコルのレスポンス形式:

STATUS SPACE META\r\n
BODY
  • STATUS:1~2桁の数値(例:20、51、59)
  • META:1024バイト以下の情報(例:MIME型、エラーメッセージ)
  • BODY:ステータス20の場合、ファイル本体を含む

5. CertificateGenerator(TLS証明書管理)

CertificateGeneratorクラスは、OpenSSL拡張を用いて自己署名証明書を自動生成・管理します。初回起動時に証明書が生成され、以降は再利用されます。

// CertificateGenerator.php:16-50の抜粋
public function generate(string $hostname): string
{
    $pemPath = $this->certDir . '/server.pem';

    // 既存証明書の確認
    if (file_exists($pemPath)) {
        return $pemPath;
    }

    // ディレクトリ作成
    if (!is_dir($this->certDir)) {
        mkdir($this->certDir, 0755, true);
    }

    // RSA 2048ビット秘密鍵生成
    $privateKey = openssl_pkey_new([
        'private_key_bits' => 2048,
        'private_key_type' => OPENSSL_KEYTYPE_RSA,
    ]);

    // CSR(証明書署名要求)生成
    $dn = ['commonName' => $hostname];
    $csr = openssl_csr_new($dn, $privateKey);

    // 自己署名証明書生成(365日有効)
    $cert = openssl_csr_sign($csr, null, $privateKey, 365);

    // PEM形式でエクスポート
    $certPem = '';
    openssl_x509_export($cert, $certPem);

    $keyPem = '';
    openssl_pkey_export($privateKey, $keyPem);

    // ファイルに保存(パーミッション:0600)
    file_put_contents($pemPath, $certPem . $keyPem);
    chmod($pemPath, 0600);

    return $pemPath;
}

Sources: php-gm-server/src/CertificateGenerator.php:16-50

証明書生成パラメータ

  • 暗号化方式:RSA
  • 鍵長:2048ビット
  • 有効期間:365日(1年)
  • 署名方式:自己署名(CA署名なし)

データフロー

全体的なデータフロー

sequenceDiagram
    participant Client as "Geminiクライアント"
    participant Loop as "Event Loop"
    participant Server as "GeminiServer"
    participant Parser as "RequestParser"
    participant Handler as "StaticFileHandler"
    participant Builder as "ResponseBuilder"
    
    Client->>Server: TLS接続
    activate Server
    
    Client->>Server: リクエスト送信<br/>gemini://host/path\r\n
    
    Server->>Server: dataイベント<br/>バッファに蓄積
    Server->>Server: \\r\\n検出
    
    Server->>Parser: parse(buffer)
    activate Parser
    Parser->>Parser: URL解析
    Parser-->>Server: {host, path}
    deactivate Parser
    
    Server->>Handler: handle(path)
    activate Handler
    Handler->>Handler: ドキュメントルート+パス結合
    Handler->>Handler: realpath()正規化
    Handler->>Handler: パストラバーサル検証
    Handler->>Handler: ファイル読み込み
    Handler->>Handler: MIME型判定
    Handler-->>Server: {mime, body}
    deactivate Handler
    
    Server->>Builder: success(mime, body)
    activate Builder
    Builder-->>Server: "20 mime\r\nbody"
    deactivate Builder
    
    Server->>Server: conn.write(response)
    Server->>Client: レスポンス送信<br/>TLS暗号化
    
    Server->>Server: conn.end()
    deactivate Server
    
    Client->>Client: レスポンス受信・処理
Loading

Sources: php-gm-server/src/GeminiServer.php:44-82

非同期I/O活用戦略

React PHPの非同期パターン

本システムはReact PHPの以下の非同期パターンを採用しており、複数接続を効率的に処理できます:

graph LR
    subgraph "同期型処理"
        A["リクエスト1受信"] -->|ブロッキング| B["ファイル読み込み"]
        B -->|ブロッキング| C["レスポンス1送信"]
        C -->|待機中| D["リクエスト2受信不可"]
    end
    
    subgraph "非同期型処理\n(React PHP)"
        E["リクエスト1受信"] -->|非同期| F["ファイル読み込み\n(バックグラウンド)"]
        E -->|即座に切り替え| G["リクエスト2受信"]
        G -->|非同期| H["ファイル読み込み\n(バックグラウンド)"]
        F -->|完了| I["レスポンス1送信"]
        H -->|完了| J["レスポンス2送信"]
    end
    
    style A fill:#c8e6c9
    style E fill:#c8e6c9
    style I fill:#c8e6c9
    style J fill:#c8e6c9
Loading

イベントドリブンな接続処理

GeminiServerは各接続に対して以下のイベントを登録し、非同期で処理します:

// GeminiServer.php:44-82の接続ハンドラー(簡略版)
$server->on('connection', function (ConnectionInterface $conn) {
    $buffer = '';
    
    // データ受信イベント
    $conn->on('data', function (string $data) use ($conn, &$buffer) {
        // バッファ処理、パース、ファイル配信、レスポンス送信
        // この処理は他の接続に影響を与えない
    });
    
    // エラーイベント
    $conn->on('error', function (\Throwable $e) {
        // エラーハンドリング
    });
});

Sources: php-gm-server/src/GeminiServer.php:44-82

ファイル読み込みの非同期化

現在の実装ではfile_get_contents()を同期的に使用していますが、React PHPでは以下のような非同期ファイル読み込みが可能です:

  • React\Filesystem パッケージを使用した非同期ファイル操作
  • Promise ベースの処理フロー
  • 大規模ファイル配信時のストリーミング対応

これにより、複数クライアントからの大規模ファイルリクエストが相互に干渉しない処理が実現できます。

セキュリティ設計

TLS/SSL通信

本システムはReact PHPのSecureServerを用いてすべての通信をTLS/SSLで暗号化します。

// GeminiServer.php:35-40の初期化
$tcp = new TcpServer("{$this->host}:{$this->port}", $loop);
$server = new SecureServer($tcp, $loop, [
    'local_cert' => $this->certPath,
    'allow_self_signed' => true,
    'verify_peer' => false,
]);

Sources: php-gm-server/src/GeminiServer.php:35-40

設定項目 意味
local_cert PEMファイルパス サーバー証明書・秘密鍵
allow_self_signed true 自己署名証明書を許可
verify_peer false クライアント証明書検証なし

パストラバーサル防止

StaticFileHandlerはrealpath()による正規化とドキュメントルート内チェックで、パストラバーサル攻撃を防止します。

// StaticFileHandler.php:43-50の検証
$realPath = realpath($filePath);
if ($realPath === false) {
    return null;
}

// ドキュメントルート内チェック
if ($realPath !== $this->docRoot && !str_starts_with($realPath, $this->docRoot . DIRECTORY_SEPARATOR)) {
    return null;
}

Sources: php-gm-server/src/StaticFileHandler.php:43-50

リクエスト長制限

GeminiプロトコルのリクエストサイズはGemini仕様により1024バイトに制限されています。本システムは複数箇所でこれを検証します。

// RequestParser.php:9, 18
private const MAX_REQUEST_LENGTH = 1024;

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

Sources: php-gm-server/src/RequestParser.php:9-19

GeminiServerもバッファ長チェックで、バッファオーバーフロー時に即座にエラーを返します。

// GeminiServer.php:55-59
if (strlen($buffer) > 1024) {
    $conn->write(ResponseBuilder::badRequest('Request too long'));
    $conn->end();
}

Sources: php-gm-server/src/GeminiServer.php:55-59

設計パターン

責任の分離(Single Responsibility Principle)

各コンポーネントは明確な責任を持ち、以下の原則に従います:

コンポーネント 責任 依存度
GeminiServer ネットワーク層、接続管理、イベント駆動 RequestParser, StaticFileHandler, ResponseBuilder
RequestParser Geminiリクエスト形式の解析 なし(静的メソッド)
StaticFileHandler ファイルシステムアクセス、MIME型判定 なし
ResponseBuilder Geminiレスポンス形式の構築 なし(静的メソッド)
CertificateGenerator TLS証明書の生成・管理 なし

Null返却パターン

エラーハンドリングは例外ではなく null 返却で実装されています。これにより:

  • パース失敗時:RequestParser::parse() → null
  • ファイル未検出時:StaticFileHandler::handle() → null
  • 証明書読み込み失敗時:自動再生成

これは軽量で予測可能なエラーハンドリングを実現しています。

設定の外部化

エントリーポイントbin/server.phpでは、環境変数により設定を外部化しています:

// bin/server.php:11-15
$host = getenv('GEMINI_HOST') ?: '0.0.0.0';
$port = (int) (getenv('GEMINI_PORT') ?: 1965);
$docRoot = getenv('GEMINI_DOC_ROOT') ?: __DIR__ . '/../content';
$certDir = getenv('GEMINI_CERT_DIR') ?: __DIR__ . '/../certs';
$hostname = getenv('GEMINI_HOSTNAME') ?: 'localhost';

Sources: php-gm-server/bin/server.php:11-15

これにより、コード変更なしにサーバーの動作を制御できます。

依存パッケージと外部統合

コア依存パッケージ

本システムの外部依存は最小限に抑えられています:

{
    "require": {
        "php": ">=8.1",
        "react/socket": "^1.16",
        "react/event-loop": "^1.5"
    }
}

Sources: php-gm-server/composer.json:5-8

パッケージ 用途 主要クラス
react/socket TCP/TLSソケット通信 TcpServer, SecureServer
react/event-loop イベントループ駆動 Loop

外部との統合ポイント

graph TB
    PHP["PHP 8.1+"]
    OpenSSL["OpenSSL拡張<br/>証明書生成"]
    FileSystem["ファイルシステム<br/>ドキュメント配信"]
    Network["ネットワークスタック<br/>TLS/SSL"]
    
    GeminiServer -->|"OpenSSL関数"| OpenSSL
    GeminiServer -->|"file_get_contents()"| FileSystem
    GeminiServer -->|"TcpServer/SecureServer"| Network
    GeminiServer -->|"PHP実行環境"| PHP
    
    style OpenSSL fill:#fff3e0
    style FileSystem fill:#f1f8e9
    style Network fill:#f3e5f5
Loading

Sources: php-gm-server/composer.json

処理効率と最適化

バッファ管理

GeminiServerはリクエストライン終了(\r\n検出)までバッファにデータを蓄積します。一度完全なリクエストが検出されると、バッファの先頭1行のみを解析対象とします。

// RequestParser.php:16
$line = explode("\r\n", $raw, 2)[0];  // 最初の行のみ抽出

Sources: php-gm-server/src/RequestParser.php:16

ファイルハンドリング

StaticFileHandlerはファイル情報キャッシング等を行わず、各リクエストでファイルシステムにアクセスしています。これは実装の簡潔さを優先した設計です。本番環境では以下の最適化が検討できます:

  • ファイルメタデータのキャッシング(stat情報)
  • ディレクトリリスト生成キャッシング
  • メモリマップによる大規模ファイル配信

システムの拡張性

コンポーネント間の疎結合設計

GeminiServerのコンストラクタでStaticFileHandlerを依存性注入することで、実装を容易に差し替えられます:

// GeminiServer.php:19-24
public function __construct(
    string $host,
    int $port,
    StaticFileHandler $fileHandler,
    string $certPath,
) {
    // ...
}

Sources: php-gm-server/src/GeminiServer.php:19-24

これにより、例えば以下のようなカスタム実装に置き換えることが可能です:

  • データベースからのコンテンツ配信
  • 動的コンテンツ生成
  • ロードバランシング機能

新規プロトコル対応

RequestParserとResponseBuilderをプロトコル別に拡張することで、Gemini以外のプロトコルに対応できます:

GenericProtocolServer
├── GeminiRequestParser / GeminiResponseBuilder
├── CustomRequestParser / CustomResponseBuilder
└── ...

Related Pages

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