using System.Security.Cryptography; using System.Text; using System.Web; using System.Collections.Specialized; using Uwaa.HTTP.Websockets; namespace Uwaa.HTTP; /// /// Contains the method, path, query, headers, and content of a HTTP request. /// public record HttpRequest { /// /// The HTTP method of the request. /// public HttpMethod Method { get; set; } = HttpMethod.Any; /// /// The HTTP path being requested. /// public string Path { get; set; } = string.Empty; /// /// Additional paramters in the path of the request. /// public NameValueCollection Query { get; set; } /// /// HTTP header fields included in the request. /// public HttpFields Fields { get; set; } /// /// The body of the HTTP request, if any. /// public HttpContent? Content { get; set; } /// /// If true, the connection is requesting to be upgrading to a websocket. /// public bool IsWebsocket => Fields.Upgrade == "websocket"; /// /// Whether or not the request is valid. /// public bool Valid => Method != HttpMethod.Any && Path.StartsWith('/'); public HttpRequest(HttpMethod method, string path, HttpContent? content = null) : this(method, path, new HttpFields(), content) { } public HttpRequest(HttpMethod method, string path, HttpFields fields, HttpContent? content = null) { Method = method; Fields = fields; Content = content; string[] pathParts = path.Split('?', 2, StringSplitOptions.RemoveEmptyEntries); Path = pathParts[0]; Query = HttpUtility.ParseQueryString(pathParts.Length > 1 ? pathParts[1] : string.Empty); } /// /// Returns true if the client is accepting the provided type, otherwise false. /// public bool CanAccept(MIMEType type) { if (Fields.Accept == null) return true; for (int i = 0; i < Fields.Accept.Length; i++) if (Fields.Accept[i].Match(type)) return true; return false; } internal async Task WriteTo(HttpStream stream) { StringBuilder sb = new StringBuilder(); sb.Append(Method.ToString()); sb.Append(' '); sb.Append(Path); if (Query != null && Query.Count > 0) { sb.Append('?'); for (int i = 0; i < Query.Count; i++) { if (i > 0) sb.Append('&'); sb.Append(HttpUtility.UrlEncode(Query.GetKey(i))); sb.Append('='); sb.Append(HttpUtility.UrlEncode(Query[i])); } } sb.Append(" HTTP/1.1\r\n"); void writeField(string key, string value) { sb.Append(key); sb.Append(": "); sb.Append(value); sb.Append("\r\n"); } if (Content.HasValue) { Fields.ContentLength = Content.Value.Content.Length; Fields.ContentType = Content.Value.Type.ToString(); } else { Fields.ContentLength = null; Fields.ContentType = null; } Fields.EmitAll(writeField); sb.Append("\r\n"); await stream.Write(Encoding.ASCII.GetBytes(sb.ToString())); if (Content.HasValue) await stream.Write(Content.Value.Content); await stream.Flush(); } /// /// Generates a response which upgrades the connection to a websocket. /// /// The websocket execution function to call. /// Subprotocols which can be accepted. If null or empty, any protocol will be accepted. /// Returns a response. /// /// If an upgrade has not been requested or no subprotocol can be negotiated, this will return null. /// public SwitchingProtocols? UpgradeToWebsocket(WebsocketHandler callback, params string[]? protocols) { if (Fields.WebSocketKey == null) return null; //Subprotocol negotiation string? chosenProtocol = null; string? requestedProtocols = Fields.WebSocketProtocol; if (requestedProtocols != null && protocols != null && protocols.Length > 0) { foreach (string requested in requestedProtocols.ToLower().Split(',', StringSplitOptions.TrimEntries)) { foreach (string supported in protocols) { if (requested.Equals(supported, StringComparison.InvariantCultureIgnoreCase)) { chosenProtocol = supported; goto a; } } } return null; } a: string acceptKey = Convert.ToBase64String(SHA1.HashData(Encoding.ASCII.GetBytes(Fields.WebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))); return new SwitchingProtocols(acceptKey, chosenProtocol, callback); } }