148 lines
5.7 KiB
C#
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;
|
|
}
|
|
}
|