using System.Net;
using Sunfish.Shares;
namespace Sunfish.Http;
/// <summary>Cabeceras HTTP: conservan el orden, nombres sin distinguir mayusculas.</summary>
public sealed class HttpHeaders
{
private readonly List<KeyValuePair<string, string>> _items = new();
public IEnumerable<KeyValuePair<string, string>> All => _items;
public string? this[string name]
{
get
{
foreach (var item in _items)
if (string.Equals(item.Key, name, StringComparison.OrdinalIgnoreCase))
return item.Value;
return null;
}
set
{
_items.RemoveAll(i => string.Equals(i.Key, name, StringComparison.OrdinalIgnoreCase));
if (value is not null)
_items.Add(new(name, value));
}
}
public void Add(string name, string value) => _items.Add(new(name, value));
/// <summary>Anade texto a la ultima cabecera (lineas de continuacion obsoletas).</summary>
internal bool AppendToLast(string text)
{
if (_items.Count == 0)
return false;
var last = _items[^1];
_items[^1] = new(last.Key, last.Value + " " + text);
return true;
}
/// <summary>Contiene el elemento (separado por comas), p. ej. Connection: keep-alive, Upgrade.</summary>
public bool HasToken(string name, string token)
{
string? value = this[name];
if (value is null)
return false;
foreach (var part in value.Split(','))
if (part.Trim().Equals(token, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
}
/// <summary>Peticion HTTP ya analizada.</summary>
public sealed class HttpRequest
{
private Dictionary<string, string>? _query;
public string Method { get; init; } = "GET";
/// <summary>Ruta tal como llega (codificada con %XX), sin query.</summary>
public string Path { get; init; } = "/";
/// <summary>Query sin el '?', codificada.</summary>
public string QueryString { get; init; } = "";
public int VersionMajor { get; init; } = 1;
public int VersionMinor { get; init; }
/// <summary>HTTP/0.9: solo "GET /ruta", la respuesta va sin linea de estado ni cabeceras.</summary>
public bool IsHttp09 => VersionMajor == 0;
public bool IsHttp11OrLater => VersionMajor > 1 || (VersionMajor == 1 && VersionMinor >= 1);
public HttpHeaders Headers { get; } = new();
public IPEndPoint? RemoteEndPoint { get; init; }
public IPEndPoint? LocalEndPoint { get; init; }
/// <summary>Cuerpo de la peticion (vacio si no lleva).</summary>
public Stream Body { get; internal set; } = Stream.Null;
public bool IsHead => Method == "HEAD";
/// <summary>El cliente quiere mantener la conexion (1.1 por defecto, 1.0 si lo pide).</summary>
public bool WantsKeepAlive =>
!IsHttp09 && (IsHttp11OrLater
? !Headers.HasToken("Connection", "close")
: Headers.HasToken("Connection", "keep-alive"));
/// <summary>Parametros de la query decodificados. Un parametro sin '=' vale "".</summary>
public IReadOnlyDictionary<string, string> Query => _query ??= ParseQuery(QueryString);
public string? GetQuery(string name) => Query.TryGetValue(name, out var value) ? value : null;
public bool HasQuery(string name) => Query.ContainsKey(name);
internal static Dictionary<string, string> ParseQuery(string query)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
int eq = pair.IndexOf('=');
string name = Decode(eq < 0 ? pair : pair[..eq]);
string value = eq < 0 ? "" : Decode(pair[(eq + 1)..]);
result.TryAdd(name, value);
}
return result;
}
private static string Decode(string text) => PathRules.PercentDecode(text.Replace('+', ' '));
}
/// <summary>Peticion mal formada: se responde con el estado indicado y se cierra.</summary>
public sealed class HttpParseException(int status, string message) : Exception(message)
{
public int Status { get; } = status;
}