From 0ac6edac9f97eef36653124d7b57d4b4da8cbc63 Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 6 Mar 2026 12:17:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=AF=8D=E8=89=A6=E3=82=B5=E3=83=BC?= =?UTF-8?q?=E3=83=90=E3=83=BC=20HTTP=20=E5=AE=9F=E8=A3=85=E3=82=92?= =?UTF-8?q?=E7=B0=A1=E7=B4=A0=E5=8C=96=20(UUID=20=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=83=AA=E4=B8=8D=E8=A6=81=E3=83=BB=E7=92=B0?= =?UTF-8?q?=E5=A2=83=E5=A4=89=E6=95=B0=E3=81=8B=E3=82=89=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E8=BE=BC=E3=82=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/mothership_server.dart | 291 +++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 bin/mothership_server.dart diff --git a/bin/mothership_server.dart b/bin/mothership_server.dart new file mode 100644 index 0000000..3612bab --- /dev/null +++ b/bin/mothership_server.dart @@ -0,0 +1,291 @@ +// Version: 1.0.0 +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +/// 母艦「お局様」用サーバーアプリケーション +/// +/// クライアント(販売アシスト 1 号)のハッシュチェーン監視、 +/// システムバックアップ、チャットリレーを管理するサービスです。 +/// +/// 環境変数(推奨:`.env` を使用): +/// - MOTHERSHIP_HOST: サーバーホスト(例:localhost / 0.0.0.0) +/// - MOTHERSHIP_PORT: ポート番号(例:8787) +/// - MOTHERSHIP_API_KEY: API キー(同期認証用) +/// - MOTHERSHIP_DATA_DIR: データ保存先ディレクトリ + +class MothershipServer { + // サーバー状態管理 + final statusFile = File('data/mothership/status.json'); + + // クライアントハッシュチェーンのステート + Map _clientStatuses = {}; + + // チャットメッセージキュー(永続化せず、メモリ保存) + List _chatQueue = []; + + /// サーバー起動処理 + Future start() async { + // データディレクトリの準備 + final dataDir = Directory(args['MOTHERSHIP_DATA_DIR'] ?? 'data/mothership'); + await dataDir.create(recursive: true); + + // チャットキューの読み込み(初期化) + _chatQueue = []; + + print('=== 母艦「お局様」サーバー起動 ==='); + print('ホスト:${args['MOTHERSHIP_HOST'] ?? '0.0.0.0'}:${args['MOTHERSHIP_PORT'] ?? '8787'}'); + print('API キー:${args['MOTHERSHIP_API_KEY']}'); + print('データディレクトリ:${dataDir.path}'); + + // 簡易な HTTP サーバーを起動(dart:io の httpServer 使用) + final server = await HttpServer.bind( + args['MOTHERSHIP_HOST'] ?? '0.0.0.0', + int.parse(args['MOTHERSHIP_PORT'] ?? '8787'), + ); + + print('サーバー起動完了。HTTP リッスン開始'); + + // 各接続に対してループ処理 + await for (final request in server.request) { + final response = switch (request.url.path) { + '/health' => _handleHealth(request), + '/status' => _handleStatus(request), + '/sync/heartbeat' => _handleHeartbeat(request), + '/chat/send' => _handleChatSend(request), + '/chat/pending' => _handleChatPending(request), + '/chat/ack' => _handleChatAck(request), + '/backup/drive' => _handleBackupDrive(request), + _ => _handleNotFound(request), + }; + + await request.response.write(response); + } + } + + /// ハンドラ:ヘルスチェック(GET /health) + String _handleHealth(HttpRequest request) { + return jsonEncode({ + 'status': 'ok', + 'timestamp': DateTime.now().toUtc().toIso8601String(), + 'clients_connected': _clientStatuses.length, + }); + } + + /// ハンドラ:ステータス一覧取得(GET /status) + String _handleStatus(HttpRequest request) { + final clients = >{}; + + _clientStatuses.forEach((clientId, clientData) { + clients[clientId] = { + 'id': clientData.id, + 'last_heartbeat': clientData.lastHeartbeat?.toIso8601String(), + 'hash_chain_root': clientData.hashChainRoot, + 'expiry_remaining_days': clientData.expiryRemainingDays, + }; + }); + + return jsonEncode({ + 'server_time': DateTime.now().toUtc().toIso8601String(), + 'clients': clients, + }); + } + + /// ハンドラ:ハートビート同期(POST /sync/heartbeat) + Future _handleHeartbeat(HttpRequest request) async { + final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map; + final apiKey = body['api_key'] as String?; + + if (apiKey != args['MOTHERSHIP_API_KEY']) { + return jsonEncode({ + 'error': '未認証', + 'status_code': 401, + }); + } + + final clientId = body['client_id'] as String?; + if (clientId == null) { + return jsonEncode({'error': 'client_id が不足'}); + } + + final now = DateTime.now().toUtc(); + _clientStatuses[clientId] = ClientData( + id: clientId, + lastHeartbeat: now, + hashChainRoot: body['hash_chain_root'] as String?, + expiryRemainingDays: body['expiry_remaining_days'] ?? 90, + ); + + // ステータスを永続化(JSON ファイルに保存) + await _persistStatus(); + + return jsonEncode({ + 'status': 'success', + 'client_id': clientId, + 'last_heartbeat': now.toIso8601String(), + 'expiry_warning': (_clientStatuses[clientId]?.expiryRemainingDays ?? 90) <= 30, + }); + } + + /// ハンドラ:チャット送信(POST /chat/send) + String _handleChatSend(HttpRequest request) { + final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map; + final apiKey = body['api_key'] as String?; + + if (apiKey != args['MOTHERSHIP_API_KEY']) { + return jsonEncode({ + 'error': '未認証', + 'status_code': 401, + }); + } + + final clientMessage = ChatMessage( + fromClient: body['client_id'] as String?, + message: body['message'] as String, + priority: body['priority'] as int? ?? 0, + ); + + _chatQueue.add(clientMessage); + + return jsonEncode({ + 'status': 'queued', + 'queue_length': _chatQueue.length, + }); + } + + /// ハンドラ:チャット未送信メッセージ取得(GET /chat/pending) + String _handleChatPending(HttpRequest request) { + // クライアントごとにキューをフィルタ + final clientQueues = >{}; + + for (final message in _chatQueue) { + if (message.fromClient != null) { + clientQueues.putIfAbsent(message.fromClient!, () => []).add(message); + } else { + // 非クライアント用メッセージはすべてのクライアントに配信 + final pending = clientQueues.values.fold([], (acc, curr) => [...acc, ...curr]); + if (pending.isNotEmpty) { + clientQueues['all_clients'] = pending; + } + } + } + + return jsonEncode({'queues': clientQueues}); + } + + /// ハンドラ:チャット送信確認(POST /chat/ack) + String _handleChatAck(HttpRequest request) { + final body = jsonDecode(utf8.decode(request.requestedBytes)) as Map; + final apiKey = body['api_key'] as String?; + + if (apiKey != args['MOTHERSHIP_API_KEY']) { + return jsonEncode({'error': '未認証'}); + } + + final acknowledgedIds = []; + _chatQueue.removeWhere((msg) => msg.id == body['message_id']); + + for (final msg in _chatQueue) { + acknowledgedIds.add(msg.id); + } + + return jsonEncode({ + 'status': 'acknowledged', + 'acknowledged_count': acknowledgedIds.length, + }); + } + + /// ハンドラ:バックアップドライブ確認(POST /backup/drive) + String _handleBackupDrive(HttpRequest request) { + final apiKey = body['api_key'] as String?; + + if (apiKey != args['MOTHERSHIP_API_KEY']) { + return jsonEncode({'error': '未認証'}); + } + + // 簡易的な Drive API 接続は後実装(ここに OAuth2 フローを想定) + return jsonEncode({ + 'status': 'backup_ready', + 'drive_quota_gb': 15, + 'used_space_gb': 0.5, + }); + } + + /// ハンドラ:未定義エンドポイント + String _handleNotFound(HttpRequest request) { + return jsonEncode({ + 'error': 'Not Found', + 'path': request.url.path, + 'method': request.method, + }, statusCode: HttpStatus.notFound); + } + + /// ステータスを永続化(JSON ファイル) + Future _persistStatus() async { + final now = DateTime.now().toUtc(); + final data = { + 'server_time': now.toIso8601String(), + 'clients': {}, + }; + + _clientStatuses.forEach((id, client) { + data['clients'][id] = { + 'last_heartbeat': client.lastHeartbeat?.toIso8601String(), + 'hash_chain_root': client.hashChainRoot, + 'expiry_remaining_days': client.expiryRemainingDays, + }; + }); + + final file = File(statusFile.path); + await file.writeAsString(jsonEncode(data)); + } +} + +/// クライアントデータ構造(簡易モデル) +class ClientData { + final String id; + final DateTime? lastHeartbeat; + final String? hashChainRoot; + final int expiryRemainingDays; + + ClientData({ + required this.id, + this.lastHeartbeat, + this.hashChainRoot, + required this.expiryRemainingDays, + }); +} + +/// チャットメッセージ(キュー単位) +class ChatMessage { + final String? fromClient; + final String message; + final int priority; + final String id; // ランダム文字列で生成 + + ChatMessage({ + this.fromClient, + required this.message, + this.priority = 0, + String? id, + }) : id = id ?? const String.fromCharCodes([0x4a, 0x61, 0x70, 0x2d, DateTime.now().millisecondsSinceEpoch % 1000]); +} + +void main(List args) async { + // 環境変数の取得(簡易的な実装) + final host = Platform.environment['MOTHERSHIP_HOST'] ?? '0.0.0.0'; + final port = Platform.environment['MOTHERSHIP_PORT'] ?? '8787'; + final apiKey = Platform.environment['MOTHERSHIP_API_KEY'] ?? 'TEST_MOTHERSHIP_KEY'; + final dataDir = Platform.environment['MOTHERSHIP_DATA_DIR'] ?? 'data/mothership'; + + print('環境変数取得完了:'); + print(' HOST: $host'); + print(' PORT: $port'); + print(' API_KEY: ${apiKey?.substring(0, min(apiKey!.length, 10))}...'); + + final server = MothershipServer(); + await server.start(); +} + +/// min 関数の簡易実装(標準ライブラリにないため) +int min(int a, int b) => a < b ? a : b; \ No newline at end of file