:3
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Mocha2023.Classes
|
||||
{
|
||||
public static class ClientNetwork
|
||||
{
|
||||
private static readonly Lazy<string[]> TrustedProxyNetworks = new(
|
||||
LoadTrustedProxyNetworks);
|
||||
|
||||
public static IPAddress? GetClientIp(HttpRequest request)
|
||||
{
|
||||
IPAddress? remoteAddress = request.HttpContext.Connection.RemoteIpAddress;
|
||||
|
||||
if (remoteAddress != null && IsTrustedForwardingProxy(remoteAddress))
|
||||
{
|
||||
if (TryGetHeaderAddress(request, "CF-Connecting-IP", out IPAddress? cloudflareAddress))
|
||||
return Normalize(cloudflareAddress);
|
||||
|
||||
if (request.Headers.TryGetValue("X-Forwarded-For", out var forwardedFor))
|
||||
{
|
||||
string? first = forwardedFor.FirstOrDefault()?.Split(',')[0].Trim();
|
||||
if (IPAddress.TryParse(first, out IPAddress? forwardedAddress))
|
||||
return Normalize(forwardedAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return Normalize(remoteAddress);
|
||||
}
|
||||
|
||||
private static bool IsTrustedForwardingProxy(IPAddress address)
|
||||
{
|
||||
IPAddress normalized = Normalize(address)!;
|
||||
if (IPAddress.IsLoopback(normalized))
|
||||
return true;
|
||||
|
||||
return TrustedProxyNetworks.Value.Any(network =>
|
||||
IpNetwork.Contains(network, normalized));
|
||||
}
|
||||
|
||||
private static string[] LoadTrustedProxyNetworks()
|
||||
{
|
||||
string configured =
|
||||
Program.LoadLocalSetting("TRUSTED_PROXY_NETWORKS") ??
|
||||
string.Empty;
|
||||
|
||||
var networks = new List<string>();
|
||||
foreach (string candidate in configured.Split(
|
||||
new[] { ',', ';', ' ', '\t', '\r', '\n' },
|
||||
StringSplitOptions.RemoveEmptyEntries |
|
||||
StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (IpNetwork.TryNormalize(candidate, out string normalized))
|
||||
{
|
||||
networks.Add(normalized);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[NETWORK SECURITY] Ignoring invalid trusted proxy network: {candidate}");
|
||||
}
|
||||
}
|
||||
|
||||
return networks.Distinct(StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
public static bool IsPrivateOrLoopback(IPAddress? address)
|
||||
{
|
||||
if (address == null)
|
||||
return false;
|
||||
if (IPAddress.IsLoopback(address))
|
||||
return true;
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
return IsPrivateOrLoopback(address.MapToIPv4());
|
||||
|
||||
if (address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
byte[] bytes = address.GetAddressBytes();
|
||||
return bytes[0] == 10 ||
|
||||
(bytes[0] == 172 && bytes[1] is >= 16 and <= 31) ||
|
||||
(bytes[0] == 192 && bytes[1] == 168) ||
|
||||
(bytes[0] == 169 && bytes[1] == 254);
|
||||
}
|
||||
|
||||
byte[] ipv6 = address.GetAddressBytes();
|
||||
return address.IsIPv6LinkLocal ||
|
||||
(ipv6.Length == 16 && (ipv6[0] & 0xFE) == 0xFC);
|
||||
}
|
||||
|
||||
private static IPAddress? Normalize(IPAddress? address) =>
|
||||
address?.IsIPv4MappedToIPv6 == true ? address.MapToIPv4() : address;
|
||||
|
||||
private static bool TryGetHeaderAddress(
|
||||
HttpRequest request,
|
||||
string header,
|
||||
out IPAddress? address)
|
||||
{
|
||||
address = null;
|
||||
string? value = request.Headers[header].FirstOrDefault()?.Trim();
|
||||
return !string.IsNullOrWhiteSpace(value) && IPAddress.TryParse(value, out address);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IpNetwork
|
||||
{
|
||||
public static bool TryNormalize(string? value, out string normalized)
|
||||
{
|
||||
normalized = string.Empty;
|
||||
string input = (value ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return false;
|
||||
|
||||
string[] parts = input.Split('/', 2, StringSplitOptions.TrimEntries);
|
||||
if (!IPAddress.TryParse(parts[0], out IPAddress? address))
|
||||
return false;
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
address = address.MapToIPv4();
|
||||
|
||||
int maxBits = address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128;
|
||||
int prefix = maxBits;
|
||||
if (parts.Length == 2 &&
|
||||
(!int.TryParse(parts[1], out prefix) || prefix < 0 || prefix > maxBits))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] network = ApplyMask(address.GetAddressBytes(), prefix);
|
||||
var canonical = new IPAddress(network);
|
||||
normalized = prefix == maxBits
|
||||
? canonical.ToString()
|
||||
: $"{canonical}/{prefix}";
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Contains(string networkText, IPAddress address)
|
||||
{
|
||||
if (!TryNormalize(networkText, out string normalized))
|
||||
return false;
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
address = address.MapToIPv4();
|
||||
|
||||
string[] parts = normalized.Split('/', 2);
|
||||
if (!IPAddress.TryParse(parts[0], out IPAddress? networkAddress))
|
||||
return false;
|
||||
if (networkAddress.AddressFamily != address.AddressFamily)
|
||||
return false;
|
||||
|
||||
int maxBits = address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128;
|
||||
int prefix = parts.Length == 2 && int.TryParse(parts[1], out int parsed)
|
||||
? parsed
|
||||
: maxBits;
|
||||
|
||||
byte[] expected = ApplyMask(networkAddress.GetAddressBytes(), prefix);
|
||||
byte[] actual = ApplyMask(address.GetAddressBytes(), prefix);
|
||||
return expected.SequenceEqual(actual);
|
||||
}
|
||||
|
||||
private static byte[] ApplyMask(byte[] bytes, int prefixLength)
|
||||
{
|
||||
byte[] output = bytes.ToArray();
|
||||
int fullBytes = prefixLength / 8;
|
||||
int remainingBits = prefixLength % 8;
|
||||
|
||||
if (remainingBits != 0 && fullBytes < output.Length)
|
||||
{
|
||||
int mask = 0xFF << (8 - remainingBits);
|
||||
output[fullBytes] = (byte)(output[fullBytes] & mask);
|
||||
fullBytes++;
|
||||
}
|
||||
|
||||
for (int index = fullBytes; index < output.Length; index++)
|
||||
output[index] = 0;
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
public static class VpnDetectionService
|
||||
{
|
||||
private sealed record CacheEntry(bool IsAnonymous, DateTime ExpiresAtUtc);
|
||||
|
||||
private static readonly ConcurrentDictionary<string, CacheEntry> Cache =
|
||||
new(StringComparer.Ordinal);
|
||||
private static readonly HttpClient Http = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(4)
|
||||
};
|
||||
|
||||
public static async Task<bool> IsVpnOrProxyAsync(
|
||||
IPAddress address,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (ClientNetwork.IsPrivateOrLoopback(address))
|
||||
return false;
|
||||
|
||||
string ip = address.ToString();
|
||||
DateTime now = DateTime.UtcNow;
|
||||
if (Cache.TryGetValue(ip, out CacheEntry? cached) && cached.ExpiresAtUtc > now)
|
||||
return cached.IsAnonymous;
|
||||
|
||||
try
|
||||
{
|
||||
string apiKey = Program.LoadLocalSetting("PROXYCHECK_API_KEY")?.Trim() ?? string.Empty;
|
||||
string keyQuery = string.IsNullOrWhiteSpace(apiKey)
|
||||
? string.Empty
|
||||
: "&key=" + Uri.EscapeDataString(apiKey);
|
||||
string url = "https://proxycheck.io/v3/" + Uri.EscapeDataString(ip) +
|
||||
"?tag=0&p=0" + keyQuery;
|
||||
|
||||
using HttpResponseMessage response = await Http.GetAsync(url, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Cache[ip] = new CacheEntry(false, now.AddMinutes(10));
|
||||
return false;
|
||||
}
|
||||
|
||||
await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using JsonDocument document = await JsonDocument.ParseAsync(
|
||||
stream,
|
||||
cancellationToken: cancellationToken);
|
||||
JsonElement root = document.RootElement;
|
||||
|
||||
bool detected = false;
|
||||
if (root.TryGetProperty(ip, out JsonElement details))
|
||||
detected = ReadDetection(details);
|
||||
else
|
||||
detected = ReadDetection(root);
|
||||
|
||||
Cache[ip] = new CacheEntry(
|
||||
detected,
|
||||
now.AddHours(detected ? 24 : 8));
|
||||
return detected;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is HttpRequestException or TaskCanceledException or JsonException)
|
||||
{
|
||||
Cache[ip] = new CacheEntry(false, now.AddMinutes(10));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ReadDetection(JsonElement details)
|
||||
{
|
||||
if (details.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
if (details.TryGetProperty("detections", out JsonElement detections) &&
|
||||
detections.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (ReadBool(detections, "anonymous") ||
|
||||
ReadBool(detections, "vpn") ||
|
||||
ReadBool(detections, "proxy") ||
|
||||
ReadBool(detections, "tor") ||
|
||||
ReadBool(detections, "hosting"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return ReadBool(details, "anonymous") ||
|
||||
ReadBool(details, "vpn") ||
|
||||
ReadBool(details, "proxy") ||
|
||||
ReadBool(details, "hosting") ||
|
||||
string.Equals(ReadString(details, "proxy"), "yes", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ReadString(details, "type"), "vpn", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ReadString(details, "type"), "tor", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(ReadNetworkType(details), "hosting", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool ReadBool(JsonElement element, string propertyName)
|
||||
{
|
||||
if (!element.TryGetProperty(propertyName, out JsonElement value))
|
||||
return false;
|
||||
return value.ValueKind == JsonValueKind.True ||
|
||||
(value.ValueKind == JsonValueKind.String &&
|
||||
bool.TryParse(value.GetString(), out bool parsed) && parsed);
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement element, string propertyName) =>
|
||||
element.TryGetProperty(propertyName, out JsonElement value) &&
|
||||
value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
|
||||
private static string? ReadNetworkType(JsonElement element)
|
||||
{
|
||||
if (!element.TryGetProperty("network", out JsonElement network) ||
|
||||
network.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return ReadString(network, "type");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user