Newer
Older
Sunfish / src / Sunfish.Core / Http / HttpRequest.cs
@Ivan Dominguez Ivan Dominguez 21 hours ago 4 KB 3.0
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;
}