- Add detailed logging for `CreatePeer` and `UpdatePeer` operations to track input and processed data. - Simplify `SetSubscriptions`, `SetCustomPrefixes`, and `SetCustomAsns` calls by removing conditional checks.
654 lines
22 KiB
C#
654 lines
22 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using BGPLite.Api.Entities;
|
|
using BGPLite.Configuration;
|
|
using BGPLite.Protocol;
|
|
using BGPLite.Routing;
|
|
using BGPLite.Server;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace BGPLite.Api;
|
|
|
|
public sealed class ManagementApi : IHostedService, IDisposable
|
|
{
|
|
private readonly PeerStore _store;
|
|
private readonly RouteTable _routeTable;
|
|
private readonly AppConfig _config;
|
|
private readonly BgpMetrics _metrics;
|
|
private readonly IPrefixService? _prefixService;
|
|
private readonly ISessionManager? _sessionManager;
|
|
private readonly ILogger<ManagementApi> _logger;
|
|
private readonly int _port;
|
|
private HttpListener? _listener;
|
|
private Task? _listenTask;
|
|
private CancellationTokenSource _cts = new();
|
|
|
|
public ManagementApi(
|
|
PeerStore store,
|
|
RouteTable routeTable,
|
|
AppConfig config,
|
|
BgpMetrics metrics,
|
|
ILogger<ManagementApi> logger,
|
|
IPrefixService? prefixService = null,
|
|
ISessionManager? sessionManager = null)
|
|
{
|
|
_store = store;
|
|
_routeTable = routeTable;
|
|
_config = config;
|
|
_metrics = metrics;
|
|
_prefixService = prefixService;
|
|
_sessionManager = sessionManager;
|
|
_logger = logger;
|
|
_port = config.ApiPort;
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_listener = new HttpListener();
|
|
_listener.Prefixes.Add($"http://+:{_port}/");
|
|
_listener.Start();
|
|
|
|
_logger.LogInformation("Management API listening on http://+:{Port}/", _port);
|
|
_listenTask = ListenAsync(_cts.Token);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_cts.Cancel();
|
|
_listener?.Stop();
|
|
if (_listenTask is not null)
|
|
{
|
|
try { await _listenTask; } catch { }
|
|
}
|
|
}
|
|
|
|
private async Task ListenAsync(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var ctx = await _listener!.GetContextAsync();
|
|
if (ct.IsCancellationRequested) break;
|
|
_ = HandleAsync(ctx);
|
|
}
|
|
catch (HttpListenerException) when (ct.IsCancellationRequested) { break; }
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Management API error");
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Router
|
|
|
|
private async Task HandleAsync(HttpListenerContext ctx)
|
|
{
|
|
AddCorsHeaders(ctx);
|
|
|
|
if (ctx.Request.HttpMethod == "OPTIONS")
|
|
{
|
|
ctx.Response.StatusCode = 204;
|
|
ctx.Response.Close();
|
|
return;
|
|
}
|
|
|
|
var path = ctx.Request.Url!.AbsolutePath;
|
|
var method = ctx.Request.HttpMethod;
|
|
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
try
|
|
{
|
|
var response = Route(method, segments, ctx);
|
|
await WriteResponse(ctx, response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "API error {Method} {Path}: {Message}", method, path, ex.Message);
|
|
await WriteResponse(ctx, ApiResponse.Error(ex.InnerException?.Message ?? ex.Message, 500));
|
|
}
|
|
}
|
|
|
|
private ApiResponse Route(string method, string[] segments, HttpListenerContext ctx)
|
|
{
|
|
// /api/server
|
|
if (IsGet(method, segments, "api", "server"))
|
|
return HandleGetServer();
|
|
|
|
// /api/me
|
|
if (IsGet(method, segments, "api", "me"))
|
|
return HandleGetMe(ctx);
|
|
|
|
// /api/peers
|
|
if (IsPost(method, segments, "api", "peers"))
|
|
return HandleCreatePeer(ctx).GetAwaiter().GetResult();
|
|
|
|
// /api/peers/{id}
|
|
if (segments.Length == 3 && segments[0] == "api" && segments[1] == "peers" && method == "GET")
|
|
return HandleGetPeer(segments[2]);
|
|
if (segments.Length == 3 && segments[0] == "api" && segments[1] == "peers" && method == "PUT")
|
|
return HandleUpdatePeer(segments[2], ctx).GetAwaiter().GetResult();
|
|
if (segments.Length == 3 && segments[0] == "api" && segments[1] == "peers" && method == "DELETE")
|
|
return HandleDeletePeer(segments[2]);
|
|
|
|
// /api/peers/{id}/prefixes
|
|
if (segments.Length == 4 && segments[0] == "api" && segments[1] == "peers" && segments[3] == "prefixes" && method == "GET")
|
|
return HandleExportPrefixes(segments[2], ctx);
|
|
|
|
// /api/asn-lists
|
|
if (IsGet(method, segments, "api", "asn-lists"))
|
|
return HandleGetAsnListsAsync().GetAwaiter().GetResult();
|
|
|
|
// /api/sessions
|
|
if (IsGet(method, segments, "api", "sessions"))
|
|
return HandleGetSessions();
|
|
|
|
// /api/routes
|
|
if (IsGet(method, segments, "api", "routes"))
|
|
return HandleGetRoutes();
|
|
|
|
// /api/as/{asn}/prefixes
|
|
if (segments.Length == 4 && segments[0] == "api" && segments[1] == "as" && segments[3] == "prefixes" && method == "GET")
|
|
return HandleGetAsnPrefixes(segments[2], ctx);
|
|
|
|
return ApiResponse.Error("Not found", 404);
|
|
}
|
|
|
|
private static bool IsGet(string method, string[] segments, string s0, string s1)
|
|
=> method == "GET" && segments.Length == 2 && segments[0] == s0 && segments[1] == s1;
|
|
|
|
private static bool IsPost(string method, string[] segments, string s0, string s1)
|
|
=> method == "POST" && segments.Length == 2 && segments[0] == s0 && segments[1] == s1;
|
|
|
|
#endregion
|
|
|
|
#region GET /api/server
|
|
|
|
private ApiResponse HandleGetServer()
|
|
{
|
|
var bgp = _config.Bgp;
|
|
var ip = bgp.RouterId;
|
|
return ApiResponse.Ok(new
|
|
{
|
|
asn = bgp.Asn,
|
|
routerId = ip,
|
|
bgpPort = 179,
|
|
apiPort = _port,
|
|
holdTime = bgp.HoldTime,
|
|
keepalive = bgp.KeepAlive,
|
|
setup = new[]
|
|
{
|
|
$"router bgp {bgp.Asn}",
|
|
$" neighbor <YOUR_IP> remote-as {bgp.Asn}",
|
|
$" neighbor <YOUR_IP> ebgp-multihop 2",
|
|
$" neighbor <YOUR_IP> update-source <YOUR_INTERFACE>",
|
|
$" neighbor <YOUR_IP> soft-reconfiguration inbound",
|
|
$"!",
|
|
$"address-family ipv4 unicast",
|
|
$" neighbor <YOUR_IP> activate",
|
|
$" neighbor <YOUR_IP> route-map BGPLite-IN in",
|
|
$" neighbor <YOUR_IP> route-map BGPLite-OUT out",
|
|
$"exit-address-family"
|
|
},
|
|
bird = new[]
|
|
{
|
|
$"# ---------- FILTER ----------",
|
|
$"filter bgplite_in {{",
|
|
$" gw = <YOUR_GATEWAY>;",
|
|
$" accept;",
|
|
$"}}",
|
|
$"",
|
|
$"# ---------- eBGP ----------",
|
|
$"protocol bgp bgplite {{",
|
|
$" local as <YOUR_ASN>;",
|
|
$" neighbor {ip} as {bgp.Asn};",
|
|
$" source address <YOUR_IP>;",
|
|
$" multihop;",
|
|
$" hold time {bgp.HoldTime};",
|
|
$" ipv4 {{",
|
|
$" import filter bgplite_in;",
|
|
$" export none;",
|
|
$" graceful restart on;",
|
|
$" }};",
|
|
$"}}"
|
|
},
|
|
mikrotik = new[]
|
|
{
|
|
$"/routing bgp connection",
|
|
$"add name=bgplite remote.address={ip}/32 remote.as={bgp.Asn}",
|
|
$" local.default-address=<YOUR_IP> local.role=ebgp",
|
|
$" as=<YOUR_ASN> multihop=yes hold-time={bgp.HoldTime}s afi=ip",
|
|
$" output.filter-chain=discard output.no-client-to-client-reflection=yes",
|
|
$" input.filter=bgplite-in",
|
|
$"!",
|
|
$"/routing filter rule",
|
|
$"add chain=bgplite-in action=accept set-in-nexthop-direct=yes"
|
|
}
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GET /api/me
|
|
|
|
private ApiResponse HandleGetMe(HttpListenerContext ctx)
|
|
{
|
|
var clientIp = GetClientIp(ctx);
|
|
|
|
var peerInfo = _store.GetPeerByIp(clientIp);
|
|
if (peerInfo is null)
|
|
return ApiResponse.Ok(new { ip = clientIp, peer = (object?)null });
|
|
|
|
var peer = _store.GetDbPeerById(peerInfo.Id);
|
|
if (peer is null)
|
|
return ApiResponse.Ok(new { ip = clientIp, peer = (object?)null });
|
|
|
|
var subscriptions = _store.GetSubscriptions(peer.Id);
|
|
var customPrefixes = _store.GetCustomPrefixes(peer.Id);
|
|
var customAsns = _store.GetCustomAsns(peer.Id);
|
|
var communities = _store.GetCommunitiesByIp(clientIp);
|
|
|
|
return ApiResponse.Ok(new
|
|
{
|
|
ip = clientIp,
|
|
peer = new
|
|
{
|
|
id = peer.Id,
|
|
ip = peer.Ip,
|
|
asn = peer.Asn,
|
|
description = peer.Description,
|
|
status = peer.Status,
|
|
createdAt = peer.CreatedAt,
|
|
lastSessionAt = peer.LastSessionAt,
|
|
lists = subscriptions,
|
|
customPrefixes,
|
|
customAsns,
|
|
communities = communities.Select(CommunityToString),
|
|
allRoutes = communities.Count == 0
|
|
}
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region /api/peers
|
|
|
|
private async Task<ApiResponse> HandleCreatePeer(HttpListenerContext ctx)
|
|
{
|
|
using var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8);
|
|
var body = await reader.ReadToEndAsync();
|
|
var data = JsonSerializer.Deserialize<CreatePeerRequest>(body, _jsonOpts);
|
|
|
|
if (data is null)
|
|
return ApiResponse.Error("Invalid request body", 400);
|
|
|
|
var asnLists = data.AsnLists ?? [];
|
|
var customPrefixes = new List<(string Prefix, byte Length)>();
|
|
|
|
_logger.LogInformation("CreatePeer raw body: {Body}", body);
|
|
_logger.LogInformation("CreatePeer deserialized: AsnLists={Lists}, CustomPrefixes={Prefixes}, CustomAsns={Asns}",
|
|
string.Join(",", asnLists), string.Join(",", data.CustomPrefixes ?? []),
|
|
string.Join(",", data.CustomAsns ?? []));
|
|
|
|
if (data.CustomPrefixes is not null)
|
|
{
|
|
foreach (var cidr in data.CustomPrefixes)
|
|
{
|
|
var slash = cidr.IndexOf('/');
|
|
if (slash < 0)
|
|
return ApiResponse.Error($"Invalid CIDR: {cidr}", 400);
|
|
var prefix = cidr[..slash];
|
|
var length = byte.Parse(cidr[(slash + 1)..]);
|
|
customPrefixes.Add((prefix, length));
|
|
}
|
|
}
|
|
|
|
var id = _store.CreatePeer(data.Ip, data.Asn, data.Description);
|
|
_store.SetSubscriptions(id, asnLists);
|
|
_store.SetCustomPrefixes(id, customPrefixes);
|
|
_store.SetCustomAsns(id, data.CustomAsns ?? []);
|
|
|
|
var peer = _store.GetDbPeerById(id);
|
|
|
|
_logger.LogInformation("Created peer {Ip} AS{Asn} ({Id}): {Subs} lists, {Prefixes} custom prefixes, {Asns} custom AS",
|
|
data.Ip, data.Asn, id, asnLists.Count, customPrefixes.Count, data.CustomAsns?.Count ?? 0);
|
|
|
|
if (_sessionManager is not null)
|
|
_ = _sessionManager.RefreshPeerAsync(data.Ip);
|
|
|
|
return ApiResponse.Ok(new
|
|
{
|
|
id,
|
|
ip = data.Ip,
|
|
asn = data.Asn,
|
|
description = data.Description,
|
|
status = peer?.Status ?? "inactive",
|
|
createdAt = peer?.CreatedAt,
|
|
lists = asnLists,
|
|
customPrefixes = data.CustomPrefixes ?? [],
|
|
customAsns = data.CustomAsns ?? []
|
|
});
|
|
}
|
|
|
|
private ApiResponse HandleGetPeer(string peerId)
|
|
{
|
|
var peer = _store.GetDbPeerById(peerId);
|
|
if (peer is null)
|
|
return ApiResponse.Error("Peer not found", 404);
|
|
|
|
var subscriptions = _store.GetSubscriptions(peer.Id);
|
|
var customPrefixes = _store.GetCustomPrefixes(peer.Id);
|
|
var customAsns = _store.GetCustomAsns(peer.Id);
|
|
var communities = _store.GetCommunitiesByIp(peer.Ip);
|
|
|
|
return ApiResponse.Ok(new
|
|
{
|
|
id = peer.Id,
|
|
ip = peer.Ip,
|
|
asn = peer.Asn,
|
|
description = peer.Description,
|
|
status = peer.Status,
|
|
createdAt = peer.CreatedAt,
|
|
lastSessionAt = peer.LastSessionAt,
|
|
lists = subscriptions,
|
|
customPrefixes,
|
|
customAsns,
|
|
communities = communities.Select(CommunityToString),
|
|
allRoutes = communities.Count == 0
|
|
});
|
|
}
|
|
|
|
private async Task<ApiResponse> HandleUpdatePeer(string peerId, HttpListenerContext ctx)
|
|
{
|
|
var peer = _store.GetDbPeerById(peerId);
|
|
if (peer is null)
|
|
return ApiResponse.Error("Peer not found", 404);
|
|
|
|
using var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8);
|
|
var body = await reader.ReadToEndAsync();
|
|
var data = JsonSerializer.Deserialize<UpdatePeerRequest>(body, _jsonOpts);
|
|
|
|
if (data is null)
|
|
return ApiResponse.Error("Invalid request body", 400);
|
|
|
|
if (data.Description is not null)
|
|
_store.SetDescription(peerId, data.Description);
|
|
|
|
if (data.Lists is not null)
|
|
_store.SetSubscriptions(peerId, data.Lists);
|
|
|
|
var customPrefixes = data.CustomPrefixes ?? [];
|
|
_logger.LogInformation("UpdatePeer {Id}: CustomPrefixes={Count}, CustomAsns={AsnCount}, raw={Raw}",
|
|
peerId, customPrefixes.Count, (data.CustomAsns ?? []).Count, body);
|
|
var parsed = new List<(string, byte)>();
|
|
foreach (var cidr in customPrefixes)
|
|
{
|
|
var slash = cidr.IndexOf('/');
|
|
if (slash < 0) return ApiResponse.Error($"Invalid CIDR: {cidr}", 400);
|
|
parsed.Add((cidr[..slash], byte.Parse(cidr[(slash + 1)..])));
|
|
}
|
|
_store.SetCustomPrefixes(peerId, parsed);
|
|
|
|
_store.SetCustomAsns(peerId, data.CustomAsns ?? []);
|
|
|
|
_logger.LogInformation("Updated peer {Id}", peerId);
|
|
|
|
if (_sessionManager is not null)
|
|
_ = _sessionManager.RefreshPeerAsync(peer.Ip);
|
|
|
|
return HandleGetPeer(peerId);
|
|
}
|
|
|
|
private ApiResponse HandleDeletePeer(string peerId)
|
|
{
|
|
var peer = _store.GetDbPeerById(peerId);
|
|
if (peer is null)
|
|
return ApiResponse.Error("Peer not found", 404);
|
|
|
|
_store.DeletePeer(peerId);
|
|
_logger.LogInformation("Deleted peer {Id} ({Ip})", peerId, peer.Ip);
|
|
return ApiResponse.Ok(new { id = peerId, deleted = true });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region /api/peers/{id}/prefixes
|
|
|
|
private ApiResponse HandleExportPrefixes(string peerId, HttpListenerContext ctx)
|
|
{
|
|
var peer = _store.GetDbPeerById(peerId);
|
|
if (peer is null)
|
|
return ApiResponse.Error("Peer not found", 404);
|
|
|
|
var prefixes = CollectPeerPrefixes(peerId).GetAwaiter().GetResult();
|
|
|
|
var format = ctx.Request.QueryString["format"] ?? "txt";
|
|
if (format == "json")
|
|
return ApiResponse.Ok(prefixes);
|
|
|
|
ctx.Response.ContentType = "text/plain";
|
|
return ApiResponse.Ok(string.Join("\n", prefixes));
|
|
}
|
|
|
|
private async Task<List<string>> CollectPeerPrefixes(string peerId)
|
|
{
|
|
var prefixes = new List<string>();
|
|
|
|
// Custom prefixes
|
|
prefixes.AddRange(_store.GetCustomPrefixes(peerId));
|
|
|
|
if (_prefixService is null)
|
|
return prefixes.Distinct().OrderBy(p => p).ToList();
|
|
|
|
var subscriptions = _store.GetSubscriptions(peerId);
|
|
var subscribedLists = _config.RipeStat?.AsnLists
|
|
.Where(l => subscriptions.Contains(l.Name))
|
|
.ToList() ?? [];
|
|
|
|
// ASN-based lists
|
|
var asns = subscribedLists.Where(l => l.Asns.Count > 0).SelectMany(l => l.Asns).ToList();
|
|
if (asns.Count > 0)
|
|
{
|
|
try
|
|
{
|
|
var fetched = await _prefixService.GetPrefixesForAsns(asns);
|
|
foreach (var (prefix, length, _) in fetched)
|
|
prefixes.Add($"{BgpConstants.UintToIPAddress(prefix)}/{length}");
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// Country-based lists
|
|
if (subscribedLists.Any(l => l.Asns.Count == 0 && l.Country is not null))
|
|
{
|
|
try
|
|
{
|
|
var ruPrefixes = await _prefixService.GetRuPrefixesAsync();
|
|
foreach (var (prefix, length, _) in ruPrefixes)
|
|
prefixes.Add($"{BgpConstants.UintToIPAddress(prefix)}/{length}");
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
return prefixes.Distinct().OrderBy(p => p).ToList();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region /api/peers/{id}/communities
|
|
|
|
#endregion
|
|
|
|
#region GET /api/asn-lists
|
|
|
|
private async Task<ApiResponse> HandleGetAsnListsAsync()
|
|
{
|
|
var lists = _config.RipeStat?.AsnLists ?? [];
|
|
var result = new List<object>();
|
|
|
|
foreach (var l in lists)
|
|
{
|
|
int prefixCount = 0;
|
|
if (_prefixService is not null)
|
|
{
|
|
if (l.Asns.Count > 0)
|
|
{
|
|
foreach (var asn in l.Asns)
|
|
{
|
|
try { prefixCount += await _prefixService.GetPrefixCountAsync(asn); }
|
|
catch (Exception ex) { _logger.LogWarning(ex, "Failed to get prefix count for AS{Asn}", asn); }
|
|
}
|
|
}
|
|
else if (l.Country is not null)
|
|
{
|
|
try { prefixCount = (await _prefixService.GetRuPrefixesAsync()).Count; }
|
|
catch (Exception ex) { _logger.LogWarning(ex, "Failed to get RU prefix count"); }
|
|
}
|
|
}
|
|
|
|
result.Add(new
|
|
{
|
|
id = l.Name,
|
|
l.Name,
|
|
l.Description,
|
|
l.Country,
|
|
prefixCount,
|
|
type = l.Country is not null ? "country" : "asn"
|
|
});
|
|
}
|
|
|
|
return ApiResponse.Ok(result);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GET /api/sessions
|
|
|
|
private ApiResponse HandleGetSessions()
|
|
{
|
|
return ApiResponse.Ok(new
|
|
{
|
|
active = _metrics.ActiveSessions
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GET /api/routes
|
|
|
|
private ApiResponse HandleGetRoutes()
|
|
{
|
|
var routes = _routeTable.GetAll();
|
|
var byCommunity = routes
|
|
.SelectMany(r => r.Communities.Length == 0
|
|
? [(community: 0u, route: r)]
|
|
: r.Communities.Select(c => (community: c, route: r)))
|
|
.GroupBy(x => x.community)
|
|
.ToDictionary(g => g.Key == 0 ? "default" : CommunityToString(g.Key), g => g.Count());
|
|
|
|
return ApiResponse.Ok(new { total = routes.Count, byCommunity });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GET /api/as/{asn}/prefixes
|
|
|
|
private ApiResponse HandleGetAsnPrefixes(string asnStr, HttpListenerContext ctx)
|
|
{
|
|
if (!uint.TryParse(asnStr, out var asn))
|
|
return ApiResponse.Error("Invalid ASN", 400);
|
|
|
|
var countOnly = ctx.Request.QueryString["count"] == "true";
|
|
|
|
if (countOnly)
|
|
{
|
|
if (_prefixService is null)
|
|
return ApiResponse.Error("Prefix service not available", 503);
|
|
|
|
try
|
|
{
|
|
var count = _prefixService.GetPrefixCountAsync(asn).GetAwaiter().GetResult();
|
|
return ApiResponse.Ok(new { asn, prefixCount = count });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ApiResponse.Error(ex.Message, 500);
|
|
}
|
|
}
|
|
|
|
return ApiResponse.Ok(new { asn, message = "Use ?count=true for prefix count" });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
private static string GetClientIp(HttpListenerContext ctx)
|
|
{
|
|
var realIp = ctx.Request.Headers["X-Real-IP"];
|
|
if (!string.IsNullOrEmpty(realIp)) return realIp;
|
|
|
|
var forwarded = ctx.Request.Headers["X-Forwarded-For"];
|
|
if (!string.IsNullOrEmpty(forwarded))
|
|
{
|
|
var first = forwarded.Split(',')[0].Trim();
|
|
if (first.Length > 0) return first;
|
|
}
|
|
|
|
return ctx.Request.RemoteEndPoint?.Address.ToString() ?? "unknown";
|
|
}
|
|
|
|
private static uint ParseCommunity(string community)
|
|
{
|
|
var colon = community.IndexOf(':');
|
|
var asn = uint.Parse(community[..colon]);
|
|
var value = uint.Parse(community[(colon + 1)..]);
|
|
return (asn << 16) | (value & 0xFFFF);
|
|
}
|
|
|
|
private static string CommunityToString(uint community)
|
|
{
|
|
return $"{community >> 16}:{community & 0xFFFF}";
|
|
}
|
|
|
|
private static async Task WriteResponse(HttpListenerContext ctx, ApiResponse response)
|
|
{
|
|
ctx.Response.StatusCode = response.StatusCode;
|
|
ctx.Response.ContentType = "application/json";
|
|
var json = JsonSerializer.Serialize(response.Body);
|
|
var bytes = Encoding.UTF8.GetBytes(json);
|
|
await ctx.Response.OutputStream.WriteAsync(bytes);
|
|
ctx.Response.Close();
|
|
}
|
|
|
|
private static void AddCorsHeaders(HttpListenerContext ctx)
|
|
{
|
|
ctx.Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
|
ctx.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
ctx.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
_listener?.Close();
|
|
}
|
|
|
|
private record CreatePeerRequest(string Ip, uint Asn, string? Description, [property: JsonPropertyName("lists")] List<string>? AsnLists, List<string>? CustomPrefixes, List<uint>? CustomAsns);
|
|
private record UpdatePeerRequest(string? Description, [property: JsonPropertyName("lists")] List<string>? Lists, List<string>? CustomPrefixes, List<uint>? CustomAsns);
|
|
|
|
private record ApiResponse(object? Body, int StatusCode = 200)
|
|
{
|
|
public static ApiResponse Ok(object data) => new(data);
|
|
public static ApiResponse Error(string message, int code) => new(new { error = message }, code);
|
|
}
|
|
|
|
private static readonly JsonSerializerOptions _jsonOpts = new() { PropertyNameCaseInsensitive = true };
|
|
}
|