using System;
using System.Collections;
using System.Collections.Generic;
using Best.HTTP.Cookies;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.HostSetting;
using Best.HTTP.Request.Authenticators;
using Best.HTTP.Request.Settings;
using Best.HTTP.Request.Timings;
using Best.HTTP.Request.Upload;
using Best.HTTP.Response.Decompression;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
namespace Best.HTTP
{
///
/// Delegate for a callback function that is called after the request is fully processed.
///
public delegate void OnRequestFinishedDelegate(HTTPRequest req, HTTPResponse resp);
///
/// Delegate for enumerating headers during request preparation.
///
/// The header name.
/// A list of header values.
public delegate void OnHeaderEnumerationDelegate(string header, List values);
///
/// Represents an HTTP request that allows you to send HTTP requests to remote servers and receive responses asynchronously.
///
///
///
/// - Asynchronous HTTP requestsUtilize a Task-based API for performing HTTP requests asynchronously.
/// - Unity coroutine supportSeamlessly integrate with Unity's coroutine system for coroutine-based request handling.
/// - HTTP method supportSupport for various HTTP methods including GET, POST, PUT, DELETE, and more.
/// - Compression and decompressionAutomatic request and response compression and decompression for efficient data transfer.
/// - Timing informationCollect detailed timing information about the request for performance analysis.
/// - Upload and download supportSupport for uploading and downloading files with progress tracking.
/// - CustomizableExtensive options for customizing request headers, handling cookies, and more.
/// - Redirection handlingAutomatic handling of request redirections for a seamless experience.
/// - Proxy server supportAbility to route requests through proxy servers for enhanced privacy and security.
/// - AuthenticationAutomatic authentication handling using authenticators for secure communication.
/// - Cancellation supportAbility to cancel requests to prevent further processing and release resources.
///
///
public sealed class HTTPRequest : IEnumerator
{
///
/// Creates an HTTP GET request with the specified URL.
///
/// The URL of the request.
/// An HTTPRequest instance for the GET request.
public static HTTPRequest CreateGet(string url) => new HTTPRequest(url);
///
/// Creates an HTTP GET request with the specified URI.
///
/// The URI of the request.
/// An HTTPRequest instance for the GET request.
public static HTTPRequest CreateGet(Uri uri) => new HTTPRequest(uri);
///
/// Creates an HTTP GET request with the specified URL and registers a callback function to be called
/// when the request is fully processed.
///
/// The URL of the request.
/// A callback function to be called when the request is finished.
/// An HTTPRequest instance for the GET request.
public static HTTPRequest CreateGet(string url, OnRequestFinishedDelegate callback) => new HTTPRequest(url, callback);
///
/// Creates an HTTP GET request with the specified URI and registers a callback function to be called
/// when the request is fully processed.
///
/// The URI of the request.
/// A callback function to be called when the request is finished.
/// An HTTPRequest instance for the GET request.
public static HTTPRequest CreateGet(Uri uri, OnRequestFinishedDelegate callback) => new HTTPRequest(uri, callback);
///
/// Creates an HTTP POST request with the specified URL.
///
/// The URL of the request.
/// An HTTPRequest instance for the POST request.
public static HTTPRequest CreatePost(string url) => new HTTPRequest(url, HTTPMethods.Post);
///
/// Creates an HTTP POST request with the specified URI.
///
/// The URI of the request.
/// An HTTPRequest instance for the POST request.
public static HTTPRequest CreatePost(Uri uri) => new HTTPRequest(uri, HTTPMethods.Post);
///
/// Creates an HTTP POST request with the specified URL and registers a callback function to be called
/// when the request is fully processed.
///
/// The URL of the request.
/// A callback function to be called when the request is finished.
/// An HTTPRequest instance for the POST request.
public static HTTPRequest CreatePost(string url, OnRequestFinishedDelegate callback) => new HTTPRequest(url, HTTPMethods.Post, callback);
///
/// Creates an HTTP POST request with the specified URI and registers a callback function to be called
/// when the request is fully processed.
///
/// The URI of the request.
/// A callback function to be called when the request is finished.
/// An HTTPRequest instance for the POST request.
public static HTTPRequest CreatePost(Uri uri, OnRequestFinishedDelegate callback) => new HTTPRequest(uri, HTTPMethods.Post, callback);
///
/// Creates an HTTP PUT request with the specified URL.
///
/// The URL of the request.
/// An HTTPRequest instance for the PUT request.
public static HTTPRequest CreatePut(string url) => new HTTPRequest(url, HTTPMethods.Put);
///
/// Creates an HTTP PUT request with the specified URI.
///
/// The URI of the request.
/// An HTTPRequest instance for the PUT request.
public static HTTPRequest CreatePut(Uri uri) => new HTTPRequest(uri, HTTPMethods.Put);
///
/// Creates an HTTP PUT request with the specified URL and registers a callback function to be called
/// when the request is fully processed.
///
/// The URL of the request.
/// A callback function to be called when the request is finished.
/// An HTTPRequest instance for the PUT request.
public static HTTPRequest CreatePut(string url, OnRequestFinishedDelegate callback) => new HTTPRequest(url, HTTPMethods.Put, callback);
///
/// Creates an HTTP PUT request with the specified URI and registers a callback function to be called
/// when the request is fully processed.
///
/// The URI of the request.
/// A callback function to be called when the request is finished.
/// An HTTPRequest instance for the PUT request.
public static HTTPRequest CreatePut(Uri uri, OnRequestFinishedDelegate callback) => new HTTPRequest(uri, HTTPMethods.Put, callback);
///
/// Cached uppercase values to save some cpu cycles and GC alloc per request.
///
public static readonly string[] MethodNames = {
HTTPMethods.Get.ToString().ToUpper(),
HTTPMethods.Head.ToString().ToUpper(),
HTTPMethods.Post.ToString().ToUpper(),
HTTPMethods.Put.ToString().ToUpper(),
HTTPMethods.Delete.ToString().ToUpper(),
HTTPMethods.Patch.ToString().ToUpper(),
HTTPMethods.Merge.ToString().ToUpper(),
HTTPMethods.Options.ToString().ToUpper(),
HTTPMethods.Connect.ToString().ToUpper(),
HTTPMethods.Query.ToString().ToUpper()
};
///
/// The method that how we want to process our request the server.
///
public HTTPMethods MethodType { get; set; }
///
/// The original request's Uri.
///
public Uri Uri { get; set; }
///
/// If redirected it contains the RedirectUri.
///
public Uri CurrentUri { get { return this.RedirectSettings.IsRedirected ? this.RedirectSettings.RedirectUri : Uri; } }
///
/// A host-key that can be used to find the right host-variant for the request.
///
public HostKey CurrentHostKey { get => HostKey.From(this); }
///
/// The response received from the server.
///
/// If an exception occurred during reading of the response stream or can't connect to the server, this will be null!
public HTTPResponse Response { get; set; }
///
/// Download related options and settings.
///
public DownloadSettings DownloadSettings = new DownloadSettings();
///
/// Upload related options and settings.
///
public UploadSettings UploadSettings = new UploadSettings();
///
/// Timeout settings for the request.
///
public TimeoutSettings TimeoutSettings;
///
/// Retry settings for the request.
///
public RetrySettings RetrySettings;
///
/// Proxy settings for the request.
///
public ProxySettings ProxySettings;
///
/// Redirect settings for the request.
///
public RedirectSettings RedirectSettings { get; private set; } = new RedirectSettings(10);
///
/// The callback function that will be called after the request is fully processed.
///
public OnRequestFinishedDelegate Callback { get; set; }
///
/// Indicates if is called on this request.
///
public bool IsCancellationRequested { get => this.CancellationTokenSource != null ? this.CancellationTokenSource.IsCancellationRequested : true; }
///
/// Gets the cancellation token source for this request.
///
internal System.Threading.CancellationTokenSource CancellationTokenSource { get; private set; }
///
/// Action called when function is invoked.
///
public Action OnCancellationRequested;
///
/// Stores any exception that occurs during processing of the request or response.
///
/// This property if for debugging purposes as seen here!
public Exception Exception { get; internal set; }
///
/// Any user-object that can be passed with the request.
///
public object Tag { get; set; }
///
/// Current state of this request.
///
public HTTPRequestStates State {
get { return this._state; }
internal set {
if (!HTTPUpdateDelegator.Instance.IsMainThread() && HTTPUpdateDelegator.Instance.CurrentThreadingMode == ThreadingMode.UnityUpdate)
HTTPManager.Logger.Error(nameof(HTTPRequest), $"State.Set({this._state} => {value}) isn't called on the main thread({HTTPUpdateDelegator.Instance.MainThreadId})!", this.Context);
// In a case where the request is aborted its state is set to a >= Finished state then,
// on another thread the reqest processing will fail too queuing up a >= Finished state again.
if (this._state >= HTTPRequestStates.Finished && value >= HTTPRequestStates.Finished)
{
HTTPManager.Logger.Warning(nameof(HTTPRequest), $"State.Set({this._state} => {value})", this.Context);
return;
}
if (HTTPManager.Logger.IsDiagnostic)
HTTPManager.Logger.Verbose(nameof(HTTPRequest), $"State.Set({this._state} => {value})", this.Context);
this._state = value;
}
}
private volatile HTTPRequestStates _state;
///
/// Timing information about the request.
///
public TimingCollector Timing { get; private set; }
///
/// An IAuthenticator implementation that can be used to authenticate the request.
///
/// Out-of-the-box included authenticators are and .
public IAuthenticator Authenticator;
#if UNITY_WEBGL
///
/// Its value will be set to the XmlHTTPRequest's withCredentials field, required to send 3rd party cookies with the request.
///
///
/// More details can be found here:
///
/// - Mozilla Developer Networks - XMLHttpRequest.withCredentials
///
///
public bool WithCredentials { get; set; }
#endif
///
/// Logging context of the request.
///
public LoggingContext Context { get; private set; }
private Dictionary> Headers { get; set; }
///
/// Creates an HTTP GET request with the specified URL.
///
/// The URL of the request.
public HTTPRequest(string url)
:this(new Uri(url)) {}
///
/// Creates an HTTP GET request with the specified URL and registers a callback function to be called
/// when the request is fully processed.
///
/// The URL of the request.
/// A callback function to be called when the request is finished.
public HTTPRequest(string url, OnRequestFinishedDelegate callback)
: this(new Uri(url), callback) { }
///
/// Creates an HTTP GET request with the specified URL and HTTP method type.
///
/// The URL of the request.
/// The HTTP method type for the request (e.g., GET, POST, PUT).
public HTTPRequest(string url, HTTPMethods methodType)
: this(new Uri(url), methodType) { }
///
/// Creates an HTTP request with the specified URL, HTTP method type, and registers a callback function to be called
/// when the request is fully processed.
///
/// The URL of the request.
/// The HTTP method type for the request (e.g., GET, POST, PUT).
/// A callback function to be called when the request is finished.
public HTTPRequest(string url, HTTPMethods methodType, OnRequestFinishedDelegate callback)
: this(new Uri(url), methodType, callback) { }
///
/// Creates an HTTP GET request with the specified URI.
///
/// The URI of the request.
public HTTPRequest(Uri uri)
: this(uri, HTTPMethods.Get, null)
{
}
///
/// Creates an HTTP GET request with the specified URI and registers a callback function to be called
/// when the request is fully processed.
///
/// The URI of the request.
/// A callback function to be called when the request is finished.
public HTTPRequest(Uri uri, OnRequestFinishedDelegate callback)
: this(uri, HTTPMethods.Get, callback)
{
}
///
/// Creates an HTTP request with the specified URI and HTTP method type.
///
/// The URI of the request.
/// The HTTP method type for the request (e.g., GET, POST, PUT).
public HTTPRequest(Uri uri, HTTPMethods methodType)
: this(uri, methodType, null)
{
}
///
/// Creates an HTTP request with the specified URI, HTTP method type, and registers a callback function
/// to be called when the request is fully processed.
///
/// The URI of the request.
/// The HTTP method type for the request (e.g., GET, POST, PUT).
/// A callback function to be called when the request is finished.
public HTTPRequest(Uri uri, HTTPMethods methodType, OnRequestFinishedDelegate callback)
{
this.Uri = uri;
this.MethodType = methodType;
this.TimeoutSettings = new TimeoutSettings(this);
this.ProxySettings = new ProxySettings() { Proxy = HTTPManager.Proxy };
this.RetrySettings = new RetrySettings(methodType == HTTPMethods.Get ? 1 : 0);
this.Callback = callback;
#if UNITY_WEBGL && !UNITY_EDITOR
// Just because cookies are enabled, it doesn't justify creating XHR with WithCredentials == 1.
//this.WithCredentials = this.CookieSettings.IsCookiesEnabled;
#endif
this.Context = new LoggingContext(this);
this.Timing = new TimingCollector(this);
this.CancellationTokenSource = new System.Threading.CancellationTokenSource();
}
///
/// Adds a header-value pair to the Headers. Use it to add custom headers to the request.
///
/// AddHeader("User-Agent', "FooBar 1.0")
public void AddHeader(string name, string value) => this.Headers = Headers.AddHeader(name, value);
///
/// For the given header name, removes any previously added values and sets the given one.
///
public void SetHeader(string name, string value) => this.Headers = this.Headers.SetHeader(name, value);
///
/// Removes the specified header and all of its associated values. Returns true, if the header found and succesfully removed.
///
public bool RemoveHeader(string name) => Headers.RemoveHeader(name);
///
/// Returns true if the given head name is already in the .
///
public bool HasHeader(string name) => Headers.HasHeader(name);
///
/// Returns the first header or null for the given header name.
///
public string GetFirstHeaderValue(string name) => Headers.GetFirstHeaderValue(name);
///
/// Returns all header values for the given header or null.
///
public List GetHeaderValues(string name) => Headers.GetHeaderValues(name);
///
/// Removes all headers.
///
public void RemoveHeaders() => Headers.RemoveHeaders();
///
/// Sets the Range header to download the content from the given byte position. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
///
/// Start position of the download.
public void SetRangeHeader(long firstBytePos)
{
SetHeader("Range", string.Format("bytes={0}-", firstBytePos));
}
///
/// Sets the Range header to download the content from the given byte position to the given last position. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
///
/// Start position of the download.
/// The end position of the download.
public void SetRangeHeader(long firstBytePos, long lastBytePos)
{
SetHeader("Range", string.Format("bytes={0}-{1}", firstBytePos, lastBytePos));
}
internal void RemoveUnsafeHeaders()
{
// https://www.rfc-editor.org/rfc/rfc9110.html#name-redirection-3xx
/* 2. Remove header fields that were automatically generated by the implementation, replacing them with updated values as appropriate to the new request. This includes:
1. Connection-specific header fields (see Section 7.6.1),
2. Header fields specific to the client's proxy configuration, including (but not limited to) Proxy-Authorization,
3. Origin-specific header fields (if any), including (but not limited to) Host,
4. Validating header fields that were added by the implementation's cache (e.g., If-None-Match, If-Modified-Since), and
5. Resource-specific header fields, including (but not limited to) Referer, Origin, Authorization, and Cookie.
3. Consider removing header fields that were not automatically generated by the implementation
(i.e., those present in the request because they were added by the calling context) where there are security implications;
this includes but is not limited to Authorization and Cookie.
* */
// 2.1
RemoveHeader("Connection");
RemoveHeader("Proxy-Connection");
RemoveHeader("Keep-Alive");
RemoveHeader("TE");
RemoveHeader("Transfer-Encoding");
RemoveHeader("Upgrade");
// 2.2
RemoveHeader("Proxy-Authorization");
// 2.3
RemoveHeader("Host");
// 2.4
RemoveHeader("If-None-Match");
RemoveHeader("If-Modified-Since");
// 2.5 & 3
RemoveHeader("Referer");
RemoveHeader("Origin");
RemoveHeader("Authorization");
RemoveHeader("Cookie");
RemoveHeader("Accept-Encoding");
RemoveHeader("Content-Length");
}
internal void Prepare()
{
// Upload settings
this.UploadSettings?.SetupRequest(this, true);
}
public void EnumerateHeaders(OnHeaderEnumerationDelegate callback, bool callBeforeSendCallback)
{
#if !UNITY_WEBGL || UNITY_EDITOR
if (!HasHeader("Host"))
{
if (CurrentUri.Port == 80 || CurrentUri.Port == 443)
SetHeader("Host", CurrentUri.Host);
else
SetHeader("Host", CurrentUri.Authority);
}
DecompressorFactory.SetupHeaders(this);
if (!DownloadSettings.DisableCache)
HTTPManager.LocalCache?.SetupValidationHeaders(this);
var hostSettings = HTTPManager.PerHostSettings.Get(this.CurrentUri.Host);
// Websocket would be very, very sad if its "connection: upgrade" header would be overwritten!
if (!HasHeader("Connection"))
AddHeader("Connection", hostSettings.HTTP1ConnectionSettings.TryToReuseConnections ? "Keep-Alive, TE" : "Close, TE");
if (hostSettings.HTTP1ConnectionSettings.TryToReuseConnections /*&& !HasHeader("Keep-Alive")*/)
{
// Send the server a slightly larger value to make sure it's not going to close sooner than the client
int seconds = (int)Math.Ceiling(hostSettings.HTTP1ConnectionSettings.MaxConnectionIdleTime.TotalSeconds + 1);
AddHeader("Keep-Alive", "timeout=" + seconds);
}
if (!HasHeader("TE"))
AddHeader("TE", "identity");
if (!string.IsNullOrEmpty(HTTPManager.UserAgent) && !HasHeader("User-Agent"))
AddHeader("User-Agent", HTTPManager.UserAgent);
#endif
long contentLength = -1;
if (this.UploadSettings.UploadStream == null)
{
contentLength = 0;
}
else
{
contentLength = this.UploadSettings.UploadStream.Length;
if (contentLength == BodyLengths.UnknownWithChunkedTransferEncoding)
SetHeader("Transfer-Encoding", "chunked");
if (!HasHeader("Content-Type"))
SetHeader("Content-Type", "application/octet-stream");
}
// Always set the Content-Length header if possible
// http://tools.ietf.org/html/rfc2616#section-4.4 : For compatibility with HTTP/1.0 applications, HTTP/1.1 requests containing a message-body MUST include a valid Content-Length header field unless the server is known to be HTTP/1.1 compliant.
// 2018.06.03: Changed the condition so that content-length header will be included for zero length too.
// 2022.05.25: Don't send a Content-Length (: 0) header if there's an Upgrade header. Upgrade is set for websocket, and it might be not true that the client doesn't send any bytes.
if (contentLength >= BodyLengths.NoBody && !HasHeader("Content-Length") && !HasHeader("Upgrade"))
SetHeader("Content-Length", contentLength.ToString());
// Server authentication
this.Authenticator?.SetupRequest(this);
// Cookies
//this.CookieSettings?.SetupRequest(this);
CookieJar.SetupRequest(this);
// Write out the headers to the stream
if (callback != null && this.Headers != null)
foreach (var kvp in this.Headers)
callback(kvp.Key, kvp.Value);
}
///
/// Starts processing the request.
///
public HTTPRequest Send()
{
// TODO: Are we really want to 'reset' the token source? Two problems i see:
// 1.) User code will not know about this change
// 2.) We might dispose the source while the DNS and TCP queries are running and checking the source request's Token.
//if (this.IsRedirected)
//{
// this.CancellationTokenSource?.Dispose();
// this.CancellationTokenSource = new System.Threading.CancellationTokenSource();
//}
this.Exception = null;
return HTTPManager.SendRequest(this);
}
///
/// Cancels any further processing of the HTTP request.
///
public void Abort()
{
HTTPManager.Logger.Verbose("HTTPRequest", $"Abort({this.State})", this.Context);
if (this.State >= HTTPRequestStates.Finished || this.CancellationTokenSource == null)
return;
//this.IsCancellationRequested = true;
this.CancellationTokenSource.Cancel();
// There's a race-condition here too, another thread might set it too.
// In this case, both state going to be queued up that we have to handle in RequestEvents.cs.
if (this.TimeoutSettings.IsTimedOut(HTTPManager.CurrentFrameDateTime))
{
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this, this.TimeoutSettings.IsConnectTimedOut(HTTPManager.CurrentFrameDateTime) ? HTTPRequestStates.ConnectionTimedOut : HTTPRequestStates.TimedOut, null));
}
else
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this, HTTPRequestStates.Aborted, null));
if (this.OnCancellationRequested != null)
{
try
{
this.OnCancellationRequested(this);
}
catch { }
}
}
///
/// Resets the request for a state where switching MethodType is possible.
///
public void Clear()
{
RemoveHeaders();
this.RedirectSettings.Reset();
this.Exception = null;
this.CancellationTokenSource?.Dispose();
this.CancellationTokenSource = new System.Threading.CancellationTokenSource();
this.UploadSettings?.Dispose();
}
#region System.Collections.IEnumerator implementation
///
/// implementation, required for support.
///
public object Current { get { return null; } }
///
/// implementation, required for support.
///
/// true if the request isn't finished yet.
public bool MoveNext() => this.State < HTTPRequestStates.Finished;
///
/// implementation throwing , required for support.
///
///
public void Reset() => throw new NotImplementedException();
#endregion
///
/// Disposes of resources used by the HTTPRequest instance.
///
internal void Dispose()
{
this.UploadSettings?.Dispose();
this.Response?.Dispose();
this.CancellationTokenSource?.Dispose();
this.CancellationTokenSource = null;
}
public override string ToString()
{
return $"[HTTPRequest {this.State}, {this.Context.Hash}, {this.CurrentUri}, {this.CurrentHostKey}]";
}
}
}