/*
* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: ObjectCacheServer
* File: PeerDiscoveryEndpoint.cs
*
* PeerDiscoveryEndpoint.cs is part of ObjectCacheServer which is part of the larger
* VNLib collection of libraries and utilities.
*
* ObjectCacheServer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ObjectCacheServer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
using System;
using System.Net;
using System.Linq;
using System.Text.Json;
using VNLib.Hashing;
using VNLib.Hashing.IdentityUtility;
using VNLib.Plugins;
using VNLib.Plugins.Essentials;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Data.Caching.ObjectCache.Server.Distribution;
using VNLib.Data.Caching.Extensions.Clustering;
namespace VNLib.Data.Caching.ObjectCache.Server.Endpoints
{
[ConfigurationName("discovery_endpoint")]
internal sealed class PeerDiscoveryEndpoint : ResourceEndpointBase
{
private readonly IPeerMonitor PeerMonitor;
private readonly NodeConfig Config;
//Loosen up protection settings
///
protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
{
DisableBrowsersOnly = true,
DisableSessionsRequired = true
};
public PeerDiscoveryEndpoint(PluginBase plugin, IConfigScope config)
{
string? path = config["path"].GetString();
InitPathAndLog(path, plugin.Log);
//Get the peer monitor
PeerMonitor = plugin.GetOrCreateSingleton();
//Get the node config
Config = plugin.GetOrCreateSingleton();
}
protected override VfReturnType Get(HttpEntity entity)
{
//Get auth token
string? authToken = entity.Server.Headers[HttpRequestHeader.Authorization];
if(string.IsNullOrWhiteSpace(authToken))
{
entity.CloseResponse(HttpStatusCode.Unauthorized);
return VfReturnType.VirtualSkip;
}
string subject = string.Empty;
string challenge = string.Empty;
//Parse auth token
using(JsonWebToken jwt = JsonWebToken.Parse(authToken))
{
//try to verify against cache node first
if (!Config.KeyStore.VerifyJwt(jwt, true))
{
//failed...
//try to verify against client key
if (!Config.KeyStore.VerifyJwt(jwt, false))
{
//invalid token
entity.CloseResponse(HttpStatusCode.Unauthorized);
return VfReturnType.VirtualSkip;
}
}
using JsonDocument payload = jwt.GetPayload();
//Get client info to pass back
subject = payload.RootElement.GetProperty("sub").GetString() ?? string.Empty;
challenge = payload.RootElement.GetProperty("chl").GetString() ?? string.Empty;
}
//Valid key, get peer list to send to client
CacheNodeAdvertisment[] peers = PeerMonitor.GetAllPeers()
.Where(static p => p.Advertisment != null)
.Select(static p => p.Advertisment!)
.ToArray();
//Build response jwt
using JsonWebToken response = new();
//set header from cache config
response.WriteHeader(Config.KeyStore.GetJwtHeader());
response.InitPayloadClaim()
.AddClaim("iss", Config.Config.NodeId)
//Audience is the requestor id
.AddClaim("sub", subject)
.AddClaim("iat", entity.RequestedTimeUtc.ToUnixTimeSeconds())
//Send all peers as a json array
.AddClaim("peers", peers)
//Send the challenge back
.AddClaim("chl", challenge)
.CommitClaims();
//Sign the response
Config.KeyStore.SignJwt(response);
//Send response to client
entity.CloseResponse(HttpStatusCode.OK, Net.Http.ContentType.Text, response.DataBuffer);
return VfReturnType.VirtualSkip;
}
}
}