168 lines
5.2 KiB
C#
168 lines
5.2 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Web;
|
|
using System.Collections.Specialized;
|
|
using Uwaa.HTTP.Websockets;
|
|
|
|
namespace Uwaa.HTTP;
|
|
|
|
/// <summary>
|
|
/// Contains the method, path, query, headers, and content of a HTTP request.
|
|
/// </summary>
|
|
public record HttpRequest
|
|
{
|
|
/// <summary>
|
|
/// The HTTP method of the request.
|
|
/// </summary>
|
|
public HttpMethod Method { get; set; } = HttpMethod.Any;
|
|
|
|
/// <summary>
|
|
/// The HTTP path being requested.
|
|
/// </summary>
|
|
public string Path { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Additional paramters in the path of the request.
|
|
/// </summary>
|
|
public NameValueCollection Query { get; set; }
|
|
|
|
/// <summary>
|
|
/// HTTP header fields included in the request.
|
|
/// </summary>
|
|
public HttpFields Fields { get; set; }
|
|
|
|
/// <summary>
|
|
/// The body of the HTTP request, if any.
|
|
/// </summary>
|
|
public HttpContent? Content { get; set; }
|
|
|
|
/// <summary>
|
|
/// If true, the connection is requesting to be upgrading to a websocket.
|
|
/// </summary>
|
|
public bool IsWebsocket => Fields.Upgrade == "websocket";
|
|
|
|
/// <summary>
|
|
/// Whether or not the request is valid.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the client is accepting the provided type, otherwise false.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a response which upgrades the connection to a websocket.
|
|
/// </summary>
|
|
/// <param name="callback">The websocket execution function to call.</param>
|
|
/// <param name="protocols">Subprotocols which can be accepted. If null or empty, any protocol will be accepted.</param>
|
|
/// <returns>Returns a <seealso cref="SwitchingProtocols"/> response.</returns>
|
|
/// <remarks>
|
|
/// If an upgrade has not been requested or no subprotocol can be negotiated, this will return null.
|
|
/// </remarks>
|
|
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);
|
|
}
|
|
}
|