mirror of
https://github.com/TechnitiumSoftware/DnsServer.git
synced 2026-03-02 22:59:14 +00:00
612 lines
27 KiB
C#
612 lines
27 KiB
C#
/*
|
|
Technitium DNS Server
|
|
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
using DnsServerCore.HttpApi.Models;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Primitives;
|
|
using System;
|
|
using System.Buffers.Text;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Security;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
using TechnitiumLibrary;
|
|
using TechnitiumLibrary.Net.Dns;
|
|
using TechnitiumLibrary.Net.Http.Client;
|
|
using TechnitiumLibrary.Net.Proxy;
|
|
|
|
namespace DnsServerCore.HttpApi
|
|
{
|
|
public sealed class HttpApiClient : IDisposable
|
|
{
|
|
#region variables
|
|
|
|
readonly static JsonSerializerOptions _serializerOptions;
|
|
|
|
readonly Uri _serverUrl;
|
|
string? _token;
|
|
|
|
readonly HttpClient _httpClient;
|
|
bool _loggedIn;
|
|
|
|
#endregion
|
|
|
|
#region constructor
|
|
|
|
static HttpApiClient()
|
|
{
|
|
_serializerOptions = new JsonSerializerOptions();
|
|
_serializerOptions.PropertyNameCaseInsensitive = true;
|
|
}
|
|
|
|
public HttpApiClient(string serverUrl, NetProxy? proxy = null, bool preferIPv6 = false, bool ignoreCertificateErrors = false, IDnsClient? dnsClient = null)
|
|
: this(new Uri(serverUrl), proxy, preferIPv6, ignoreCertificateErrors, dnsClient)
|
|
{ }
|
|
|
|
public HttpApiClient(Uri serverUrl, NetProxy? proxy = null, bool preferIPv6 = false, bool ignoreCertificateErrors = false, IDnsClient? dnsClient = null)
|
|
{
|
|
_serverUrl = serverUrl;
|
|
|
|
HttpClientNetworkHandler handler = new HttpClientNetworkHandler();
|
|
handler.Proxy = proxy;
|
|
handler.NetworkType = preferIPv6 ? HttpClientNetworkType.PreferIPv6 : HttpClientNetworkType.Default;
|
|
handler.DnsClient = dnsClient;
|
|
|
|
if (ignoreCertificateErrors)
|
|
{
|
|
handler.InnerHandler.SslOptions.RemoteCertificateValidationCallback = delegate (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
|
|
{
|
|
return true;
|
|
};
|
|
}
|
|
else
|
|
{
|
|
handler.EnableDANE = true;
|
|
}
|
|
|
|
_httpClient = new HttpClient(handler);
|
|
_httpClient.BaseAddress = _serverUrl;
|
|
_httpClient.DefaultRequestHeaders.Add("user-agent", "Technitium DNS Server HTTP API Client");
|
|
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
|
|
bool _disposed;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_httpClient?.Dispose();
|
|
|
|
_disposed = true;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region private
|
|
|
|
private static void CheckResponseStatus(JsonElement rootElement)
|
|
{
|
|
if (!rootElement.TryGetProperty("status", out JsonElement jsonStatus))
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
string? status = jsonStatus.GetString()?.ToLowerInvariant();
|
|
switch (status)
|
|
{
|
|
case "ok":
|
|
return;
|
|
|
|
case "error":
|
|
{
|
|
Exception? innerException = null;
|
|
|
|
if (rootElement.TryGetProperty("innerErrorMessage", out JsonElement jsonInnerErrorMessage))
|
|
innerException = new HttpApiClientException(jsonInnerErrorMessage.GetString()!);
|
|
|
|
if (rootElement.TryGetProperty("errorMessage", out JsonElement jsonErrorMessage))
|
|
{
|
|
if (innerException is null)
|
|
throw new HttpApiClientException(jsonErrorMessage.GetString()!);
|
|
|
|
throw new HttpApiClientException(jsonErrorMessage.GetString()!, innerException);
|
|
}
|
|
|
|
throw new HttpApiClientException();
|
|
}
|
|
|
|
case "invalid-token":
|
|
{
|
|
if (rootElement.TryGetProperty("errorMessage", out JsonElement jsonErrorMessage))
|
|
throw new InvalidTokenHttpApiClientException(jsonErrorMessage.GetString()!);
|
|
|
|
throw new InvalidTokenHttpApiClientException();
|
|
}
|
|
|
|
case "2fa-required":
|
|
{
|
|
if (rootElement.TryGetProperty("errorMessage", out JsonElement jsonErrorMessage))
|
|
throw new TwoFactorAuthRequiredHttpApiClientException(jsonErrorMessage.GetString()!);
|
|
|
|
throw new TwoFactorAuthRequiredHttpApiClientException();
|
|
}
|
|
|
|
default:
|
|
throw new HttpApiClientException("Unknown status value was received: " + status);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region public
|
|
|
|
public async Task<SessionInfo> LoginAsync(string username, string password, string? totp = null, bool includeInfo = false, CancellationToken cancellationToken = default)
|
|
{
|
|
if (_loggedIn)
|
|
throw new HttpApiClientException("Already logged in.");
|
|
|
|
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(_serverUrl, $"api/user/login"));
|
|
|
|
Dictionary<string, string> parameters = new Dictionary<string, string>
|
|
{
|
|
{ "user", username },
|
|
{ "pass", password },
|
|
{ "includeInfo", includeInfo.ToString() }
|
|
};
|
|
|
|
if (totp is not null)
|
|
parameters.Add("totp", totp);
|
|
|
|
httpRequest.Content = new FormUrlEncodedContent(parameters);
|
|
|
|
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
SessionInfo? sessionInfo = rootElement.Deserialize<SessionInfo>(_serializerOptions);
|
|
if (sessionInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
_token = sessionInfo.Token;
|
|
_loggedIn = true;
|
|
|
|
return sessionInfo;
|
|
}
|
|
|
|
public async Task LogoutAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exist to logout.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/user/logout?token={_token}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
_token = null;
|
|
_loggedIn = false;
|
|
}
|
|
|
|
public void UseApiToken(string token)
|
|
{
|
|
if (_loggedIn)
|
|
throw new HttpApiClientException("Already logged in. Please logout before using a different API token.");
|
|
|
|
_token = token;
|
|
_loggedIn = true;
|
|
}
|
|
|
|
public async Task<DashboardStats> GetDashboardStatsAsync(string actingUsername, DashboardStatsType type = DashboardStatsType.LastHour, bool utcFormat = false, string acceptLanguage = "en-US,en;q=0.5", bool dontTrimQueryTypeData = false, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
string path = $"api/dashboard/stats/get?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&type={type}&utc={utcFormat}&dontTrimQueryTypeData={dontTrimQueryTypeData}";
|
|
|
|
if (type == DashboardStatsType.Custom)
|
|
path += $"&start={startDate:O}&end={endDate:O}";
|
|
|
|
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(_serverUrl, path));
|
|
httpRequest.Headers.Add("Accept-Language", acceptLanguage);
|
|
|
|
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
DashboardStats? stats = rootElement.GetProperty("response").Deserialize<DashboardStats>(_serializerOptions);
|
|
if (stats is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return stats;
|
|
}
|
|
|
|
public async Task<DashboardStats> GetDashboardTopStatsAsync(string actingUsername, DashboardTopStatsType statsType, int limit = 1000, DashboardStatsType type = DashboardStatsType.LastHour, DateTime startDate = default, DateTime endDate = default, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
string path = $"api/dashboard/stats/getTop?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&type={type}&statsType={statsType}&limit={limit}";
|
|
|
|
if (type == DashboardStatsType.Custom)
|
|
path += $"&start={startDate:O}&end={endDate:O}";
|
|
|
|
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(_serverUrl, path));
|
|
|
|
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
DashboardStats? stats = rootElement.GetProperty("response").Deserialize<DashboardStats>(_serializerOptions);
|
|
if (stats is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return stats;
|
|
}
|
|
|
|
public async Task SetClusterSettingsAsync(string actingUsername, IReadOnlyDictionary<string, string> clusterParameters, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
if (clusterParameters.Count == 0)
|
|
throw new ArgumentException("At least one parameter must be provided.", nameof(clusterParameters));
|
|
|
|
foreach (KeyValuePair<string, string> parameter in clusterParameters)
|
|
{
|
|
switch (parameter.Key)
|
|
{
|
|
case "token":
|
|
case "node":
|
|
throw new ArgumentException($"The '{parameter.Key}' is an invalid Settings parameter.", nameof(clusterParameters));
|
|
}
|
|
}
|
|
|
|
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, new Uri(_serverUrl, $"api/settings/set?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}"));
|
|
|
|
httpRequest.Content = new FormUrlEncodedContent(clusterParameters);
|
|
|
|
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(httpResponse.Content.ReadAsStream(cancellationToken), cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
}
|
|
|
|
public async Task ForceUpdateBlockListsAsync(string actingUsername, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/settings/forceUpdateBlockLists?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
}
|
|
|
|
public async Task TemporaryDisableBlockingAsync(string actingUsername, int minutes, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/settings/temporaryDisableBlocking?token={_token}&actingUser={HttpUtility.UrlEncode(actingUsername)}&minutes={minutes}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
}
|
|
|
|
public async Task<ClusterInfo> GetClusterStateAsync(bool includeServerIpAddresses = false, bool includeNodeCertificates = false, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/state?token={_token}&includeServerIpAddresses={includeServerIpAddresses}&includeNodeCertificates={includeNodeCertificates}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize<ClusterInfo>(_serializerOptions);
|
|
if (clusterInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return clusterInfo;
|
|
}
|
|
|
|
public async Task<ClusterInfo> DeleteClusterAsync(bool forceDelete = false, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/delete?token={_token}&forceDelete={forceDelete}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize<ClusterInfo>(_serializerOptions);
|
|
if (clusterInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return clusterInfo;
|
|
}
|
|
|
|
public async Task<ClusterInfo> JoinClusterAsync(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyCollection<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/join?token={_token}&secondaryNodeId={secondaryNodeId}&secondaryNodeUrl={HttpUtility.UrlEncode(secondaryNodeUrl.OriginalString)}&secondaryNodeIpAddresses={HttpUtility.UrlEncode(secondaryNodeIpAddresses.Join())}&secondaryNodeCertificate={Base64Url.EncodeToString(secondaryNodeCertificate.Export(X509ContentType.Cert))}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize<ClusterInfo>(_serializerOptions);
|
|
if (clusterInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return clusterInfo;
|
|
}
|
|
|
|
public async Task<ClusterInfo> DeleteSecondaryNodeAsync(int secondaryNodeId, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/deleteSecondary?token={_token}&secondaryNodeId={secondaryNodeId}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize<ClusterInfo>(_serializerOptions);
|
|
if (clusterInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return clusterInfo;
|
|
}
|
|
|
|
public async Task<ClusterInfo> UpdateSecondaryNodeAsync(int secondaryNodeId, Uri secondaryNodeUrl, IReadOnlyCollection<IPAddress> secondaryNodeIpAddresses, X509Certificate2 secondaryNodeCertificate, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/primary/updateSecondary?token={_token}&secondaryNodeId={secondaryNodeId}&secondaryNodeUrl={HttpUtility.UrlEncode(secondaryNodeUrl.OriginalString)}&secondaryNodeIpAddresses={HttpUtility.UrlEncode(secondaryNodeIpAddresses.Join())}&secondaryNodeCertificate={Base64Url.EncodeToString(secondaryNodeCertificate.Export(X509ContentType.Cert))}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize<ClusterInfo>(_serializerOptions);
|
|
if (clusterInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return clusterInfo;
|
|
}
|
|
|
|
public async Task<(Stream, DateTime)> TransferConfigFromPrimaryNodeAsync(DateTime ifModifiedSince = default, IReadOnlyCollection<string>? includeZones = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, $"api/admin/cluster/primary/transferConfig?token={_token}&includeZones={(includeZones is null ? "" : includeZones.Join(','))}");
|
|
httpRequest.Headers.IfModifiedSince = ifModifiedSince;
|
|
|
|
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
|
|
|
return (httpResponse.Content.ReadAsStream(cancellationToken), httpResponse.Content.Headers.LastModified?.UtcDateTime ?? DateTime.UtcNow);
|
|
}
|
|
|
|
public async Task<ClusterInfo> LeaveClusterAsync(bool forceLeave = false, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/secondary/leave?token={_token}&forceLeave={forceLeave}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
|
|
ClusterInfo? clusterInfo = rootElement.GetProperty("response").Deserialize<ClusterInfo>(_serializerOptions);
|
|
if (clusterInfo is null)
|
|
throw new HttpApiClientException("Invalid JSON response was received.");
|
|
|
|
return clusterInfo;
|
|
}
|
|
|
|
public async Task NotifySecondaryNodeAsync(int primaryNodeId, Uri primaryNodeUrl, IReadOnlyCollection<IPAddress> primaryNodeIpAddresses, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
Stream stream = await _httpClient.GetStreamAsync($"api/admin/cluster/secondary/notify?token={_token}&primaryNodeId={primaryNodeId}&primaryNodeUrl={HttpUtility.UrlEncode(primaryNodeUrl.OriginalString)}&primaryNodeIpAddresses={HttpUtility.UrlEncode(primaryNodeIpAddresses.Join())}", cancellationToken);
|
|
|
|
using JsonDocument jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
JsonElement rootElement = jsonDoc.RootElement;
|
|
|
|
CheckResponseStatus(rootElement);
|
|
}
|
|
|
|
public async Task ProxyRequest(HttpContext context, string actingUsername, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_loggedIn)
|
|
throw new HttpApiClientException("No active session exists. Please login and try again.");
|
|
|
|
//read input http request and send http response to node
|
|
HttpRequest inHttpRequest = context.Request;
|
|
|
|
StringBuilder queryString = new StringBuilder();
|
|
|
|
queryString.Append("?actingUser=").Append(HttpUtility.UrlEncode(actingUsername));
|
|
|
|
foreach (KeyValuePair<string, StringValues> query in inHttpRequest.Query)
|
|
{
|
|
string key = query.Key;
|
|
string value = query.Value.ToString();
|
|
|
|
switch (key)
|
|
{
|
|
case "token":
|
|
//use http client token
|
|
value = _token!;
|
|
break;
|
|
|
|
case "node":
|
|
//skip node name
|
|
continue;
|
|
}
|
|
|
|
queryString.Append('&').Append(key).Append('=').Append(HttpUtility.UrlEncode(value));
|
|
}
|
|
|
|
HttpRequestMessage httpRequest = new HttpRequestMessage(new HttpMethod(inHttpRequest.Method), new Uri(_serverUrl, inHttpRequest.Path + queryString.ToString()));
|
|
|
|
if (inHttpRequest.HasFormContentType)
|
|
{
|
|
if (inHttpRequest.Form.Keys.Count > 0)
|
|
{
|
|
Dictionary<string, string> formParams = new Dictionary<string, string>(inHttpRequest.Form.Count);
|
|
|
|
foreach (KeyValuePair<string, StringValues> formParam in inHttpRequest.Form)
|
|
{
|
|
string key = formParam.Key;
|
|
string value = formParam.Value.ToString();
|
|
|
|
switch (key)
|
|
{
|
|
case "token":
|
|
//use http client token
|
|
value = _token!;
|
|
break;
|
|
|
|
case "node":
|
|
//skip node name
|
|
continue;
|
|
}
|
|
|
|
formParams[key] = value;
|
|
}
|
|
|
|
httpRequest.Content = new FormUrlEncodedContent(formParams);
|
|
}
|
|
else if (inHttpRequest.Form.Files.Count > 0)
|
|
{
|
|
MultipartFormDataContent formData = new MultipartFormDataContent();
|
|
|
|
foreach (IFormFile file in inHttpRequest.Form.Files)
|
|
formData.Add(new StreamContent(file.OpenReadStream()), file.Name, file.FileName);
|
|
|
|
httpRequest.Content = formData;
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
httpRequest.Content = new StreamContent(inHttpRequest.Body);
|
|
}
|
|
|
|
foreach (KeyValuePair<string, StringValues> inHeader in inHttpRequest.Headers)
|
|
{
|
|
if (!httpRequest.Headers.TryAddWithoutValidation(inHeader.Key, inHeader.Value.ToString()))
|
|
{
|
|
if (!inHttpRequest.HasFormContentType)
|
|
{
|
|
//add content headers only when there is no form data
|
|
if (!httpRequest.Content.Headers.TryAddWithoutValidation(inHeader.Key, inHeader.Value.ToString()))
|
|
throw new InvalidOperationException();
|
|
}
|
|
}
|
|
}
|
|
|
|
HttpResponseMessage httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
|
|
|
//receive http response and write to output http response
|
|
HttpResponse outHttpResponse = context.Response;
|
|
|
|
foreach (KeyValuePair<string, IEnumerable<string>> header in httpResponse.Headers)
|
|
{
|
|
if (header.Key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase) && (httpResponse.Headers.TransferEncodingChunked == true))
|
|
continue; //skip chunked header to allow kestrel to do the chunking
|
|
|
|
if (!outHttpResponse.Headers.TryAdd(header.Key, header.Value.Join()))
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
foreach (KeyValuePair<string, IEnumerable<string>> header in httpResponse.Content.Headers)
|
|
{
|
|
if (header.Key.Equals("content-length", StringComparison.OrdinalIgnoreCase) && (httpResponse.Headers.TransferEncodingChunked == true))
|
|
continue; //skip content length when data is chunked
|
|
|
|
if (!outHttpResponse.Headers.TryAdd(header.Key, header.Value.Join()))
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
await httpResponse.Content.CopyToAsync(outHttpResponse.Body, cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region properties
|
|
|
|
public Uri ServerUrl
|
|
{ get { return _serverUrl; } }
|
|
|
|
#endregion
|
|
}
|
|
}
|