ファイル配信システム詳細 - ha1t/php-gm-server GitHub Wiki
このページは、PHP GM Server のファイル配信システムの実装詳細を説明します。クライアントからのリクエストを受け取ってからファイルをレスポンスとして送信するまでの完全なフロー、MIME タイプの自動判定、ディレクトリ解決メカニズム、セキュリティ対策を網羅しています。GeminiServerコンポーネントとStaticFileHandlerコンポーネントの詳細な動作仕様の補足として機能します。
ファイル配信システムは、GeminiServer のリクエストハンドラーから StaticFileHandler へのリクエスト処理、返されたファイル情報に基づく ResponseBuilder でのレスポンス構築という 2 段階で実現されます。
以下の流れでファイルが配信されます:
sequenceDiagram
participant Client as クライアント
participant Server as GeminiServer
participant Parser as RequestParser
participant Handler as StaticFileHandler
participant Builder as ResponseBuilder
Client->>Server: TLS 接続確立
Client->>Server: リクエスト送信<br/>(gemini://host/path\r\n)
Server->>Server: バッファに蓄積
Server->>Server: \r\n 検出
Server->>Parser: parse(buffer)
Parser-->>Server: {host, path}
Server->>Handler: handle(path)
Handler-->>Server: {mime, body} or null
alt ファイル存在
Server->>Builder: success(mime, body)
Builder-->>Server: "20 mime\r\nBODY"
else ファイル不在
Server->>Builder: notFound()
Builder-->>Server: "51 Not found\r\n"
end
Server-->>Client: Gemini レスポンス
Server->>Server: 接続終了
Sources: php-gm-server/src/GeminiServer.php:44-82
StaticFileHandler.handle() メソッドは、以下の処理順序でファイルを解決します:
flowchart TD
A["handle(path) 呼び出し"] --> B["ドキュメントルート + path<br/>を結合"]
B --> C{"ディレクトリ?"}
C -->|はい| D["末尾の / を削除<br/>+ /index.gmi 追加"]
C -->|いいえ| E["ファイルパス確定"]
D --> E
E --> F["realpath() で正規化<br/>&シンボリックリンク解決"]
F --> G{"realpath に成功?"}
G -->|失敗| H["null を返却<br/>ファイル不在"]
G -->|成功| I{"パストラバーサル<br/>チェック"}
I -->|ドキュメント<br/>ルート外| H
I -->|OK| J{"ファイル存在?"}
J -->|いいえ| H
J -->|はい| K["ファイル内容読み込み<br/>file_get_contents()"]
K --> L{"読み込み成功?"}
L -->|失敗| H
L -->|成功| M["拡張子から<br/>MIME タイプ判定"]
M --> N["MIME_MAP で検索"]
N --> O{"マップに存在?"}
O -->|はい| P["マッピング済み MIME"]
O -->|いいえ| Q["application/octet-stream"]
P --> R["{mime, body} を返却"]
Q --> R
Sources: php-gm-server/src/StaticFileHandler.php:35-68
StaticFileHandler は拡張子とMIMEタイプのマッピングテーブル(MIME_MAP)を内部で保持します。以下の拡張子に対してはマッピングが定義されており、それ以外の拡張子ではデフォルトの application/octet-stream が使用されます。
| 拡張子 | MIME タイプ | 説明 |
|---|---|---|
.gmi, .gemini
|
text/gemini |
Gemini テキスト形式 |
.txt |
text/plain |
プレーンテキスト |
.html |
text/html |
HTML ドキュメント |
.css |
text/css |
CSS スタイルシート |
.js |
text/javascript |
JavaScript |
.json |
application/json |
JSON データ |
.png |
image/png |
PNG 画像 |
.jpg, .jpeg
|
image/jpeg |
JPEG 画像 |
.gif |
image/gif |
GIF 画像 |
.svg |
image/svg+xml |
SVG 画像 |
.pdf |
application/pdf |
PDF ドキュメント |
Sources: php-gm-server/src/StaticFileHandler.php:11-25
$ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
$mime = self::MIME_MAP[$ext] ?? 'application/octet-stream';拡張子は小文字に正規化され、MIME_MAP で検索されます。Sources: php-gm-server/src/StaticFileHandler.php:61-62
クライアントがディレクトリパスにアクセスした場合(例:/about/、/)、そのディレクトリ内に存在する index.gmi ファイルが自動的に配信されます。
StaticFileHandler.handle() は、渡されたパスがディレクトリかどうかを is_dir() で判定し、ディレクトリの場合は以下の処理を実施します:
if (is_dir($filePath)) {
$filePath = rtrim($filePath, '/') . '/index.gmi';
}処理順序:
- ドキュメントルートとリクエストパスを結合
-
is_dir()でディレクトリ判定 - ディレクトリなら末尾の
/を削除後に/index.gmiを追加 - 通常のファイル解決フローに進行
Sources: php-gm-server/src/StaticFileHandler.php:39-41
| リクエストパス | 配信ファイル | MIME タイプ |
|---|---|---|
/ |
DOCROOT/index.gmi |
text/gemini |
/about/ |
DOCROOT/about/index.gmi |
text/gemini |
/sub/ |
DOCROOT/sub/index.gmi |
text/gemini |
index.gmi が存在しない場合、ファイル不在(ステータスコード 51)として処理されます。
Sources: php-gm-server/tests/StaticFileHandlerTest.php:48-61
マッピングテーブルに含まれない拡張子のファイルに対しては、デフォルト MIME タイプ application/octet-stream が適用されます。これは、MIME タイプが不明なバイナリデータを示すタイプであり、クライアント側でのダウンロード処理を促す意図があります。
$mime = self::MIME_MAP[$ext] ?? 'application/octet-stream';例えば、.exe、.zip、.bin など、MIME_MAP に未登録の拡張子を持つファイルはすべて application/octet-stream として配信されます。
Sources: php-gm-server/src/StaticFileHandler.php:62
ファイル不在または読み込み失敗時は、StaticFileHandler.handle() は null を返却します。この null を受け取った GeminiServer は、ResponseBuilder.notFound() でステータスコード 51 のレスポンスを生成します。
以下のいずれかの条件に該当した場合、ファイル不在として処理されます:
- realpath() 失敗:ファイルまたはディレクトリが存在しない場合
- パストラバーサル検出:解決後のパスがドキュメントルート外にある場合
- 非ファイルエスケープ:ディレクトリ、シンボリックリンク、その他の特殊ファイルの場合
- ファイル読み込み失敗:ファイルが存在しても読み込み権限がない、または読み込み中にエラーが発生した場合
StaticFileHandler は、realpath() で正規化したパスが、ドキュメントルート内に含まれているかを厳密にチェックします:
$realPath = realpath($filePath);
if ($realPath === false) {
return null;
}
if ($realPath !== $this->docRoot && !str_starts_with($realPath, $this->docRoot . DIRECTORY_SEPARATOR)) {
return null;
}この実装により、/../../../etc/passwd のようなパストラバーサル攻撃は確実に防止されます。
Sources: php-gm-server/src/StaticFileHandler.php:43-50
ファイル不在時は、以下の形式でレスポンスが返却されます:
51 Not found
Sources: php-gm-server/src/ResponseBuilder.php:14-16
ファイル不在またはその他のエラーが発生した場合、GeminiServer は適切なステータスコードでレスポンスを送信した直後に、接続(ConnectionInterface)を終了します。
if ($result === null) {
$conn->write(ResponseBuilder::notFound());
$conn->end();
return;
}Sources: php-gm-server/src/GeminiServer.php:74-78
Gemini プロトコルの仕様により、リクエスト行は最大 1024 バイトに制限されています。この制限を超えるリクエストは、ステータスコード 59(Bad Request)で拒否されます。
if (strlen($buffer) > 1024) {
$conn->write(ResponseBuilder::badRequest('Request too long'));
$conn->end();
}Sources: php-gm-server/src/GeminiServer.php:55-58
サーバー起動時に GEMINI_DOC_ROOT 環境変数が指定されない場合、デフォルトドキュメントルートは __DIR__ . '/../content' に設定されます。この content ディレクトリ内には、index.gmi などのコンテンツが配置されます。
$docRoot = getenv('GEMINI_DOC_ROOT') ?: __DIR__ . '/../content';
$fileHandler = new StaticFileHandler($docRoot);Sources: php-gm-server/bin/server.php:13, 21
- GeminiServerコンポーネント — サーバーの接続ハンドリングとリクエスト処理の詳細
- StaticFileHandlerコンポーネント — ファイル解決の実装詳細
- ResponseBuilderコンポーネント — Gemini レスポンス形式の構築
- Geminiプロトコル仕様実装 — Gemini プロトコルのステータスコード仕様
- セキュリティ設計 — パストラバーサル対策の詳細
- 環境変数設定リファレンス — GEMINI_DOC_ROOT などの設定方法