diff --git a/.gitignore b/.gitignore index 26f73ea1..a805ccb2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ /dena/ /denaN/ /denaL/ + +/backup/ diff --git a/build.gradle b/build.gradle index f92ab856..39a32e98 100644 --- a/build.gradle +++ b/build.gradle @@ -52,8 +52,8 @@ dependencies { implementation 'com.github.mobius-software-ltd:mqtt-parser:parser-1.0.3' implementation 'net.arnx:jsonic:1.3.0' implementation 'org.json:json:20180813' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.10.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.19.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.19.2' implementation 'org.msgpack:jackson-dataformat-msgpack:0.8.18' implementation 'org.bouncycastle:bcpkix-jdk15on:1.64' implementation 'commons-net:commons-net:3.6' diff --git a/doc/mcp-server-setting-guide.md b/doc/mcp-server-setting-guide.md new file mode 100644 index 00000000..bdc8b6d6 --- /dev/null +++ b/doc/mcp-server-setting-guide.md @@ -0,0 +1,256 @@ +# PacketProxy MCP サーバー設定ガイド + +## 概要 + +このガイドでは、PacketProxy MCP サーバーをClaude Desktopから使用するための設定方法を説明します。 + +**🔄 HTTP ベースアプローチ**: このガイドではHTTPベースのMCP接続を使用し、PacketProxy GUI内のMCPサーバーを直接利用します。これによりPythonの依存関係問題を回避できます。 + +## 前提条件 + +- PacketProxy がビルド済み (`./gradlew build` 実行済み) +- Claude Desktop がインストール済み +- Node.js がインストール済み (npxコマンド用) +- Java 17以降がインストール済み + +## 設定手順 + +### 1. PacketProxy GUIの起動とMCPサーバーの有効化 + +まず PacketProxy GUI を起動し、MCP サーバーを有効にします: + +```bash +# PacketProxyを起動 +java -jar build/libs/PacketProxy.jar +``` + +GUI起動後: +1. **Options** → **Extensions** を選択 +2. **MCP Server** を選択 +3. **Enable** にチェックを入れる +4. **Start Server** ボタンをクリック + +ログに以下が表示されることを確認: + +``` +MCP Server started +HTTP endpoint available at http://localhost:8765/mcp +``` + +### 2. アクセストークンの取得 + +PacketProxy GUIでアクセストークンを有効化し、トークンを取得します: + +1. **Options** → **Setting** を選択 +2. **Import/Export configs** セクションで **Enable** にチェックを入れる +3. 表示されたアクセストークンをコピーしておく + +### 3. Claude Desktop設定ファイルの編集 + +Claude Desktopの設定ファイルを編集します: + +```bash +# 設定ファイルの場所 +open ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +以下の内容を追加してください(`your_access_token_here`の部分を手順2で取得したアクセストークンに置き換えてください): + +```json +{ + "mcpServers": { + "packetproxy": { + "command": "node", + "args": [ + "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" + ], + "env": { + "PACKETPROXY_ACCESS_TOKEN": "your_access_token_here" + } + } + } +} +``` + +**既存の設定がある場合の例:** + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"] + }, + "packetproxy": { + "command": "node", + "args": [ + "/Users/kakira/PacketProxy/scripts/mcp-http-bridge.js" + ], + "env": { + "PACKETPROXY_ACCESS_TOKEN": "your_access_token_here" + } + } + } +} +``` + +### 4. Claude Desktopの再起動 + +設定を反映するためにClaude Desktopを完全に終了して再起動してください: + +1. Claude Desktop → Quit Claude +2. Claude Desktopを再起動 + +## 使用方法 + +### PacketProxyとClaude Desktopの連携テスト + +**前提条件:** +1. PacketProxy GUIが起動している +2. MCP Serverが有効化され、起動している +3. Claude Desktopが再起動済み + +Claude Desktopで新しい会話を開始し、以下を試してください: + +``` +PacketProxyのツールを使って、利用可能な機能を教えてください +``` + +**期待される応答:** +- `get_history`: パケット履歴の取得 +- `get_configs`: 設定情報の取得 +- `get_packet_detail`: パケット詳細情報の取得 + +### 実際のPacketProxy操作 + +PacketProxyでトラフィックをキャプチャした後、Claude Desktopから操作できます: + +``` +PacketProxyからパケット履歴を5件取得してください +``` + +``` +PacketProxyの設定情報を確認してください +``` + +``` +パケットID 1の詳細を取得してください +``` + +## トラブルシューティング + +### 設定確認 + +**1. 設定ファイルの確認** + +```bash +cat "/Users/kakira/Library/Application Support/Claude/claude_desktop_config.json" +# packetproxyの設定にnpxと@modelcontextprotocol/server-fetchが含まれていることを確認 +``` + +**2. PacketProxyのHTTPエンドポイント確認** + +```bash +# PacketProxy GUIでMCPサーバーが起動している場合 +curl -X POST http://localhost:8765/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' +``` + +**3. Node.jsとnpxの確認** + +```bash +node --version +npx --version +# 両方のコマンドがバージョンを返すことを確認 +``` + +### よくある問題と対処法 + +**1. Claude Desktopでツールが認識されない** +- Claude Desktopを完全に再起動 +- 設定ファイルのJSON構文を確認 +- PacketProxy GUIでMCPサーバーが起動していることを確認 + +**2. HTTP接続エラー** + +```bash +# PacketProxyのHTTPエンドポイントが応答するか確認 +curl -v http://localhost:8765/mcp + +# ポート8765が使用中か確認 +lsof -i :8765 +``` + +**3. Node.jsブリッジのテスト** + +```bash +# ブリッジが正常に動作するかテスト +echo '{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}}, "id": 0}' | node /Users/kakira/PacketProxy/scripts/mcp-http-bridge.js +``` + +**4. PacketProxy JARファイルが見つからない** + +```bash +# ビルドを実行 +cd /Users/kakira/PacketProxy +./gradlew build + +# JARファイルの存在確認 +ls -la build/libs/PacketProxy.jar +``` + +## 利用可能なツール + +### `get_history` + +PacketProxyのパケット履歴を取得します。 + +**パラメータ:** +- `limit` (integer, optional): 取得するパケット数 (デフォルト: 100) +- `offset` (integer, optional): オフセット (デフォルト: 0) + +**使用例:** + +``` +最新のパケット履歴を5件取得してください +``` + +### `get_configs` + +PacketProxyの設定情報を取得します。 + +**パラメータ:** +- `categories` (array, optional): 取得する設定カテゴリ + +**使用例:** + +``` +PacketProxyの現在の設定を確認してください +``` + +### `get_packet_detail` + +特定のパケットの詳細情報を取得します。 + +**パラメータ:** +- `packet_id` (integer, required): パケットID +- `include_body` (boolean, optional): ボディを含めるか (デフォルト: false) +- `include_pair` (boolean, optional): ペアパケットを含めるか (デフォルト: false) + +**使用例:** + +``` +パケットID 123の詳細情報を取得してください +``` + +## サポート + +問題が発生した場合は、以下を確認してください: + +1. PacketProxyのビルドが成功していること +2. Claude Desktopの設定ファイルが正しい形式であること +3. スクリプトファイルに実行権限があること +4. Python3が正常に動作すること + +詳細な技術仕様については `doc/mcp-server-spec.md` を参照してください。 diff --git a/doc/mcp-server-spec.md b/doc/mcp-server-spec.md new file mode 100644 index 00000000..099c9c2d --- /dev/null +++ b/doc/mcp-server-spec.md @@ -0,0 +1,1244 @@ +# PacketProxy MCP サーバー仕様書 + +## 概要 + +PacketProxy MCP サーバーは、Model Context Protocol (MCP) を使用してPacketProxyの機能を外部から操作するためのインターフェースを提供します。AIエージェントやその他のクライアントがPacketProxyのパケット履歴取得、設定変更、パケット再送などの操作を実行できます。 + +## 基本仕様 + +- **プロトコル**: MCP (Model Context Protocol) - JSON-RPC over stdin/stdout +- **補完API**: HTTP REST API (localhost:32350) +- **認証**: アクセストークン方式 +- **実装**: PacketProxy Extension として実装 + +## アーキテクチャ + +``` +┌─────────────────┐ JSON-RPC ┌─────────────────┐ +│ MCP Client │ ←─────────────→ │ MCP Extension │ +│ (Claude, etc) │ stdin/stdout │ │ +└─────────────────┘ └─────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ PacketProxy │ + │ Core APIs │ + └─────────────────┘ +``` + +## 認証 + +**すべてのMCPツール**は認証が必要です。各ツールの呼び出し時に`access_token`パラメータを指定する必要があります。 + +### アクセストークンの取得方法 + +1. PacketProxyの**Settings**タブを開く +2. **Import/Export configs (Experimental)**セクションを見つける +3. **Enabled**チェックボックスを有効にする +4. 自動生成された**AccessToken**をコピーする +5. MCPツール呼び出し時に`access_token`パラメータとして使用する + +### 環境変数による自動認証 (推奨) + +MCP HTTP Bridgeを使用する場合、環境変数にアクセストークンを設定することで、自動的に認証情報が追加されます: + +```bash +export PACKETPROXY_ACCESS_TOKEN="your_access_token_here" +``` + +この設定により、各ツール呼び出し時に手動で`access_token`パラメータを指定する必要がなくなります。 + +### 認証エラーの場合 + +- アクセストークンが未設定: PacketProxyでconfig sharingを有効にしてください +- アクセストークンが無効: Settings画面で正しいトークンを確認してください +- アクセストークンが空: 必須パラメータのため、必ず指定してください +- 環境変数が設定されていない場合: `PACKETPROXY_ACCESS_TOKEN`環境変数を確認してください + +## MCPツール一覧 + +### 1. `get_history` - パケット履歴取得 + +PacketProxyのパケット履歴を検索・取得します。フィルタリング、並び順指定、ページング機能を提供します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_history", + "arguments": { + "access_token": "your_access_token_here", + "limit": 100, + "offset": 0, + "filter": "method == GET && url =~ /api/", + "order": "time desc" + } + }, + "id": 1 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `limit` (number, optional): 取得件数 (デフォルト: 100) +- `offset` (number, optional): オフセット (デフォルト: 0) +- `filter` (string, optional): PacketProxy Filter構文による絞り込み +- `order` (string, optional): 並び順指定 (デフォルト: "id desc") +- 形式: `"カラム名 方向"` (例: `"time desc"`, `"length asc"`) +- 対応カラム: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status +- 方向: `asc` (昇順) または `desc` (降順) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "packets": [ + { + "id": 123, + "method": "GET", + "url": "/api/users", + "status": 200, + "length": 1024, + "time": "2025-01-15T10:30:00Z", + "server_name": "api.example.com", + "client_ip": "192.168.1.100" + } + ], + "total_count": 1500, + "has_more": true, + "filter_applied": "method == GET && url =~ /api/", + "order_applied": "time desc" + }, + "id": 1 +} +``` + +### 2. `get_packet_detail` - パケット詳細取得 + +特定のパケットの詳細情報を取得します。指定したパケットIDがリクエストの場合は対応するレスポンスも、レスポンスの場合は対応するリクエストも同時に返します。ペア取得機能は`include_pair`オプションで制御できます。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_packet_detail", + "arguments": { + "access_token": "your_access_token_here", + "packet_id": 123, + "include_body": true, + "include_pair": false + } + }, + "id": 2 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `packet_id` (number, required): パケットID(リクエストまたはレスポンスのどちらでも指定可能) +- `include_body` (boolean, optional): リクエスト/レスポンスボディを含める (デフォルト: true) +- `include_pair` (boolean, optional): ペアパケット(リクエスト指定時はレスポンス、レスポンス指定時はリクエスト)を含める (デフォルト: false) + +**レスポンス(ペアが見つかった場合):** + +```json +{ + "jsonrpc": "2.0", + "result": { + "paired": true, + "requested_packet_id": 123, + "group": 1001, + "conn": 5, + "request": { + "id": 123, + "direction": "client", + "method": "GET", + "url": "/api/users", + "version": "HTTP/1.1", + "headers": [ + {"name": "Host", "value": "api.example.com"}, + {"name": "User-Agent", "value": "Mozilla/5.0..."} + ], + "body": "", + "length": 256, + "time": "2025-01-15T10:30:00Z", + "resend": false, + "modified": false, + "type": "HTTP", + "encode": "HTTP", + "client": {"ip": "192.168.1.100", "port": 54321}, + "server": {"ip": "192.168.1.1", "port": 80} + }, + "response": { + "id": 124, + "direction": "server", + "status": 200, + "status_text": "OK", + "headers": [ + {"name": "Content-Type", "value": "application/json"}, + {"name": "Content-Length", "value": "1024"} + ], + "body": "{\"users\": [...]}", + "length": 1024, + "time": "2025-01-15T10:30:01Z", + "resend": false, + "modified": false, + "type": "HTTP", + "encode": "HTTP", + "client": {"ip": "192.168.1.100", "port": 54321}, + "server": {"ip": "192.168.1.1", "port": 80} + } + }, + "id": 2 +} +``` + +**レスポンス(ペアが見つからない場合):** + +```json +{ + "jsonrpc": "2.0", + "result": { + "paired": false, + "requested_packet_id": 123, + "group": 1001, + "conn": 5, + "request": { + "id": 123, + "direction": "client", + "method": "GET", + "url": "/api/users", + "version": "HTTP/1.1", + "headers": [ + {"name": "Host", "value": "api.example.com"}, + {"name": "User-Agent", "value": "Mozilla/5.0..."} + ], + "body": "", + "length": 256, + "time": "2025-01-15T10:30:00Z", + "resend": false, + "modified": false, + "type": "HTTP", + "encode": "HTTP", + "client": {"ip": "192.168.1.100", "port": 54321}, + "server": {"ip": "192.168.1.1", "port": 80} + }, + "response": null + }, + "id": 2 +} +``` + +**主な機能:** +- **ペア検索機能**: 指定されたパケットに対応するリクエスト/レスポンスを自動的に検索(`include_pair=true`の場合) +- **統一レスポンス形式**: リクエストIDを指定してもレスポンスIDを指定しても、同じ形式でリクエスト/レスポンス両方の詳細を返す +- **ペア情報**: `paired`フィールドでペアが見つかったかどうかを示す +- **詳細情報の追加**: 各パケットに`direction`(client/server)フィールドを追加 +- **接続情報**: `group`と`conn`フィールドでパケット間の関連性を示す +- **ペア取得制御**: `include_pair=false`を指定することで、指定したパケットのみを取得可能 + +### 3. `get_logs` - ログ取得 + +PacketProxyのログを取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_logs", + "arguments": { + "access_token": "your_access_token_here", + "level": "info", + "limit": 100, + "since": "2025-01-15T00:00:00Z", + "filter": "error|exception" + } + }, + "id": 3 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `level` (string, optional): ログレベル "debug" | "info" | "warn" | "error" +- `limit` (number, optional): 取得件数 (デフォルト: 100) +- `since` (string, optional): 開始時刻 (ISO 8601形式) +- `filter` (string, optional): 正規表現フィルタ + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "logs": [ + { + "timestamp": "2025-01-15T10:30:00Z", + "level": "info", + "message": "PacketProxy started successfully", + "thread": "main", + "class": "packetproxy.PacketProxy" + } + ], + "total_count": 1500, + "has_more": true + }, + "id": 3 +} +``` + +### 4. `get_config` - 設定情報取得 + +PacketProxyの設定情報をHTTP API (`http://localhost:32349/config`) 経由で取得します。PacketProxyHub互換の完全な設定形式で返されます。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_config", + "arguments": { + "categories": ["listenPorts", "servers"], + "access_token": "your_access_token_here" + } + }, + "id": 4 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `categories` (array, optional): 取得するカテゴリ (空の場合は全て) +- `listenPorts`: リッスンポート設定 +- `servers`: サーバー設定 +- `modifications`: 改変設定 +- `sslPassThroughs`: SSL パススルー設定 + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "content": [ + { + "type": "text", + "text": "{\"listenPorts\":[{\"id\":1,\"enabled\":true,\"ca_name\":\"PacketProxy per-user CA\",\"port\":8080,\"type\":\"HTTP_PROXY\",\"server_id\":1}],\"servers\":[{\"id\":1,\"ip\":\"target.com\",\"port\":443,\"encoder\":\"HTTPS\",\"use_ssl\":true,\"resolved_by_dns\":false,\"resolved_by_dns6\":false,\"http_proxy\":false,\"comment\":\"\",\"specifiedByHostName\":false}]}" + } + ] + }, + "id": 4 +} +``` + +### 5. `update_config` - 設定変更 + +Update PacketProxy configuration settings with complete configuration object. +IMPORTANT: Requires a complete configuration object, not partial updates. + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "update_config", + "arguments": { + "config_json": { + "listenPorts": [ + { + "id": 1, + "enabled": true, + "ca_name": "PacketProxy per-user CA", + "port": 8080, + "type": "HTTP_PROXY", + "server_id": 1 + } + ], + "servers": [ + { + "id": 1, + "ip": "target.com", + "port": 443, + "encoder": "HTTPS", + "use_ssl": true, + "resolved_by_dns": false, + "resolved_by_dns6": false, + "http_proxy": false, + "comment": "", + "specifiedByHostName": false + } + ], + "modifications": [ + { + "id": 1, + "enabled": true, + "server_id": 1, + "direction": "CLIENT_REQUEST", + "pattern": ".*", + "method": "SIMPLE", + "replaced": "X-Test: 1" + } + ], + "sslPassThroughs": [ + { + "id": 1, + "enabled": true, + "server_name": "secure.com", + "listen_port": 443 + } + ] + }, + "backup": true, + "suppress_dialog": false, + "access_token": "your_access_token_here" + } + }, + "id": 5 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `config_json` (object, required): PacketProxyHub-compatible configuration JSON containing COMPLETE configuration object. Must include all required arrays: listenPorts, servers, modifications, sslPassThroughs (can be empty arrays). Partial configurations will cause null pointer errors. Recommended workflow: 1) Call get_config() first, 2) Modify specific fields in the returned object, 3) Pass the entire modified object here. +- `backup` (boolean, optional): 既存設定をバックアップ (デフォルト: true) +- `suppress_dialog` (boolean, optional): 確認ダイアログを非表示にする (デフォルト: false) + +**重要な注意事項:** + +**完全な設定オブジェクトが必要:** +- `config_json`は部分的な設定ではなく、**完全な設定オブジェクト**である必要があります +- 以下の配列は必須です(空配列でも可): +- `listenPorts`: リッスンポート設定 +- `servers`: サーバー設定 +- `modifications`: 改変設定 +- `sslPassThroughs`: SSL パススルー設定 +- 部分的な設定を渡すとnull pointerエラーが発生します + +**推奨ワークフロー:** +1. 最初に`get_config()`を呼び出して現在の完全な設定を取得 +2. 取得した設定オブジェクトの特定のフィールドを変更 +3. 変更した完全なオブジェクトを`update_config()`に渡す + +**設定削除について:** +- `config_json`に含まれないIDの項目は自動的に削除されます +- 例: serversに`id:1`のみ含まれている場合、`id:2,3...`のサーバーは削除されます +- HTTP APIは既存設定を完全に置き換える方式で動作します + +**ダイアログ制御について:** +- `suppress_dialog: false` (デフォルト): 設定上書き前に確認ダイアログを表示 +- `suppress_dialog: true`: 確認ダイアログを表示せずに自動的に設定を上書き +- ダイアログが表示される場合、ユーザーが「はい」を選択した場合のみ設定が適用されます +- ダイアログで「いいえ」を選択した場合、HTTP 401エラーが返されます + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "content": [ + { + "type": "text", + "text": "{\"success\": true, \"backup_created\": true, \"backup_info\": {\"backup_id\": \"backup_20250804_120000\", \"backup_path\": \"backup/backup_20250804_120000.json\", \"timestamp\": \"2025-08-04T12:00:00Z\"}, \"config_updated\": true}" + } + ] + }, + "id": 5 +} +``` + +### 6. `restore_config` - 設定バックアップ復元 + +指定したバックアップから設定を復元します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "restore_config", + "arguments": { + "access_token": "your_access_token_here", + "backup_id": "backup_20250115_103000", + "suppress_dialog": false + } + }, + "id": 6 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `backup_id` (string, required): 復元するバックアップID +- `suppress_dialog` (boolean, optional): 確認ダイアログを非表示にする (デフォルト: false) + +**ダイアログ制御について:** +- `suppress_dialog: false` (デフォルト): 設定復元前に確認ダイアログを表示 +- `suppress_dialog: true`: 確認ダイアログを表示せずに自動的に設定を復元 +- ダイアログが表示される場合、ユーザーが「はい」を選択した場合のみ設定が適用されます +- ダイアログで「いいえ」を選択した場合、HTTP 401エラーが返されます + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "content": [ + { + "type": "text", + "text": "{\"success\": true, \"backup_id_restored\": \"backup_20250115_103000\", \"config_restored\": true}" + } + ] + }, + "id": 6 +} +``` + +### 7. `resend_packet` - パケット再送 + +パケットを再送します。パケット改変や連続送信に対応しています。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "resend_packet", + "arguments": { + "packet_id": 123, + "count": 20, + "interval_ms": 100, + "modifications": [ + { + "target": "request", + "type": "regex_replace", + "pattern": "sessionId=\\w+", + "replacement": "sessionId=modified_{{index}}" + }, + { + "type": "header_add", + "name": "X-Test-Counter", + "value": "{{timestamp}}" + } + ], + "async": false, + "allow_duplicate_headers": false + } + }, + "id": 7 +} +``` + +**パラメータ:** +- `packet_id` (number, required): 再送するパケットID +- `count` (number, optional): 送信回数 (デフォルト: 1) +- `interval_ms` (number, optional): 送信間隔(ms) (デフォルト: 0) +- `modifications` (array, optional): パケット改変設定 +- `async` (boolean, optional): 非同期実行 (デフォルト: false) +- `allow_duplicate_headers` (boolean, optional): ヘッダー追加/変更時に重複を許可 (デフォルト: false) + +**改変設定:** +- `target`: "request" | "response" | "both" +- `type`: "regex_replace" | "header_add" | "header_modify" +- `pattern`: 正規表現パターン (regex_replaceの場合) +- `replacement` / `value`: 置換文字列 +- `name`: ヘッダー名 (header_add/header_modifyの場合) + +**ヘッダー重複制御:** +- `allow_duplicate_headers=false` (デフォルト): 同名ヘッダーが存在する場合は既存を置換 +- `allow_duplicate_headers=true`: 同名ヘッダーが存在していても新しいヘッダーを追加 + +**置換変数:** +- `{{index}}`: 送信順序 (1, 2, 3...) +- `{{timestamp}}`: Unix timestamp +- `{{random}}`: ランダム文字列(8文字) +- `{{uuid}}`: UUID v4 +- `{{datetime}}`: ISO 8601形式日時 + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "sent_count": 20, + "failed_count": 0, + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df", + "execution_time_ms": 2100 + }, + "id": 7 +} +``` + +### 8. `bulk_send` - 複数パケット一括送信 + +複数のパケットを一括で送信します。並列・順次送信モード、動的パラメータ、改変機能をサポートします。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "bulk_send", + "arguments": { + "access_token": "your_access_token_here", + "packet_ids": [123, 124, 125], + "mode": "sequential", + "count": 2, + "interval_ms": 500, + "modifications": [ + { + "type": "header_add", + "name": "X-Test-Run", + "value": "{{timestamp}}" + } + ], + "regex_params": [ + { + "pattern": "token=([a-zA-Z0-9]+)", + "value_template": "token={{random}}-{{packet_index}}", + "target": "request" + } + ], + "allow_duplicate_headers": false, + "async": false, + "timeout_ms": 30000 + } + }, + "id": 8 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `packet_ids` (array, required): 送信するパケットIDの配列 (1-100個) +- `mode` (string, optional): 送信モード "parallel" | "sequential" (デフォルト: "parallel") +- `count` (number, optional): 各パケットの送信回数 (デフォルト: 1, 最大: 1000) +- `interval_ms` (number, optional): 送信間隔(ms) (順次モードのみ, デフォルト: 0, 最大: 60000) +- `modifications` (array, optional): 全パケットに適用する改変設定 (resend_packetと同じ形式) +- `regex_params` (array, optional): 動的値置換パラメータ +- `allow_duplicate_headers` (boolean, optional): ヘッダー重複許可 (デフォルト: false) +- `async` (boolean, optional): 非同期実行 (デフォルト: false) +- `timeout_ms` (number, optional): 全体タイムアウト(ms) (デフォルト: 30000, 最大: 300000) + +**regex_params設定:** +- `packet_index` (number, optional): 対象パケットインデックス (0ベース、省略時は全パケット) +- `pattern` (string, required): マッチする正規表現パターン +- `value_template` (string, required): 置換テンプレート (変数: {{packet_index}}, {{timestamp}}, {{random}}, {{uuid}}) +- `target` (string, optional): 対象 "request" | "response" | "both" (デフォルト: "request") + +**送信モード:** +- `parallel`: 全パケットを並列送信 (高速、interval_msは無視) +- `sequential`: パケットを順次送信 (制御された実行、regex_paramsによる値の引き継ぎ) + +**レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "mode": "sequential", + "total_packets": 3, + "total_count": 6, + "sent_count": 5, + "failed_count": 1, + "execution_time_ms": 1250, + "results": [ + { + "original_packet_id": 123, + "packet_index": 0, + "success": true, + "sent_count": 2, + "failed_count": 0, + "new_packet_ids": [145, 146], + "error": null, + "execution_time_ms": 245 + }, + { + "original_packet_id": 124, + "packet_index": 1, + "success": false, + "sent_count": 0, + "failed_count": 2, + "new_packet_ids": [], + "error": "Connection timeout", + "execution_time_ms": 5000 + } + ], + "regex_params_applied": [ + { + "packet_index": 0, + "pattern": "token=([a-zA-Z0-9]+)", + "extracted_value": "abc123def", + "applied_count": 1 + } + ], + "performance": { + "packets_per_second": 4.0, + "average_response_time_ms": 312, + "concurrent_connections": 3 + }, + "job_id": "bulk_send_20250804_120030_abc123" + }, + "id": 8 +} +``` + +**非同期実行レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "async": true, + "job_id": "bulk_send_20250804_120030_abc123", + "status": "started", + "total_packets": 50, + "estimated_duration_ms": 30000, + "monitor_url": "/mcp/bulk_send/status/bulk_send_20250804_120030_abc123" + }, + "id": 8 +} +``` + +### 9. `call_vulcheck_helper` - VulCheck脆弱性テストヘルパー + +指定されたパケットにVulCheckテストケースを適用して、自動的にペイロードを生成し、指定された位置に注入してバッチ送信を実行します。 + +**重要な制限事項**: 現在、NumberとJWTの脆弱性タイプのみをサポートしています。その他の脆弱性診断については、`bulk_send`や`resend_packet`ツールを使用してください。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "call_vulcheck_helper", + "arguments": { + "access_token": "your_access_token_here", + "packet_id": 123, + "vulcheck_type": "Number", + "target_locations": [ + { + "pattern": "userId=\\d+", + "replacement": "userId=$1", + "description": "User ID parameter" + }, + { + "pattern": "amount=[0-9.]+", + "replacement": "amount=$1", + "description": "Amount field" + } + ], + "interval_ms": 100, + "mode": "sequential", + "max_payloads": 50, + "timeout_ms": 300000 + } + }, + "id": 9 +} +``` + +**パラメータ:** +- `access_token` (string, required): PacketProxy設定のアクセストークン +- `packet_id` (number, required): VulCheckテストのベースとして使用するパケットID +- `vulcheck_type` (string, required): 実行するVulCheckの種類 (Number, JWT等)。'list'を指定すると利用可能なタイプ一覧を取得 +- `target_locations` (array, required): VulCheckペイロードを注入するパケット内の対象位置の配列。正規表現パターンまたは位置範囲で指定可能 +- **正規表現アプローチ (推奨):** +- `pattern` (string, required): マッチ対象の正規表現パターン (例: `"sessionId=\\w+"`) +- `replacement` (string, optional): 置換テンプレート。省略時はマッチ全体を置換。`$1`でペイロードを表す (例: `"sessionId=$1"`) +- `description` (string, optional): この対象位置の説明 +- **位置範囲アプローチ (後方互換性のため保持):** +- `start` (number, required): 対象位置の開始位置 +- `end` (number, required): 対象位置の終了位置 +- `description` (string, optional): この対象位置の説明 +- `interval_ms` (number, optional): パケット送信間隔(ms) (デフォルト: 100) +- `mode` (string, optional): 実行モード "sequential" | "parallel" (デフォルト: "sequential") +- `max_payloads` (number, optional): 位置ごとの最大ペイロード生成数 (デフォルト: 50, 最大: 1000) +- `timeout_ms` (number, optional): 全体操作タイムアウト(ms) (デフォルト: 300000 - 5分) + +**実行モード:** +- `sequential`: ペイロードを順次送信 (間隔制御あり) +- `parallel`: 全ペイロードを並列送信 (高速、interval_msは無視) + +**利用可能なVulCheckタイプ取得:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "call_vulcheck_helper", + "arguments": { + "access_token": "your_access_token_here", + "packet_id": 123, + "vulcheck_type": "list" + } + }, + "id": 10 +} +``` + +**VulCheckタイプ一覧レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "available_vulcheck_types": "Number, JWT", + "vulcheck_types": [ + { + "name": "Number", + "description": "VulCheck tests for Number vulnerabilities", + "generators": [ + {"name": "NegativeNumber", "generate_on_start": true}, + {"name": "Zero", "generate_on_start": true}, + {"name": "Decimals", "generate_on_start": true}, + {"name": "IntegerOverflow", "generate_on_start": true} + ], + "generator_count": 9 + }, + { + "name": "JWT", + "description": "VulCheck tests for JWT vulnerabilities", + "generators": [ + {"name": "JWTPayloadModified", "generate_on_start": false}, + {"name": "JWTHeaderAlgNone", "generate_on_start": false} + ], + "generator_count": 8 + } + ], + "total_types": 2 + }, + "id": 10 +} +``` + +**実行結果レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "success": true, + "vulcheck_type": "Number", + "mode": "sequential", + "total_locations": 2, + "total_payloads_generated": 18, + "total_packets_sent": 16, + "total_failed": 2, + "execution_time_ms": 2400, + "location_results": [ + { + "start": 45, + "end": 50, + "description": "User ID parameter (match 1)", + "payloads_generated": 9, + "packets_sent": 8, + "packets_failed": 1, + "execution_time_ms": 1200, + "generated_payloads": [ + "-1", "0", "0.1", "2147483647", "2147483648", + "-2147483649", "9223372036854775807", "9223372036854775808", + "-9223372036854775809" + ] + }, + { + "start": 78, + "end": 85, + "description": "Amount field (match 1)", + "payloads_generated": 9, + "packets_sent": 8, + "packets_failed": 1, + "execution_time_ms": 1200, + "generated_payloads": [ + "-1", "0", "0.1", "2147483647", "2147483648", + "-2147483649", "9223372036854775807", "9223372036854775808", + "-9223372036854775809" + ] + } + ], + "performance": { + "average_interval_ms": 100, + "payloads_per_second": 6.67, + "success_rate_percent": 88.89 + }, + "job_id": "vulcheck_20250804_120030_def456" + }, + "id": 9 +} +``` + +**使用例:** + +```bash +# 利用可能なVulCheckタイプを確認 +curl -X POST http://localhost:32349/mcp/tools/call \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_access_token" \ + -d '{ + "name": "call_vulcheck_helper", + "arguments": { + "packet_id": 123, + "vulcheck_type": "list" + } + }' + +# Number VulCheckを実行 +curl -X POST http://localhost:32349/mcp/tools/call \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your_access_token" \ + -d '{ + "name": "call_vulcheck_helper", + "arguments": { + "packet_id": 123, + "vulcheck_type": "Number", + "target_locations": [ + {"pattern": "userId=\\d+", "replacement": "userId=$1", "description": "User ID"} + ], + "mode": "sequential", + "interval_ms": 200, + "max_payloads": 10 + } + }' +``` + +### 10. `get_job_status` - ジョブ状況取得 + +send系ツール(resend_packet/bulk_send/call_vulcheck_helper)で作成されたジョブの実行状況を取得します。 + +**リクエスト:** + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_job_status", + "arguments": { + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df" + } + }, + "id": 10 +} +``` + +**パラメータ:** +- `job_id` (string, optional): 取得するジョブのID。指定しない場合は全ジョブの概要を返す + +**特定ジョブの詳細レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df", + "total_requests": 5, + "requests_sent": 5, + "responses_received": 3, + "status": "receiving_responses", + "requests": [ + { + "temporary_id": "temp_001", + "has_request": true, + "has_response": true, + "request_packet_id": 124, + "response_packet_id": 125 + }, + { + "temporary_id": "temp_002", + "has_request": true, + "has_response": false, + "request_packet_id": 126 + } + ] + }, + "id": 10 +} +``` + +**全ジョブ概要レスポンス:** + +```json +{ + "jsonrpc": "2.0", + "result": { + "total_jobs": 3, + "jobs": [ + { + "job_id": "af2adff0-a35a-43ef-b653-cb47203727df", + "total_requests": 5, + "requests_sent": 5, + "responses_received": 3, + "status": "receiving_responses" + }, + { + "job_id": "bulk_send_20250804_120030_abc123", + "total_requests": 10, + "requests_sent": 10, + "responses_received": 10, + "status": "completed" + } + ] + }, + "id": 10 +} +``` + +**ジョブ状態:** +- `created`: ジョブは作成されたがリクエストはまだ送信されていない +- `requests_sent`: 全リクエストが送信済み、レスポンス待ち +- `receiving_responses`: 一部のレスポンスを受信中 +- `completed`: 全リクエスト・レスポンスが完了 + +**ジョブの概念:** + +PacketProxyのジョブシステムは、send系ツールで送信されたパケットの追跡を可能にします: + +- **job_id**: 各send系ツール実行時に生成されるUUID +- **temporary_id**: ジョブ内の各リクエスト/レスポンスペアを識別するUUID +- **パケット関連付け**: 送信されたパケットと受信されたレスポンスがtemporary_idで関連付けられる +- **状況追跡**: データベースに保存されたパケット履歴からジョブの進行状況をリアルタイムで取得 + +## フィルタ構文仕様 + +PacketProxyのFilterTextParserに準拠した構文を使用します。 + +### 利用可能カラム + +| カラム名 | 型 | 説明 | +|---------------|----------|-----------------| +| `id` | integer | パケットID | +| `request` | string | リクエスト内容 | +| `response` | string | レスポンス内容 | +| `length` | integer | パケットサイズ | +| `client_ip` | string | クライアントIP | +| `client_port` | integer | クライアントポート | +| `server_ip` | string | サーバーIP | +| `server_port` | integer | サーバーポート | +| `time` | datetime | タイムスタンプ | +| `resend` | boolean | 再送フラグ | +| `modified` | boolean | 改変フラグ | +| `type` | string | プロトコルタイプ | +| `encode` | string | エンコーダ種別 | +| `alpn` | string | ALPN情報 | +| `group` | integer | グループID | +| `full_text` | string | 全文検索 (大文字小文字区別) | +| `full_text_i` | string | 全文検索 (大文字小文字無視) | + +### 演算子 + +| 演算子 | 説明 | 例 | +|--------|----------|-------------------------------------| +| `==` | 等しい | `method == GET` | +| `!=` | 等しくない | `status != 200` | +| `>=` | 以上 | `length >= 1000` | +| `<=` | 以下 | `status <= 299` | +| `=~` | 正規表現マッチ | `url =~ /api/v[0-9]+/` | +| `!~` | 正規表現非マッチ | `url !~ /static/` | +| `&&` | AND演算 | `method == POST && status >= 400` | +| `\|\|` | OR演算 | `method == GET \|\| method == POST` | + +### フィルタ例 + +``` +# HTTP エラー +status >= 400 && status <= 599 + +# 大きなリクエスト +length > 10000 + +# API コール +url =~ /api/ && (method == GET || method == POST) + +# 認証関連 +full_text_i =~ authorization + +# WebSocket トラフィック +type == WebSocket + +# 複合条件 +method == POST && url =~ /login && status == 401 + +# 並び順の例 +# 最新順: order: "time desc" +# サイズ順: order: "length asc" +# エラー優先: order: "status desc" +# ID逆順: order: "id desc" +``` + +## HTTP REST API (補完) + +MCP以外の方法でもアクセス可能なHTTP REST APIを提供します。 + +### エンドポイント + +``` +GET /mcp/tools # ツール一覧 +GET /mcp/history?filter=...&limit=100&order=time+desc # パケット履歴 +GET /mcp/packet/{id} # パケット詳細 +GET /mcp/configs # 設定一覧 +PUT /mcp/configs # 設定更新 +POST /mcp/resend/{packet_id} # パケット再送 +POST /mcp/bulk_send # 複数パケット一括送信 +POST /mcp/call_vulcheck_helper # VulCheck脆弱性テストヘルパー +GET /mcp/job_status?job_id=... # ジョブ状況取得 +GET /mcp/logs?level=info # ログ取得 +POST /mcp/restore/{backup_id} # バックアップ復元 +``` + +### HTTP ヘッダー制御 + +設定更新API (`POST /config`) では以下の特別なHTTPヘッダーをサポートします: + +- `X-Suppress-Dialog: true`: 確認ダイアログを非表示にして自動的に設定を上書き +- `X-Suppress-Dialog: false` (デフォルト): 確認ダイアログを表示 + +### 認証 + +HTTP APIはアクセストークンによる認証を使用します。 + +```http +Authorization: Bearer +``` + +## エラーハンドリング + +### MCP標準エラー + +```json +{ + "jsonrpc": "2.0", + "error": { + "code": -32602, + "message": "Invalid params", + "data": { + "details": "packet_id is required" + } + }, + "id": 1 +} +``` + +### カスタムエラーコード + +| コード | 説明 | +|--------|------------------------------| +| -32001 | PacketProxy connection error | +| -32002 | Invalid filter syntax | +| -32003 | Packet not found | +| -32004 | Configuration error | +| -32005 | Permission denied | + +## セキュリティ考慮事項 + +1. **アクセス制御**: 設定変更操作は適切な権限を要求 +2. **入力検証**: すべての入力パラメータを検証 +3. **ログ記録**: 重要な操作はログに記録 +4. **レート制限**: パケット再送などの高負荷操作に制限 +5. **設定バックアップ**: 重要な設定変更前に自動バックアップ + +## パフォーマンス考慮事項 + +1. **フィルタ最適化**: 複雑なフィルタは性能警告を表示 +2. **ページング**: 大量データは適切にページング +3. **キャッシュ**: 頻繁にアクセスされるデータはキャッシュ +4. **非同期処理**: 時間のかかる操作は非同期実行をサポート + +## 環境変数 + +### MCP HTTP Bridge 環境変数 + +MCP HTTP Bridgeは以下の環境変数をサポートします: + +#### `PACKETPROXY_ACCESS_TOKEN` + +- **説明**: PacketProxyのアクセストークン +- **必須**: 推奨 (手動指定の代替) +- **形式**: 文字列 +- **例**: `export PACKETPROXY_ACCESS_TOKEN="abc123def456"` +- **動作**: 設定時、すべてのMCPツール呼び出しに自動的にアクセストークンが追加されます + +#### `MCP_DEBUG` + +- **説明**: デバッグログ出力制御 +- **必須**: オプション +- **形式**: `"true"` または `"false"` +- **デフォルト**: `"false"` +- **例**: `export MCP_DEBUG="true"` +- **動作**: + - `"true"`: デバッグメッセージをstderrに出力 + - `"false"`: デバッグメッセージを出力しない (JSON-RPC通信を汚染しない) + +#### 使用例 + +```bash +# 基本設定 +export PACKETPROXY_ACCESS_TOKEN="your_access_token_here" + +# デバッグ有効化 +export MCP_DEBUG="true" +export PACKETPROXY_ACCESS_TOKEN="your_access_token_here" + +# MCP HTTP Bridge起動 +node /path/to/mcp-http-bridge.js +``` + +## 実装詳細 + +### ディレクトリ構成 + +``` +src/main/java/core/packetproxy/extensions/mcp/ +├── MCPServerExtension.java # Extension基底クラス継承 +├── MCPServer.java # MCP JSONRPCサーバー実装 +├── tools/ +│ ├── HistoryTool.java # History情報取得 +│ ├── SettingTool.java # 設定情報取得・変更 +│ ├── LogTool.java # ログ情報取得 +│ ├── ResendTool.java # パケット再送 +│ └── FilterTool.java # フィルタ関連操作 +├── MCPToolRegistry.java # ツール登録・管理 +└── backup/ + └── ConfigBackupManager.java # 設定バックアップ管理 +``` + +### 統合ポイント + +- **Extension管理**: GUIOptionExtensionsで有効/無効切り替え +- **データアクセス**: 既存のPackets, Configs, Filters等のAPIを活用 +- **パケット操作**: ResendControllerを使用した再送機能 +- **設定管理**: ConfigIOを使用したPacketProxyHub互換性 + +## バージョン履歴 + +| バージョン | 日付 | 変更内容 | +|-------|------------|------| +| 1.0.0 | 2025-08-04 | 初版作成 | + diff --git a/scripts/mcp-http-bridge.js b/scripts/mcp-http-bridge.js new file mode 100755 index 00000000..327699b5 --- /dev/null +++ b/scripts/mcp-http-bridge.js @@ -0,0 +1,375 @@ +#!/usr/bin/env node + +/** + * MCP HTTP Bridge + * Bridges HTTP MCP requests to PacketProxy's HTTP endpoint + */ + +const http = require('http'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Configuration +const PACKETPROXY_HTTP_URL = 'http://localhost:8765/mcp'; +const LOCK_FILE = path.join(os.tmpdir(), 'mcp-http-bridge.lock'); +const DEBUG_MODE = process.env.MCP_DEBUG === 'true'; + +// Debug logging function +function debugLog(message) { + if (DEBUG_MODE) { + console.error(message); + } +} + +// Single instance enforcement +function ensureSingleInstance() { + try { + // Check if lock file exists and process is still running + if (fs.existsSync(LOCK_FILE)) { + const pidStr = fs.readFileSync(LOCK_FILE, 'utf8').trim(); + const pid = parseInt(pidStr, 10); + + if (!isNaN(pid)) { + try { + // Check if process is still running + process.kill(pid, 0); + console.error(`[ERROR] Another instance is already running (PID: ${pid})`); + process.exit(1); + } catch (err) { + // Process not running, remove stale lock file + fs.unlinkSync(LOCK_FILE); + } + } else { + // Invalid PID in lock file, remove it + fs.unlinkSync(LOCK_FILE); + } + } + + // Create lock file with current PID + fs.writeFileSync(LOCK_FILE, process.pid.toString()); + + // Clean up lock file on exit + const cleanup = () => { + try { + if (fs.existsSync(LOCK_FILE)) { + fs.unlinkSync(LOCK_FILE); + } + } catch (err) { + // Ignore cleanup errors + } + }; + + process.on('exit', cleanup); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + process.on('uncaughtException', (err) => { + console.error('[FATAL] Uncaught exception:', err); + cleanup(); + process.exit(1); + }); + + } catch (error) { + console.error(`[ERROR] Failed to ensure single instance: ${error.message}`); + process.exit(1); + } +} + +// MCP Server implementation +class MCPHttpBridge { + constructor() { + this.initialized = false; + this.serverInfo = { + name: "PacketProxy MCP Bridge", + version: "1.0.0" + }; + } + + async handleRequest(request) { + debugLog(`[DEBUG] Processing request: ${request.method}`); + + switch (request.method) { + case 'initialize': + return this.handleInitialize(request); + case 'notifications/initialized': + return this.handleNotificationInitialized(request); + case 'tools/list': + return this.handleToolsList(request); + case 'tools/call': + return this.handleToolsCall(request); + case 'resources/list': + return this.handleResourcesList(request); + case 'resources/templates/list': + return this.handleResourcesTemplatesList(request); + case 'prompts/list': + return this.handlePromptsList(request); + default: + return this.createErrorResponse(request.id, -32601, `Method not found: ${request.method}`); + } + } + + async handleInitialize(request) { + this.initialized = true; + debugLog('[DEBUG] Initialize request - forwarding to PacketProxy'); + + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward initialize request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleNotificationInitialized(request) { + debugLog('[DEBUG] Notification initialized received (no response needed)'); + // Notifications don't require a response, return null + return null; + } + + async handleResourcesList(request) { + debugLog('[DEBUG] Resources list request - forwarding to PacketProxy'); + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleResourcesTemplatesList(request) { + debugLog('[DEBUG] Resources templates list request - forwarding to PacketProxy'); + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handlePromptsList(request) { + debugLog('[DEBUG] Prompts list request - forwarding to PacketProxy'); + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleToolsList(request) { + if (!this.initialized) { + return this.createErrorResponse(request.id, -32002, "Server not initialized"); + } + + debugLog('[DEBUG] Tools list request - forwarding to PacketProxy'); + + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async handleToolsCall(request) { + if (!this.initialized) { + return this.createErrorResponse(request.id, -32002, "Server not initialized"); + } + + debugLog(`[DEBUG] Tools call request: ${request.params?.name}`); + + try { + const response = await this.forwardToPacketProxy(request); + return response; + } catch (error) { + console.error(`[ERROR] Failed to forward request: ${error.message}`); + return this.createErrorResponse(request.id, -32603, `Internal error: ${error.message}`); + } + } + + async forwardToPacketProxy(request) { + return new Promise((resolve, reject) => { + // 環境変数からアクセストークンを取得 + const accessToken = process.env.PACKETPROXY_ACCESS_TOKEN; + debugLog(`[DEBUG] Environment variable PACKETPROXY_ACCESS_TOKEN: ${accessToken ? '[SET]' : '[NOT SET]'}`); + debugLog(`[DEBUG] Request method: ${request.method}`); + debugLog(`[DEBUG] Request params: ${JSON.stringify(request.params)}`); + + // tools/callリクエストの場合、arguments内にアクセストークンを追加 + if (accessToken && request.method === 'tools/call' && request.params) { + if (!request.params.arguments) { + request.params.arguments = {}; + } + if (!request.params.arguments.access_token) { + request.params.arguments.access_token = accessToken; + debugLog(`[DEBUG] Added access token to tools/call arguments`); + } + } + // その他のリクエストでparamsが存在する場合 + else if (accessToken && request.params && typeof request.params === 'object') { + if (!request.params.access_token) { + request.params.access_token = accessToken; + debugLog(`[DEBUG] Added access token to request params`); + } + } else if (!accessToken) { + debugLog(`[WARNING] PACKETPROXY_ACCESS_TOKEN environment variable not set`); + } + + const postData = JSON.stringify(request); + + debugLog(`[DEBUG] Forwarding to PacketProxy: ${postData}`); + debugLog(`[DEBUG] Target URL: ${PACKETPROXY_HTTP_URL}`); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + }, + timeout: 60000 // 60 second timeout + }; + + const req = http.request(PACKETPROXY_HTTP_URL, options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + debugLog(`[DEBUG] Raw response from PacketProxy (status: ${res.statusCode}): ${data}`); + + if (res.statusCode !== 200) { + console.error(`[ERROR] HTTP error from PacketProxy: ${res.statusCode} ${res.statusMessage}`); + reject(new Error(`HTTP error: ${res.statusCode} ${res.statusMessage}`)); + return; + } + + try { + const response = JSON.parse(data); + debugLog(`[DEBUG] PacketProxy response parsed successfully`); + resolve(response); + } catch (error) { + console.error(`[ERROR] Failed to parse PacketProxy response: ${error.message}`); + console.error(`[ERROR] Raw data: ${data}`); + reject(new Error(`Failed to parse PacketProxy response: ${error.message}`)); + } + }); + }); + + req.on('error', (error) => { + console.error(`[ERROR] HTTP request to PacketProxy failed: ${error.message}`); + console.error(`[ERROR] Error code: ${error.code}`); + reject(new Error(`HTTP request failed: ${error.message}`)); + }); + + req.on('timeout', () => { + console.error(`[ERROR] HTTP request to PacketProxy timed out`); + req.destroy(); + reject(new Error('HTTP request timed out')); + }); + + req.write(postData); + req.end(); + }); + } + + createErrorResponse(id, code, message) { + return { + jsonrpc: "2.0", + id: id, + error: { + code: code, + message: message + } + }; + } +} + +// Main execution +async function main() { + // Ensure only one instance can run + ensureSingleInstance(); + + debugLog('[DEBUG] PacketProxy MCP HTTP Bridge starting...'); + + const bridge = new MCPHttpBridge(); + + process.stdin.setEncoding('utf8'); + + let buffer = ''; + + process.stdin.on('data', async (chunk) => { + buffer += chunk; + + // Process complete lines + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + try { + debugLog(`[DEBUG] Received: ${trimmedLine}`); + const request = JSON.parse(trimmedLine); + const response = await bridge.handleRequest(request); + + // Only send response if it's not null (notifications don't need responses) + if (response !== null) { + const responseStr = JSON.stringify(response); + process.stdout.write(responseStr + '\n'); + debugLog(`[DEBUG] Sent: ${responseStr.length} characters`); + } else { + debugLog(`[DEBUG] No response needed (notification)`); + } + } catch (error) { + console.error(`[ERROR] Failed to process request: ${error.message}`); + // Try to extract ID from the malformed request + let requestId = null; + try { + const partialRequest = JSON.parse(trimmedLine); + requestId = partialRequest.id || null; + } catch (e) { + // If we can't parse at all, use null ID + } + + const errorResponse = { + jsonrpc: "2.0", + id: requestId, + error: { + code: -32700, + message: "Parse error" + } + }; + process.stdout.write(JSON.stringify(errorResponse) + '\n'); + } + } + }); + + process.stdin.on('end', () => { + debugLog('[DEBUG] Bridge shutting down'); + process.exit(0); + }); + + // Handle process termination + process.on('SIGINT', () => { + debugLog('[DEBUG] Received SIGINT, shutting down'); + process.exit(0); + }); +} + +if (require.main === module) { + main().catch((error) => { + console.error(`[FATAL] ${error.message}`); + process.exit(1); + }); +} + +module.exports = MCPHttpBridge; \ No newline at end of file diff --git a/src/main/java/core/packetproxy/DuplexFactory.java b/src/main/java/core/packetproxy/DuplexFactory.java index f1d7e5a3..059f15e1 100644 --- a/src/main/java/core/packetproxy/DuplexFactory.java +++ b/src/main/java/core/packetproxy/DuplexFactory.java @@ -386,6 +386,9 @@ public byte[] onServerChunkReceived(byte[] data) throws Exception { server_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.SERVER, duplex.hashCode(), group_id); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + server_packet.setJobId(oneshot.getJobId()); + server_packet.setTemporaryId(oneshot.getTemporaryId()); packets.update(server_packet); server_packet.setReceivedData(data); if (data.length < SKIP_LENGTH) { @@ -416,6 +419,9 @@ public byte[] onClientChunkSend(byte[] data) throws Exception { client_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.CLIENT, duplex.hashCode(), UniqueID.getInstance().createId()); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + client_packet.setJobId(oneshot.getJobId()); + client_packet.setTemporaryId(oneshot.getTemporaryId()); client_packet.setModified(); client_packet.setDecodedData(data); client_packet.setModifiedData(data); @@ -564,6 +570,9 @@ public byte[] onServerChunkReceived(byte[] data) throws Exception { server_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.SERVER, original_duplex.hashCode(), group_id); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + server_packet.setJobId(oneshot.getJobId()); + server_packet.setTemporaryId(oneshot.getTemporaryId()); packets.update(server_packet); server_packet.setReceivedData(data); if (data.length < SKIP_LENGTH) { @@ -594,6 +603,9 @@ public byte[] onClientChunkSend(byte[] data) throws Exception { client_packet = new Packet(0, oneshot.getClient(), oneshot.getServer(), oneshot.getServerName(), oneshot.getUseSSL(), oneshot.getEncoder(), oneshot.getAlpn(), Packet.Direction.CLIENT, original_duplex.hashCode(), UniqueID.getInstance().createId()); + // OneShotPacketからjob_idとtemporary_idを引き継ぎ + client_packet.setJobId(oneshot.getJobId()); + client_packet.setTemporaryId(oneshot.getTemporaryId()); packets.update(client_packet); client_packet.setModified(); client_packet.setDecodedData(data); diff --git a/src/main/java/core/packetproxy/common/ConfigHttpServer.java b/src/main/java/core/packetproxy/common/ConfigHttpServer.java index 779645ee..4f531770 100644 --- a/src/main/java/core/packetproxy/common/ConfigHttpServer.java +++ b/src/main/java/core/packetproxy/common/ConfigHttpServer.java @@ -130,20 +130,26 @@ public Response serve(IHTTPSession session) { try { - GUIMain.getInstance().setAlwaysOnTop(true); - GUIMain.getInstance().setVisible(true); + // Check if dialog suppression is requested + String suppressDialog = session.getHeaders().get("x-suppress-dialog"); + boolean showDialog = !"true".equals(suppressDialog); - GUIMain.getInstance().getTabbedPane().setSelectedIndex(GUIMain.Panes.OPTIONS.ordinal()); + if (showDialog) { + GUIMain.getInstance().setAlwaysOnTop(true); + GUIMain.getInstance().setVisible(true); - int option = JOptionPane.showConfirmDialog(GUIMain.getInstance(), - I18nString.get("Do you want to overwrite config?"), I18nString.get("Loading config"), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + GUIMain.getInstance().getTabbedPane().setSelectedIndex(GUIMain.Panes.OPTIONS.ordinal()); - GUIMain.getInstance().setAlwaysOnTop(false); + int option = JOptionPane.showConfirmDialog(GUIMain.getInstance(), + I18nString.get("Do you want to overwrite config?"), I18nString.get("Loading config"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - if (option == JOptionPane.NO_OPTION) { + GUIMain.getInstance().setAlwaysOnTop(false); - return NanoHTTPD.newFixedLengthResponse(Response.Status.UNAUTHORIZED, MIME_HTML, null); + if (option == JOptionPane.NO_OPTION) { + + return NanoHTTPD.newFixedLengthResponse(Response.Status.UNAUTHORIZED, MIME_HTML, null); + } } HashMap map = new HashMap(); diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java new file mode 100644 index 00000000..96578c1b --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServer.java @@ -0,0 +1,233 @@ +package packetproxy.extensions.mcp; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.util.function.Consumer; +import packetproxy.extensions.mcp.tools.ToolRegistry; + +public class MCPServer { + + private final Gson gson; + private final ToolRegistry toolRegistry; + private final Consumer logger; + private boolean running = false; + private BufferedReader reader; + private PrintWriter writer; + + public MCPServer(Consumer logger) { + this.gson = new GsonBuilder().setPrettyPrinting().create(); + this.toolRegistry = new ToolRegistry(); + this.logger = logger; + this.reader = new BufferedReader(new InputStreamReader(System.in)); + this.writer = new PrintWriter(System.out, true); + } + + public void run() throws IOException { + running = true; + logger.accept("MCP Server listening on stdin/stdout"); + + while (running) { + try { + String line = reader.readLine(); + if (line == null) { + break; // EOF + } + + line = line.trim(); + if (line.isEmpty()) { + continue; + } + + logger.accept("Received: " + line); + processRequest(line); + + } catch (Exception e) { + logger.accept("Error processing request: " + e.getMessage()); + log("MCP Server error: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + public void stop() { + running = false; + // Don't close System.in/System.out streams as they are global + // Just mark as stopped - the run() loop will break on next read + } + + public JsonObject processTestRequest(JsonObject request) throws Exception { + String method = request.get("method").getAsString(); + JsonElement id = request.get("id"); + JsonObject params = request.has("params") ? request.getAsJsonObject("params") : new JsonObject(); + + JsonObject response = new JsonObject(); + response.addProperty("jsonrpc", "2.0"); + // Always ensure ID is present - use original request ID or default + if (id != null && !id.isJsonNull()) { + response.add("id", id); + } else { + // If no valid ID in request, use default ID + response.addProperty("id", 0); + } + + try { + JsonObject result = handleMethod(method, params); + response.add("result", result); + } catch (Exception e) { + JsonObject error = new JsonObject(); + error.addProperty("code", -32603); + error.addProperty("message", "Internal error: " + e.getMessage()); + response.add("error", error); + // Don't throw exception - return error response instead + logger.accept("Method error: " + e.getMessage()); + } + + return response; + } + + private void processRequest(String requestLine) { + try { + JsonObject request = JsonParser.parseString(requestLine).getAsJsonObject(); + + String method = request.get("method").getAsString(); + JsonElement id = request.get("id"); + JsonObject params = request.has("params") ? request.getAsJsonObject("params") : new JsonObject(); + + JsonObject response = new JsonObject(); + response.addProperty("jsonrpc", "2.0"); + // Always ensure ID is present - use original request ID or default + if (id != null && !id.isJsonNull()) { + response.add("id", id); + } else { + // If no valid ID in request, use default ID + response.addProperty("id", 0); + } + + try { + JsonObject result = handleMethod(method, params); + response.add("result", result); + } catch (Exception e) { + JsonObject error = new JsonObject(); + error.addProperty("code", -32603); + error.addProperty("message", "Internal error: " + e.getMessage()); + response.add("error", error); + logger.accept("Method error: " + e.getMessage()); + } + + String responseString = gson.toJson(response); + writer.println(responseString); + logger.accept("Sent: " + responseString); + + } catch (Exception e) { + // Invalid JSON request + JsonObject errorResponse = new JsonObject(); + errorResponse.addProperty("jsonrpc", "2.0"); + + // Try to extract ID from the malformed request if possible + JsonElement requestId = null; + try { + JsonObject partialRequest = JsonParser.parseString(requestLine).getAsJsonObject(); + if (partialRequest.has("id")) { + requestId = partialRequest.get("id"); + } + } catch (Exception parseError) { + // If we can't parse at all, use null ID + } + errorResponse.add("id", requestId); + + JsonObject error = new JsonObject(); + error.addProperty("code", -32700); + error.addProperty("message", "Parse error"); + errorResponse.add("error", error); + + writer.println(gson.toJson(errorResponse)); + logger.accept("Parse error: " + e.getMessage()); + } + } + + private JsonObject handleMethod(String method, JsonObject params) throws Exception { + switch (method) { + case "initialize" : + return handleInitialize(params); + case "tools/list" : + return handleToolsList(); + case "tools/call" : + return handleToolsCall(params); + case "resources/list" : + return handleResourcesList(); + case "resources/templates/list" : + return handleResourcesTemplatesList(); + case "prompts/list" : + return handlePromptsList(); + default : + throw new Exception("Unknown method: " + method); + } + } + + private JsonObject handleInitialize(JsonObject params) { + JsonObject result = new JsonObject(); + + JsonObject capabilities = new JsonObject(); + JsonObject tools = new JsonObject(); + tools.addProperty("listChanged", true); + capabilities.add("tools", tools); + + JsonObject serverInfo = new JsonObject(); + serverInfo.addProperty("name", "PacketProxy MCP Server"); + serverInfo.addProperty("version", "1.0.0"); + + result.add("capabilities", capabilities); + result.addProperty("protocolVersion", "2024-11-05"); + result.add("serverInfo", serverInfo); + + logger.accept("Client initialized"); + return result; + } + + private JsonObject handleToolsList() { + JsonObject result = new JsonObject(); + result.add("tools", toolRegistry.getToolsList()); + return result; + } + + private JsonObject handleToolsCall(JsonObject params) throws Exception { + if (!params.has("name")) { + throw new Exception("Tool name is required"); + } + + String toolName = params.get("name").getAsString(); + JsonObject arguments = params.has("arguments") ? params.getAsJsonObject("arguments") : new JsonObject(); + + return toolRegistry.callTool(toolName, arguments); + } + + private JsonObject handleResourcesList() { + JsonObject result = new JsonObject(); + JsonObject[] resources = {}; + result.add("resources", gson.toJsonTree(resources)); + return result; + } + + private JsonObject handleResourcesTemplatesList() { + JsonObject result = new JsonObject(); + JsonObject[] resourceTemplates = {}; + result.add("resourceTemplates", gson.toJsonTree(resourceTemplates)); + return result; + } + + private JsonObject handlePromptsList() { + JsonObject result = new JsonObject(); + JsonObject[] prompts = {}; + result.add("prompts", gson.toJsonTree(prompts)); + return result; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java new file mode 100644 index 00000000..0ca82f47 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/MCPServerExtension.java @@ -0,0 +1,274 @@ +package packetproxy.extensions.mcp; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonObject; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import packetproxy.model.Extension; + +public class MCPServerExtension extends Extension { + + private MCPServer server; + private HttpServer httpServer; + private JTextArea logArea; + private JButton startButton; + private JButton stopButton; + private JCheckBox maskTokenCheckBox; + private boolean isRunning = false; + private static final int HTTP_PORT = 8765; + private java.util.List logMessages = new java.util.ArrayList<>(); + + public MCPServerExtension() { + super(); + this.setName("MCP Server"); + } + + public MCPServerExtension(String name, String path) throws Exception { + super(name, path); + this.setName("MCP Server"); + } + + @Override + public JComponent createPanel() throws Exception { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + // Status panel + JPanel statusPanel = new JPanel(); + statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.X_AXIS)); + statusPanel.add(new JLabel("MCP Server Status: ")); + + startButton = new JButton("Start Server"); + stopButton = new JButton("Stop Server"); + stopButton.setEnabled(false); + + maskTokenCheckBox = new JCheckBox("Mask access_token in logs", true); + maskTokenCheckBox.setToolTipText("When enabled, access_token values are masked with asterisks in log display"); + maskTokenCheckBox.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 15, 0, 0)); + maskTokenCheckBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + refreshLogDisplay(); + } + }); + + startButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + startServer(); + } + }); + + stopButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + stopServer(); + } + }); + + statusPanel.add(startButton); + statusPanel.add(stopButton); + statusPanel.add(maskTokenCheckBox); + + // Log area + logArea = new JTextArea(20, 80); + logArea.setEditable(false); + JScrollPane scrollPane = new JScrollPane(logArea); + + panel.add(statusPanel); + panel.add(new JLabel("Server Logs:")); + panel.add(scrollPane); + + return panel; + } + + @Override + public JMenuItem historyClickHandler() { + return null; // MCP Serverは右クリックメニューに追加しない + } + + private void startServer() { + if (isRunning) { + return; + } + + try { + server = new MCPServer(this::addLog); + + // Start HTTP server for MCP + httpServer = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + httpServer.createContext("/mcp", new MCPHttpHandler()); + httpServer.setExecutor(null); // creates a default executor + httpServer.start(); + + Thread serverThread = new Thread(() -> { + try { + server.run(); + } catch (Exception e) { + addLog("Server error: " + e.getMessage()); + e.printStackTrace(); + } + }); + serverThread.setDaemon(true); + serverThread.start(); + + isRunning = true; + startButton.setEnabled(false); + stopButton.setEnabled(true); + addLog("MCP Server started"); + addLog("HTTP endpoint available at http://localhost:" + HTTP_PORT + "/mcp"); + log("MCP Server started with HTTP endpoint on port " + HTTP_PORT); + + } catch (Exception e) { + addLog("Failed to start server: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void stopServer() { + if (!isRunning) { + return; + } + + try { + if (server != null) { + server.stop(); + server = null; + } + + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + + isRunning = false; + startButton.setEnabled(true); + stopButton.setEnabled(false); + addLog("MCP Server stopped"); + log("MCP Server stopped"); + + } catch (Exception e) { + addLog("Failed to stop server: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void addLog(String message) { + synchronized (logMessages) { + logMessages.add(message); + } + if (logArea != null) { + javax.swing.SwingUtilities.invokeLater(() -> { + String displayMessage = maskTokenCheckBox.isSelected() ? maskAccessToken(message) : message; + logArea.append("[" + new java.util.Date() + "] " + displayMessage + "\n"); + logArea.setCaretPosition(logArea.getDocument().getLength()); + }); + } + } + + private void refreshLogDisplay() { + if (logArea != null) { + javax.swing.SwingUtilities.invokeLater(() -> { + logArea.setText(""); + synchronized (logMessages) { + for (String message : logMessages) { + String displayMessage = maskTokenCheckBox.isSelected() ? maskAccessToken(message) : message; + logArea.append("[" + new java.util.Date() + "] " + displayMessage + "\n"); + } + } + logArea.setCaretPosition(logArea.getDocument().getLength()); + }); + } + } + + private String maskAccessToken(String message) { + // Pattern to match "access_token":"value" or "access_token": "value" or + // access_token=value + return message.replaceAll("(?i)(\"?access_token\"?\\s*[:=]\\s*\"?)([^\"\\s&,}]+)(\"?)", "$1****$3"); + } + + // HTTP handler for MCP requests + private class MCPHttpHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + addLog("Received HTTP request: " + exchange.getRequestMethod() + " " + exchange.getRequestURI()); + + // Enable CORS + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + return; + } + + if (!"POST".equals(exchange.getRequestMethod())) { + String response = "Only POST method is supported"; + exchange.sendResponseHeaders(405, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + return; + } + + try { + // Read request body + InputStream is = exchange.getRequestBody(); + String requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8); + addLog("Request body: " + requestBody); + + // Process MCP request if server is available + String responseBody; + if (server != null) { + try { + JsonObject request = com.google.gson.JsonParser.parseString(requestBody).getAsJsonObject(); + JsonObject result = server.processTestRequest(request); + responseBody = result.toString(); + addLog("Response: " + responseBody); + } catch (Exception e) { + addLog("Error processing request: " + e.getMessage()); + responseBody = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Internal error: " + + e.getMessage() + "\"},\"id\":null}"; + } + } else { + responseBody = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32002,\"message\":\"Server not available\"},\"id\":null}"; + } + + // Send response + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, responseBody.getBytes(StandardCharsets.UTF_8).length); + OutputStream os = exchange.getResponseBody(); + os.write(responseBody.getBytes(StandardCharsets.UTF_8)); + os.close(); + + } catch (Exception e) { + addLog("HTTP handler error: " + e.getMessage()); + String errorResponse = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"Parse error\"},\"id\":null}"; + exchange.sendResponseHeaders(500, errorResponse.length()); + OutputStream os = exchange.getResponseBody(); + os.write(errorResponse.getBytes()); + os.close(); + } + } + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java new file mode 100644 index 00000000..631fed33 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/AuthenticatedMCPTool.java @@ -0,0 +1,99 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonObject; +import packetproxy.model.ConfigString; + +/** + * 認証機能付きMCPツールの基底クラス + */ +public abstract class AuthenticatedMCPTool implements MCPTool { + + /** + * AccessTokenの検証を行う + */ + protected void validateAccessToken(JsonObject arguments) throws Exception { + // MCP clientから渡されたAccessTokenを取得 + if (!arguments.has("access_token")) { + throw new Exception( + "access_token parameter is required. Please provide your PacketProxy access token from Settings."); + } + + String providedToken = arguments.get("access_token").getAsString(); + if (providedToken == null) { + throw new Exception( + "access_token parameter is required. Please provide your PacketProxy access token from Settings or leave empty (\"\") to use environment variable."); + } + + // 空文字列の場合は環境変数から取得する想定なのでvalidationをスキップ + if (providedToken.trim().isEmpty()) { + log("Empty access_token provided, assuming environment variable usage"); + return; + } + + // PacketProxy設定からAccessTokenを取得 + String configuredToken = new ConfigString("SharingConfigsAccessToken").getString(); + if (configuredToken.isEmpty()) { + throw new Exception( + "Access token not configured in PacketProxy. Please enable 'Import/Export configs' in PacketProxy Settings and copy the generated access token."); + } + + // トークンの照合 + if (!configuredToken.equals(providedToken)) { + log("Access token validation failed"); + throw new Exception( + "Invalid access token. Please check your access token from PacketProxy Settings > Import/Export configs section."); + } + + log("Access token validation successful"); + } + + /** + * 設定済みAccessTokenを取得(HTTPリクエスト用) + */ + protected String getConfiguredAccessToken() throws Exception { + String accessToken = new ConfigString("SharingConfigsAccessToken").getString(); + if (accessToken.isEmpty()) { + throw new Exception("Access token not configured. Please enable config sharing in settings."); + } + return accessToken; + } + + /** + * 入力スキーマにaccess_tokenパラメータを追加 + */ + protected JsonObject addAccessTokenToSchema(JsonObject schema) { + JsonObject accessTokenProp = new JsonObject(); + accessTokenProp.addProperty("type", "string"); + accessTokenProp.addProperty("description", + "Access token for authentication. Leave empty (\"\") to use environment variable (handled by scripts/mcp-http-bridge.js), or provide explicit token string"); + schema.add("access_token", accessTokenProp); + return schema; + } + + /** + * access_tokenをマスクした安全なargumentsの文字列表現を返す + */ + protected String getSafeArgumentsString(JsonObject arguments) { + JsonObject safeArgs = arguments.deepCopy(); + if (safeArgs.has("access_token")) { + safeArgs.addProperty("access_token", "****"); + } + return safeArgs.toString(); + } + + /** + * サブクラスで実装する認証後の実際の処理 + */ + protected abstract JsonObject executeAuthenticated(JsonObject arguments) throws Exception; + + /** + * 認証チェック付きでツールを実行 + */ + @Override + public final JsonObject call(JsonObject arguments) throws Exception { + validateAccessToken(arguments); + return executeAuthenticated(arguments); + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java new file mode 100644 index 00000000..f183bd02 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/BulkSendTool.java @@ -0,0 +1,936 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import packetproxy.controller.ResendController; +import packetproxy.controller.ResendController.ResendWorker; +import packetproxy.model.OneShotPacket; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +/** + * 複数パケット一括送信ツール フェーズ2: 順次送信モード、modifications適用、regex_params機能 + */ +public class BulkSendTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "bulk_send"; + } + + @Override + public String getDescription() { + return "Send multiple packets in bulk with optional modifications. " + + "Use packet_ids array to specify which packets to send (can repeat same ID for multiple variations). " + + "Use regex_params to apply different modifications to each packet based on packet_index (0-based). " + + "For full header replacement, use patterns like 'User-Agent: [^\\r\\n]*'. " + + "For partial replacement, use capture groups like 'Content-Length: ([0-9]+)'. " + + "The value_template supports variables like {{timestamp}}, {{random}}, {{uuid}}, {{packet_index}}. " + + "Note: avoid using both packet_ids array with duplicates AND count parameter simultaneously to prevent unexpected multiplication of packets."; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + // packet_ids (required) + JsonObject packetIdsProp = new JsonObject(); + packetIdsProp.addProperty("type", "array"); + packetIdsProp.addProperty("description", "Array of packet IDs to send (1-100 packets)"); + JsonObject packetIdsItems = new JsonObject(); + packetIdsItems.addProperty("type", "integer"); + packetIdsProp.add("items", packetIdsItems); + schema.add("packet_ids", packetIdsProp); + + // mode (optional) + JsonObject modeProp = new JsonObject(); + modeProp.addProperty("type", "string"); + JsonArray modeEnum = new JsonArray(); + modeEnum.add("parallel"); + modeEnum.add("sequential"); + modeProp.add("enum", modeEnum); + modeProp.addProperty("description", "Sending mode: parallel (fast) or sequential (controlled)"); + modeProp.addProperty("default", "parallel"); + schema.add("mode", modeProp); + + // count (optional) + JsonObject countProp = new JsonObject(); + countProp.addProperty("type", "integer"); + countProp.addProperty("description", "Number of times to send each packet (default: 1)"); + countProp.addProperty("default", 1); + countProp.addProperty("minimum", 1); + countProp.addProperty("maximum", 1000); + schema.add("count", countProp); + + // interval_ms (optional) + JsonObject intervalProp = new JsonObject(); + intervalProp.addProperty("type", "integer"); + intervalProp.addProperty("description", + "Interval between sends in milliseconds (sequential mode only, default: 0, maximum: 60000)"); + intervalProp.addProperty("default", 0); + intervalProp.addProperty("minimum", 0); + intervalProp.addProperty("maximum", 60000); + schema.add("interval_ms", intervalProp); + + // regex_params (optional) + JsonObject regexParamsProp = new JsonObject(); + regexParamsProp.addProperty("type", "array"); + regexParamsProp.addProperty("description", "Regex parameters for dynamic value replacement across packets"); + JsonObject regexParamItem = new JsonObject(); + regexParamItem.addProperty("type", "object"); + JsonObject regexParamProps = new JsonObject(); + + JsonObject packetIndexProp = new JsonObject(); + packetIndexProp.addProperty("type", "integer"); + packetIndexProp.addProperty("description", "Target packet index (0-based)"); + regexParamProps.add("packet_index", packetIndexProp); + + JsonObject regexPatternProp = new JsonObject(); + regexPatternProp.addProperty("type", "string"); + regexPatternProp.addProperty("description", "Regex pattern to match"); + regexParamProps.add("pattern", regexPatternProp); + + JsonObject valueTemplateProp = new JsonObject(); + valueTemplateProp.addProperty("type", "string"); + valueTemplateProp.addProperty("description", + "Template with variables: {{packet_index}}, {{timestamp}}, {{random}}, {{uuid}}"); + regexParamProps.add("value_template", valueTemplateProp); + + JsonObject regexTargetProp = new JsonObject(); + regexTargetProp.addProperty("type", "string"); + JsonArray regexTargetEnum = new JsonArray(); + regexTargetEnum.add("request"); + regexTargetEnum.add("response"); + regexTargetEnum.add("both"); + regexTargetProp.add("enum", regexTargetEnum); + regexTargetProp.addProperty("description", "Target: request, response, or both (default: request)"); + regexTargetProp.addProperty("default", "request"); + regexParamProps.add("target", regexTargetProp); + + regexParamItem.add("properties", regexParamProps); + regexParamsProp.add("items", regexParamItem); + schema.add("regex_params", regexParamsProp); + + // modifications (optional) - ResendPacketToolと同じ形式 + JsonObject modificationsProp = new JsonObject(); + modificationsProp.addProperty("type", "array"); + modificationsProp.addProperty("description", "Array of modification rules to apply to all packets"); + JsonObject modificationItem = new JsonObject(); + modificationItem.addProperty("type", "object"); + JsonObject modificationProps = new JsonObject(); + + JsonObject targetProp = new JsonObject(); + targetProp.addProperty("type", "string"); + JsonArray targetEnum = new JsonArray(); + targetEnum.add("request"); + targetEnum.add("response"); + targetEnum.add("both"); + targetProp.add("enum", targetEnum); + targetProp.addProperty("description", "Target to modify: request, response, or both"); + modificationProps.add("target", targetProp); + + JsonObject typeProp = new JsonObject(); + typeProp.addProperty("type", "string"); + JsonArray typeEnum = new JsonArray(); + typeEnum.add("regex_replace"); + typeEnum.add("header_add"); + typeEnum.add("header_modify"); + typeProp.add("enum", typeEnum); + typeProp.addProperty("description", "Type of modification"); + modificationProps.add("type", typeProp); + + JsonObject patternProp = new JsonObject(); + patternProp.addProperty("type", "string"); + patternProp.addProperty("description", "Regex pattern for regex_replace type"); + modificationProps.add("pattern", patternProp); + + JsonObject replacementProp = new JsonObject(); + replacementProp.addProperty("type", "string"); + replacementProp.addProperty("description", "Replacement string for regex_replace or value for headers"); + modificationProps.add("replacement", replacementProp); + + JsonObject nameProp = new JsonObject(); + nameProp.addProperty("type", "string"); + nameProp.addProperty("description", "Header name for header_add/header_modify"); + modificationProps.add("name", nameProp); + + JsonObject valueProp = new JsonObject(); + valueProp.addProperty("type", "string"); + valueProp.addProperty("description", "Header value for header_add/header_modify"); + modificationProps.add("value", valueProp); + + modificationItem.add("properties", modificationProps); + modificationsProp.add("items", modificationItem); + schema.add("modifications", modificationsProp); + + // allow_duplicate_headers (optional) + JsonObject allowDuplicateHeadersProp = new JsonObject(); + allowDuplicateHeadersProp.addProperty("type", "boolean"); + allowDuplicateHeadersProp.addProperty("description", + "Allow duplicate headers when adding/modifying headers (default: false - replace existing headers)"); + allowDuplicateHeadersProp.addProperty("default", false); + schema.add("allow_duplicate_headers", allowDuplicateHeadersProp); + + // timeout_ms (optional) + JsonObject timeoutProp = new JsonObject(); + timeoutProp.addProperty("type", "integer"); + timeoutProp.addProperty("description", "Timeout for entire bulk operation in milliseconds (default: 30000)"); + timeoutProp.addProperty("default", 30000); + timeoutProp.addProperty("minimum", 1000); + timeoutProp.addProperty("maximum", 300000); + schema.add("timeout_ms", timeoutProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("BulkSendTool called with arguments: " + getSafeArgumentsString(arguments)); + log("BulkSendTool: Starting bulk send operation"); + + // パラメータ取得 + if (!arguments.has("packet_ids")) { + throw new IllegalArgumentException("packet_ids parameter is required"); + } + + JsonArray packetIdsArray = arguments.getAsJsonArray("packet_ids"); + if (packetIdsArray.size() == 0) { + throw new IllegalArgumentException("packet_ids array cannot be empty"); + } + if (packetIdsArray.size() > 100) { + throw new IllegalArgumentException("packet_ids array cannot exceed 100 packets"); + } + + String mode = arguments.has("mode") ? arguments.get("mode").getAsString() : "parallel"; + int count = arguments.has("count") ? arguments.get("count").getAsInt() : 1; + int intervalMs = arguments.has("interval_ms") ? arguments.get("interval_ms").getAsInt() : 0; + boolean allowDuplicateHeaders = arguments.has("allow_duplicate_headers") + ? arguments.get("allow_duplicate_headers").getAsBoolean() + : false; + int timeoutMs = arguments.has("timeout_ms") ? arguments.get("timeout_ms").getAsInt() : 30000; + + JsonArray modifications = arguments.has("modifications") + ? arguments.getAsJsonArray("modifications") + : new JsonArray(); + + JsonArray regexParams = arguments.has("regex_params") + ? arguments.getAsJsonArray("regex_params") + : new JsonArray(); + + // 送信モードの検証 + if (!"parallel".equals(mode) && !"sequential".equals(mode)) { + throw new IllegalArgumentException("mode must be 'parallel' or 'sequential'"); + } + + // 順次送信の場合、interval_msをチェック + if ("sequential".equals(mode) && intervalMs < 0) { + throw new IllegalArgumentException("interval_ms must be non-negative for sequential mode"); + } + + log("BulkSendTool: packet_ids=" + packetIdsArray.size() + ", mode=" + mode + ", count=" + count + ", interval=" + + intervalMs + "ms, allowDuplicateHeaders=" + allowDuplicateHeaders + ", timeout=" + timeoutMs + + "ms, modifications=" + modifications.size() + ", regex_params=" + regexParams.size()); + + // パケットIDを取得 + List packetIds = new ArrayList<>(); + for (JsonElement element : packetIdsArray) { + packetIds.add(element.getAsInt()); + } + + // ジョブIDを生成 + String jobId = UUID.randomUUID().toString(); + + long startTime = System.currentTimeMillis(); + int totalPackets = packetIds.size(); + int totalCount = totalPackets * count; + int sentCount = 0; + int failedCount = 0; + List results = new ArrayList<>(); + List regexParamsApplied = new ArrayList<>(); + Map extractedValues = new HashMap<>(); // regex_paramsで抽出された値を保存 + + try { + if ("parallel".equals(mode)) { + // 並列送信 + for (int i = 0; i < packetIds.size(); i++) { + int packetId = packetIds.get(i); + BulkSendResult result = processSinglePacket(packetId, i, count, modifications, regexParams, + extractedValues, allowDuplicateHeaders, jobId); + results.add(result); + sentCount += result.sentCount; + failedCount += result.failedCount; + regexParamsApplied.addAll(result.regexParamsApplied); + } + } else { + // 順次送信 + for (int i = 0; i < packetIds.size(); i++) { + int packetId = packetIds.get(i); + BulkSendResult result = processSinglePacketSequential(packetId, i, count, modifications, + regexParams, extractedValues, allowDuplicateHeaders, intervalMs, jobId); + results.add(result); + sentCount += result.sentCount; + failedCount += result.failedCount; + regexParamsApplied.addAll(result.regexParamsApplied); + + // 次のパケットまでインターバル(最後のパケット以外) + if (intervalMs > 0 && i < packetIds.size() - 1) { + Thread.sleep(intervalMs); + } + } + } + + } catch (Exception e) { + log("BulkSendTool: Bulk send operation failed: " + e.getMessage()); + throw e; + } + + long executionTime = System.currentTimeMillis() - startTime; + + // 結果作成 + JsonObject result = new JsonObject(); + result.addProperty("success", failedCount == 0); + result.addProperty("mode", mode); + result.addProperty("total_packets", totalPackets); + result.addProperty("total_count", totalCount); + result.addProperty("sent_count", sentCount); + result.addProperty("failed_count", failedCount); + result.addProperty("execution_time_ms", executionTime); + + // 詳細結果 + JsonArray resultsArray = new JsonArray(); + for (BulkSendResult r : results) { + JsonObject resultObj = new JsonObject(); + resultObj.addProperty("original_packet_id", r.originalPacketId); + resultObj.addProperty("packet_index", r.packetIndex); + resultObj.addProperty("success", r.success); + resultObj.addProperty("sent_count", r.sentCount); + resultObj.addProperty("failed_count", r.failedCount); + + if (r.error != null) { + resultObj.addProperty("error", r.error); + } + resultObj.addProperty("execution_time_ms", r.executionTimeMs); + + resultsArray.add(resultObj); + } + result.add("results", resultsArray); + + // regex_params適用結果 + JsonArray regexParamsAppliedArray = new JsonArray(); + for (RegexParamApplied rpa : regexParamsApplied) { + JsonObject rpaObj = new JsonObject(); + rpaObj.addProperty("packet_index", rpa.packetIndex); + rpaObj.addProperty("pattern", rpa.pattern); + rpaObj.addProperty("extracted_value", rpa.extractedValue); + rpaObj.addProperty("applied_count", rpa.appliedCount); + regexParamsAppliedArray.add(rpaObj); + } + result.add("regex_params_applied", regexParamsAppliedArray); + + // パフォーマンス統計 + JsonObject performance = new JsonObject(); + double packetsPerSecond = totalCount > 0 ? (double) sentCount / (executionTime / 1000.0) : 0.0; + double avgResponseTime = results.size() > 0 + ? results.stream().mapToLong(r -> r.executionTimeMs).average().orElse(0.0) + : 0.0; + + performance.addProperty("packets_per_second", Math.round(packetsPerSecond * 100.0) / 100.0); + performance.addProperty("average_response_time_ms", Math.round(avgResponseTime)); + performance.addProperty("concurrent_connections", totalPackets); + result.add("performance", performance); + + result.addProperty("job_id", jobId); + + log("BulkSendTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + + "ms"); + return result; + } + + /** + * 単一パケットの処理(並列送信) + */ + private BulkSendResult processSinglePacket(int packetId, int packetIndex, int count, JsonArray modifications, + JsonArray regexParams, Map extractedValues, boolean allowDuplicateHeaders, String jobId) { + + BulkSendResult result = new BulkSendResult(); + result.originalPacketId = packetId; + result.packetIndex = packetIndex; + result.regexParamsApplied = new ArrayList<>(); + + long startTime = System.currentTimeMillis(); + + try { + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + result.success = false; + result.failedCount = count; + result.error = "Packet with ID " + packetId + " not found"; + return result; + } + + // OneShotPacketを作成 + OneShotPacket originalOneShot = createOneShotPacket(originalPacket); + if (originalOneShot == null) { + result.success = false; + result.failedCount = count; + result.error = "Cannot create OneShotPacket from packet ID " + packetId; + return result; + } + + // regex_paramsを適用 + OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, + extractedValues, result.regexParamsApplied); + + // modificationsを適用 + OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, packetIndex + 1, + allowDuplicateHeaders); + + // 複数回送信用のパケット配列を作成(各パケットに固有のtemporary_idを付与) + OneShotPacket[] packetsToSend = new OneShotPacket[count]; + for (int i = 0; i < count; i++) { + String temporaryId = UUID.randomUUID().toString(); + packetsToSend[i] = new OneShotPacket(modifiedPacket.getId(), modifiedPacket.getListenPort(), + modifiedPacket.getClient(), modifiedPacket.getServer(), modifiedPacket.getServerName(), + modifiedPacket.getUseSSL(), modifiedPacket.getData(), modifiedPacket.getEncoder(), + modifiedPacket.getAlpn(), modifiedPacket.getDirection(), modifiedPacket.getConn(), + modifiedPacket.getGroup(), jobId, temporaryId); + } + + // ResendControllerを使用して並列送信 + CountDownLatch latch = new CountDownLatch(1); + List receivedPackets = new ArrayList<>(); + List sendErrors = new ArrayList<>(); + + ResendController.getInstance().resend(new ResendWorker(packetsToSend) { + @Override + protected void process(List chunks) { + synchronized (receivedPackets) { + receivedPackets.addAll(chunks); + } + } + + @Override + protected void done() { + try { + get(); // 例外があれば取得 + } catch (Exception e) { + synchronized (sendErrors) { + sendErrors.add(e); + } + } + latch.countDown(); + } + }); + + // 完了を待機 + boolean completed = latch.await(30, TimeUnit.SECONDS); + if (!completed) { + result.success = false; + result.failedCount = count; + result.error = "Timeout waiting for packet sending completion"; + return result; + } + + // 結果を設定 + if (!sendErrors.isEmpty()) { + result.success = false; + result.failedCount = count; + result.error = "Send failed: " + sendErrors.get(0).getMessage(); + } else { + result.success = true; + result.sentCount = count; + } + + } catch (Exception e) { + result.success = false; + result.failedCount = count; + result.error = e.getMessage(); + log("BulkSendTool: Failed to process packet " + packetId + ": " + e.getMessage()); + } finally { + result.executionTimeMs = System.currentTimeMillis() - startTime; + } + + return result; + } + + /** + * 単一パケットの処理(順次送信) + */ + private BulkSendResult processSinglePacketSequential(int packetId, int packetIndex, int count, + JsonArray modifications, JsonArray regexParams, Map extractedValues, + boolean allowDuplicateHeaders, int intervalMs, String jobId) { + + BulkSendResult result = new BulkSendResult(); + result.originalPacketId = packetId; + result.packetIndex = packetIndex; + result.regexParamsApplied = new ArrayList<>(); + + long startTime = System.currentTimeMillis(); + + try { + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + result.success = false; + result.failedCount = count; + result.error = "Packet with ID " + packetId + " not found"; + return result; + } + + // OneShotPacketを作成 + OneShotPacket originalOneShot = createOneShotPacket(originalPacket); + if (originalOneShot == null) { + result.success = false; + result.failedCount = count; + result.error = "Cannot create OneShotPacket from packet ID " + packetId; + return result; + } + + // 順次送信の場合、各送信で異なる処理を実行 + int successCount = 0; + int failCount = 0; + + for (int i = 0; i < count; i++) { + try { + // regex_paramsを適用(送信回数も考慮) + OneShotPacket regexModifiedPacket = applyRegexParams(originalOneShot, regexParams, packetIndex, + extractedValues, result.regexParamsApplied); + + // modificationsを適用 + OneShotPacket modifiedPacket = applyModifications(regexModifiedPacket, modifications, + packetIndex * count + i + 1, allowDuplicateHeaders); + + // ジョブ情報を付与 + String temporaryId = UUID.randomUUID().toString(); + OneShotPacket jobPacket = new OneShotPacket(modifiedPacket.getId(), modifiedPacket.getListenPort(), + modifiedPacket.getClient(), modifiedPacket.getServer(), modifiedPacket.getServerName(), + modifiedPacket.getUseSSL(), modifiedPacket.getData(), modifiedPacket.getEncoder(), + modifiedPacket.getAlpn(), modifiedPacket.getDirection(), modifiedPacket.getConn(), + modifiedPacket.getGroup(), jobId, temporaryId); + + // 単発送信 + ResendController.getInstance().resend(jobPacket); + successCount++; + + // 同一パケット内の送信間隔 + if (intervalMs > 0 && i < count - 1) { + Thread.sleep(intervalMs); + } + + } catch (Exception e) { + log("BulkSendTool: Failed to send packet " + packetId + " (attempt " + (i + 1) + "): " + + e.getMessage()); + failCount++; + } + } + + result.success = failCount == 0; + result.sentCount = successCount; + result.failedCount = failCount; + + } catch (Exception e) { + result.success = false; + result.failedCount = count; + result.error = e.getMessage(); + log("BulkSendTool: Failed to process packet " + packetId + ": " + e.getMessage()); + } finally { + result.executionTimeMs = System.currentTimeMillis() - startTime; + } + + return result; + } + + /** + * OneShotPacketを作成(ResendPacketToolと同じロジック) + */ + private OneShotPacket createOneShotPacket(Packet originalPacket) throws Exception { + if (originalPacket.getModifiedData().length > 0) { + return originalPacket.getOneShotFromModifiedData(); + } else if (originalPacket.getSentData().length > 0) { + return originalPacket.getOneShotPacket(originalPacket.getSentData()); + } else { + return originalPacket.getOneShotFromDecodedData(); + } + } + + /** + * regex_paramsを適用 + */ + private OneShotPacket applyRegexParams(OneShotPacket original, JsonArray regexParams, int packetIndex, + Map extractedValues, List appliedList) throws Exception { + + if (regexParams.size() == 0) { + return original; + } + + log("BulkSendTool: Applying " + regexParams.size() + " regex params to packet (index=" + packetIndex + ")"); + + byte[] data = original.getData().clone(); + String dataStr = new String(data); + + for (JsonElement paramElement : regexParams) { + JsonObject param = paramElement.getAsJsonObject(); + + // packet_indexが指定されている場合、対象パケットかチェック + if (param.has("packet_index") && param.get("packet_index").getAsInt() != packetIndex) { + continue; + } + + String pattern = param.get("pattern").getAsString(); + String valueTemplate = param.get("value_template").getAsString(); + String target = param.has("target") ? param.get("target").getAsString() : "request"; + + // 値テンプレートを処理 + String processedValue = processValueTemplate(valueTemplate, packetIndex, extractedValues); + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(dataStr); + + if (matcher.find()) { + // マッチした値を抽出(後続パケットで使用可能) + String extractedValue = null; + try { + // キャプチャグループがあるかチェック + if (matcher.groupCount() > 0) { + extractedValue = matcher.group(1); + } else { + // キャプチャグループがない場合は全体をマッチ + extractedValue = matcher.group(0); + } + + if (extractedValue != null) { + String key = "packet_" + packetIndex + "_" + pattern; + extractedValues.put(key, extractedValue); + } + } catch (Exception ex) { + log("BulkSendTool: Failed to extract value: " + ex.getMessage()); + } + + // 置換実行 + String beforeReplace = dataStr; + dataStr = matcher.replaceAll(processedValue); + + // デバッグログ: 置換前後の比較 + if (!beforeReplace.equals(dataStr)) { + log("BulkSendTool: Replacement successful - pattern: " + pattern); + log("BulkSendTool: Before: " + beforeReplace.substring(Math.max(0, matcher.start() - 20), + Math.min(beforeReplace.length(), matcher.end() + 20))); + log("BulkSendTool: After: " + dataStr + .substring(Math.max(0, dataStr.indexOf(processedValue) - 20), Math.min(dataStr.length(), + dataStr.indexOf(processedValue) + processedValue.length() + 20))); + } else { + log("BulkSendTool: Warning: No replacement occurred for pattern: " + pattern); + } + + // 適用結果を記録 + RegexParamApplied applied = new RegexParamApplied(); + applied.packetIndex = packetIndex; + applied.pattern = pattern; + applied.extractedValue = extractedValue; + applied.appliedCount = 1; + appliedList.add(applied); + + log("BulkSendTool: Regex param applied - pattern: " + pattern + ", value: " + processedValue); + } else { + log("BulkSendTool: Pattern not found in data - pattern: " + pattern); + // デバッグ用: データの一部を表示 + String debugData = dataStr.length() > 200 ? dataStr.substring(0, 200) + "..." : dataStr; + log("BulkSendTool: Data sample: " + debugData.replace("\r\n", "\\r\\n")); + } + + } catch (Exception e) { + log("BulkSendTool: Regex param failed: " + e.getMessage()); + e.printStackTrace(); + } + } + + data = dataStr.getBytes(); + + // 新しいOneShotPacketを作成 + OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), + original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, + original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), + original.getGroup()); + + return modifiedPacket; + } + + /** + * value_templateを処理(ResendPacketToolのprocessReplacementVariablesを拡張) + */ + private String processValueTemplate(String template, int packetIndex, Map extractedValues) { + String result = template; + + // {{packet_index}} - パケットインデックス + result = result.replace("{{packet_index}}", String.valueOf(packetIndex)); + + // {{timestamp}} - Unix timestamp + result = result.replace("{{timestamp}}", String.valueOf(System.currentTimeMillis() / 1000)); + + // {{random}} - ランダム文字列 + if (result.contains("{{random}}")) { + String randomStr = generateRandomString(8); + result = result.replace("{{random}}", randomStr); + } + + // {{uuid}} - UUID v4 + if (result.contains("{{uuid}}")) { + String uuid = UUID.randomUUID().toString(); + result = result.replace("{{uuid}}", uuid); + } + + // {{datetime}} - ISO 8601形式日時 + if (result.contains("{{datetime}}")) { + String datetime = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); + result = result.replace("{{datetime}}", datetime); + } + + // 抽出された値を置換({{extracted:key}}形式) + for (Map.Entry entry : extractedValues.entrySet()) { + String placeholder = "{{extracted:" + entry.getKey() + "}}"; + result = result.replace(placeholder, entry.getValue()); + } + + return result; + } + + /** + * パケットに改変を適用(ResendPacketToolのロジックを完全実装) + */ + private OneShotPacket applyModifications(OneShotPacket original, JsonArray modifications, int index, + boolean allowDuplicateHeaders) throws Exception { + + if (modifications.size() == 0) { + return original; + } + + log("BulkSendTool: Applying " + modifications.size() + " modifications to packet (index=" + index + ")"); + + byte[] data = original.getData().clone(); + String dataStr = new String(data); + + for (JsonElement modElement : modifications) { + JsonObject modification = modElement.getAsJsonObject(); + + String target = modification.has("target") ? modification.get("target").getAsString() : "request"; + String type = modification.get("type").getAsString(); + + log("BulkSendTool: Applying modification type=" + type + ", target=" + target); + + switch (type) { + case "regex_replace" : + dataStr = applyRegexReplace(dataStr, modification, index); + break; + case "header_add" : + dataStr = applyHeaderAdd(dataStr, modification, index, allowDuplicateHeaders); + break; + case "header_modify" : + dataStr = applyHeaderModify(dataStr, modification, index, allowDuplicateHeaders); + break; + default : + log("BulkSendTool: Unknown modification type: " + type); + break; + } + } + + data = dataStr.getBytes(); + + // 新しいOneShotPacketを作成(job情報は後で付与) + OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), + original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, + original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), + original.getGroup()); + + return modifiedPacket; + } + + /** + * 正規表現置換を適用(ResendPacketToolから移植) + */ + private String applyRegexReplace(String data, JsonObject modification, int index) { + String pattern = modification.get("pattern").getAsString(); + String replacement = modification.get("replacement").getAsString(); + + // 置換変数を処理 + replacement = processReplacementVariables(replacement, index); + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + String result = matcher.replaceAll(replacement); + log("BulkSendTool: Regex replace applied - pattern: " + pattern + ", replacement: " + replacement); + return result; + } catch (Exception e) { + log("BulkSendTool: Regex replace failed: " + e.getMessage()); + return data; + } + } + + /** + * ヘッダー追加を適用(ResendPacketToolから移植) + */ + private String applyHeaderAdd(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // HTTP形式のデータの場合、ヘッダー部分に追加 + if (data.contains("\r\n\r\n")) { + int headerEnd = data.indexOf("\r\n\r\n"); + String headers = data.substring(0, headerEnd); + String body = data.substring(headerEnd); + + // 重複を許可しない場合、既存ヘッダーがあるかチェック + if (!allowDuplicateHeaders) { + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(headers); + if (matcher.find()) { + // 既存ヘッダーを置換 + String result = matcher.replaceFirst(name + ": " + value) + body; + log("BulkSendTool: Header replaced (no duplicates allowed) - " + name + ": " + value); + return result; + } + } + + // 新しいヘッダーを追加 + String newHeader = name + ": " + value + "\r\n"; + String result = headers + "\r\n" + newHeader + body; + log("BulkSendTool: Header added - " + name + ": " + value); + return result; + } + + return data; + } + + /** + * ヘッダー変更を適用(ResendPacketToolから移植) + */ + private String applyHeaderModify(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // 既存ヘッダーを置換 + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + if (matcher.find()) { + String replacement = name + ": " + value; + String result; + if (allowDuplicateHeaders) { + // 重複を許可する場合は最初のヘッダーのみ変更 + result = matcher.replaceFirst(replacement); + } else { + // 重複を許可しない場合は全ての同名ヘッダーを置換 + result = matcher.replaceAll(replacement); + } + log("BulkSendTool: Header modified - " + name + ": " + value + " (allowDuplicates=" + + allowDuplicateHeaders + ")"); + return result; + } else { + // ヘッダーが見つからない場合は追加 + return applyHeaderAdd(data, modification, index, allowDuplicateHeaders); + } + } catch (Exception e) { + log("BulkSendTool: Header modify failed: " + e.getMessage()); + return data; + } + } + + /** + * 置換変数を処理(ResendPacketToolから移植) + */ + private String processReplacementVariables(String input, int index) { + String result = input; + + // {{index}} - 送信順序 + result = result.replace("{{index}}", String.valueOf(index)); + + // {{timestamp}} - Unix timestamp + result = result.replace("{{timestamp}}", String.valueOf(System.currentTimeMillis() / 1000)); + + // {{random}} - ランダム文字列 + if (result.contains("{{random}}")) { + String randomStr = generateRandomString(8); + result = result.replace("{{random}}", randomStr); + } + + // {{uuid}} - UUID v4 + if (result.contains("{{uuid}}")) { + String uuid = UUID.randomUUID().toString(); + result = result.replace("{{uuid}}", uuid); + } + + // {{datetime}} - ISO 8601形式日時 + if (result.contains("{{datetime}}")) { + String datetime = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); + result = result.replace("{{datetime}}", datetime); + } + + return result; + } + + /** + * ランダム文字列生成(ResendPacketToolから移植) + */ + private String generateRandomString(int length) { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) { + sb.append(chars.charAt(random.nextInt(chars.length()))); + } + + return sb.toString(); + } + + /** + * 個別パケットの送信結果 + */ + private static class BulkSendResult { + int originalPacketId; + int packetIndex; + boolean success; + int sentCount; + int failedCount; + String error; + long executionTimeMs; + List regexParamsApplied; + } + + /** + * regex_paramsの適用結果 + */ + private static class RegexParamApplied { + int packetIndex; + String pattern; + String extractedValue; + int appliedCount; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java new file mode 100644 index 00000000..f91d160e --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ConfigTool.java @@ -0,0 +1,140 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class ConfigTool extends AuthenticatedMCPTool { + + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "get_config"; + } + + @Override + public String getDescription() { + return "Get PacketProxy configuration settings"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject categoriesProp = new JsonObject(); + categoriesProp.addProperty("type", "array"); + categoriesProp.addProperty("description", "Categories to retrieve (empty for all)"); + + JsonObject itemsProp = new JsonObject(); + itemsProp.addProperty("type", "string"); + JsonArray enumValues = new JsonArray(); + enumValues.add("listenPorts"); + enumValues.add("servers"); + enumValues.add("modifications"); + enumValues.add("sslPassThroughs"); + enumValues.add("resolutions"); + enumValues.add("interceptOptions"); + enumValues.add("clientCertificates"); + enumValues.add("generalConfigs"); + enumValues.add("extensions"); + enumValues.add("filters"); + enumValues.add("openVPNForwardPorts"); + enumValues.add("charSets"); + itemsProp.add("enum", enumValues); + categoriesProp.add("items", itemsProp); + + schema.add("categories", categoriesProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("ConfigTool called with arguments: " + getSafeArgumentsString(arguments)); + + try { + // HTTP APIで設定を取得 + String configJson = getConfigFromHttpApi(); + + // categoriesでフィルタリングが指定されている場合はフィルタリングを適用 + JsonObject allConfig = gson.fromJson(configJson, JsonObject.class); + JsonObject filteredConfig = filterByCategories(allConfig, arguments); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(filteredConfig)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject mcpResult = new JsonObject(); + mcpResult.add("content", contentArray); + + log("ConfigTool returning configuration from HTTP API"); + return mcpResult; + + } catch (Exception e) { + log("ConfigTool error: " + e.getMessage()); + throw new Exception("Failed to get configuration: " + e.getMessage()); + } + } + + private String getConfigFromHttpApi() throws Exception { + // 設定済みAccessTokenを取得(HTTPリクエスト用) + String accessToken = getConfiguredAccessToken(); + + // HTTP GETリクエスト + URL url = new URL("http://localhost:32349/config"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Authorization", accessToken); + conn.setConnectTimeout(5000); + conn.setReadTimeout(10000); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + throw new Exception("HTTP API returned status: " + responseCode + ". Check if config sharing is enabled."); + } + + // レスポンスを読み取り + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + conn.disconnect(); + + return response.toString(); + } + + private JsonObject filterByCategories(JsonObject config, JsonObject arguments) { + if (!arguments.has("categories")) { + return config; // カテゴリ指定がない場合は全て返す + } + + JsonArray categories = arguments.getAsJsonArray("categories"); + if (categories.size() == 0) { + return config; // 空の場合は全て返す + } + + JsonObject filtered = new JsonObject(); + for (int i = 0; i < categories.size(); i++) { + String category = categories.get(i).getAsString(); + if (config.has(category)) { + filtered.add(category, config.get(category)); + } + } + + return filtered; + } + +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java new file mode 100644 index 00000000..eb21cda0 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/HistoryTool.java @@ -0,0 +1,392 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import javax.swing.RowFilter; +import packetproxy.common.FilterTextParser; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +public class HistoryTool extends AuthenticatedMCPTool { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "get_history"; + } + + @Override + public String getDescription() { + return "Get packet history from PacketProxy with filtering and ordering capabilities"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject limitProp = new JsonObject(); + limitProp.addProperty("type", "integer"); + limitProp.addProperty("description", "Maximum number of packets to return"); + limitProp.addProperty("default", 100); + schema.add("limit", limitProp); + + JsonObject offsetProp = new JsonObject(); + offsetProp.addProperty("type", "integer"); + offsetProp.addProperty("description", "Number of packets to skip"); + offsetProp.addProperty("default", 0); + schema.add("offset", offsetProp); + + JsonObject filterProp = new JsonObject(); + filterProp.addProperty("type", "string"); + filterProp.addProperty("description", "PacketProxy Filter syntax for filtering packets. " + + "Available columns: id, request, response, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, alpn, group, full_text, full_text_i. " + + "Note: method, url, status are NOT available for filtering (only for ordering). " + + "Operators: == (equals), != (not equals), >= (greater or equal), <= (less or equal), =~ (regex match), !~ (regex not match), && (AND), || (OR). " + + "Examples: 'type == HTTP', 'length > 1000', 'full_text_i =~ authorization', 'client_port == 80 && server_port == 443'"); + schema.add("filter", filterProp); + + JsonObject orderProp = new JsonObject(); + orderProp.addProperty("type", "string"); + orderProp.addProperty("description", "Order by column and direction. Format: 'column asc' or 'column desc'. " + + "Available columns: id, length, client_ip, client_port, server_ip, server_port, time, resend, modified, type, encode, group, method, url, status. " + + "Examples: 'time desc', 'id asc', 'length desc', 'status asc'"); + orderProp.addProperty("default", "id desc"); + schema.add("order", orderProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("HistoryTool called with arguments: " + getSafeArgumentsString(arguments)); + + int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; + int offset = arguments.has("offset") ? arguments.get("offset").getAsInt() : 0; + String filter = arguments.has("filter") ? arguments.get("filter").getAsString() : null; + String order = arguments.has("order") ? arguments.get("order").getAsString() : "id desc"; + + // Validate parameters + if (limit < 1 || limit > 1000) { + throw new Exception("Limit must be between 1 and 1000"); + } + if (offset < 0) { + throw new Exception("Offset must be non-negative"); + } + + try { + Packets packets = Packets.getInstance(); + List allPackets = packets.queryAll(); + List filteredPackets = allPackets; + + // Apply filter if provided + if (filter != null && !filter.trim().isEmpty()) { + filteredPackets = applyFilter(allPackets, filter); + } + + // Apply ordering + filteredPackets = applyOrdering(filteredPackets, order); + + JsonArray packetsArray = new JsonArray(); + + int totalCount = filteredPackets.size(); + int startIndex = Math.min(offset, totalCount); + int endIndex = Math.min(startIndex + limit, totalCount); + + for (int i = startIndex; i < endIndex; i++) { + Packet packet = filteredPackets.get(i); + JsonObject packetJson = convertPacketToJson(packet); + packetsArray.add(packetJson); + } + + JsonObject data = new JsonObject(); + data.add("packets", packetsArray); + data.addProperty("total_count", totalCount); + data.addProperty("has_more", endIndex < totalCount); + if (filter != null && !filter.trim().isEmpty()) { + data.addProperty("filter_applied", filter); + } + data.addProperty("order_applied", order); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(data)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + log("HistoryTool returning " + packetsArray.size() + " packets (filtered from " + allPackets.size() + + " total)"); + return result; + + } catch (Exception e) { + log("HistoryTool error: " + e.getMessage()); + throw new Exception("Failed to get packet history: " + e.getMessage()); + } + } + + private List applyFilter(List packets, String filterText) throws Exception { + List filtered = new ArrayList<>(); + + try { + // Parse the filter using FilterTextParser + RowFilter rowFilter = FilterTextParser.parse(filterText); + + for (Packet packet : packets) { + // Create a mock table entry to test the filter + Object[] rowData = createRowDataFromPacket(packet); + MockTableEntry entry = new MockTableEntry(rowData); + + if (rowFilter.include(entry)) { + filtered.add(packet); + } + } + } catch (Exception e) { + log("Filter parsing error: " + e.getMessage()); + throw new Exception("Invalid filter syntax: " + e.getMessage()); + } + + return filtered; + } + + private List applyOrdering(List packets, String orderString) throws Exception { + if (orderString == null || orderString.trim().isEmpty()) { + return packets; + } + + String[] parts = orderString.trim().split("\\s+"); + if (parts.length != 2) { + throw new Exception("Invalid order format. Expected 'column asc|desc', got: " + orderString); + } + + String column = parts[0].toLowerCase(); + String direction = parts[1].toLowerCase(); + + if (!direction.equals("asc") && !direction.equals("desc")) { + throw new Exception("Invalid order direction. Expected 'asc' or 'desc', got: " + direction); + } + + boolean ascending = direction.equals("asc"); + List sortedPackets = new ArrayList<>(packets); + + Comparator comparator = getComparatorForColumn(column); + if (comparator == null) { + throw new Exception("Invalid order column: " + column); + } + + if (!ascending) { + comparator = comparator.reversed(); + } + + sortedPackets.sort(comparator); + return sortedPackets; + } + + private Comparator getComparatorForColumn(String column) { + switch (column) { + case "id" : + return Comparator.comparing(Packet::getId); + case "length" : + return Comparator.comparing(p -> p.getDecodedData().length); + case "client_ip" : + return Comparator.comparing(Packet::getClientIP, Comparator.nullsLast(String::compareTo)); + case "client_port" : + return Comparator.comparing(Packet::getClientPort); + case "server_ip" : + return Comparator.comparing(Packet::getServerIP, Comparator.nullsLast(String::compareTo)); + case "server_port" : + return Comparator.comparing(Packet::getServerPort); + case "time" : + return Comparator.comparing(Packet::getDate, Comparator.nullsLast(Comparator.naturalOrder())); + case "resend" : + return Comparator.comparing(Packet::getResend); + case "modified" : + return Comparator.comparing(Packet::getModified); + case "type" : + return Comparator.comparing(Packet::getContentType, Comparator.nullsLast(String::compareTo)); + case "encode" : + return Comparator.comparing(Packet::getEncoder, Comparator.nullsLast(String::compareTo)); + case "group" : + return Comparator.comparing(Packet::getGroup); + case "method" : + return Comparator.comparing(this::extractMethod, Comparator.nullsLast(String::compareTo)); + case "url" : + return Comparator.comparing(this::extractUrl, Comparator.nullsLast(String::compareTo)); + case "status" : + return Comparator.comparing(this::extractStatus, Comparator.nullsLast(Integer::compareTo)); + default : + return null; + } + } + + private String extractMethod(Packet packet) { + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 1) { + return requestLine[0]; + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private String extractUrl(Packet packet) { + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 2) { + return requestLine[1]; + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private Integer extractStatus(Packet packet) { + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 3 && requestLine[0].startsWith("HTTP/")) { + return Integer.parseInt(requestLine[1]); + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private Object[] createRowDataFromPacket(Packet packet) { + Object[] rowData = new Object[17]; // Based on columnMapper size + + rowData[0] = packet.getId(); // id + + // Extract request and response data + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + rowData[1] = request; // request + rowData[2] = ""; // response (not available in current packet data) + } catch (Exception e) { + rowData[1] = ""; + rowData[2] = ""; + } + + rowData[3] = packet.getDecodedData().length; // length + rowData[4] = packet.getClientIP(); // client_ip + rowData[5] = packet.getClientPort(); // client_port + rowData[6] = packet.getServerIP(); // server_ip + rowData[7] = packet.getServerPort(); // server_port + rowData[8] = packet.getDate(); // time + rowData[9] = packet.getResend(); // resend + rowData[10] = packet.getModified(); // modified + rowData[11] = packet.getContentType(); // type + rowData[12] = packet.getEncoder(); // encode + rowData[13] = ""; // alpn (not available) + rowData[14] = packet.getGroup(); // group + rowData[15] = (String) rowData[1]; // full_text (same as request) + rowData[16] = ((String) rowData[1]).toLowerCase(); // full_text_i (lowercase) + + return rowData; + } + + // Mock table entry class for filter testing + private static class MockTableEntry extends RowFilter.Entry { + private final Object[] data; + + public MockTableEntry(Object[] data) { + this.data = data; + } + + @Override + public Object getModel() { + return null; + } + + @Override + public int getValueCount() { + return data.length; + } + + @Override + public Object getValue(int index) { + return index < data.length ? data[index] : null; + } + + @Override + public String getStringValue(int index) { + Object value = getValue(index); + return value != null ? value.toString() : ""; + } + + @Override + public Object getIdentifier() { + return null; + } + } + + private JsonObject convertPacketToJson(Packet packet) { + JsonObject packetJson = new JsonObject(); + + packetJson.addProperty("id", packet.getId()); + packetJson.addProperty("length", packet.getDecodedData().length); + packetJson.addProperty("client_ip", packet.getClientIP()); + packetJson.addProperty("client_port", packet.getClientPort()); + packetJson.addProperty("server_ip", packet.getServerIP()); + packetJson.addProperty("server_port", packet.getServerPort()); + packetJson.addProperty("time", dateFormat.format(packet.getDate())); + packetJson.addProperty("resend", packet.getResend()); + packetJson.addProperty("modified", packet.getModified()); + packetJson.addProperty("type", packet.getContentType()); + packetJson.addProperty("encode", packet.getEncoder()); + packetJson.addProperty("group", packet.getGroup()); + + // HTTPの場合、methodとurlとstatusを抽出 + try { + String request = new String(packet.getDecodedData(), "UTF-8"); + String[] lines = request.split("\n"); + if (lines.length > 0) { + String[] requestLine = lines[0].split(" "); + if (requestLine.length >= 2) { + packetJson.addProperty("method", requestLine[0]); + packetJson.addProperty("url", requestLine[1]); + } + + // レスポンスの場合、ステータスコードを抽出 + if (requestLine.length >= 3 && requestLine[0].startsWith("HTTP/")) { + try { + int status = Integer.parseInt(requestLine[1]); + packetJson.addProperty("status", status); + } catch (NumberFormatException e) { + // ステータスコードが数値でない場合は無視 + } + } + } + } catch (Exception e) { + // HTTP以外のパケットの場合は無視 + } + + return packetJson; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java new file mode 100644 index 00000000..e9807675 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/JobStatusTool.java @@ -0,0 +1,262 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +/** + * ジョブの状況を取得するツール + */ +public class JobStatusTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "get_job_status"; + } + + @Override + public String getDescription() { + return "Get status information for jobs created by send tools (resend_packet/bulk_send/call_vulcheck_helper). " + + "Returns job details including request/response packet counts and completion status."; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject jobIdProp = new JsonObject(); + jobIdProp.addProperty("type", "string"); + jobIdProp.addProperty("description", "Job ID to get status for. If not provided, returns status for all jobs."); + schema.add("job_id", jobIdProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("JobStatusTool called with arguments: " + getSafeArgumentsString(arguments)); + + String jobId = arguments.has("job_id") ? arguments.get("job_id").getAsString() : null; + + if (jobId != null && !jobId.trim().isEmpty()) { + // 特定のジョブの詳細を取得 + return getJobDetail(jobId); + } else { + // 全ジョブの概要を取得 + return getAllJobsStatus(); + } + } + + /** + * 特定のジョブの詳細情報を取得 + */ + private JsonObject getJobDetail(String jobId) throws Exception { + log("JobStatusTool: Getting detail for job " + jobId); + + // job_idが一致するパケットを取得 + List allPackets = Packets.getInstance().queryAll(); + List jobPackets = new ArrayList<>(); + + log("JobStatusTool: Searching for job " + jobId + " in " + allPackets.size() + " total packets"); + + for (Packet packet : allPackets) { + String packetJobId = packet.getJobId(); + if (packetJobId != null) { + log("JobStatusTool: Packet " + packet.getId() + " has job_id: " + packetJobId); + } + if (jobId.equals(packetJobId)) { + jobPackets.add(packet); + log("JobStatusTool: Found matching packet " + packet.getId() + " for job " + jobId); + } + } + + log("JobStatusTool: Found " + jobPackets.size() + " packets for job " + jobId); + + if (jobPackets.isEmpty()) { + throw new IllegalArgumentException("Job not found: " + jobId); + } + + // temporary_id ごとにパケットを整理 + Map jobRequests = new HashMap<>(); + + for (Packet packet : jobPackets) { + String temporaryId = packet.getTemporaryId(); + if (temporaryId == null || temporaryId.trim().isEmpty()) { + continue; + } + + JobRequest jobRequest = jobRequests.computeIfAbsent(temporaryId, k -> new JobRequest()); + jobRequest.temporaryId = temporaryId; + + if (packet.getDirection() == Packet.Direction.CLIENT) { + // リクエストパケット + jobRequest.requestPacketId = packet.getId(); + jobRequest.hasRequest = true; + } else if (packet.getDirection() == Packet.Direction.SERVER) { + // レスポンスパケット + jobRequest.responsePacketId = packet.getId(); + jobRequest.hasResponse = true; + } + } + + // 結果を構築 + JsonObject result = new JsonObject(); + result.addProperty("job_id", jobId); + result.addProperty("total_requests", jobRequests.size()); + + int requestsSent = 0; + int responsesReceived = 0; + + for (JobRequest jobRequest : jobRequests.values()) { + if (jobRequest.hasRequest) { + requestsSent++; + } + if (jobRequest.hasResponse) { + responsesReceived++; + } + } + + result.addProperty("requests_sent", requestsSent); + result.addProperty("responses_received", responsesReceived); + + // ジョブの状態を判定 + String status; + if (requestsSent == 0) { + status = "created"; + } else if (requestsSent < jobRequests.size()) { + status = "sending_requests"; + } else if (responsesReceived == 0) { + status = "requests_sent"; + } else if (responsesReceived < requestsSent) { + status = "receiving_responses"; + } else { + status = "completed"; + } + result.addProperty("status", status); + + // 各リクエストの詳細 + JsonArray requestsArray = new JsonArray(); + for (JobRequest jobRequest : jobRequests.values()) { + JsonObject reqObj = new JsonObject(); + reqObj.addProperty("temporary_id", jobRequest.temporaryId); + reqObj.addProperty("has_request", jobRequest.hasRequest); + reqObj.addProperty("has_response", jobRequest.hasResponse); + + if (jobRequest.hasRequest) { + reqObj.addProperty("request_packet_id", jobRequest.requestPacketId); + } + if (jobRequest.hasResponse) { + reqObj.addProperty("response_packet_id", jobRequest.responsePacketId); + } + + requestsArray.add(reqObj); + } + result.add("requests", requestsArray); + + log("JobStatusTool: Job " + jobId + " has " + requestsSent + " requests sent, " + responsesReceived + + " responses received, status: " + status); + + return result; + } + + /** + * 全ジョブの概要を取得 + */ + private JsonObject getAllJobsStatus() throws Exception { + log("JobStatusTool: Getting status for all jobs"); + + // 全パケットからjob_idが設定されているものを取得 + List allPackets = Packets.getInstance().queryAll(); + Map jobs = new HashMap<>(); + + log("JobStatusTool: Total packets in database: " + allPackets.size()); + + int packetsWithJobId = 0; + for (Packet packet : allPackets) { + String jobId = packet.getJobId(); + if (jobId == null || jobId.trim().isEmpty()) { + continue; + } + + packetsWithJobId++; + log("JobStatusTool: Found packet " + packet.getId() + " with job_id: " + jobId + ", temporary_id: " + + packet.getTemporaryId()); + + JobSummary jobSummary = jobs.computeIfAbsent(jobId, k -> new JobSummary()); + jobSummary.jobId = jobId; + + String temporaryId = packet.getTemporaryId(); + if (temporaryId != null && !temporaryId.trim().isEmpty()) { + jobSummary.temporaryIds.add(temporaryId); + + if (packet.getDirection() == Packet.Direction.CLIENT) { + jobSummary.requestsSent++; + } else if (packet.getDirection() == Packet.Direction.SERVER) { + jobSummary.responsesReceived++; + } + } + } + + log("JobStatusTool: Found " + packetsWithJobId + " packets with job_id"); + + // 結果を構築 + JsonObject result = new JsonObject(); + result.addProperty("total_jobs", jobs.size()); + + JsonArray jobsArray = new JsonArray(); + for (JobSummary jobSummary : jobs.values()) { + JsonObject jobObj = new JsonObject(); + jobObj.addProperty("job_id", jobSummary.jobId); + jobObj.addProperty("total_requests", jobSummary.temporaryIds.size()); + jobObj.addProperty("requests_sent", jobSummary.requestsSent); + jobObj.addProperty("responses_received", jobSummary.responsesReceived); + + // ステータスを判定 + String status; + if (jobSummary.requestsSent == 0) { + status = "created"; + } else if (jobSummary.responsesReceived == 0) { + status = "requests_sent"; + } else if (jobSummary.responsesReceived < jobSummary.requestsSent) { + status = "receiving_responses"; + } else { + status = "completed"; + } + jobObj.addProperty("status", status); + + jobsArray.add(jobObj); + } + result.add("jobs", jobsArray); + + log("JobStatusTool: Found " + jobs.size() + " jobs"); + return result; + } + + /** + * ジョブのリクエスト情報 + */ + private static class JobRequest { + String temporaryId; + boolean hasRequest = false; + boolean hasResponse = false; + int requestPacketId = -1; + int responsePacketId = -1; + } + + /** + * ジョブの概要情報 + */ + private static class JobSummary { + String jobId; + List temporaryIds = new ArrayList<>(); + int requestsSent = 0; + int responsesReceived = 0; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java new file mode 100644 index 00000000..fa2c1ef6 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/LogTool.java @@ -0,0 +1,320 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import packetproxy.gui.GUILog; + +public class LogTool extends AuthenticatedMCPTool { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "get_logs"; + } + + @Override + public String getDescription() { + return "Get logs from PacketProxy"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject levelProp = new JsonObject(); + levelProp.addProperty("type", "string"); + levelProp.addProperty("description", "Log level filter: debug, info, warn, error"); + levelProp.addProperty("default", "info"); + schema.add("level", levelProp); + + JsonObject limitProp = new JsonObject(); + limitProp.addProperty("type", "integer"); + limitProp.addProperty("description", "Maximum number of log entries to return"); + limitProp.addProperty("default", 100); + schema.add("limit", limitProp); + + JsonObject sinceProp = new JsonObject(); + sinceProp.addProperty("type", "string"); + sinceProp.addProperty("description", "Start time in ISO 8601 format (e.g., 2025-01-15T00:00:00Z)"); + schema.add("since", sinceProp); + + JsonObject filterProp = new JsonObject(); + filterProp.addProperty("type", "string"); + filterProp.addProperty("description", "Regular expression filter for log messages"); + schema.add("filter", filterProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("LogTool called with arguments: " + getSafeArgumentsString(arguments)); + + String level = arguments.has("level") ? arguments.get("level").getAsString() : "info"; + int limit = arguments.has("limit") ? arguments.get("limit").getAsInt() : 100; + String since = arguments.has("since") ? arguments.get("since").getAsString() : null; + String filter = arguments.has("filter") ? arguments.get("filter").getAsString() : null; + + // Validate parameters + if (limit < 1 || limit > 1000) { + throw new Exception("Limit must be between 1 and 1000"); + } + + if (!isValidLogLevel(level)) { + throw new Exception("Invalid log level. Use: debug, info, warn, error"); + } + + LocalDateTime sinceDateTime = null; + if (since != null) { + try { + sinceDateTime = LocalDateTime.parse(since.replace("Z", ""), + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + } catch (DateTimeParseException e) { + throw new Exception("Invalid date format. Use ISO 8601 format (e.g., 2025-01-15T00:00:00Z)"); + } + } + + Pattern filterPattern = null; + if (filter != null && !filter.trim().isEmpty()) { + try { + filterPattern = Pattern.compile(filter, Pattern.CASE_INSENSITIVE); + } catch (Exception e) { + throw new Exception("Invalid regex pattern: " + e.getMessage()); + } + } + + try { + // 実際のログ取得処理 + // PacketProxyのログはutil.Loggingを通してGUILogに保存されているため、 + // そこからログエントリを取得する + List logEntries = getLogEntriesFromGUILog(level, sinceDateTime, filterPattern, limit); + + JsonArray logsArray = new JsonArray(); + for (LogEntry entry : logEntries) { + JsonObject logJson = new JsonObject(); + logJson.addProperty("timestamp", dateFormat.format(entry.getTimestamp())); + logJson.addProperty("level", entry.getLevel()); + logJson.addProperty("message", entry.getMessage()); + logJson.addProperty("thread", entry.getThread()); + logJson.addProperty("class", entry.getClassName()); + logsArray.add(logJson); + } + + JsonObject data = new JsonObject(); + data.add("logs", logsArray); + data.addProperty("total_count", logEntries.size()); + data.addProperty("has_more", logEntries.size() >= limit); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(data)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + log("LogTool returning " + logsArray.size() + " log entries"); + return result; + + } catch (Exception e) { + log("LogTool error: " + e.getMessage()); + throw new Exception("Failed to get logs: " + e.getMessage()); + } + } + + private boolean isValidLogLevel(String level) { + return level.equals("debug") || level.equals("info") || level.equals("warn") || level.equals("error"); + } + + private List getLogEntriesFromGUILog(String level, LocalDateTime since, Pattern filter, int limit) { + List entries = new ArrayList<>(); + + try { + GUILog guiLog = GUILog.getInstance(); + String logText = guiLog.getLogText(); + + if (logText != null && !logText.trim().isEmpty()) { + // ログテキストを行ごとに分析 + String[] lines = logText.split("\n"); + + for (String line : lines) { + if (line.trim().isEmpty()) { + continue; + } + + LogEntry entry = parseLogLine(line.trim()); + if (entry != null) { + entries.add(entry); + } + } + } + + // 最新のログが上に来るようにリバース + java.util.Collections.reverse(entries); + + } catch (Exception e) { + log("Error getting log entries: " + e.getMessage()); + } + + // フィルタリング適用 + List filteredEntries = new ArrayList<>(); + for (LogEntry entry : entries) { + // レベルフィルタ + if (!matchesLogLevel(entry.getLevel(), level)) { + continue; + } + + // 時間フィルタ + if (since != null) { + LocalDateTime entryTime = LocalDateTime.ofInstant(entry.getTimestamp().toInstant(), + ZoneId.systemDefault()); + if (entryTime.isBefore(since)) { + continue; + } + } + + // 正規表現フィルタ + if (filter != null && !filter.matcher(entry.getMessage()).find()) { + continue; + } + + filteredEntries.add(entry); + + // 制限チェック + if (filteredEntries.size() >= limit) { + break; + } + } + + return filteredEntries; + } + + private boolean matchesLogLevel(String entryLevel, String filterLevel) { + // レベルの優先度: debug < info < warn < error + int entryPriority = getLogLevelPriority(entryLevel); + int filterPriority = getLogLevelPriority(filterLevel); + return entryPriority >= filterPriority; + } + + private int getLogLevelPriority(String level) { + switch (level.toLowerCase()) { + case "debug" : + return 0; + case "info" : + return 1; + case "warn" : + return 2; + case "error" : + return 3; + default : + return 1; // デフォルトはinfo + } + } + + private LogEntry parseLogLine(String line) { + try { + // PacketProxyのログ形式: "yyyy/MM/dd HH:mm:ss message" + // util.Loggingの形式に基づく + if (line.length() < 19) { + return null; // 最小の日時フォーマット長より短い + } + + String dateTimePart = line.substring(0, 19); + String messagePart = line.length() > 26 ? line.substring(26) : ""; + + // 日時をパース + java.util.Date timestamp; + try { + LocalDateTime localDateTime = LocalDateTime.parse(dateTimePart, dtf); + timestamp = java.util.Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } catch (DateTimeParseException e) { + // 日時パースに失敗した場合は現在時刻を使用 + timestamp = new java.util.Date(); + } + + // ログレベルを推定(メッセージ内容から) + String level = "info"; // デフォルト + String lowerMessage = messagePart.toLowerCase(); + if (lowerMessage.contains("error") || lowerMessage.contains("exception") || lowerMessage.contains("failed") + || lowerMessage.contains("fail")) { + level = "error"; + } else if (lowerMessage.contains("warn") || lowerMessage.contains("warning")) { + level = "warn"; + } else if (lowerMessage.contains("debug")) { + level = "debug"; + } + + // スレッド名とクラス名を推定 + String thread = "main"; // デフォルト + String className = "packetproxy"; // デフォルト + + // メッセージからクラス名を抽出を試行 + if (messagePart.contains("MCP")) { + className = "packetproxy.extensions.mcp"; + } else if (messagePart.contains("Server")) { + className = "packetproxy.extensions.mcp.MCPServer"; + } else if (messagePart.contains("Tool")) { + className = "packetproxy.extensions.mcp.tools"; + } + + return new LogEntry(timestamp, level, messagePart, thread, className); + + } catch (Exception e) { + // パースに失敗した場合はnullを返す + return null; + } + } + + // ログエントリを表すクラス + private static class LogEntry { + private final java.util.Date timestamp; + private final String level; + private final String message; + private final String thread; + private final String className; + + public LogEntry(java.util.Date timestamp, String level, String message, String thread, String className) { + this.timestamp = timestamp; + this.level = level; + this.message = message; + this.thread = thread; + this.className = className; + } + + public java.util.Date getTimestamp() { + return timestamp; + } + + public String getLevel() { + return level; + } + + public String getMessage() { + return message; + } + + public String getThread() { + return thread; + } + + public String getClassName() { + return className; + } + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java new file mode 100644 index 00000000..fb933f25 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/MCPTool.java @@ -0,0 +1,26 @@ +package packetproxy.extensions.mcp.tools; + +import com.google.gson.JsonObject; + +public interface MCPTool { + + /** + * ツール名を取得 + */ + String getName(); + + /** + * ツールの説明を取得 + */ + String getDescription(); + + /** + * 入力スキーマを取得 (JSON Schema properties形式) + */ + JsonObject getInputSchema(); + + /** + * ツールを実行 + */ + JsonObject call(JsonObject arguments) throws Exception; +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java new file mode 100644 index 00000000..128f6af0 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/PacketDetailTool.java @@ -0,0 +1,274 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.List; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +public class PacketDetailTool extends AuthenticatedMCPTool { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "get_packet_detail"; + } + + @Override + public String getDescription() { + return "Get detailed information about a specific packet"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject packetIdProp = new JsonObject(); + packetIdProp.addProperty("type", "integer"); + packetIdProp.addProperty("description", "ID of the packet to retrieve"); + schema.add("packet_id", packetIdProp); + + JsonObject includeBodyProp = new JsonObject(); + includeBodyProp.addProperty("type", "boolean"); + includeBodyProp.addProperty("description", "Whether to include request/response body"); + includeBodyProp.addProperty("default", true); + schema.add("include_body", includeBodyProp); + + JsonObject includePairProp = new JsonObject(); + includePairProp.addProperty("type", "boolean"); + includePairProp.addProperty("description", + "Whether to include paired packet (request when response specified, response when request specified)"); + includePairProp.addProperty("default", false); + schema.add("include_pair", includePairProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("PacketDetailTool called with arguments: " + getSafeArgumentsString(arguments)); + + if (!arguments.has("packet_id")) { + throw new Exception("packet_id is required"); + } + + int packetId = arguments.get("packet_id").getAsInt(); + boolean includeBody = !arguments.has("include_body") || arguments.get("include_body").getAsBoolean(); + boolean includePair = arguments.has("include_pair") && arguments.get("include_pair").getAsBoolean(); + + try { + Packets packets = Packets.getInstance(); + Packet packet = packets.query(packetId); + + if (packet == null) { + throw new Exception("Packet not found: " + packetId); + } + + JsonObject data = buildPacketDetail(packet, includeBody, includePair); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", gson.toJson(data)); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + log("PacketDetailTool returning packet " + packetId); + return result; + + } catch (Exception e) { + log("PacketDetailTool error: " + e.getMessage()); + throw new Exception("Failed to get packet detail: " + e.getMessage()); + } + } + + private JsonObject buildPacketDetail(Packet packet, boolean includeBody, boolean includePair) throws Exception { + JsonObject result = new JsonObject(); + + if (includePair) { + // Try to find the paired packet (request/response) + Packet pairedPacket = findPairedPacket(packet); + + if (pairedPacket != null) { + // Build paired request/response structure + Packet requestPacket = (packet.getDirection() == Packet.Direction.CLIENT) ? packet : pairedPacket; + Packet responsePacket = (packet.getDirection() == Packet.Direction.SERVER) ? packet : pairedPacket; + + // Request details + JsonObject request = buildSinglePacketDetail(requestPacket, includeBody, "request"); + result.add("request", request); + + // Response details + JsonObject response = buildSinglePacketDetail(responsePacket, includeBody, "response"); + result.add("response", response); + + // Add pairing information + result.addProperty("paired", true); + result.addProperty("requested_packet_id", packet.getId()); + result.addProperty("group", packet.getGroup()); + result.addProperty("conn", packet.getConn()); + } else { + // Single packet (no pair found) + JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, + packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); + if (packet.getDirection() == Packet.Direction.CLIENT) { + result.add("request", singlePacket); + result.add("response", null); + } else { + result.add("request", null); + result.add("response", singlePacket); + } + result.addProperty("paired", false); + result.addProperty("requested_packet_id", packet.getId()); + result.addProperty("group", packet.getGroup()); + result.addProperty("conn", packet.getConn()); + } + } else { + // Return only the requested packet + JsonObject singlePacket = buildSinglePacketDetail(packet, includeBody, + packet.getDirection() == Packet.Direction.CLIENT ? "request" : "response"); + if (packet.getDirection() == Packet.Direction.CLIENT) { + result.add("request", singlePacket); + result.add("response", null); + } else { + result.add("request", null); + result.add("response", singlePacket); + } + result.addProperty("paired", false); + result.addProperty("requested_packet_id", packet.getId()); + result.addProperty("group", packet.getGroup()); + result.addProperty("conn", packet.getConn()); + } + + return result; + } + + private Packet findPairedPacket(Packet packet) throws Exception { + Packets packets = Packets.getInstance(); + + // Look for a packet with same group and conn but opposite direction + Packet.Direction targetDirection = (packet.getDirection() == Packet.Direction.CLIENT) + ? Packet.Direction.SERVER + : Packet.Direction.CLIENT; + + // Search through packets with same group + // Note: This is a simple implementation. In a real system, you might want to + // add specific query methods to Packets class for better performance + List allPackets = packets.queryAll(); + for (Packet p : allPackets) { + if (p.getGroup() == packet.getGroup() && p.getConn() == packet.getConn() + && p.getDirection() == targetDirection && p.getId() != packet.getId()) { + return p; + } + } + return null; + } + + private JsonObject buildSinglePacketDetail(Packet packet, boolean includeBody, String type) throws Exception { + JsonObject result = new JsonObject(); + + // Basic packet info + result.addProperty("id", packet.getId()); + result.addProperty("length", packet.getDecodedData().length); + result.addProperty("time", dateFormat.format(packet.getDate())); + result.addProperty("resend", packet.getResend()); + result.addProperty("modified", packet.getModified()); + result.addProperty("type", packet.getContentType()); + result.addProperty("encode", packet.getEncoder()); + result.addProperty("direction", packet.getDirection().toString().toLowerCase()); + + // Client/Server info + JsonObject client = new JsonObject(); + client.addProperty("ip", packet.getClientIP()); + client.addProperty("port", packet.getClientPort()); + result.add("client", client); + + JsonObject server = new JsonObject(); + server.addProperty("ip", packet.getServerIP()); + server.addProperty("port", packet.getServerPort()); + result.add("server", server); + + // Parse HTTP data if possible + try { + String data = new String(packet.getDecodedData(), StandardCharsets.UTF_8); + parseHttpData(result, data, includeBody); + } catch (Exception e) { + // Not HTTP or parsing failed, include raw data + if (includeBody) { + result.addProperty("raw_data", new String(packet.getDecodedData(), StandardCharsets.UTF_8)); + } + } + + return result; + } + + private void parseHttpData(JsonObject result, String data, boolean includeBody) { + String[] parts = data.split("\r\n\r\n", 2); + if (parts.length == 0) + return; + + String headers = parts[0]; + String body = parts.length > 1 ? parts[1] : ""; + + String[] lines = headers.split("\r\n"); + if (lines.length == 0) + return; + + // Parse request/response line + String firstLine = lines[0]; + if (firstLine.startsWith("HTTP/")) { + // Response + String[] statusParts = firstLine.split(" ", 3); + if (statusParts.length >= 2) { + try { + int status = Integer.parseInt(statusParts[1]); + result.addProperty("status", status); + if (statusParts.length >= 3) { + result.addProperty("status_text", statusParts[2]); + } + } catch (NumberFormatException e) { + // Invalid status code + } + } + } else { + // Request + String[] requestParts = firstLine.split(" ", 3); + if (requestParts.length >= 2) { + result.addProperty("method", requestParts[0]); + result.addProperty("url", requestParts[1]); + if (requestParts.length >= 3) { + result.addProperty("version", requestParts[2]); + } + } + } + + // Parse headers + JsonArray headersArray = new JsonArray(); + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + int colonIndex = line.indexOf(':'); + if (colonIndex > 0) { + JsonObject header = new JsonObject(); + header.addProperty("name", line.substring(0, colonIndex).trim()); + header.addProperty("value", line.substring(colonIndex + 1).trim()); + headersArray.add(header); + } + } + result.add("headers", headersArray); + + // Include body if requested + if (includeBody && !body.isEmpty()) { + result.addProperty("body", body); + } + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java new file mode 100644 index 00000000..ebcfa5bf --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ResendPacketTool.java @@ -0,0 +1,441 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Random; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import packetproxy.controller.ResendController; +import packetproxy.model.OneShotPacket; +import packetproxy.model.Packet; +import packetproxy.model.Packets; + +/** + * パケット再送ツール パケットを指定回数再送し、改変オプションもサポート + */ +public class ResendPacketTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "resend_packet"; + } + + @Override + public String getDescription() { + return "Resend a packet with optional modifications and multiple count support"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject packetIdProp = new JsonObject(); + packetIdProp.addProperty("type", "integer"); + packetIdProp.addProperty("description", "ID of the packet to resend"); + schema.add("packet_id", packetIdProp); + + JsonObject countProp = new JsonObject(); + countProp.addProperty("type", "integer"); + countProp.addProperty("description", "Number of times to send the packet (default: 1)"); + countProp.addProperty("default", 1); + schema.add("count", countProp); + + JsonObject intervalProp = new JsonObject(); + intervalProp.addProperty("type", "integer"); + intervalProp.addProperty("description", "Interval between sends in milliseconds (default: 0)"); + intervalProp.addProperty("default", 0); + schema.add("interval_ms", intervalProp); + + JsonObject modificationsProp = new JsonObject(); + modificationsProp.addProperty("type", "array"); + modificationsProp.addProperty("description", "Array of modification rules to apply to the packet"); + JsonObject modificationItem = new JsonObject(); + modificationItem.addProperty("type", "object"); + JsonObject modificationProps = new JsonObject(); + + JsonObject targetProp = new JsonObject(); + targetProp.addProperty("type", "string"); + JsonArray targetEnum = new JsonArray(); + targetEnum.add("request"); + targetEnum.add("response"); + targetEnum.add("both"); + targetProp.add("enum", targetEnum); + targetProp.addProperty("description", "Target to modify: request, response, or both"); + modificationProps.add("target", targetProp); + + JsonObject typeProp = new JsonObject(); + typeProp.addProperty("type", "string"); + JsonArray typeEnum = new JsonArray(); + typeEnum.add("regex_replace"); + typeEnum.add("header_add"); + typeEnum.add("header_modify"); + typeProp.add("enum", typeEnum); + typeProp.addProperty("description", "Type of modification"); + modificationProps.add("type", typeProp); + + JsonObject patternProp = new JsonObject(); + patternProp.addProperty("type", "string"); + patternProp.addProperty("description", "Regex pattern for regex_replace type"); + modificationProps.add("pattern", patternProp); + + JsonObject replacementProp = new JsonObject(); + replacementProp.addProperty("type", "string"); + replacementProp.addProperty("description", "Replacement string for regex_replace or value for headers"); + modificationProps.add("replacement", replacementProp); + + JsonObject nameProp = new JsonObject(); + nameProp.addProperty("type", "string"); + nameProp.addProperty("description", "Header name for header_add/header_modify"); + modificationProps.add("name", nameProp); + + JsonObject valueProp = new JsonObject(); + valueProp.addProperty("type", "string"); + valueProp.addProperty("description", "Header value for header_add/header_modify"); + modificationProps.add("value", valueProp); + + modificationItem.add("properties", modificationProps); + modificationsProp.add("items", modificationItem); + schema.add("modifications", modificationsProp); + + JsonObject asyncProp = new JsonObject(); + asyncProp.addProperty("type", "boolean"); + asyncProp.addProperty("description", "Execute asynchronously (default: false)"); + asyncProp.addProperty("default", false); + schema.add("async", asyncProp); + + JsonObject allowDuplicateHeadersProp = new JsonObject(); + allowDuplicateHeadersProp.addProperty("type", "boolean"); + allowDuplicateHeadersProp.addProperty("description", + "Allow duplicate headers when adding/modifying headers (default: false - replace existing headers)"); + allowDuplicateHeadersProp.addProperty("default", false); + schema.add("allow_duplicate_headers", allowDuplicateHeadersProp); + + // access_tokenを追加 + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("ResendPacketTool called with arguments: " + getSafeArgumentsString(arguments)); + log("ResendPacketTool: Starting packet resend operation"); + + // パラメータ取得 + if (!arguments.has("packet_id")) { + throw new IllegalArgumentException("packet_id parameter is required"); + } + + int packetId = arguments.get("packet_id").getAsInt(); + int count = arguments.has("count") ? arguments.get("count").getAsInt() : 1; + int intervalMs = arguments.has("interval_ms") ? arguments.get("interval_ms").getAsInt() : 0; + boolean async = arguments.has("async") ? arguments.get("async").getAsBoolean() : false; + boolean allowDuplicateHeaders = arguments.has("allow_duplicate_headers") + ? arguments.get("allow_duplicate_headers").getAsBoolean() + : false; + + JsonArray modifications = arguments.has("modifications") + ? arguments.getAsJsonArray("modifications") + : new JsonArray(); + + log("ResendPacketTool: packet_id=" + packetId + ", count=" + count + ", interval=" + intervalMs + "ms, async=" + + async + ", allowDuplicateHeaders=" + allowDuplicateHeaders); + + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + throw new IllegalArgumentException("Packet with ID " + packetId + " not found"); + } + + // 適切なデータを使ってOneShotPacketを作成 + // 改変データがあれば改変データを、なければ送信データを使用 + OneShotPacket originalOneShot; + if (originalPacket.getModifiedData().length > 0) { + originalOneShot = originalPacket.getOneShotFromModifiedData(); + } else if (originalPacket.getSentData().length > 0) { + originalOneShot = originalPacket.getOneShotPacket(originalPacket.getSentData()); + } else { + // デコードされたデータをフォールバックとして使用 + originalOneShot = originalPacket.getOneShotFromDecodedData(); + } + + if (originalOneShot == null) { + throw new IllegalArgumentException("Cannot create OneShotPacket from packet ID " + packetId); + } + + log("ResendPacketTool: Original packet found, preparing for resend"); + + // ジョブIDを生成 + String jobId = UUID.randomUUID().toString(); + + long startTime = System.currentTimeMillis(); + int sentCount = 0; + int failedCount = 0; + + try { + ResendController resendController = ResendController.getInstance(); + + if (modifications.size() == 0) { + // 改変なしの場合は単純再送 + log("ResendPacketTool: Simple resend without modifications, count=" + count); + for (int i = 0; i < count; i++) { + String temporaryId = UUID.randomUUID().toString(); + OneShotPacket jobPacket = new OneShotPacket(originalOneShot.getId(), + originalOneShot.getListenPort(), originalOneShot.getClient(), originalOneShot.getServer(), + originalOneShot.getServerName(), originalOneShot.getUseSSL(), originalOneShot.getData(), + originalOneShot.getEncoder(), originalOneShot.getAlpn(), originalOneShot.getDirection(), + originalOneShot.getConn(), originalOneShot.getGroup(), jobId, temporaryId); + resendController.resend(jobPacket); + sentCount++; + + // インターバル待機(最後の送信後は待機しない) + if (intervalMs > 0 && i < count - 1) { + Thread.sleep(intervalMs); + } + } + } else { + // 複数回送信または改変ありの場合 + log("ResendPacketTool: Complex resend with count=" + count + " and modifications=" + + modifications.size()); + + for (int i = 0; i < count; i++) { + try { + String temporaryId = UUID.randomUUID().toString(); + OneShotPacket modifiedPacket = applyModifications(originalOneShot, modifications, i + 1, + allowDuplicateHeaders, jobId, temporaryId); + resendController.resend(modifiedPacket); + sentCount++; + + // インターバル待機(最後の送信後は待機しない) + if (intervalMs > 0 && i < count - 1) { + Thread.sleep(intervalMs); + } + } catch (Exception e) { + log("ResendPacketTool: Failed to send packet " + (i + 1) + ": " + e.getMessage()); + failedCount++; + } + } + } + } catch (Exception e) { + log("ResendPacketTool: Resend operation failed: " + e.getMessage()); + failedCount = count - sentCount; + throw e; + } + + long executionTime = System.currentTimeMillis() - startTime; + + // 結果作成 + JsonObject result = new JsonObject(); + result.addProperty("success", failedCount == 0); + result.addProperty("sent_count", sentCount); + result.addProperty("failed_count", failedCount); + result.addProperty("execution_time_ms", executionTime); + result.addProperty("job_id", jobId); + + log("ResendPacketTool: Completed. Sent: " + sentCount + ", Failed: " + failedCount + ", Time: " + executionTime + + "ms"); + return result; + } + + /** + * パケットに改変を適用 + */ + private OneShotPacket applyModifications(OneShotPacket original, JsonArray modifications, int index, + boolean allowDuplicateHeaders, String jobId, String temporaryId) throws Exception { + if (modifications.size() == 0) { + return original; + } + + log("ResendPacketTool: Applying " + modifications.size() + " modifications to packet (index=" + index + ")"); + + byte[] data = original.getData().clone(); + String dataStr = new String(data); + + for (JsonElement modElement : modifications) { + JsonObject modification = modElement.getAsJsonObject(); + + String target = modification.has("target") ? modification.get("target").getAsString() : "request"; + String type = modification.get("type").getAsString(); + + log("ResendPacketTool: Applying modification type=" + type + ", target=" + target); + + switch (type) { + case "regex_replace" : + dataStr = applyRegexReplace(dataStr, modification, index); + break; + case "header_add" : + dataStr = applyHeaderAdd(dataStr, modification, index, allowDuplicateHeaders); + break; + case "header_modify" : + dataStr = applyHeaderModify(dataStr, modification, index, allowDuplicateHeaders); + break; + default : + log("ResendPacketTool: Unknown modification type: " + type); + break; + } + } + + data = dataStr.getBytes(); + + // 新しいOneShotPacketを作成 + OneShotPacket modifiedPacket = new OneShotPacket(original.getId(), original.getListenPort(), + original.getClient(), original.getServer(), original.getServerName(), original.getUseSSL(), data, + original.getEncoder(), original.getAlpn(), original.getDirection(), original.getConn(), + original.getGroup(), jobId, temporaryId); + + return modifiedPacket; + } + + /** + * 正規表現置換を適用 + */ + private String applyRegexReplace(String data, JsonObject modification, int index) { + String pattern = modification.get("pattern").getAsString(); + String replacement = modification.get("replacement").getAsString(); + + // 置換変数を処理 + replacement = processReplacementVariables(replacement, index); + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + String result = matcher.replaceAll(replacement); + log("ResendPacketTool: Regex replace applied - pattern: " + pattern + ", replacement: " + replacement); + return result; + } catch (Exception e) { + log("ResendPacketTool: Regex replace failed: " + e.getMessage()); + return data; + } + } + + /** + * ヘッダー追加を適用 + */ + private String applyHeaderAdd(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // HTTP形式のデータの場合、ヘッダー部分に追加 + if (data.contains("\r\n\r\n")) { + int headerEnd = data.indexOf("\r\n\r\n"); + String headers = data.substring(0, headerEnd); + String body = data.substring(headerEnd); + + // 重複を許可しない場合、既存ヘッダーがあるかチェック + if (!allowDuplicateHeaders) { + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(headers); + if (matcher.find()) { + // 既存ヘッダーを置換 + String result = matcher.replaceFirst(name + ": " + value) + body; + log("ResendPacketTool: Header replaced (no duplicates allowed) - " + name + ": " + value); + return result; + } + } + + // 新しいヘッダーを追加 + String newHeader = name + ": " + value + "\r\n"; + String result = headers + "\r\n" + newHeader + body; + log("ResendPacketTool: Header added - " + name + ": " + value); + return result; + } + + return data; + } + + /** + * ヘッダー変更を適用 + */ + private String applyHeaderModify(String data, JsonObject modification, int index, boolean allowDuplicateHeaders) { + String name = modification.get("name").getAsString(); + String value = modification.get("value").getAsString(); + + // 置換変数を処理 + value = processReplacementVariables(value, index); + + // 既存ヘッダーを置換 + String pattern = "(?i)" + Pattern.quote(name) + ":\\s*[^\r\n]*"; + + try { + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(data); + if (matcher.find()) { + String replacement = name + ": " + value; + String result; + if (allowDuplicateHeaders) { + // 重複を許可する場合は最初のヘッダーのみ変更 + result = matcher.replaceFirst(replacement); + } else { + // 重複を許可しない場合は全ての同名ヘッダーを置換 + result = matcher.replaceAll(replacement); + } + log("ResendPacketTool: Header modified - " + name + ": " + value + " (allowDuplicates=" + + allowDuplicateHeaders + ")"); + return result; + } else { + // ヘッダーが見つからない場合は追加 + return applyHeaderAdd(data, modification, index, allowDuplicateHeaders); + } + } catch (Exception e) { + log("ResendPacketTool: Header modify failed: " + e.getMessage()); + return data; + } + } + + /** + * 置換変数を処理 + */ + private String processReplacementVariables(String input, int index) { + String result = input; + + // {{index}} - 送信順序 + result = result.replace("{{index}}", String.valueOf(index)); + + // {{timestamp}} - Unix timestamp + result = result.replace("{{timestamp}}", String.valueOf(System.currentTimeMillis() / 1000)); + + // {{random}} - ランダム文字列 + if (result.contains("{{random}}")) { + String randomStr = generateRandomString(8); + result = result.replace("{{random}}", randomStr); + } + + // {{uuid}} - UUID v4 + if (result.contains("{{uuid}}")) { + String uuid = UUID.randomUUID().toString(); + result = result.replace("{{uuid}}", uuid); + } + + // {{datetime}} - ISO 8601形式日時 + if (result.contains("{{datetime}}")) { + String datetime = Instant.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); + result = result.replace("{{datetime}}", datetime); + } + + return result; + } + + /** + * ランダム文字列生成 + */ + private String generateRandomString(int length) { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) { + sb.append(chars.charAt(random.nextInt(chars.length()))); + } + + return sb.toString(); + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java new file mode 100644 index 00000000..f10aaf40 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/RestoreConfigTool.java @@ -0,0 +1,139 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public class RestoreConfigTool extends AuthenticatedMCPTool { + + private final Gson gson = new Gson(); + + @Override + public String getName() { + return "restore_config"; + } + + @Override + public String getDescription() { + return "Restore PacketProxy configuration from backup file with optional dialog suppression"; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject backupIdProp = new JsonObject(); + backupIdProp.addProperty("type", "string"); + backupIdProp.addProperty("description", "Backup ID to restore from (e.g., backup_20250103_120000)"); + schema.add("backup_id", backupIdProp); + + JsonObject suppressDialogProp = new JsonObject(); + suppressDialogProp.addProperty("type", "boolean"); + suppressDialogProp.addProperty("description", + "Suppress confirmation dialog for configuration restore (default: false)"); + suppressDialogProp.addProperty("default", false); + schema.add("suppress_dialog", suppressDialogProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("RestoreConfigTool called with arguments: " + getSafeArgumentsString(arguments)); + + if (!arguments.has("backup_id")) { + throw new Exception("backup_id parameter is required"); + } + + String backupId = arguments.get("backup_id").getAsString(); + boolean suppressDialog = arguments.has("suppress_dialog") + ? arguments.get("suppress_dialog").getAsBoolean() + : false; + + try { + log("RestoreConfigTool step 1: Loading backup configuration"); + JsonObject backupConfig = loadBackupConfig(backupId); + log("RestoreConfigTool step 2: Backup configuration loaded successfully"); + + log("RestoreConfigTool step 3: Restoring configuration using UpdateConfigTool"); + JsonObject updateArgs = new JsonObject(); + updateArgs.add("config_json", backupConfig); + updateArgs.addProperty("backup", true); + updateArgs.addProperty("suppress_dialog", suppressDialog); + updateArgs.addProperty("access_token", arguments.get("access_token").getAsString()); + + UpdateConfigTool updateTool = new UpdateConfigTool(); + JsonObject updateResult = updateTool.call(updateArgs); + log("RestoreConfigTool step 4: Configuration restored successfully"); + + log("RestoreConfigTool step 5: Building response data"); + JsonObject data = new JsonObject(); + data.addProperty("success", true); + data.addProperty("backup_id_restored", backupId); + data.addProperty("config_restored", true); + + String jsonText = data.toString(); + log("RestoreConfigTool step 6: Response data JSON: " + jsonText); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", jsonText); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + String resultJson = result.toString(); + log("RestoreConfigTool step 7: Final result JSON length: " + resultJson.length()); + log("RestoreConfigTool step 8: Configuration restore completed successfully"); + return result; + + } catch (Exception e) { + log("RestoreConfigTool error: " + e.getMessage()); + e.printStackTrace(); + throw new Exception("Failed to restore configuration: " + e.getMessage()); + } + } + + private JsonObject loadBackupConfig(String backupId) throws Exception { + // Construct backup file path + File backupDir = new File("backup"); + if (!backupDir.exists()) { + throw new Exception("Backup directory does not exist"); + } + + String backupFileName = backupId + ".json"; + File backupFile = new File(backupDir, backupFileName); + + if (!backupFile.exists()) { + throw new Exception("Backup file not found: " + backupFileName); + } + + log("Loading backup from: " + backupFile.getAbsolutePath()); + + try (FileReader reader = new FileReader(backupFile)) { + JsonObject backupConfig = gson.fromJson(reader, JsonObject.class); + + if (backupConfig == null) { + throw new Exception("Invalid backup file format"); + } + + log("Backup configuration loaded successfully from: " + backupFileName); + return backupConfig; + + } catch (IOException e) { + log("Failed to read backup file: " + e.getMessage()); + throw new Exception("Failed to read backup file: " + e.getMessage()); + } catch (Exception e) { + log("Failed to parse backup file: " + e.getMessage()); + throw new Exception("Failed to parse backup file: " + e.getMessage()); + } + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java new file mode 100644 index 00000000..fb1d6130 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/ToolRegistry.java @@ -0,0 +1,80 @@ +package packetproxy.extensions.mcp.tools; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.HashMap; +import java.util.Map; + +public class ToolRegistry { + + private final Map tools; + + public ToolRegistry() { + this.tools = new HashMap<>(); + registerDefaultTools(); + } + + private void registerDefaultTools() { + // 基本的なツールを登録 + registerTool(new HistoryTool()); + registerTool(new PacketDetailTool()); + registerTool(new LogTool()); + registerTool(new ConfigTool()); + registerTool(new UpdateConfigTool()); + registerTool(new RestoreConfigTool()); + registerTool(new ResendPacketTool()); + registerTool(new BulkSendTool()); + registerTool(new VulCheckHelperTool()); + registerTool(new JobStatusTool()); + } + + public void registerTool(MCPTool tool) { + tools.put(tool.getName(), tool); + } + + public JsonArray getToolsList() { + JsonArray toolsArray = new JsonArray(); + + for (MCPTool tool : tools.values()) { + JsonObject toolInfo = new JsonObject(); + toolInfo.addProperty("name", tool.getName()); + toolInfo.addProperty("description", tool.getDescription()); + + JsonObject inputSchema = new JsonObject(); + inputSchema.addProperty("type", "object"); + inputSchema.add("properties", tool.getInputSchema()); + + toolInfo.add("inputSchema", inputSchema); + toolsArray.add(toolInfo); + } + + return toolsArray; + } + + public JsonObject callTool(String toolName, JsonObject arguments) throws Exception { + MCPTool tool = tools.get(toolName); + if (tool == null) { + throw new Exception("Unknown tool: " + toolName); + } + + JsonObject toolResult = tool.call(arguments); + + // MCP仕様に準拠した応答形式に変換 + JsonObject mcpResponse = new JsonObject(); + + // content配列を作成 (必須) + JsonArray content = new JsonArray(); + JsonObject textContent = new JsonObject(); + textContent.addProperty("type", "text"); + textContent.addProperty("text", toolResult.toString()); + content.add(textContent); + + mcpResponse.add("content", content); + mcpResponse.addProperty("isError", false); + + // 元の結果をstructuredContentとして保持(オプション) + mcpResponse.add("structuredContent", toolResult); + + return mcpResponse; + } +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java new file mode 100644 index 00000000..0fbb9f50 --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/UpdateConfigTool.java @@ -0,0 +1,263 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class UpdateConfigTool extends AuthenticatedMCPTool { + + private final Gson gson = new Gson(); + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + @Override + public String getName() { + return "update_config"; + } + + @Override + public String getDescription() { + return "Update PacketProxy configuration settings with complete configuration object. IMPORTANT: Requires a complete configuration object, not partial updates."; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject configJsonProp = new JsonObject(); + configJsonProp.addProperty("type", "object"); + configJsonProp.addProperty("description", + "PacketProxyHub-compatible configuration JSON containing COMPLETE configuration object. Must include all required arrays: listenPorts, servers, modifications, sslPassThroughs (can be empty arrays). Partial configurations will cause null pointer errors. Recommended workflow: 1) Call get_config() first, 2) Modify specific fields in the returned object, 3) Pass the entire modified object here."); + schema.add("config_json", configJsonProp); + + JsonObject backupProp = new JsonObject(); + backupProp.addProperty("type", "boolean"); + backupProp.addProperty("description", "Create backup of existing configuration (default: true)"); + backupProp.addProperty("default", true); + schema.add("backup", backupProp); + + JsonObject suppressDialogProp = new JsonObject(); + suppressDialogProp.addProperty("type", "boolean"); + suppressDialogProp.addProperty("description", + "Suppress confirmation dialog for configuration update (default: false)"); + suppressDialogProp.addProperty("default", false); + schema.add("suppress_dialog", suppressDialogProp); + + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("UpdateConfigTool called with arguments: " + getSafeArgumentsString(arguments)); + + if (!arguments.has("config_json")) { + throw new Exception("config_json parameter is required"); + } + + JsonObject configJson = arguments.getAsJsonObject("config_json"); + boolean backup = arguments.has("backup") ? arguments.get("backup").getAsBoolean() : true; + boolean suppressDialog = arguments.has("suppress_dialog") + ? arguments.get("suppress_dialog").getAsBoolean() + : false; + + try { + log("UpdateConfigTool step 1: Starting configuration update"); + + JsonObject backupInfo = null; + + if (backup) { + log("UpdateConfigTool step 2: Creating backup"); + backupInfo = createConfigBackup(); + log("UpdateConfigTool step 3: Backup created successfully"); + } + + log("UpdateConfigTool step 4: Updating configuration"); + updateConfiguration(configJson, suppressDialog); + log("UpdateConfigTool step 5: Configuration updated successfully"); + + log("UpdateConfigTool step 6: Building response data"); + JsonObject data = new JsonObject(); + data.addProperty("success", true); + data.addProperty("backup_created", backup); + if (backupInfo != null) { + data.add("backup_info", backupInfo); + } + data.addProperty("config_updated", true); + + String jsonText = data.toString(); + log("UpdateConfigTool step 7: Response data JSON: " + jsonText); + + JsonObject content = new JsonObject(); + content.addProperty("type", "text"); + content.addProperty("text", jsonText); + + JsonArray contentArray = new JsonArray(); + contentArray.add(content); + + JsonObject result = new JsonObject(); + result.add("content", contentArray); + + String resultJson = result.toString(); + log("UpdateConfigTool step 8: Final result JSON length: " + resultJson.length()); + log("UpdateConfigTool step 9: Configuration update completed successfully"); + return result; + + } catch (Exception e) { + log("UpdateConfigTool error: " + e.getMessage()); + e.printStackTrace(); + throw new Exception("Failed to update configuration: " + e.getMessage()); + } + } + + private JsonObject createConfigBackup() throws Exception { + Date now = new Date(); + String timestamp = dateFormat.format(now); + String backupId = "backup_" + timestamp.replace(":", "").replace("-", "").replace("T", "_").replace("Z", ""); + + // Create backup directory if it doesn't exist + File backupDir = new File("backup"); + if (!backupDir.exists()) { + backupDir.mkdirs(); + } + + String backupPath = backupDir.getPath() + File.separator + backupId + ".json"; + + try { + // HTTP APIで設定を直接取得(認証チェックを回避) + String configText = getConfigFromHttpApiForBackup(); + JsonObject backupConfig = gson.fromJson(configText, JsonObject.class); + + // Write backup to file + try (FileWriter writer = new FileWriter(backupPath)) { + gson.toJson(backupConfig, writer); + writer.flush(); + } + + log("Configuration backed up to: " + backupPath); + log("Backup content size: " + configText.length() + " characters"); + + } catch (IOException e) { + log("Failed to write backup file: " + e.getMessage()); + throw new Exception("Failed to create backup file: " + e.getMessage()); + } catch (Exception e) { + log("Failed to create backup: " + e.getMessage()); + throw new Exception("Failed to create configuration backup: " + e.getMessage()); + } + + JsonObject backupInfo = new JsonObject(); + backupInfo.addProperty("backup_id", backupId); + backupInfo.addProperty("backup_path", backupPath); + backupInfo.addProperty("timestamp", timestamp); + + log("Created configuration backup: " + backupId); + log("Backup info JSON: " + backupInfo.toString()); + return backupInfo; + } + + private void updateConfiguration(JsonObject configJson, boolean suppressDialog) throws Exception { + log("UpdateConfigTool starting configuration update using HTTP API"); + + // HTTP POST APIで設定を更新(削除処理も自動実行) + updateConfigViaHttpApi(configJson.toString(), suppressDialog); + + log("UpdateConfigTool configuration update completed using HTTP API"); + } + + private void updateConfigViaHttpApi(String configJsonString, boolean suppressDialog) throws Exception { + // 設定済みAccessTokenを取得(HTTPリクエスト用) + String accessToken = getConfiguredAccessToken(); + + // HTTP POSTリクエスト + URL url = new URL("http://localhost:32349/config"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Authorization", accessToken); + conn.setRequestProperty("Content-Type", "application/json"); + if (suppressDialog) { + conn.setRequestProperty("X-Suppress-Dialog", "true"); + } + conn.setDoOutput(true); + conn.setConnectTimeout(5000); + conn.setReadTimeout(60000); + + // リクエストボディを送信 + try (OutputStream os = conn.getOutputStream()) { + byte[] input = configJsonString.getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + // エラーレスポンスがある場合は読み取り + String errorMessage = "HTTP API returned status: " + responseCode; + if (conn.getErrorStream() != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()))) { + StringBuilder error = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + error.append(line); + } + if (error.length() > 0) { + errorMessage += ". Error: " + error.toString(); + } + } + } + throw new Exception( + errorMessage + ". Check if config sharing is enabled and user confirmed the operation."); + } + + // 成功レスポンスを読み取り(必要に応じて) + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + log("HTTP API response: " + response.toString()); + } + + conn.disconnect(); + } + + private String getConfigFromHttpApiForBackup() throws Exception { + // 設定済みAccessTokenを取得(HTTPリクエスト用) + String accessToken = getConfiguredAccessToken(); + + // HTTP GETリクエスト + URL url = new URL("http://localhost:32349/config"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Authorization", accessToken); + conn.setConnectTimeout(5000); + conn.setReadTimeout(60000); + + int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + throw new Exception("HTTP API returned status: " + responseCode + ". Check if config sharing is enabled."); + } + + // レスポンスを読み取り + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + conn.disconnect(); + + return response.toString(); + } + +} diff --git a/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java new file mode 100644 index 00000000..3d0a44aa --- /dev/null +++ b/src/main/java/core/packetproxy/extensions/mcp/tools/VulCheckHelperTool.java @@ -0,0 +1,581 @@ +package packetproxy.extensions.mcp.tools; + +import static packetproxy.util.Logging.log; + +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import packetproxy.VulCheckerManager; +import packetproxy.common.Range; +import packetproxy.controller.ResendController; +import packetproxy.model.OneShotPacket; +import packetproxy.model.Packet; +import packetproxy.model.Packets; +import packetproxy.vulchecker.VulChecker; +import packetproxy.vulchecker.generator.Generator; + +/** + * VulCheck脆弱性テストヘルパーツール 指定されたパケットにVulCheckテストケースを適用して連続送信を実行 + */ +public class VulCheckHelperTool extends AuthenticatedMCPTool { + + @Override + public String getName() { + return "call_vulcheck_helper"; + } + + @Override + public String getDescription() { + return "Execute VulCheck vulnerability tests with automatic payload generation and batch sending. Applies VulCheck test cases to specified packet locations and sends modified packets with configurable intervals. IMPORTANT: Currently supports Number and JWT vulnerability types only. For other vulnerability types, use bulk_send or resend_packet tools instead. For precise targeting, use regex patterns with positive lookahead assertions. Examples: To target '17' in 'X-Version: 17.0.4', use pattern: '17(?=\\.0\\.4)'. To target '123' in 'userId=123&other=456', use pattern: '(?<=userId=)123(?=&)'. To target specific values while preserving structure, use patterns like: 'sessionId=\\\\w+' with replacement 'sessionId=$1'. The pattern field supports full regex syntax including lookahead/lookbehind assertions for precise matching without affecting surrounding text. Use replacement field to control how the matched text is substituted with VulCheck payloads."; + } + + @Override + public JsonObject getInputSchema() { + JsonObject schema = new JsonObject(); + + JsonObject packetIdProp = new JsonObject(); + packetIdProp.addProperty("type", "integer"); + packetIdProp.addProperty("description", "ID of the packet to use as base for VulCheck testing"); + schema.add("packet_id", packetIdProp); + + JsonObject vulCheckTypeProp = new JsonObject(); + vulCheckTypeProp.addProperty("type", "string"); + vulCheckTypeProp.addProperty("description", + "Type of VulCheck to perform (Number, JWT, etc.). Use 'list' to get available types."); + schema.add("vulcheck_type", vulCheckTypeProp); + + JsonObject targetLocationsProp = new JsonObject(); + targetLocationsProp.addProperty("type", "array"); + targetLocationsProp.addProperty("description", + "Array of target locations in the packet where VulCheck payloads should be injected. Can specify either regex patterns or position ranges."); + JsonObject locationItem = new JsonObject(); + locationItem.addProperty("type", "object"); + JsonObject locationProps = new JsonObject(); + + // Regex pattern approach (new) + JsonObject patternProp = new JsonObject(); + patternProp.addProperty("type", "string"); + patternProp.addProperty("description", "Regex pattern to match target locations in the packet data"); + locationProps.add("pattern", patternProp); + + JsonObject replacementProp = new JsonObject(); + replacementProp.addProperty("type", "string"); + replacementProp.addProperty("description", + "Optional replacement template for pattern matches. If not specified, the entire match will be replaced."); + locationProps.add("replacement", replacementProp); + + // Position range approach (existing - for backward compatibility) + JsonObject startProp = new JsonObject(); + startProp.addProperty("type", "integer"); + startProp.addProperty("description", + "Start position of the target location in the packet data (alternative to pattern)"); + locationProps.add("start", startProp); + + JsonObject endProp = new JsonObject(); + endProp.addProperty("type", "integer"); + endProp.addProperty("description", + "End position of the target location in the packet data (alternative to pattern)"); + locationProps.add("end", endProp); + + JsonObject descriptionProp = new JsonObject(); + descriptionProp.addProperty("type", "string"); + descriptionProp.addProperty("description", "Optional description of this target location"); + locationProps.add("description", descriptionProp); + + locationItem.add("properties", locationProps); + targetLocationsProp.add("items", locationItem); + schema.add("target_locations", targetLocationsProp); + + JsonObject intervalProp = new JsonObject(); + intervalProp.addProperty("type", "integer"); + intervalProp.addProperty("description", "Interval between packet sends in milliseconds (default: 100)"); + intervalProp.addProperty("default", 100); + schema.add("interval_ms", intervalProp); + + JsonObject modeProp = new JsonObject(); + modeProp.addProperty("type", "string"); + JsonArray modeEnum = new JsonArray(); + modeEnum.add("sequential"); + modeEnum.add("parallel"); + modeProp.add("enum", modeEnum); + modeProp.addProperty("description", "Execution mode: sequential (with intervals) or parallel (all at once)"); + modeProp.addProperty("default", "sequential"); + schema.add("mode", modeProp); + + JsonObject maxPayloadsProp = new JsonObject(); + maxPayloadsProp.addProperty("type", "integer"); + maxPayloadsProp.addProperty("description", + "Maximum number of payloads to generate per location (default: 50, max: 1000)"); + maxPayloadsProp.addProperty("default", 50); + maxPayloadsProp.addProperty("maximum", 1000); + schema.add("max_payloads", maxPayloadsProp); + + JsonObject timeoutProp = new JsonObject(); + timeoutProp.addProperty("type", "integer"); + timeoutProp.addProperty("description", + "Timeout for entire operation in milliseconds (default: 300000 - 5 minutes)"); + timeoutProp.addProperty("default", 300000); + schema.add("timeout_ms", timeoutProp); + + // access_tokenを追加 + return addAccessTokenToSchema(schema); + } + + @Override + protected JsonObject executeAuthenticated(JsonObject arguments) throws Exception { + log("VulCheckHelperTool called with arguments: " + getSafeArgumentsString(arguments)); + log("VulCheckHelperTool: Starting VulCheck operation"); + + // パラメータ取得 + if (!arguments.has("packet_id")) { + throw new IllegalArgumentException("packet_id parameter is required"); + } + + if (!arguments.has("vulcheck_type")) { + throw new IllegalArgumentException("vulcheck_type parameter is required"); + } + + int packetId = arguments.get("packet_id").getAsInt(); + String vulCheckType = arguments.get("vulcheck_type").getAsString(); + + // 特別なケース: 利用可能なVulCheckタイプを一覧表示 + if ("list".equals(vulCheckType)) { + return getAvailableVulCheckTypes(); + } + + if (!arguments.has("target_locations")) { + throw new IllegalArgumentException("target_locations parameter is required"); + } + + JsonArray targetLocationsJson = arguments.getAsJsonArray("target_locations"); + int intervalMs = arguments.has("interval_ms") ? arguments.get("interval_ms").getAsInt() : 100; + String mode = arguments.has("mode") ? arguments.get("mode").getAsString() : "sequential"; + int maxPayloads = arguments.has("max_payloads") ? arguments.get("max_payloads").getAsInt() : 50; + int timeoutMs = arguments.has("timeout_ms") ? arguments.get("timeout_ms").getAsInt() : 300000; + + log("VulCheckHelperTool: packet_id=" + packetId + ", vulcheck_type=" + vulCheckType + ", locations=" + + targetLocationsJson.size() + ", mode=" + mode + ", max_payloads=" + maxPayloads); + + // パケットを取得 + Packet originalPacket = Packets.getInstance().query(packetId); + if (originalPacket == null) { + throw new IllegalArgumentException("Packet with ID " + packetId + " not found"); + } + + // OneShotPacketを作成 + OneShotPacket originalOneShot; + if (originalPacket.getModifiedData().length > 0) { + originalOneShot = originalPacket.getOneShotFromModifiedData(); + } else if (originalPacket.getSentData().length > 0) { + originalOneShot = originalPacket.getOneShotPacket(originalPacket.getSentData()); + } else { + originalOneShot = originalPacket.getOneShotFromDecodedData(); + } + + if (originalOneShot == null) { + throw new IllegalArgumentException("Cannot create OneShotPacket from packet ID " + packetId); + } + + // VulCheckerを取得 + VulChecker vulChecker = VulCheckerManager.getInstance().createInstance(vulCheckType); + if (vulChecker == null) { + throw new IllegalArgumentException( + "VulCheck type '" + vulCheckType + "' not found. Use 'list' to see available types."); + } + + // ジョブIDを生成 + String jobId = UUID.randomUUID().toString(); + + // ターゲット位置を解析 + List targetLocations = parseTargetLocations(targetLocationsJson, originalOneShot.getData()); + + long startTime = System.currentTimeMillis(); + + // VulCheckテストを実行 + VulCheckResult result = executeVulCheckTests(originalOneShot, vulChecker, targetLocations, intervalMs, mode, + maxPayloads, timeoutMs, jobId); + + long executionTime = System.currentTimeMillis() - startTime; + + // 結果を構築 + JsonObject response = new JsonObject(); + response.addProperty("success", result.overallSuccess); + response.addProperty("vulcheck_type", vulCheckType); + response.addProperty("mode", mode); + response.addProperty("total_locations", targetLocations.size()); + response.addProperty("total_payloads_generated", result.totalPayloadsGenerated); + response.addProperty("total_packets_sent", result.totalPacketsSent); + response.addProperty("total_failed", result.totalFailed); + response.addProperty("execution_time_ms", executionTime); + response.addProperty("job_id", jobId); + + // 各ターゲット位置の結果 + JsonArray locationResults = new JsonArray(); + for (LocationResult locResult : result.locationResults) { + JsonObject locJson = new JsonObject(); + locJson.addProperty("start", locResult.range.getPositionStart()); + locJson.addProperty("end", locResult.range.getPositionEnd()); + locJson.addProperty("description", locResult.description); + locJson.addProperty("payloads_generated", locResult.payloadsGenerated); + locJson.addProperty("packets_sent", locResult.packetsSent); + locJson.addProperty("packets_failed", locResult.packetsFailed); + locJson.addProperty("execution_time_ms", locResult.executionTimeMs); + + // 生成されたペイロードの一覧 + JsonArray payloadsList = new JsonArray(); + for (String payload : locResult.generatedPayloads) { + payloadsList.add(payload); + } + locJson.add("generated_payloads", payloadsList); + + locationResults.add(locJson); + } + response.add("location_results", locationResults); + + // パフォーマンス統計 + JsonObject performance = new JsonObject(); + performance.addProperty("average_interval_ms", result.averageIntervalMs); + performance.addProperty("payloads_per_second", (double) result.totalPacketsSent / (executionTime / 1000.0)); + performance.addProperty("success_rate_percent", + result.totalPacketsSent > 0 + ? ((double) (result.totalPacketsSent - result.totalFailed) / result.totalPacketsSent) * 100.0 + : 0.0); + response.add("performance", performance); + + log("VulCheckHelperTool: Completed. Generated " + result.totalPayloadsGenerated + " payloads, sent " + + result.totalPacketsSent + " packets, " + result.totalFailed + " failed, time: " + executionTime + + "ms"); + + return response; + } + + /** + * 利用可能なVulCheckタイプを取得 + */ + private JsonObject getAvailableVulCheckTypes() throws Exception { + VulCheckerManager manager = VulCheckerManager.getInstance(); + String[] vulCheckerNames = manager.getVulCheckerNameList(); + + JsonObject result = new JsonObject(); + result.addProperty("available_vulcheck_types", String.join(", ", vulCheckerNames)); + + JsonArray typesArray = new JsonArray(); + Map typeDetails = new HashMap<>(); + + for (String name : vulCheckerNames) { + VulChecker checker = manager.createInstance(name); + if (checker != null) { + JsonObject typeInfo = new JsonObject(); + typeInfo.addProperty("name", name); + typeInfo.addProperty("description", "VulCheck tests for " + name + " vulnerabilities"); + + // ジェネレータの詳細を追加 + ImmutableList generators = checker.getGenerators(); + JsonArray generatorsList = new JsonArray(); + for (Generator gen : generators) { + JsonObject genInfo = new JsonObject(); + genInfo.addProperty("name", gen.getName()); + genInfo.addProperty("generate_on_start", gen.generateOnStart()); + generatorsList.add(genInfo); + } + typeInfo.add("generators", generatorsList); + typeInfo.addProperty("generator_count", generators.size()); + + typesArray.add(typeInfo); + typeDetails.put(name, typeInfo); + } + } + + result.add("vulcheck_types", typesArray); + result.addProperty("total_types", vulCheckerNames.length); + + return result; + } + + /** + * ターゲット位置を解析してTargetLocationリストに変換 + */ + private List parseTargetLocations(JsonArray targetLocations, byte[] packetData) throws Exception { + List locations = new ArrayList<>(); + String packetStr = new String(packetData); + + for (JsonElement element : targetLocations) { + JsonObject location = element.getAsJsonObject(); + + // Check if regex pattern is specified + if (location.has("pattern")) { + // Regex-based approach + String pattern = location.get("pattern").getAsString(); + String replacement = location.has("replacement") ? location.get("replacement").getAsString() : null; + String description = location.has("description") + ? location.get("description").getAsString() + : ("Pattern: " + pattern); + + // Find all matches for the pattern + Pattern regex = Pattern.compile(pattern); + Matcher matcher = regex.matcher(packetStr); + + int matchCount = 0; + while (matcher.find()) { + matchCount++; + int start = matcher.start(); + int end = matcher.end(); + + TargetLocation targetLoc = new TargetLocation(); + targetLoc.range = Range.of(start, end); + targetLoc.description = description + " (match " + matchCount + ")"; + targetLoc.pattern = pattern; + targetLoc.replacement = replacement; + targetLoc.originalMatch = matcher.group(); + + locations.add(targetLoc); + } + + if (matchCount == 0) { + log("VulCheckHelperTool: No matches found for pattern: " + pattern); + } + } else if (location.has("start") && location.has("end")) { + // Position-based approach (backward compatibility) + int start = location.get("start").getAsInt(); + int end = location.get("end").getAsInt(); + String description = location.has("description") + ? location.get("description").getAsString() + : ("Range " + start + "-" + end); + + if (start < 0 || end < start || end > packetData.length) { + throw new IllegalArgumentException( + "Invalid range: start=" + start + ", end=" + end + ", packet length=" + packetData.length); + } + + TargetLocation targetLoc = new TargetLocation(); + targetLoc.range = Range.of(start, end); + targetLoc.description = description; + targetLoc.originalMatch = packetStr.substring(start, end); + + locations.add(targetLoc); + } else { + throw new IllegalArgumentException( + "Each target location must have either 'pattern' or both 'start' and 'end' properties"); + } + } + + if (locations.isEmpty()) { + throw new IllegalArgumentException("No valid target locations found"); + } + + return locations; + } + + /** + * VulCheckテストを実行 + */ + private VulCheckResult executeVulCheckTests(OneShotPacket originalPacket, VulChecker vulChecker, + List targetLocations, int intervalMs, String mode, int maxPayloads, int timeoutMs, + String jobId) throws Exception { + + VulCheckResult result = new VulCheckResult(); + result.overallSuccess = true; + + // 各ターゲット位置に対してテストを実行 + for (int locationIndex = 0; locationIndex < targetLocations.size(); locationIndex++) { + TargetLocation targetLocation = targetLocations.get(locationIndex); + + log("VulCheckHelperTool: Processing target location " + (locationIndex + 1) + "/" + targetLocations.size() + + " at range " + targetLocation.range.getPositionStart() + "-" + + targetLocation.range.getPositionEnd() + " (" + targetLocation.description + ")"); + + LocationResult locResult = processTargetLocation(originalPacket, vulChecker, targetLocation, intervalMs, + mode, maxPayloads, jobId); + + result.locationResults.add(locResult); + result.totalPayloadsGenerated += locResult.payloadsGenerated; + result.totalPacketsSent += locResult.packetsSent; + result.totalFailed += locResult.packetsFailed; + + if (locResult.packetsFailed > 0) { + result.overallSuccess = false; + } + } + + // 平均間隔を計算 + if (result.totalPacketsSent > 1) { + result.averageIntervalMs = intervalMs; // sequentialモードの場合 + } + + return result; + } + + /** + * 特定のターゲット位置でVulCheckテストを実行 + */ + private LocationResult processTargetLocation(OneShotPacket originalPacket, VulChecker vulChecker, + TargetLocation targetLocation, int intervalMs, String mode, int maxPayloads, String jobId) + throws Exception { + + LocationResult result = new LocationResult(); + result.range = targetLocation.range; + result.description = targetLocation.description; + + long startTime = System.currentTimeMillis(); + + // 元のパケットデータを取得 + byte[] originalData = originalPacket.getData(); + String originalText = new String(originalData); + + // ターゲット範囲のデータを取得 + if (targetLocation.range.getPositionEnd() > originalData.length) { + throw new IllegalArgumentException("Target range exceeds packet data length: range end=" + + targetLocation.range.getPositionEnd() + ", data length=" + originalData.length); + } + + String targetData = targetLocation.originalMatch != null + ? targetLocation.originalMatch + : originalText.substring(targetLocation.range.getPositionStart(), + targetLocation.range.getPositionEnd()); + + // VulCheckのジェネレータを取得してペイロードを生成 + ImmutableList generators = vulChecker.getGenerators(); + ResendController resendController = ResendController.getInstance(); + + int payloadCount = 0; + int sentCount = 0; + int failedCount = 0; + + for (Generator generator : generators) { + if (payloadCount >= maxPayloads) { + log("VulCheckHelperTool: Reached max payload limit (" + maxPayloads + ")"); + break; + } + + try { + String payload = generator.generate(targetData); + if (payload != null && !payload.equals(targetData)) { + result.generatedPayloads.add(payload); + payloadCount++; + + // パケットを改変して送信 + String modifiedText; + if (targetLocation.pattern != null && targetLocation.replacement != null) { + // Regex pattern replacement approach + String replacementTemplate = targetLocation.replacement.replace("$1", payload); + Pattern pattern = Pattern.compile(targetLocation.pattern); + Matcher matcher = pattern.matcher(originalText); + + // Find the specific match we're targeting + StringBuffer sb = new StringBuffer(); + int currentMatch = 0; + while (matcher.find()) { + if (matcher.start() == targetLocation.range.getPositionStart()) { + // This is our target match + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacementTemplate)); + break; + } else { + // Keep other matches unchanged + matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group())); + } + } + matcher.appendTail(sb); + modifiedText = sb.toString(); + } else { + // Position-based replacement approach + modifiedText = originalText.substring(0, targetLocation.range.getPositionStart()) + payload + + originalText.substring(targetLocation.range.getPositionEnd()); + } + + byte[] modifiedData = modifiedText.getBytes(); + + // ジョブ情報を付与してパケット作成 + String temporaryId = UUID.randomUUID().toString(); + OneShotPacket modifiedPacket = new OneShotPacket(originalPacket.getId(), + originalPacket.getListenPort(), originalPacket.getClient(), originalPacket.getServer(), + originalPacket.getServerName(), originalPacket.getUseSSL(), modifiedData, + originalPacket.getEncoder(), originalPacket.getAlpn(), originalPacket.getDirection(), + originalPacket.getConn(), originalPacket.getGroup(), jobId, temporaryId); + + // 送信モードに応じて処理 + if ("parallel".equals(mode)) { + // 並列送信 - すぐに送信 + try { + resendController.resend(modifiedPacket); + sentCount++; + } catch (Exception e) { + log("VulCheckHelperTool: Failed to send packet with payload: " + e.getMessage()); + failedCount++; + } + } else { + // 順次送信 - 間隔を設けて送信 + try { + resendController.resend(modifiedPacket); + sentCount++; + + if (intervalMs > 0 && payloadCount < maxPayloads && payloadCount < generators.size()) { + Thread.sleep(intervalMs); + } + } catch (Exception e) { + log("VulCheckHelperTool: Failed to send packet with payload: " + e.getMessage()); + failedCount++; + } + } + } + } catch (Exception e) { + log("VulCheckHelperTool: Failed to generate payload with generator " + generator.getName() + ": " + + e.getMessage()); + failedCount++; + } + } + + result.payloadsGenerated = payloadCount; + result.packetsSent = sentCount; + result.packetsFailed = failedCount; + result.executionTimeMs = System.currentTimeMillis() - startTime; + + log("VulCheckHelperTool: Location complete - generated " + payloadCount + " payloads, sent " + sentCount + + " packets, " + failedCount + " failed"); + + return result; + } + + /** + * VulCheckテストの全体結果 + */ + private static class VulCheckResult { + boolean overallSuccess = true; + int totalPayloadsGenerated = 0; + int totalPacketsSent = 0; + int totalFailed = 0; + double averageIntervalMs = 0; + List locationResults = new ArrayList<>(); + } + + /** + * ターゲット位置の情報 + */ + private static class TargetLocation { + Range range; + String description; + String pattern; // regex pattern (if used) + String replacement; // replacement template (if used) + String originalMatch; // original matched text + } + + /** + * 特定位置でのテスト結果 + */ + private static class LocationResult { + Range range; + String description; + int payloadsGenerated = 0; + int packetsSent = 0; + int packetsFailed = 0; + long executionTimeMs = 0; + List generatedPayloads = new ArrayList<>(); + } +} diff --git a/src/main/java/core/packetproxy/gui/GUILog.java b/src/main/java/core/packetproxy/gui/GUILog.java index 3a06d9ac..600c7fb7 100644 --- a/src/main/java/core/packetproxy/gui/GUILog.java +++ b/src/main/java/core/packetproxy/gui/GUILog.java @@ -83,4 +83,15 @@ public void appendErr(String s) { } } + + public String getLogText() { + synchronized (thread_lock) { + try { + StyledDocument doc = text.getStyledDocument(); + return doc.getText(0, doc.getLength()); + } catch (BadLocationException ex) { + return ""; + } + } + } } diff --git a/src/main/java/core/packetproxy/model/Extensions.java b/src/main/java/core/packetproxy/model/Extensions.java index cac9fdc9..7d452926 100644 --- a/src/main/java/core/packetproxy/model/Extensions.java +++ b/src/main/java/core/packetproxy/model/Extensions.java @@ -33,6 +33,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.swing.JOptionPane; +import packetproxy.extensions.mcp.MCPServerExtension; import packetproxy.extensions.randomness.RandomnessExtension; import packetproxy.extensions.samplehttp.SampleEncoders; import packetproxy.model.Database.DatabaseMessage; @@ -54,6 +55,7 @@ public static Extensions getInstance() throws Exception { { + put((new MCPServerExtension()).getName(), MCPServerExtension.class); put((new RandomnessExtension()).getName(), RandomnessExtension.class); put((new SampleEncoders()).getName(), SampleEncoders.class); } diff --git a/src/main/java/core/packetproxy/model/OneShotPacket.java b/src/main/java/core/packetproxy/model/OneShotPacket.java index 8c199a7d..ef241284 100644 --- a/src/main/java/core/packetproxy/model/OneShotPacket.java +++ b/src/main/java/core/packetproxy/model/OneShotPacket.java @@ -42,6 +42,8 @@ public class OneShotPacket implements PacketInfo, Cloneable { private boolean auto_modified; private int conn; private long group; + private String job_id; + private String temporary_id; public OneShotPacket() { } @@ -51,7 +53,15 @@ public OneShotPacket(int id, int listen_port, InetSocketAddress client_addr, Ine int conn, long group) { initialize(id, listen_port, client_addr.getAddress().getHostAddress(), client_addr.getPort(), server_addr.getAddress().getHostAddress(), server_addr.getPort(), server_name, use_ssl, data, - encoder_name, alpn, dir, conn, group); + encoder_name, alpn, dir, conn, group, null, null); + } + + public OneShotPacket(int id, int listen_port, InetSocketAddress client_addr, InetSocketAddress server_addr, + String server_name, boolean use_ssl, byte[] data, String encoder_name, String alpn, Packet.Direction dir, + int conn, long group, String job_id, String temporary_id) { + initialize(id, listen_port, client_addr.getAddress().getHostAddress(), client_addr.getPort(), + server_addr.getAddress().getHostAddress(), server_addr.getPort(), server_name, use_ssl, data, + encoder_name, alpn, dir, conn, group, job_id, temporary_id); } @Override @@ -61,7 +71,7 @@ public Object clone() throws CloneNotSupportedException { private void initialize(int id, int listen_port, String client_ip, int client_port, String server_ip, int server_port, String server_name, boolean use_ssl, byte[] data, String encoder_name, String alpn, - Packet.Direction dir, int conn, long group) { + Packet.Direction dir, int conn, long group, String job_id, String temporary_id) { this.id = id; this.listen_port = listen_port; this.client_ip = client_ip; @@ -77,6 +87,8 @@ private void initialize(int id, int listen_port, String client_ip, int client_po this.auto_modified = false; this.conn = conn; this.group = group; + this.job_id = job_id; + this.temporary_id = temporary_id; } public Packet.Direction getDirection() { @@ -175,6 +187,22 @@ public long getGroup() { return this.group; } + public String getJobId() { + return this.job_id; + } + + public void setJobId(String job_id) { + this.job_id = job_id; + } + + public String getTemporaryId() { + return this.temporary_id; + } + + public void setTemporaryId(String temporary_id) { + this.temporary_id = temporary_id; + } + public void encode() { } @@ -182,6 +210,8 @@ public Packet toPacket() throws Exception { Packet packet = new Packet(listen_port, client_ip, client_port, server_ip, server_port, server_name, use_ssl, encoder_name, alpn, direction, conn, group); packet.setDecodedData(getData()); + packet.setJobId(job_id); + packet.setTemporaryId(temporary_id); return packet; } diff --git a/src/main/java/core/packetproxy/model/Packet.java b/src/main/java/core/packetproxy/model/Packet.java index 9f28f2f9..5e06274f 100644 --- a/src/main/java/core/packetproxy/model/Packet.java +++ b/src/main/java/core/packetproxy/model/Packet.java @@ -76,6 +76,10 @@ public enum Direction { private long group; @DatabaseField private String color; + @DatabaseField + private String job_id; + @DatabaseField + private String temporary_id; public Packet() { // ORMLite needs a no-arg constructor @@ -128,7 +132,7 @@ public int getId() { public OneShotPacket getOneShotPacket(byte[] data) { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), data, - getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), getTemporaryId()); } @@ -142,7 +146,8 @@ public byte[] getModifiedData() { public OneShotPacket getOneShotFromModifiedData() { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), - getModifiedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getModifiedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), + getTemporaryId()); } public void setSentData(byte[] data) { @@ -163,7 +168,8 @@ public byte[] getReceivedData() { public OneShotPacket getOneShotFromReceivedData() { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), - getReceivedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getReceivedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), + getTemporaryId()); } public void setDecodedData(byte[] data) { @@ -176,7 +182,8 @@ public byte[] getDecodedData() { public OneShotPacket getOneShotFromDecodedData() { return new OneShotPacket(getId(), getListenPort(), getClient(), getServer(), getServerName(), getUseSSL(), - getDecodedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup()); + getDecodedData(), getEncoder(), getAlpn(), getDirection(), getConn(), getGroup(), getJobId(), + getTemporaryId()); } public void setModified() { @@ -271,6 +278,22 @@ public void setColor(String color) { this.color = color; } + public String getJobId() { + return this.job_id; + } + + public void setJobId(String job_id) { + this.job_id = job_id; + } + + public String getTemporaryId() { + return this.temporary_id; + } + + public void setTemporaryId(String temporary_id) { + this.temporary_id = temporary_id; + } + public String getSummarizedRequest() throws Exception { Encoder encoder = EncoderManager.getInstance().createInstance(encoder_name, null); if (encoder == null) { diff --git a/src/main/java/core/packetproxy/model/Packets.java b/src/main/java/core/packetproxy/model/Packets.java index 8a4730cb..0a89fa77 100644 --- a/src/main/java/core/packetproxy/model/Packets.java +++ b/src/main/java/core/packetproxy/model/Packets.java @@ -236,6 +236,13 @@ public void handleDatabaseMessage(DatabaseMessage message) { dao.executeRaw("ALTER TABLE `packets` ADD COLUMN color VARCHAR"); } + // job_idカラムとtemporary_idカラムも追加する + if (!result.contains("`job_id` VARCHAR")) { + dao.executeRaw("ALTER TABLE `packets` ADD COLUMN job_id VARCHAR"); + } + if (!result.contains("`temporary_id` VARCHAR")) { + dao.executeRaw("ALTER TABLE `packets` ADD COLUMN temporary_id VARCHAR"); + } firePropertyChange(message); break; case RECREATE : @@ -255,7 +262,7 @@ private boolean isLatestVersion() throws Exception { String result = dao.queryRaw("SELECT sql FROM sqlite_master WHERE name='packets'").getFirstResult()[0]; // System.out.println(result); return result.equals( - "CREATE TABLE `packets` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `direction` VARCHAR , `decoded_data` BLOB , `modified_data` BLOB , `sent_data` BLOB , `received_data` BLOB , `listen_port` INTEGER , `client_ip` VARCHAR , `client_port` INTEGER , `server_ip` VARCHAR , `server_name` VARCHAR , `server_port` INTEGER , `use_ssl` BOOLEAN , `content_type` VARCHAR , `encoder_name` VARCHAR , `alpn` VARCHAR , `modified` BOOLEAN , `resend` BOOLEAN , `date` BIGINT , `conn` INTEGER , `group` BIGINT , `color` VARCHAR )"); + "CREATE TABLE `packets` (`id` INTEGER PRIMARY KEY AUTOINCREMENT , `direction` VARCHAR , `decoded_data` BLOB , `modified_data` BLOB , `sent_data` BLOB , `received_data` BLOB , `listen_port` INTEGER , `client_ip` VARCHAR , `client_port` INTEGER , `server_ip` VARCHAR , `server_name` VARCHAR , `server_port` INTEGER , `use_ssl` BOOLEAN , `content_type` VARCHAR , `encoder_name` VARCHAR , `alpn` VARCHAR , `modified` BOOLEAN , `resend` BOOLEAN , `date` BIGINT , `conn` INTEGER , `group` BIGINT , `color` VARCHAR , `job_id` VARCHAR , `temporary_id` VARCHAR )"); } private void RecreateTable() throws Exception { diff --git a/src/main/java/core/packetproxy/util/Logging.java b/src/main/java/core/packetproxy/util/Logging.java index fc843d2c..edc0b9cf 100644 --- a/src/main/java/core/packetproxy/util/Logging.java +++ b/src/main/java/core/packetproxy/util/Logging.java @@ -30,7 +30,14 @@ private Logging() { public static void log(String format, Object... args) { LocalDateTime now = LocalDateTime.now(); String ns = dtf.format(now); - String ss = ns + " " + String.format(format, args); + String ss; + if (args.length == 0) { + // 引数がない場合は、formatをそのまま使用(String.formatを使わない) + ss = ns + " " + format; + } else { + // 引数がある場合のみString.formatを使用 + ss = ns + " " + String.format(format, args); + } System.out.println(ss); guiLog.append(ss); } @@ -38,7 +45,14 @@ public static void log(String format, Object... args) { public static void err(String format, Object... args) { LocalDateTime now = LocalDateTime.now(); String ns = dtf.format(now); - String ss = ns + " " + String.format(format, args); + String ss; + if (args.length == 0) { + // 引数がない場合は、formatをそのまま使用(String.formatを使わない) + ss = ns + " " + format; + } else { + // 引数がある場合のみString.formatを使用 + ss = ns + " " + String.format(format, args); + } System.err.println(ss); guiLog.appendErr(ss); }