CI/CD: Add GitLab CI/CD pipeline for Hostinger deployment
- 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>
This commit is contained in:
254
TeleBot/TeleBot.Tests/Security/TorConnectivityTests.cs
Normal file
254
TeleBot/TeleBot.Tests/Security/TorConnectivityTests.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user