- Updated .gitlab-ci.yml with complete build, test, and deploy stages
- Added authentication redirect fix in Program.cs (302 redirect for admin routes)
- Fixed Cookie vs Bearer authentication conflict for admin panel
- Configure pipeline to build from .NET 9.0 source
- Deploy to Hostinger VPS with proper environment variables
- Include rollback capability for production deployments
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
255 lines
8.0 KiB
C#
255 lines
8.0 KiB
C#
using System;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
using TeleBot.Http;
|
|
using Xunit;
|
|
|
|
namespace TeleBot.Tests.Security
|
|
{
|
|
/// <summary>
|
|
/// Integration tests that verify actual TOR connectivity.
|
|
/// These tests require a running TOR service on localhost:9050
|
|
/// Skip these tests if TOR is not available (CI/CD environments).
|
|
/// </summary>
|
|
public class TorConnectivityTests : IDisposable
|
|
{
|
|
private readonly Mock<ILogger> _mockLogger;
|
|
private bool _torAvailable;
|
|
|
|
public TorConnectivityTests()
|
|
{
|
|
_mockLogger = new Mock<ILogger>();
|
|
_torAvailable = CheckTorAvailability();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if TOR is available on localhost:9050
|
|
/// </summary>
|
|
private bool CheckTorAvailability()
|
|
{
|
|
try
|
|
{
|
|
using var client = new TcpClient();
|
|
var result = client.BeginConnect("127.0.0.1", 9050, null, null);
|
|
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(1));
|
|
if (success)
|
|
{
|
|
client.EndConnect(result);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TorConnection_WhenAvailable_CanConnect()
|
|
{
|
|
// Skip if TOR not available
|
|
if (!_torAvailable)
|
|
{
|
|
// Log skip reason
|
|
return;
|
|
}
|
|
|
|
// Arrange
|
|
var config = CreateTorConfiguration();
|
|
var handler = Socks5HttpHandler.Create(config, _mockLogger.Object);
|
|
using var client = new HttpClient(handler);
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
|
|
// Act & Assert
|
|
try
|
|
{
|
|
// Try to connect to TOR check service
|
|
var response = await client.GetAsync("https://check.torproject.org/api/ip");
|
|
|
|
Assert.True(response.IsSuccessStatusCode,
|
|
"Should successfully connect through TOR proxy");
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
|
|
// The TOR check API returns JSON with "IsTor" field
|
|
Assert.Contains("IsTor", content,
|
|
"Response should indicate TOR connection status");
|
|
}
|
|
catch (HttpRequestException ex) when (ex.InnerException is SocketException)
|
|
{
|
|
// TOR might not be running - skip test
|
|
return;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TorConnection_ChecksRealIP_IsDifferent()
|
|
{
|
|
// Skip if TOR not available
|
|
if (!_torAvailable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Arrange
|
|
var config = CreateTorConfiguration();
|
|
var torHandler = Socks5HttpHandler.Create(config, _mockLogger.Object);
|
|
var directHandler = Socks5HttpHandler.CreateDirect(_mockLogger.Object);
|
|
|
|
string? torIp = null;
|
|
string? directIp = null;
|
|
|
|
try
|
|
{
|
|
// Get IP through TOR
|
|
using (var torClient = new HttpClient(torHandler))
|
|
{
|
|
torClient.Timeout = TimeSpan.FromSeconds(30);
|
|
var response = await torClient.GetAsync("https://api.ipify.org");
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
torIp = await response.Content.ReadAsStringAsync();
|
|
}
|
|
}
|
|
|
|
// Get IP directly
|
|
using (var directClient = new HttpClient(directHandler))
|
|
{
|
|
directClient.Timeout = TimeSpan.FromSeconds(10);
|
|
var response = await directClient.GetAsync("https://api.ipify.org");
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
directIp = await response.Content.ReadAsStringAsync();
|
|
}
|
|
}
|
|
|
|
// Assert - IPs should be different (TOR exit node vs real IP)
|
|
if (!string.IsNullOrEmpty(torIp) && !string.IsNullOrEmpty(directIp))
|
|
{
|
|
Assert.NotEqual(torIp, directIp,
|
|
$"TOR IP ({torIp}) should be different from direct IP ({directIp})");
|
|
}
|
|
}
|
|
catch (HttpRequestException)
|
|
{
|
|
// Network issue - skip test
|
|
return;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TorConnection_Timeout_IsReasonable()
|
|
{
|
|
// Skip if TOR not available
|
|
if (!_torAvailable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Arrange
|
|
var config = CreateTorConfiguration();
|
|
var handler = Socks5HttpHandler.Create(config, _mockLogger.Object);
|
|
using var client = new HttpClient(handler);
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
|
|
// Act
|
|
var startTime = DateTime.UtcNow;
|
|
try
|
|
{
|
|
var response = await client.GetAsync("https://check.torproject.org");
|
|
var elapsed = DateTime.UtcNow - startTime;
|
|
|
|
// Assert - TOR adds latency but should still be reasonable
|
|
Assert.True(elapsed < TimeSpan.FromSeconds(30),
|
|
$"TOR connection took {elapsed.TotalSeconds}s - should be under 30s");
|
|
}
|
|
catch (HttpRequestException)
|
|
{
|
|
// Connection failed - could be TOR issue, skip
|
|
return;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TorProxy_Address_IsLocalhost()
|
|
{
|
|
// Arrange
|
|
var config = CreateTorConfiguration();
|
|
|
|
// Act
|
|
var handler = Socks5HttpHandler.Create(config, _mockLogger.Object);
|
|
|
|
// Assert - Security check
|
|
var proxy = handler.Proxy as WebProxy;
|
|
Assert.NotNull(proxy);
|
|
Assert.Contains("127.0.0.1", proxy.Address?.ToString() ?? "");
|
|
Assert.DoesNotContain("0.0.0.0", proxy.Address?.ToString() ?? "");
|
|
}
|
|
|
|
[Fact]
|
|
public void TorProxy_Protocol_IsSocks5()
|
|
{
|
|
// Arrange
|
|
var config = CreateTorConfiguration();
|
|
|
|
// Act
|
|
var handler = Socks5HttpHandler.Create(config, _mockLogger.Object);
|
|
|
|
// Assert - Verify SOCKS5 protocol
|
|
var proxy = handler.Proxy as WebProxy;
|
|
Assert.NotNull(proxy);
|
|
Assert.Contains("socks5://", proxy.Address?.ToString() ?? "");
|
|
}
|
|
|
|
// Helper to create TOR configuration
|
|
private IConfiguration CreateTorConfiguration()
|
|
{
|
|
var configData = new Dictionary<string, string>
|
|
{
|
|
["Privacy:EnableTor"] = "true",
|
|
["Privacy:TorSocksPort"] = "9050"
|
|
};
|
|
|
|
return new ConfigurationBuilder()
|
|
.AddInMemoryCollection(configData!)
|
|
.Build();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Cleanup if needed
|
|
}
|
|
}
|
|
|
|
// Helper class for TCP client check
|
|
class TcpClient : IDisposable
|
|
{
|
|
private readonly System.Net.Sockets.TcpClient _client;
|
|
|
|
public TcpClient()
|
|
{
|
|
_client = new System.Net.Sockets.TcpClient();
|
|
}
|
|
|
|
public IAsyncResult BeginConnect(string host, int port, AsyncCallback? callback, object? state)
|
|
{
|
|
return _client.BeginConnect(host, port, callback, state);
|
|
}
|
|
|
|
public void EndConnect(IAsyncResult asyncResult)
|
|
{
|
|
_client.EndConnect(asyncResult);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_client?.Dispose();
|
|
}
|
|
}
|
|
}
|