アーキテクチャ設計 - 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
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
Sources: php-gm-server/src/GeminiServer.php:31-34
GeminiServerクラスは、Geminiプロトコルサーバーの中核を担当します。React PHPのTcpServerとSecureServerを組み合わせ、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は接続ごとに以下の処理を実行します:
- 接続初期化:各接続に対してバッファを初期化
-
データ蓄積:
dataイベントでデータをバッファに追加 -
リクエスト完全性判定:
\r\nの検出でリクエスト完全と判断 - 長さ検証:1024バイト超過時は即座にエラー応答
- 下流処理: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
Sources: php-gm-server/src/GeminiServer.php:44-82
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返却 |
| パス | デフォルト /
|
正規化 |
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
Sources: php-gm-server/src/StaticFileHandler.php:7-69
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 |
| application/pdf | |
| その他 | application/octet-stream |
Sources: php-gm-server/src/StaticFileHandler.php:11-25
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の場合、ファイル本体を含む
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: レスポンス受信・処理
Sources: php-gm-server/src/GeminiServer.php:44-82
本システムは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
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 ベースの処理フロー
- 大規模ファイル配信時のストリーミング対応
これにより、複数クライアントからの大規模ファイルリクエストが相互に干渉しない処理が実現できます。
本システムは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
各コンポーネントは明確な責任を持ち、以下の原則に従います:
| コンポーネント | 責任 | 依存度 |
|---|---|---|
| GeminiServer | ネットワーク層、接続管理、イベント駆動 | RequestParser, StaticFileHandler, ResponseBuilder |
| RequestParser | Geminiリクエスト形式の解析 | なし(静的メソッド) |
| StaticFileHandler | ファイルシステムアクセス、MIME型判定 | なし |
| ResponseBuilder | Geminiレスポンス形式の構築 | なし(静的メソッド) |
| CertificateGenerator | TLS証明書の生成・管理 | なし |
エラーハンドリングは例外ではなく 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
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
└── ...
- プロジェクト概要 — PHP GM Serverの概要と技術スタック
- Geminiプロトコル仕様実装 — Geminiプロトコル仕様の詳細
- GeminiServerコンポーネント — GeminiServerクラスの詳細
- RequestParserコンポーネント — RequestParserクラスの詳細
- ResponseBuilderコンポーネント — ResponseBuilderクラスの詳細
- StaticFileHandlerコンポーネント — StaticFileHandlerクラスの詳細
- CertificateGeneratorコンポーネント — CertificateGeneratorクラスの詳細
- セキュリティ設計 — セキュリティ対策の詳細
- TLS証明書管理 — TLS証明書管理の詳細
- リクエスト処理フロー — リクエスト処理の完全フロー
- レスポンス構築フロー — レスポンス構築の完全フロー
- 環境変数設定リファレンス — サーバー設定の詳細
- インストール・セットアップガイド — システムセットアップ方法