using System.Globalization;
using System.Net;
using System.Net.Sockets;
using Sunfish.Http;
namespace Sunfish.Ftp;
/// <summary>
/// Conexion de datos de una sesion FTP, en modo pasivo (el cliente se conecta a un puerto que
/// abre el servidor) o activo (el servidor se conecta al cliente). Por seguridad solo se aceptan
/// conexiones de, o hacia, la misma IP que abrio el control (evita el "FTP bounce" y el robo de
/// la conexion de datos), y en activo nunca a puertos por debajo de 1024.
/// </summary>
internal sealed class DataChannel : IDisposable
{
public static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(30);
private readonly IPAddress _client;
private Socket? _passive;
private IPEndPoint? _active;
public DataChannel(IPAddress client) => _client = Normalize(client);
public bool Ready => _passive is not null || _active is not null;
/// <summary>Abre un puerto pasivo en la direccion local indicada; devuelve el puerto.</summary>
public int OpenPassive(IPAddress local, string range)
{
Reset();
var (first, last) = ParseRange(range);
List<int> candidates = first == 0
? [0]
: Enumerable.Range(first, last - first + 1).OrderBy(_ => Random.Shared.Next()).ToList();
foreach (int port in candidates)
{
var socket = new Socket(local.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
socket.Bind(new IPEndPoint(local, port));
socket.Listen(1);
_passive = socket;
return ((IPEndPoint)socket.LocalEndPoint!).Port;
}
catch (SocketException)
{
socket.Dispose();
}
}
throw new SocketException((int)SocketError.AddressAlreadyInUse);
}
/// <summary>PORT/EPRT: guarda el destino si es la IP del cliente y un puerto no privilegiado.</summary>
public bool SetActive(IPEndPoint endpoint)
{
if (!Normalize(endpoint.Address).Equals(_client) || endpoint.Port < 1024)
return false;
Reset();
_active = endpoint;
return true;
}
/// <summary>Establece la conexion de datos preparada (y la consume).</summary>
public async Task<Stream> ConnectAsync(CancellationToken ct)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(ConnectTimeout);
try
{
if (_passive is { } listener)
{
while (true)
{
var socket = await listener.AcceptAsync(timeout.Token);
if (Normalize(((IPEndPoint)socket.RemoteEndPoint!).Address).Equals(_client))
return Wrap(socket, ct);
socket.Dispose(); // otra IP: se ignora y se sigue esperando al cliente
}
}
if (_active is { } target)
{
var socket = new Socket(target.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
try
{
await socket.ConnectAsync(target, timeout.Token);
return Wrap(socket, ct);
}
catch
{
socket.Dispose();
throw;
}
}
throw new InvalidOperationException("No data connection prepared");
}
finally
{
Reset();
}
}
private static Stream Wrap(Socket socket, CancellationToken ct)
{
socket.NoDelay = true;
return new TimeoutStream(new NetworkStream(socket, ownsSocket: true), ct)
{
ReadLimit = TimeSpan.FromSeconds(120),
WriteLimit = TimeSpan.FromSeconds(120),
};
}
public void Reset()
{
_passive?.Dispose();
_passive = null;
_active = null;
}
public void Dispose() => Reset();
internal static IPAddress Normalize(IPAddress address) =>
address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address;
/// <summary>"50000-50050" -> (50000, 50050); vacio o invalido -> (0, 0): cualquier puerto libre.</summary>
internal static (int First, int Last) ParseRange(string range)
{
var parts = range.Split('-', StringSplitOptions.TrimEntries);
if (parts.Length == 2 &&
int.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out int a) &&
int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out int b) &&
a is >= 1024 and <= 65535 && b >= a && b <= 65535)
return (a, b);
return (0, 0);
}
/// <summary>"h1,h2,h3,h4,p1,p2" de PORT.</summary>
internal static IPEndPoint? ParsePort(string argument)
{
var n = argument.Split(',', StringSplitOptions.TrimEntries);
if (n.Length != 6)
return null;
var bytes = new byte[6];
for (int i = 0; i < 6; i++)
if (!byte.TryParse(n[i], NumberStyles.None, CultureInfo.InvariantCulture, out bytes[i]))
return null;
return new IPEndPoint(new IPAddress(bytes[..4]), bytes[4] * 256 + bytes[5]);
}
/// <summary>"|1|132.235.1.2|6275|" o "|2|::1|6275|" de EPRT.</summary>
internal static IPEndPoint? ParseEprt(string argument)
{
if (argument.Length < 5)
return null;
char d = argument[0];
var parts = argument.Split(d);
if (parts.Length != 5 || !IPAddress.TryParse(parts[2], out var address) ||
!int.TryParse(parts[3], NumberStyles.None, CultureInfo.InvariantCulture, out int port) || port is < 1 or > 65535)
return null;
return new IPEndPoint(address, port);
}
}