Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/HttpTool.cs

148 lines
5.7 KiB
C#

using System.Net.Http;
using System.Text;
using System.Text.Json;
namespace AxCopilot.Services.Agent;
/// <summary>
/// 로컬/사내 HTTP API 호출 도구.
/// GET/POST/PUT/DELETE 요청, JSON 파싱, 헤더 설정을 지원합니다.
/// </summary>
public class HttpTool : IAgentTool
{
public string Name => "http_tool";
public string Description =>
"Make HTTP requests to local or internal APIs. " +
"Supports GET, POST, PUT, DELETE methods with JSON body and custom headers. " +
"Only allows localhost and internal network addresses (security restriction). " +
"Use this for testing APIs, fetching data from internal services, or webhooks.";
public ToolParameterSchema Parameters => new()
{
Properties = new()
{
["method"] = new()
{
Type = "string",
Description = "HTTP method",
Enum = ["GET", "POST", "PUT", "DELETE", "PATCH"],
},
["url"] = new()
{
Type = "string",
Description = "Request URL (localhost or internal network only)",
},
["body"] = new()
{
Type = "string",
Description = "Request body (JSON string, for POST/PUT/PATCH)",
},
["headers"] = new()
{
Type = "string",
Description = "Custom headers as JSON object, e.g. {\"Authorization\": \"Bearer token\"}",
},
["timeout"] = new()
{
Type = "string",
Description = "Request timeout in seconds (default: 30, max: 120)",
},
},
Required = ["method", "url"],
};
private static readonly HttpClient _client = new() { Timeout = TimeSpan.FromSeconds(30) };
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
{
var method = args.GetProperty("method").GetString()?.ToUpperInvariant() ?? "GET";
var url = args.GetProperty("url").GetString() ?? "";
var body = args.TryGetProperty("body", out var b) ? b.GetString() ?? "" : "";
var headers = args.TryGetProperty("headers", out var h) ? h.GetString() ?? "" : "";
var timeout = args.TryGetProperty("timeout", out var t) ? int.TryParse(t.GetString(), out var ts) ? Math.Min(ts, 120) : 30 : 30;
// 보안: 허용된 호스트만
if (!IsAllowedHost(url))
return ToolResult.Fail("보안 제한: localhost, 127.0.0.1, 사내 네트워크(10.x, 172.16-31.x, 192.168.x)만 허용됩니다.");
try
{
var httpMethod = new HttpMethod(method);
using var request = new HttpRequestMessage(httpMethod, url);
// 헤더 설정
if (!string.IsNullOrEmpty(headers))
{
using var headerDoc = JsonDocument.Parse(headers);
foreach (var prop in headerDoc.RootElement.EnumerateObject())
request.Headers.TryAddWithoutValidation(prop.Name, prop.Value.GetString());
}
// 본문 설정
if (!string.IsNullOrEmpty(body) && method is "POST" or "PUT" or "PATCH")
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var response = await _client.SendAsync(request, cts.Token);
var statusCode = (int)response.StatusCode;
var responseBody = await response.Content.ReadAsStringAsync(cts.Token);
// 응답 포맷팅
var sb = new StringBuilder();
sb.AppendLine($"HTTP {statusCode} {response.ReasonPhrase}");
sb.AppendLine($"Content-Type: {response.Content.Headers.ContentType}");
sb.AppendLine();
// JSON이면 포맷
if (response.Content.Headers.ContentType?.MediaType?.Contains("json") == true)
{
try
{
using var doc = JsonDocument.Parse(responseBody);
responseBody = JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
}
catch { /* not valid JSON, keep raw */ }
}
if (responseBody.Length > 8000)
responseBody = responseBody[..8000] + $"\n... (truncated, total {responseBody.Length} chars)";
sb.Append(responseBody);
return ToolResult.Ok(sb.ToString());
}
catch (TaskCanceledException)
{
return ToolResult.Fail($"요청 시간 초과 ({timeout}초)");
}
catch (Exception ex)
{
return ToolResult.Fail($"HTTP 요청 실패: {ex.Message}");
}
}
private static bool IsAllowedHost(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
var host = uri.Host;
if (host is "localhost" or "127.0.0.1" or "::1") return true;
// 사내 네트워크 대역
if (System.Net.IPAddress.TryParse(host, out var ip))
{
var bytes = ip.GetAddressBytes();
if (bytes.Length == 4)
{
if (bytes[0] == 10) return true; // 10.0.0.0/8
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true; // 172.16.0.0/12
if (bytes[0] == 192 && bytes[1] == 168) return true; // 192.168.0.0/16
}
}
return false;
}
}