GeminiServerコンポーネント - ha1t/php-gm-server GitHub Wiki
GeminiServerコンポーネントは、php-gm-serverプロジェクトの中核を担うメインサーバークラスです。ReactPHPのイベントループを活用した非同期I/Oの実装により、TCP/TLS接続を管理し、クライアントからのGeminiリクエストを受け付けて処理フローを調整します。リクエストパース、ファイルハンドリング、レスポンス生成まで、一連の処理流を統括します。
GeminiServerクラスはシステムの司令塔として機能します。以下の責務を担当します:
-
TCPサーバーの初期化と起動:
TcpServerを生成してバインドし、指定ホスト・ポートでリッスン開始 -
TLS/SSL暗号化通信の実装:
SecureServerでTCPをラップし、自己署名証明書を用いたセキュアな通信を実現 - クライアント接続の受け入れと管理: 接続イベント、データイベント、エラーイベントのハンドラーを登録
-
バッファ管理: 不完全なリクエストデータを蓄積し、終端(
\r\n)検出時に処理開始 - リクエスト処理フローの調整: RequestParser、StaticFileHandler、ResponseBuilderを連携させて完全な処理フロー実現
- エラーハンドリング: リクエスト長超過、パース失敗、ファイル不在など各エラー条件に対応
ReactPHPのソケット実装では、ネットワーク通信層を段階的に構築します。GeminiServerはこの2層構造を採用しています。
TcpServer(TCP层)
↓ ラップ
SecureServer(TLS/SSL层)
↓ イベント
ConnectionInterface(クライアント接続)
TcpServer (React\Socket\TcpServer)は、OS レベルのTCPソケットをバインドし、インカミング接続を受け入れるための最下位層です。指定されたホストとポート(Gemini標準ポート1965)でリッスンし、新しい接続が確立されるたびにConnectionInterfaceオブジェクトを生成します。
SecureServer (React\Socket\SecureServer)は、TcpServerをラップして、暗号化通信レイヤーを追加します。OpenSSLを使用してTLSハンドシェイクを実行し、クライアント側の証明書検証は無効化(verify_peer: false)します。これはGemini仕様のTOFU(Trust On First Use)モデルに対応しています。
実装例:
$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: src/GeminiServer.php:35-40
GeminiServerはReact\EventLoop\Loop::get()で取得したグローバルイベントループに統合され、非同期の事象駆動型処理を実現します。
$loop = Loop::get();
// ...
$loop->run();このrun()呼び出しによって、イベントループがブロッキング状態に入り、登録されたすべてのコールバックを監視します。TcpServerもSecureServerも、このループ内で非同期的に動作し、複数のクライアント接続を同時処理できます。
Sources: src/GeminiServer.php:33,89
SecureServerのconnectionイベント時に、新規接続ごとにコールバック関数が実行されます。コールバック内で各接続に対するデータやエラーリスナーを登録します。
graph TD
A["SecureServer connection イベント発火"] -->|ConnectionInterface 生成| B["コールバック実行"]
B --> C["error イベント登録"]
B --> D["data イベント登録"]
C --> E["接続エラー検出時に出力"]
D --> F["受け取ったデータをバッファに蓄積"]
エラーハンドラー (connection.on('error')):
接続中に発生したいかなるエラー(ネットワークエラー、TLSハンドシェイク失敗など)をキャッチし、エラーメッセージを出力します。ハンドラー実行後は自動的に接続がクローズされます。
Sources: src/GeminiServer.php:44-47
クライアントからのデータは分割して到達することがあるため、GeminiServerはバッファを保有して段階的にデータを蓄積します。
$buffer = '';
$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;
}
// 以降の処理
});-
データ追加フェーズ:
dataイベントごとにバッファに新着データを+=で追記 -
完全性チェック:
\r\n(CRLF終端)の検出でリクエスト完全性を判定 -
長さチェック: リクエスト未完全かつ1024バイト超の場合、即座に
badRequestレスポンスを返却してコネクション終了 -
長さチェック合格時: リクエスト完全まで待機(
returnで再度dataイベント待機)
このバッファ戦略により、バッファオーバーフロー攻撃を防止し、リクエスト長制限(Gemini仕様)を強制できます。
Sources: src/GeminiServer.php:49-60
バッファに\r\nが検出されたら、リクエスト文字列をRequestParserに渡してパースします。
$request = RequestParser::parse($buffer);
if ($request === null) {
$conn->write(ResponseBuilder::badRequest('Invalid request'));
$conn->end();
return;
}
echo "Request: {$request['host']}{$request['path']}\n";RequestParserは以下の検証を実施します:
| 検証項目 | 詳細 |
|---|---|
| スキーム検証 |
geminiスキーム以外はnullを返却 |
| ホスト名抽出 | 空文字列の場合はnullを返却 |
| パス抽出 | パス未指定の場合は/をデフォルトとして設定 |
| 長さチェック | リクエスト長が1024バイト超の場合はnullを返却 |
Sources: src/RequestParser.php:14-47
パース成功後、リクエストパスをStaticFileHandlerに渡して、ファイル解決とMIMEタイプ判定を実施します。
$result = $this->fileHandler->handle($request['path']);
if ($result === null) {
$conn->write(ResponseBuilder::notFound());
$conn->end();
return;
}
$conn->write(ResponseBuilder::success($result['mime'], $result['body']));
$conn->end();StaticFileHandlerの処理フロー:
graph TD
A["パスを受け取る"] --> B["ドキュメントルート + パスを結合"]
B --> C{ディレクトリ?}
C -->|はい| D["パス末尾に index.gmi を追加"]
C -->|いいえ| E["realpath で正規化"]
D --> E
E --> F{パストラバーサル<br/>チェック}
F -->|失敗| G["null を返却"]
F -->|成功| H{ファイル存在?}
H -->|いいえ| G
H -->|はい| I["ファイル内容を読み込み"]
I --> J["拡張子から MIME タイプを判定"]
J --> K["mime と body を返却"]
パストラバーサル防止メカニズム:
StaticFileHandlerはrealpath()で完全パスを正規化した後、str_starts_with()でドキュメントルート配下であることを確認します。これにより、../を含むパス攻撃を検出・拒否できます。
$realPath = realpath($filePath);
if ($realPath !== $this->docRoot && !str_starts_with($realPath, $this->docRoot . DIRECTORY_SEPARATOR)) {
return null;
}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 |
| application/pdf |
Sources: src/StaticFileHandler.php:35-68
ファイルハンドラーから結果が返却されたら、ResponseBuilderでGeminiプロトコル形式のレスポンスを構築し、クライアント接続に書き込みます。
graph TD
A["ファイルハンドラーの結果"] --> B{結果判定}
B -->|見つからない| C["ResponseBuilder::notFound"]
C --> D["51 Not found を送信"]
B -->|成功| E["ResponseBuilder::success"]
E --> F["20 MIME を送信"]
F --> G["ボディを続続送信"]
D --> H["接続をクローズ"]
G --> H
Geminiレスポンス形式:
STATUS SPACE META\r\n
BODY
-
成功時(20):
20 text/gemini\r\n [ファイル内容] -
見つからない(51):
51 Not found\r\n -
不正なリクエスト(59):
59 Bad request\r\n
Sources: src/ResponseBuilder.php:9-22
GeminiServerは以下のエラー条件を処理します:
| エラー条件 | ステータスコード | 処理内容 |
|---|---|---|
| 接続エラー(TLSハンドシェイク失敗など) | なし | エラーメッセージ出力後、接続自動クローズ |
| リクエスト長超過(>1024バイト) | 59 | "Request too long" メッセージで応答、接続クローズ |
| リクエストパース失敗 | 59 | "Invalid request" メッセージで応答、接続クローズ |
| ファイル不在 | 51 | "Not found" で応答、接続クローズ |
すべてのエラーレスポンス送信後、$conn->end()で接続を明示的にクローズします。これにより、クライアント側でレスポンスの完全受信を認識できます。
Sources: src/GeminiServer.php:45-82
以下は、TLS接続確立からレスポンス送信まで、一連の処理を時系列で示したシーケンス図です。
sequenceDiagram
participant Client as クライアント<br/>(Gemini端末)
participant Server as GeminiServer<br/>(イベントループ)
participant Parser as RequestParser
participant Handler as StaticFileHandler
participant Builder as ResponseBuilder
Client->>Server: TLS接続確立<br/>(SecureServer処理)
Server->>Server: connection イベント発火<br/>バッファ初期化
Client->>Server: リクエスト送信<br/>(gemini://...)
Server->>Server: data イベント発火<br/>バッファに蓄積
Server->>Server: CRLF検出?
alt CRLF未検出
Server->>Server: 長さ > 1024?
alt YES
Server->>Client: 59 Request too long
Server->>Server: 接続クローズ
else NO
Server->>Server: 次の data を待機
end
else CRLF検出
Server->>Parser: parse(バッファ)
alt パース失敗
Parser-->>Server: null
Server->>Client: 59 Invalid request
Server->>Server: 接続クローズ
else パース成功
Parser-->>Server: {host, path}
Server->>Handler: handle(パス)
alt ファイル見つからない
Handler-->>Server: null
Server->>Client: 51 Not found
Server->>Server: 接続クローズ
else ファイル見つかった
Handler-->>Server: {mime, body}
Server->>Builder: success(mime, body)
Builder-->>Server: Geminiレスポンス
Server->>Client: 20 MIME\r\nBODY
Server->>Server: 接続クローズ
end
end
end
GeminiServerコンストラクタは以下のパラメータを受け取ります:
public function __construct(
string $host, // バインドするホストアドレス(デフォルト: 0.0.0.0)
int $port, // バインドするポート番号(デフォルト: 1965)
StaticFileHandler $fileHandler, // ファイルハンドラーインスタンス
string $certPath, // TLS証明書ファイルパス(PEM形式)
)初期化例(bin/server.php):
$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';
$certGenerator = new CertificateGenerator($certDir);
$certPath = $certGenerator->generate($hostname);
$fileHandler = new StaticFileHandler($docRoot);
$server = new GeminiServer($host, $port, $fileHandler, $certPath);
$server->run();Sources: bin/server.php:11-23
各クライアント接続に対して、バッファ変数$bufferがクロージャ内のuseスコープで管理されます。$conn->end()でコネクションがクローズされると、そのコネクション対応のクロージャもスコープ外となり、バッファメモリは自動的にガベージコレクションの対象となります。
$buffer = ''; // コネクションごとにスタック上に確保
$conn->on('data', function (string $data) use ($conn, &$buffer) {
// クロージャ内でバッファを管理
});
// $conn->end() 実行 → コネクション終了 → クロージャスコープ終了 → $buffer解放このアーキテクチャにより、長時間実行されるサーバープロセスでもメモリリークを防止できます。
- 非同期I/O: ReactPHPのイベントループにより、ブロッキング処理なしで複数クライアントを同時処理
-
バッファストリーミング: ファイル全体をメモリに読み込むため、大容量ファイル配信時のメモリ使用量は
file_get_contents()の制約を受ける -
パストラバーサル防止:
realpath()呼び出しでディスクI/O発生(セキュリティとのトレードオフ)
- リクエスト長上限: 1024バイト(Gemini仕様)
- TLS必須: 自己署名証明書のみサポート
- ホスト検証: デフォルト無効(TOFU モデル準拠)
- ファイル配信: メモリ読み込みのため、極度に大きなファイルは非効率
Sources: src/GeminiServer.php:1-91、composer.json:1-23
- プロジェクト概要 — php-gm-serverの全体像、技術スタック、5つのコアコンポーネント
- アーキテクチャ設計 — システム全体のコンポーネント構成、イベントループ駆動設計
- Geminiプロトコル仕様実装 — Geminiプロトコルのリクエスト・レスポンス形式、ステータスコード仕様
- RequestParserコンポーネント — Geminiリクエストのパース処理、検証ロジック
- ResponseBuilderコンポーネント — Geminiレスポンス形式の構築
- StaticFileHandlerコンポーネント — ファイル解決、MIMEタイプ判定、パストラバーサル防止
- TLS証明書管理 — 証明書自動生成、SecureServer設定
- エラーハンドリング戦略 — エラーレスポンス分類、ステータスコード使い分け
- リクエスト処理フロー — 接続からレスポンス送信までの完全フロー