- Initial commit
This commit is contained in:
8
Runtime/HTTP/Request/Authentication.meta
Normal file
8
Runtime/HTTP/Request/Authentication.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72097693cd5ac1242be01fd21cc24411
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
63
Runtime/HTTP/Request/Authentication/Credentials.cs
Normal file
63
Runtime/HTTP/Request/Authentication/Credentials.cs
Normal file
@ -0,0 +1,63 @@
|
||||
namespace Best.HTTP.Request.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication types that supported by Best.HTTP.
|
||||
/// The authentication is defined by the server, so the Basic and Digest are not interchangeable. If you don't know what to use, the preferred way is to choose Unknow.
|
||||
/// </summary>
|
||||
public enum AuthenticationTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// If the authentication type is not known this will do a challenge turn to receive what methode should be choosen.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// The most basic authentication type. It's easy to do, and easy to crack, don't use it with plain http://
|
||||
/// </summary>
|
||||
Basic,
|
||||
|
||||
/// <summary>
|
||||
/// HTTP Digest authentication
|
||||
/// </summary>
|
||||
Digest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hold all information that required to authenticate to a remote server.
|
||||
/// </summary>
|
||||
public sealed class Credentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the Authentication. If you don't know what to use, the preferred way is to choose Unknow.
|
||||
/// </summary>
|
||||
public AuthenticationTypes Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username to authenticate on the remote server.
|
||||
/// </summary>
|
||||
public string UserName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password to use in the authentication process. The password will be stored only in this class.
|
||||
/// </summary>
|
||||
public string Password { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set up the authentication credentials with the username and password. The Type will be set to Unknown.
|
||||
/// </summary>
|
||||
public Credentials(string userName, string password)
|
||||
:this(AuthenticationTypes.Unknown, userName, password)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set up the authentication credentials with the given authentication type, username and password.
|
||||
/// </summary>
|
||||
public Credentials(AuthenticationTypes type, string userName, string password)
|
||||
{
|
||||
this.Type = type;
|
||||
this.UserName = userName;
|
||||
this.Password = password;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Authentication/Credentials.cs.meta
Normal file
18
Runtime/HTTP/Request/Authentication/Credentials.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c6eedacad59ed845b685312bc312734
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authentication/Credentials.cs
|
||||
uploadId: 783279
|
||||
280
Runtime/HTTP/Request/Authentication/Digest.cs
Normal file
280
Runtime/HTTP/Request/Authentication/Digest.cs
Normal file
@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Best.HTTP.Request.Authentication
|
||||
{
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Internal class that stores all information that received from a server in a WWW-Authenticate and need to construct a valid Authorization header. Based on rfc 2617 (http://tools.ietf.org/html/rfc2617).
|
||||
/// Used only internally by the plugin.
|
||||
/// </summary>
|
||||
public sealed class Digest
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// The Uri that this Digest is bound to.
|
||||
/// </summary>
|
||||
public Uri Uri { get; private set; }
|
||||
|
||||
public AuthenticationTypes Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A string to be displayed to users so they know which username and password to use.
|
||||
/// This string should contain at least the name of the host performing the authentication and might additionally indicate the collection of users who might have access.
|
||||
/// </summary>
|
||||
public string Realm { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A flag, indicating that the previous request from the client was rejected because the nonce value was stale.
|
||||
/// If stale is TRUE (case-insensitive), the client may wish to simply retry the request with a new encrypted response, without the user for a new username and password.
|
||||
/// The server should only set stale to TRUE if it receives a request for which the nonce is invalid but with a valid digest for that nonce
|
||||
/// (indicating that the client knows the correct username/password).
|
||||
/// If stale is FALSE, or anything other than TRUE, or the stale directive is not present, the username and/or password are invalid, and new values must be obtained.
|
||||
/// </summary>
|
||||
public bool Stale { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Properties
|
||||
|
||||
/// <summary>
|
||||
/// A server-specified data string which should be uniquely generated each time a 401 response is made.
|
||||
/// Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
|
||||
/// </summary>
|
||||
private string Nonce { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A string of data, specified by the server, which should be returned by the client unchanged in the Authorization header of subsequent requests with URIs in the same protection space.
|
||||
/// It is recommended that this string be base64 or data.
|
||||
/// </summary>
|
||||
private string Opaque { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A string indicating a pair of algorithms used to produce the digest and a checksum. If this is not present it is assumed to be "MD5".
|
||||
/// If the algorithm is not understood, the challenge should be ignored (and a different one used, if there is more than one).
|
||||
/// </summary>
|
||||
private string Algorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of URIs, as specified in RFC XURI, that define the protection space.
|
||||
/// If a URI is an abs_path, it is relative to the canonical root URL (see section 1.2 above) of the server being accessed.
|
||||
/// An absoluteURI in this list may refer to a different server than the one being accessed.
|
||||
/// The client can use this list to determine the set of URIs for which the same authentication information may be sent:
|
||||
/// any URI that has a URI in this list as a prefix (after both have been made absolute) may be assumed to be in the same protection space.
|
||||
/// If this directive is omitted or its value is empty, the client should assume that the protection space consists of all URIs on the responding server.
|
||||
/// </summary>
|
||||
public List<string> ProtectedUris { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If present, it is a quoted string of one or more tokens indicating the "quality of protection" values supported by the server.
|
||||
/// The value "auth" indicates authentication. The value "auth-int" indicates authentication with integrity protection.
|
||||
/// </summary>
|
||||
private string QualityOfProtections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// his MUST be specified if a qop directive is sent (see above), and MUST NOT be specified if the server did not send a qop directive in the WWW-Authenticate header field.
|
||||
/// The nc-value is the hexadecimal count of the number of requests (including the current request) that the client has sent with the nonce value in this request.
|
||||
/// </summary>
|
||||
private int NonceCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to store the last HA1 that can be used in the next header generation when Algorithm is set to "md5-sess".
|
||||
/// </summary>
|
||||
private string HA1Sess { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
internal Digest(Uri uri)
|
||||
{
|
||||
this.Uri = uri;
|
||||
this.Algorithm = "md5";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a WWW-Authenticate header's value to retrive all information.
|
||||
/// </summary>
|
||||
public void ParseChallange(string header)
|
||||
{
|
||||
// Reset some values to its defaults.
|
||||
this.Type = AuthenticationTypes.Unknown;
|
||||
this.Stale = false;
|
||||
this.Opaque = null;
|
||||
this.HA1Sess = null;
|
||||
this.NonceCount = 0;
|
||||
this.QualityOfProtections = null;
|
||||
|
||||
if (this.ProtectedUris != null)
|
||||
this.ProtectedUris.Clear();
|
||||
|
||||
// Parse the header
|
||||
WWWAuthenticateHeaderParser qpl = new WWWAuthenticateHeaderParser(header);
|
||||
|
||||
// Then process
|
||||
foreach (var qp in qpl.Values)
|
||||
switch (qp.Key)
|
||||
{
|
||||
case "basic": this.Type = AuthenticationTypes.Basic; break;
|
||||
case "digest": this.Type = AuthenticationTypes.Digest; break;
|
||||
case "realm": this.Realm = qp.Value; break;
|
||||
case "domain":
|
||||
{
|
||||
if (string.IsNullOrEmpty(qp.Value) || qp.Value.Length == 0)
|
||||
break;
|
||||
|
||||
if (this.ProtectedUris == null)
|
||||
this.ProtectedUris = new List<string>();
|
||||
|
||||
int idx = 0;
|
||||
string val = qp.Value.Read(ref idx, ' ');
|
||||
do
|
||||
{
|
||||
this.ProtectedUris.Add(val);
|
||||
val = qp.Value.Read(ref idx, ' ');
|
||||
} while (idx < qp.Value.Length);
|
||||
|
||||
break;
|
||||
}
|
||||
case "nonce": this.Nonce = qp.Value; break;
|
||||
case "qop": this.QualityOfProtections = qp.Value; break;
|
||||
case "stale": this.Stale = bool.Parse(qp.Value); break;
|
||||
case "opaque": this.Opaque = qp.Value; break;
|
||||
case "algorithm": this.Algorithm = qp.Value; break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string that can be set to an Authorization header.
|
||||
/// </summary>
|
||||
public string GenerateResponseHeader(Credentials credentials, bool isProxy, HTTPMethods methodType, Uri currentUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case AuthenticationTypes.Basic:
|
||||
return string.Concat("Basic ", Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", credentials.UserName, credentials.Password))));
|
||||
|
||||
case AuthenticationTypes.Digest:
|
||||
{
|
||||
NonceCount++;
|
||||
|
||||
string HA1 = string.Empty;
|
||||
|
||||
// The cnonce-value is an opaque quoted string value provided by the client and used by both client and server to avoid chosen plaintext attacks, to provide mutual
|
||||
// authentication, and to provide some message integrity protection.
|
||||
string cnonce = new System.Random(this.GetHashCode()).Next(int.MinValue, int.MaxValue).ToString("X8");
|
||||
|
||||
string ncvalue = NonceCount.ToString("X8");
|
||||
switch (Algorithm.TrimAndLower())
|
||||
{
|
||||
case "md5":
|
||||
HA1 = string.Format("{0}:{1}:{2}", credentials.UserName, Realm, credentials.Password).CalculateMD5Hash();
|
||||
break;
|
||||
|
||||
case "md5-sess":
|
||||
if (string.IsNullOrEmpty(this.HA1Sess))
|
||||
this.HA1Sess = string.Format("{0}:{1}:{2}:{3}:{4}", credentials.UserName, Realm, credentials.Password, Nonce, ncvalue).CalculateMD5Hash();
|
||||
HA1 = this.HA1Sess;
|
||||
break;
|
||||
|
||||
default: //throw new NotSupportedException("Not supported hash algorithm found in Web Authentication: " + Algorithm);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// A string of 32 hex digits, which proves that the user knows a password. Set according to the qop value.
|
||||
string response = string.Empty;
|
||||
|
||||
// The server sent QoP-value can be a list of supported methodes(if sent at all - in this case it's null).
|
||||
// The rfc is not specify that this is a space or comma separeted list. So it can be "auth, auth-int" or "auth auth-int".
|
||||
// We will first check the longer value("auth-int") then the short one ("auth"). If one matches we will reset the qop to the exact value.
|
||||
string qop = this.QualityOfProtections != null ? this.QualityOfProtections.TrimAndLower() : null;
|
||||
|
||||
// When we authenticate with a proxy and we want to tunnel the request, we have to use the CONNECT method instead of the
|
||||
// request's, as the proxy will not know about the request itself.
|
||||
string method = isProxy ? "CONNECT" : HTTPRequest.MethodNames[(byte)methodType];
|
||||
|
||||
// When we authenticate with a proxy and we want to tunnel the request, the uri must match what we are sending in the CONNECT request's
|
||||
// Host header.
|
||||
string uri = isProxy ? currentUri.Host + ":" + currentUri.Port : currentUri.GetRequestPathAndQueryURL();
|
||||
|
||||
if (qop == null)
|
||||
{
|
||||
string HA2 = string.Concat(HTTPRequest.MethodNames[(byte)methodType], ":", currentUri.GetRequestPathAndQueryURL()).CalculateMD5Hash();
|
||||
response = string.Format("{0}:{1}:{2}", HA1, Nonce, HA2).CalculateMD5Hash();
|
||||
}
|
||||
else if (qop.Contains("auth-int"))
|
||||
{
|
||||
throw new NotSupportedException("Digest (auth-int)");
|
||||
|
||||
/*qop = "auth-int";
|
||||
|
||||
byte[] entityBody = null;
|
||||
|
||||
string HA2 = string.Format("{0}:{1}:{2}", method, uri, new BufferSegment(entityBody, 0, entityBody.Length).CalculateMD5Hash()).CalculateMD5Hash();
|
||||
|
||||
response = string.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, Nonce, ncvalue, cnonce, qop, HA2).CalculateMD5Hash();*/
|
||||
}
|
||||
else if (qop.Contains("auth"))
|
||||
{
|
||||
qop = "auth";
|
||||
string HA2 = string.Concat(method, ":", uri).CalculateMD5Hash();
|
||||
|
||||
response = string.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, Nonce, ncvalue, cnonce, qop, HA2).CalculateMD5Hash();
|
||||
}
|
||||
else //throw new NotSupportedException("Unrecognized Quality of Protection value found: " + this.QualityOfProtections);
|
||||
return string.Empty;
|
||||
|
||||
string result = string.Format("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", cnonce=\"{4}\", response=\"{5}\"",
|
||||
credentials.UserName, Realm, Nonce, uri, cnonce, response);
|
||||
|
||||
if (qop != null)
|
||||
result += String.Concat(", qop=\"", qop, "\", nc=", ncvalue);
|
||||
|
||||
if (!string.IsNullOrEmpty(Opaque))
|
||||
result = String.Concat(result, ", opaque=\"", Opaque, "\"");
|
||||
|
||||
return result;
|
||||
}// end of case "digest":
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public bool IsUriProtected(Uri uri)
|
||||
{
|
||||
// http://tools.ietf.org/html/rfc2617#section-3.2.1
|
||||
// An absoluteURI in this list may refer to
|
||||
// a different server than the one being accessed. The client can use
|
||||
// this list to determine the set of URIs for which the same
|
||||
// authentication information may be sent: any URI that has a URI in
|
||||
// this list as a prefix (after both have been made absolute) may be
|
||||
// assumed to be in the same protection space. If this directive is
|
||||
// omitted or its value is empty, the client should assume that the
|
||||
// protection space consists of all URIs on the responding server.
|
||||
|
||||
if (string.CompareOrdinal(uri.Host, this.Uri.Host) != 0)
|
||||
return false;
|
||||
|
||||
string uriStr = uri.ToString();
|
||||
|
||||
if (ProtectedUris != null && ProtectedUris.Count > 0)
|
||||
for (int i = 0; i < ProtectedUris.Count; ++i)
|
||||
if (uriStr.Contains(ProtectedUris[i]))
|
||||
return true;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Authentication/Digest.cs.meta
Normal file
18
Runtime/HTTP/Request/Authentication/Digest.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 113d4dfb584b960498d2b362b1ff1616
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authentication/Digest.cs
|
||||
uploadId: 783279
|
||||
57
Runtime/HTTP/Request/Authentication/DigestStore.cs
Normal file
57
Runtime/HTTP/Request/Authentication/DigestStore.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Best.HTTP.Request.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores and manages already received digest infos.
|
||||
/// </summary>
|
||||
public static class DigestStore
|
||||
{
|
||||
private static ConcurrentDictionary<string, Digest> Digests = new ConcurrentDictionary<string, Digest>();
|
||||
|
||||
/// <summary>
|
||||
/// Array of algorithms that the plugin supports. It's in the order of priority(first has the highest priority).
|
||||
/// </summary>
|
||||
private static string[] SupportedAlgorithms = new string[] { "digest", "basic" };
|
||||
|
||||
public static Digest Get(Uri uri)
|
||||
{
|
||||
if (Digests.TryGetValue(uri.Host, out var digest))
|
||||
if (!digest.IsUriProtected(uri))
|
||||
return null;
|
||||
return digest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It will retrieve or create a new Digest for the given Uri.
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <returns></returns>
|
||||
public static Digest GetOrCreate(Uri uri) => Digests.GetOrAdd(uri.Host, new Digest(uri));
|
||||
|
||||
public static void Remove(Uri uri) => Digests.TryRemove(uri.Host, out var _);
|
||||
|
||||
public static void Clear() => Digests.Clear();
|
||||
|
||||
public static string FindBest(List<string> authHeaders)
|
||||
{
|
||||
if (authHeaders == null || authHeaders.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
List<string> headers = new List<string>(authHeaders.Count);
|
||||
for (int i = 0; i < authHeaders.Count; ++i)
|
||||
headers.Add(authHeaders[i].ToLowerInvariant());
|
||||
|
||||
for (int i = 0; i < SupportedAlgorithms.Length; ++i)
|
||||
{
|
||||
int idx = headers.FindIndex((header) => header.StartsWith(SupportedAlgorithms[i]));
|
||||
if (idx != -1)
|
||||
return authHeaders[idx];
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Authentication/DigestStore.cs.meta
Normal file
18
Runtime/HTTP/Request/Authentication/DigestStore.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 323cdedca4b257a4f9b397955046ed55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authentication/DigestStore.cs
|
||||
uploadId: 783279
|
||||
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
|
||||
namespace Best.HTTP.Request.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for parsing WWW-Authenticate headers:
|
||||
/// `Digest realm="my realm", nonce="4664b327a2963503ba58bbe13ad672c0", qop=auth, opaque="f7e38bdc1c66fce214f9019ffe43117c"`
|
||||
/// </summary>
|
||||
public sealed class WWWAuthenticateHeaderParser : KeyValuePairList
|
||||
{
|
||||
public WWWAuthenticateHeaderParser(string headerValue)
|
||||
{
|
||||
Values = ParseQuotedHeader(headerValue);
|
||||
}
|
||||
|
||||
private List<HeaderValue> ParseQuotedHeader(string str)
|
||||
{
|
||||
List<HeaderValue> result = new List<HeaderValue>();
|
||||
|
||||
if (str != null)
|
||||
{
|
||||
|
||||
int idx = 0;
|
||||
|
||||
// Read Type (Basic|Digest)
|
||||
string type = str.Read(ref idx, (ch) => !char.IsWhiteSpace(ch) && !char.IsControl(ch)).TrimAndLower();
|
||||
result.Add(new HeaderValue(type));
|
||||
|
||||
// process the rest of the text
|
||||
while (idx < str.Length)
|
||||
{
|
||||
// Read key
|
||||
string key = str.Read(ref idx, '=').TrimAndLower();
|
||||
HeaderValue qp = new HeaderValue(key);
|
||||
|
||||
// Skip any white space
|
||||
str.SkipWhiteSpace(ref idx);
|
||||
|
||||
qp.Value = str.ReadPossibleQuotedText(ref idx);
|
||||
|
||||
result.Add(qp);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5696919a0b17a504a9d304c259874caf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authentication/WWWAuthenticateHeaderParser.cs
|
||||
uploadId: 783279
|
||||
8
Runtime/HTTP/Request/Authenticators.meta
Normal file
8
Runtime/HTTP/Request/Authenticators.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdceeb0c2c2ffe045b0d65b1625802c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,49 @@
|
||||
namespace Best.HTTP.Request.Authenticators
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IAuthenticator"/> implementation for Bearer Token authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bearer Token authentication is a method used to access protected resources on a server.
|
||||
/// It involves including a bearer token in the Authorization header of an HTTP request to prove the identity of the requester.
|
||||
/// </remarks>
|
||||
public class BearerTokenAuthenticator : IAuthenticator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the BearerTokenAuthenticator class with the specified Bearer Token.
|
||||
/// </summary>
|
||||
/// <param name="token">The Bearer Token to use for authentication.</param>
|
||||
public string Token { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the required Authorization header with the Bearer Token for the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request for which the Authorization header should be added.</param>
|
||||
/// <remarks>
|
||||
/// When sending an HTTP request to a server that requires Bearer Token authentication,
|
||||
/// this method sets the Authorization header with the Bearer Token to prove the identity of the requester.
|
||||
/// This allows the requester to access protected resources on the server.
|
||||
/// </remarks>
|
||||
public BearerTokenAuthenticator(string token) => this.Token = token;
|
||||
|
||||
public void SetupRequest(HTTPRequest request)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.Token))
|
||||
request.SetHeader("Authorization", $"Bearer {this.Token}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server response with a 401 (Unauthorized) status code and a WWW-Authenticate header.
|
||||
/// This authenticator does not handle challenges and always returns <c>false</c>.
|
||||
/// </summary>
|
||||
/// <param name="req">The HTTP request that received the 401 response.</param>
|
||||
/// <param name="resp">The HTTP response containing the 401 (Unauthorized) status.</param>
|
||||
/// <returns><c>false</c>, as this authenticator does not handle challenges.</returns>
|
||||
/// <remarks>
|
||||
/// Bearer Token authentication typically does not require handling challenges,
|
||||
/// as the Bearer Token is included directly in the Authorization header of the request.
|
||||
/// This method always returns <c>false</c>, as no additional challenge processing is needed.
|
||||
/// </remarks>
|
||||
public bool HandleChallange(HTTPRequest req, HTTPResponse resp) => false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d868e2c2c58ac641a6eb45359eee94b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authenticators/BearerTokenAuthenticator.cs
|
||||
uploadId: 783279
|
||||
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Request.Authentication;
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
namespace Best.HTTP.Request.Authenticators
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IAuthenticator"/> implementation for HTTP Basic or Digest authentication.
|
||||
/// </summary>
|
||||
public class CredentialAuthenticator : IAuthenticator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Authentication.Credentials"/> associated with this authenticator.
|
||||
/// </summary>
|
||||
public Credentials Credentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the CrendetialAuthenticator class with the specified <see cref="Authentication.Credentials"/>.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The <see cref="Authentication.Credentials"/> to use for authentication.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="credentials"/> is null.</exception>
|
||||
public CredentialAuthenticator(Credentials credentials)
|
||||
{
|
||||
if (credentials == null)
|
||||
throw new ArgumentNullException(nameof(credentials));
|
||||
|
||||
this.Credentials = credentials;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the required headers for the HTTP request based on the provided credentials.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request for which headers should be added.</param>
|
||||
public void SetupRequest(HTTPRequest request)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(CredentialAuthenticator), $"SetupRequest({request}, {Credentials?.Type})", request.Context);
|
||||
|
||||
if (Credentials == null)
|
||||
return;
|
||||
|
||||
switch (Credentials.Type)
|
||||
{
|
||||
case AuthenticationTypes.Basic:
|
||||
// With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
|
||||
request.SetHeader("Authorization", string.Concat("Basic ", Convert.ToBase64String(Encoding.UTF8.GetBytes(Credentials.UserName + ":" + Credentials.Password))));
|
||||
break;
|
||||
|
||||
case AuthenticationTypes.Unknown:
|
||||
case AuthenticationTypes.Digest:
|
||||
var digest = DigestStore.Get(request.CurrentUri);
|
||||
if (digest != null)
|
||||
{
|
||||
string authentication = digest.GenerateResponseHeader(Credentials, false, request.MethodType, request.CurrentUri);
|
||||
if (!string.IsNullOrEmpty(authentication))
|
||||
request.SetHeader("Authorization", authentication);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server response with a 401 (Unauthorized) status code and a WWW-Authenticate header.
|
||||
/// The authenticator might determine the authentication method to use and initiate authentication if needed.
|
||||
/// </summary>
|
||||
/// <param name="req">The HTTP request that received the 401 response.</param>
|
||||
/// <param name="resp">The HTTP response containing the 401 (Unauthorized) status.</param>
|
||||
/// <returns><c>true</c> if the challenge is handled by the authenticator and the request can be resent with authentication; otherwise, <c>false</c>.</returns>
|
||||
public bool HandleChallange(HTTPRequest req, HTTPResponse resp)
|
||||
{
|
||||
var www_authenticate = resp.GetHeaderValues("www-authenticate");
|
||||
|
||||
HTTPManager.Logger.Information(nameof(CredentialAuthenticator), $"HandleChallange({req}, {resp}, \"{www_authenticate}\")", req.Context);
|
||||
|
||||
string authHeader = DigestStore.FindBest(www_authenticate);
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
var digest = DigestStore.GetOrCreate(req.CurrentUri);
|
||||
digest.ParseChallange(authHeader);
|
||||
|
||||
if (this.Credentials != null && digest.IsUriProtected(req.CurrentUri) && (!req.HasHeader("Authorization") || digest.Stale))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69e23320fe6bb034896f36f5773b06ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authenticators/CredentialAuthenticator.cs
|
||||
uploadId: 783279
|
||||
36
Runtime/HTTP/Request/Authenticators/IAuthenticator.cs
Normal file
36
Runtime/HTTP/Request/Authenticators/IAuthenticator.cs
Normal file
@ -0,0 +1,36 @@
|
||||
namespace Best.HTTP.Request.Authenticators
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an interface for various authentication implementations used in HTTP requests.
|
||||
/// </summary>
|
||||
public interface IAuthenticator
|
||||
{
|
||||
/// <summary>
|
||||
/// Set required headers or content for the HTTP request. Called right before the request is sent out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The SetupRequest method will be called every time the request is redirected or retried.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="request">The HTTP request to which headers or content will be added.</param>
|
||||
void SetupRequest(HTTPRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server is sending a 401 (Unauthorized) response with an WWW-Authenticate header.
|
||||
/// The authenticator might find additional knowledge about the authentication requirements (like what auth method it should use).
|
||||
/// If the authenticator is confident it can successfully (re)authenticate the request it can return true and the request will be resent to the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9110.html#status.401">RFC-9110 - 401 Unauthorized</see></description></item>
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate">RFC-9110 - WWW-Authenticate header</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="req">The HTTP request that received the 401 response.</param>
|
||||
/// <param name="resp">The HTTP response containing the 401 (Unauthorized) status.</param>
|
||||
/// <returns><c>true</c> if the challange is handled by the authenticator and the request can be re-sent with authentication.</returns>
|
||||
bool HandleChallange(HTTPRequest req, HTTPResponse resp);
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Authenticators/IAuthenticator.cs.meta
Normal file
18
Runtime/HTTP/Request/Authenticators/IAuthenticator.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07b45cc6303c94540abce76b1370370a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Authenticators/IAuthenticator.cs
|
||||
uploadId: 783279
|
||||
8
Runtime/HTTP/Request/Settings.meta
Normal file
8
Runtime/HTTP/Request/Settings.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf3fc2177584a3547b6e00a24f109839
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
132
Runtime/HTTP/Request/Settings/DownloadSettings.cs
Normal file
132
Runtime/HTTP/Request/Settings/DownloadSettings.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Request.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for handling the event when headers are received in a response.
|
||||
/// </summary>
|
||||
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
|
||||
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
|
||||
/// <param name="headers">The headers received from the server.</param>
|
||||
public delegate void OnHeadersReceivedDelegate(HTTPRequest req, HTTPResponse resp, Dictionary<string, List<string>> headers);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for handling progress during the download.
|
||||
/// </summary>
|
||||
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
|
||||
/// <param name="progress">The number of bytes downloaded so far.</param>
|
||||
/// <param name="length">The total length of the content being downloaded, or -1 if the length cannot be determined.</param>
|
||||
public delegate void OnProgressDelegate(HTTPRequest req, long progress, long length);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for handling the event when the download of content starts.
|
||||
/// </summary>
|
||||
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
|
||||
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
|
||||
/// <param name="stream">The <see cref="DownloadContentStream"/> used for receiving downloaded content.</param>
|
||||
public delegate void OnDownloadStartedDelegate(HTTPRequest req, HTTPResponse resp, DownloadContentStream stream);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for creating a new <see cref="DownloadContentStream"/> object.
|
||||
/// </summary>
|
||||
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
|
||||
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
|
||||
/// <param name="bufferAvailableHandler">An interface for notifying connections that the buffer has free space for downloading data.</param>
|
||||
/// <returns>The newly created <see cref="DownloadContentStream"/>.</returns>
|
||||
public delegate DownloadContentStream OnCreateDownloadStreamDelegate(HTTPRequest req, HTTPResponse resp, IDownloadContentBufferAvailable bufferAvailableHandler);
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Delegate for handling the event when a response is upgraded.
|
||||
/// </summary>
|
||||
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
|
||||
/// <param name="resp">The <see cref="HTTPResponse"/> object.</param>
|
||||
/// <param name="contentProvider">A stream that provides content for the upgraded response.</param>
|
||||
/// <returns><c>true</c> to keep the underlying connection open; otherwise, <c>false</c>.</returns>
|
||||
public delegate bool OnUpgradedDelegate(HTTPRequest req, HTTPResponse resp, PeekableContentProviderStream contentProvider);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Represents settings for configuring an HTTP request's download behavior.
|
||||
/// </summary>
|
||||
public class DownloadSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of bytes the <see cref="DownloadContentStream"/> will buffer before pausing the download until its buffer has free space again.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the download content stream buffers data up to this specified limit, it will temporarily pause downloading until it has free space in its buffer.
|
||||
/// Increasing this value may help reduce the frequency of pauses during downloads, but it also increases memory usage.
|
||||
/// </remarks>
|
||||
public long ContentStreamMaxBuffered = 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether caching should be enabled for this request.
|
||||
/// </summary>
|
||||
public bool DisableCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the response's <see cref="DownloadContentStream"/> should be populated with downloaded data or if the content should be written only to the local cache when available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If set to <c>true</c> and the content isn't cacheable (e.g., it doesn't have any cache-related headers), the content will be downloaded but will be lost.
|
||||
/// </remarks>
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the response's <see cref="HTTPResponse.DownStream"/> should be populated with downloaded data or if the content should be written only to the local cache when available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If set to <c>true</c> and the content isn't cacheable (e.g., it doesn't have any cache-related headers), the content will be downloaded but will be lost.
|
||||
/// This is because the downloaded data would be written exclusively to the local cache and will not be stored in memory or the response's <see cref="HTTPResponse.DownStream"/> for further use.
|
||||
/// </remarks>
|
||||
public bool CacheOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This event is called when the plugin received and parsed all headers.
|
||||
/// </summary>
|
||||
public OnHeadersReceivedDelegate OnHeadersReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a function that creates a new <see cref="DownloadContentStream"/> object when needed for downloading content.
|
||||
/// </summary>
|
||||
public OnCreateDownloadStreamDelegate DownloadStreamFactory = (req, resp, bufferAvailableHandler)
|
||||
=> new DownloadContentStream(resp, req.DownloadSettings.ContentStreamMaxBuffered, bufferAvailableHandler);
|
||||
|
||||
/// <summary>
|
||||
/// Event for handling the start of the download process for 2xx status code responses.
|
||||
/// </summary>
|
||||
/// <param name="req">The <see cref="HTTPRequest"/> object.</param>
|
||||
/// <param name="resp">The <see cref="HTTPResponse"/> object representing the response.</param>
|
||||
/// <param name="stream">
|
||||
/// The <see cref="DownloadContentStream"/> containing the downloaded data. It might already be populated with some content.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This event is called when the plugin expects the server to send content. When called, the <see cref="DownloadContentStream"/>
|
||||
/// might already be populated with some content. It is specifically meant for responses with 2xx status codes.
|
||||
/// </remarks>
|
||||
public OnDownloadStartedDelegate OnDownloadStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event that is called when new data is downloaded from the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first parameter is the original <see cref="HTTPRequest"/> object itself, the second parameter is the downloaded bytes, and the third parameter is the content length.
|
||||
/// There are download modes where we can't figure out the exact length of the final content. In these cases, we guarantee that the third parameter will be at least the size of the second one.
|
||||
/// </remarks>
|
||||
public OnProgressDelegate OnDownloadProgress;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
#pragma warning disable 0649
|
||||
/// <summary>
|
||||
/// Called when a response with status code 101 (upgrade), "<c>connection: upgrade</c>" header and value or an "<c>upgrade</c>" header received.
|
||||
/// </summary>
|
||||
/// <remarks>This callback might be called on a thread other than the main one!</remarks>
|
||||
/// <remarks>Isn't available under WebGL!</remarks>
|
||||
public OnUpgradedDelegate OnUpgraded;
|
||||
#pragma warning restore 0649
|
||||
#endif
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Settings/DownloadSettings.cs.meta
Normal file
18
Runtime/HTTP/Request/Settings/DownloadSettings.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e413d08106f68d4d84ad8500cf3a765
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Settings/DownloadSettings.cs
|
||||
uploadId: 783279
|
||||
98
Runtime/HTTP/Request/Settings/ProxySettings.cs
Normal file
98
Runtime/HTTP/Request/Settings/ProxySettings.cs
Normal file
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Authentication;
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
namespace Best.HTTP.Request.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents settings related to using a proxy server for HTTP requests.
|
||||
/// </summary>
|
||||
public class ProxySettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if there is a proxy configured for the given URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to check for proxy usage.</param>
|
||||
/// <returns><c>true</c> if a proxy is configured and should be used for the URI; otherwise, <c>false</c>.</returns>
|
||||
public bool HasProxyFor(Uri uri) => Proxy != null && Proxy.UseProxyForAddress(uri);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy object used for the request.
|
||||
/// </summary>
|
||||
public Proxies.Proxy Proxy { get; set; } = HTTPManager.Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the HTTP request for passing through a proxy server.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request to set up.</param>
|
||||
public void SetupRequest(HTTPRequest request)
|
||||
{
|
||||
var currentUri = request.CurrentUri;
|
||||
|
||||
bool tryToKeepAlive = HTTPManager.PerHostSettings.Get(currentUri.Host)
|
||||
.HTTP1ConnectionSettings
|
||||
.TryToReuseConnections;
|
||||
|
||||
if (!HTTPProtocolFactory.IsSecureProtocol(currentUri) && this.HasProxyFor(currentUri) && !request.HasHeader("Proxy-Connection"))
|
||||
request.AddHeader("Proxy-Connection", tryToKeepAlive ? "Keep-Alive" : "Close");
|
||||
|
||||
// Proxy Authentication
|
||||
if (!HTTPProtocolFactory.IsSecureProtocol(currentUri) && HasProxyFor(currentUri) && this.Proxy.Credentials != null)
|
||||
{
|
||||
switch (Proxy.Credentials.Type)
|
||||
{
|
||||
case AuthenticationTypes.Basic:
|
||||
// With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
|
||||
var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Proxy.Credentials.UserName + ":" + this.Proxy.Credentials.Password));
|
||||
request.SetHeader("Proxy-Authorization", $"Basic {token}");
|
||||
break;
|
||||
|
||||
case AuthenticationTypes.Unknown:
|
||||
case AuthenticationTypes.Digest:
|
||||
var digest = DigestStore.Get(this.Proxy.Address);
|
||||
if (digest != null)
|
||||
{
|
||||
string authentication = digest.GenerateResponseHeader(this.Proxy.Credentials, false, request.MethodType, currentUri);
|
||||
if (!string.IsNullOrEmpty(authentication))
|
||||
request.SetHeader("Proxy-Authorization", authentication);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the proxy's response with status code <c>407</c>.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request that received a <c>407</c> response.</param>
|
||||
/// <returns><c>true</c> to resend the request through the proxy; otherwise, <c>false</c>.</returns>
|
||||
public bool Handle407(HTTPRequest request)
|
||||
{
|
||||
if (this.Proxy == null)
|
||||
return false;
|
||||
|
||||
return this.Proxy.SetupRequest(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the proxy address to a hash for the given request URI.
|
||||
/// </summary>
|
||||
/// <param name="requestUri">The request URI for which the proxy address is added to the hash.</param>
|
||||
/// <param name="hash">The hash to which the proxy address is added.</param>
|
||||
public void AddToHash(Uri requestUri, ref UnityEngine.Hash128 hash)
|
||||
{
|
||||
if (HasProxyFor(requestUri))
|
||||
HostKey.Append(this.Proxy.Address, ref hash);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Proxy?.Address?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Settings/ProxySettings.cs.meta
Normal file
18
Runtime/HTTP/Request/Settings/ProxySettings.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 187900d0bbfe66243ad32654d11316f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Settings/ProxySettings.cs
|
||||
uploadId: 783279
|
||||
77
Runtime/HTTP/Request/Settings/RedirectSettings.cs
Normal file
77
Runtime/HTTP/Request/Settings/RedirectSettings.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Request.Settings
|
||||
{
|
||||
public delegate bool OnBeforeRedirectionDelegate(HTTPRequest req, HTTPResponse resp, Uri redirectUri);
|
||||
|
||||
/// <summary>
|
||||
/// Represents settings related to handling HTTP request redirection.
|
||||
/// </summary>
|
||||
public class RedirectSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the request has been redirected.
|
||||
/// A request's IsRedirected might be true while <see cref="RedirectCount"/> is zero if the redirection is made to the local cache.
|
||||
/// </summary>
|
||||
public bool IsRedirected { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Uri that the request is redirected to.
|
||||
/// </summary>
|
||||
public Uri RedirectUri { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many redirection is supported for this request. The default is 10. Zero or a negative value means no redirections are supported.
|
||||
/// </summary>
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of redirections supported for this request. The default is <c>10</c>.
|
||||
/// A value of zero or a negative value means no redirections are supported.
|
||||
/// </summary>
|
||||
public int MaxRedirects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of times the request has been redirected.
|
||||
/// </summary>
|
||||
public int RedirectCount { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs before the plugin makes a new request to the new URI during redirection.
|
||||
/// The return value of this event handler controls whether the redirection is aborted (<c>false</c>) or allowed (<c>true</c>).
|
||||
/// This event is called on a thread other than the main Unity thread.
|
||||
/// </summary>
|
||||
public event OnBeforeRedirectionDelegate OnBeforeRedirection
|
||||
{
|
||||
add { onBeforeRedirection += value; }
|
||||
remove { onBeforeRedirection -= value; }
|
||||
}
|
||||
private OnBeforeRedirectionDelegate onBeforeRedirection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RedirectSettings class with the specified maximum redirections.
|
||||
/// </summary>
|
||||
/// <param name="maxRedirects">The maximum number of redirections allowed.</param>
|
||||
public RedirectSettings(int maxRedirects)
|
||||
{
|
||||
this.MaxRedirects = maxRedirects;
|
||||
this.RedirectCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="IsRedirected"/> and <see cref="RedirectCount"/> to their default values.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
this.IsRedirected = false;
|
||||
this.RedirectCount = 0;
|
||||
}
|
||||
|
||||
internal bool CallOnBeforeRedirection(HTTPRequest req, HTTPResponse resp, Uri redirectUri)
|
||||
{
|
||||
if (onBeforeRedirection != null)
|
||||
return onBeforeRedirection(req, resp, redirectUri);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Settings/RedirectSettings.cs.meta
Normal file
18
Runtime/HTTP/Request/Settings/RedirectSettings.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab5aa9485b2a13e4196c40a39756ad17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Settings/RedirectSettings.cs
|
||||
uploadId: 783279
|
||||
26
Runtime/HTTP/Request/Settings/RetrySettings.cs
Normal file
26
Runtime/HTTP/Request/Settings/RetrySettings.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace Best.HTTP.Request.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents settings related to request retry behavior.
|
||||
/// </summary>
|
||||
public class RetrySettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of times that the plugin has retried the request.
|
||||
/// </summary>
|
||||
public int Retries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of retry attempts allowed. To disable retries, set this value to <c>0</c>.
|
||||
/// The default value is <c>1</c> for GET requests, otherwise <c>0</c>.
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RetrySettings class with the specified maximum retry attempts.
|
||||
/// </summary>
|
||||
/// <param name="maxRetries">The maximum number of retry attempts allowed.</param>
|
||||
public RetrySettings(int maxRetries)
|
||||
=> this.MaxRetries = maxRetries;
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Settings/RetrySettings.cs.meta
Normal file
18
Runtime/HTTP/Request/Settings/RetrySettings.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6dc0d7446a891b94082c956de0282ef9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Settings/RetrySettings.cs
|
||||
uploadId: 783279
|
||||
76
Runtime/HTTP/Request/Settings/TimeoutSettings.cs
Normal file
76
Runtime/HTTP/Request/Settings/TimeoutSettings.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
namespace Best.HTTP.Request.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents settings related to connection-timeouts and processing duration.
|
||||
/// </summary>
|
||||
public class TimeoutSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the request was queued for processing.
|
||||
/// </summary>
|
||||
public DateTime QueuedAt { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the processing of the request started by a connection.
|
||||
/// </summary>
|
||||
public DateTime ProcessingStarted { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time to wait for establishing the connection to the target server.
|
||||
/// If set to <c>TimeSpan.Zero</c> or lower, no connect timeout logic is executed. Default value is 20 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectTimeout
|
||||
{
|
||||
get => this._connectTimeout ?? HTTPManager.PerHostSettings.Get(this._request.CurrentHostKey.Host).RequestSettings.ConnectTimeout;
|
||||
set => this._connectTimeout = value;
|
||||
}
|
||||
private TimeSpan? _connectTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum time to wait for the request to finish after the connection is established.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout
|
||||
{
|
||||
get => this._timeout ?? HTTPManager.PerHostSettings.Get(this._request.CurrentHostKey.Host).RequestSettings.RequestTimeout;
|
||||
set => this._timeout = value;
|
||||
}
|
||||
private TimeSpan? _timeout;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the request has been stuck in the connection phase for too long.
|
||||
/// </summary>
|
||||
/// <param name="now">The current timestamp.</param>
|
||||
/// <returns><c>true</c> if the connection has timed out; otherwise, <c>false</c>.</returns>
|
||||
public bool IsConnectTimedOut(DateTime now) => this.QueuedAt != DateTime.MinValue && now - this.QueuedAt > this.ConnectTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the time has passed the specified Timeout setting since processing started or if the connection has timed out.
|
||||
/// </summary>
|
||||
/// <param name="now">The current timestamp.</param>
|
||||
/// <returns><c>true</c> if the request has timed out; otherwise, <c>false</c>.</returns>
|
||||
public bool IsTimedOut(DateTime now)
|
||||
{
|
||||
bool result = (this.ProcessingStarted != DateTime.MinValue && now - this.ProcessingStarted > this.Timeout) || this.IsConnectTimedOut(now); ;
|
||||
return result;
|
||||
}
|
||||
|
||||
private HTTPRequest _request;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TimeoutSettings class for a specific <see cref="HTTPRequest"/>.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> associated with these timeout settings.</param>
|
||||
public TimeoutSettings(HTTPRequest request)
|
||||
=> this._request = request;
|
||||
|
||||
public void SetProcessing(DateTime now)
|
||||
{
|
||||
this.QueuedAt = DateTime.MinValue;
|
||||
this.ProcessingStarted = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Settings/TimeoutSettings.cs.meta
Normal file
18
Runtime/HTTP/Request/Settings/TimeoutSettings.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39256be26c7ee1b47a5beca18520959d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Settings/TimeoutSettings.cs
|
||||
uploadId: 783279
|
||||
157
Runtime/HTTP/Request/Settings/UploadSettings.cs
Normal file
157
Runtime/HTTP/Request/Settings/UploadSettings.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Best.HTTP.Request.Upload;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
|
||||
namespace Best.HTTP.Request.Settings
|
||||
{
|
||||
public delegate void OnHeadersSentDelegate(HTTPRequest req);
|
||||
|
||||
/// <summary>
|
||||
/// Options for sending the request headers and content, including upload progress monitoring.
|
||||
/// </summary>
|
||||
/// <remarks><see cref="SetupRequest"/> might be called when redirected or retried!</remarks>
|
||||
public class UploadSettings : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Size of the internal buffer, and upload progress will be fired when this size of data sent to the wire. Its default value is 4 KiB.
|
||||
/// </summary>
|
||||
public int UploadChunkSize = 4 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The stream that the plugin will use to send data to the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The stream can be any regular <see cref="System.IO.Stream"/> implementation or a specialized one inheriting from <see cref="UploadStreamBase"/>:
|
||||
/// <list type="bullet">
|
||||
/// <item><term><see cref="DynamicUploadStream"/></term><description>A specialized <see cref="UploadStreamBase"/> for data generated on-the-fly or periodically. The request remains active until the <see cref="DynamicUploadStream.Complete"/> method is invoked, ensuring continuous data feed even during temporary empty states.</description></item>
|
||||
/// <item><term><see cref="JSonDataStream{T}"/></term><description>An <see cref="UploadStreamBase"/> implementation to convert and upload the object as JSON data. It sets the <c>"Content-Type"</c> header to <c>"application/json; charset=utf-8"</c>.</description></item>
|
||||
/// <item><term><see cref="Upload.Forms.UrlEncodedStream"/></term><description>An <see cref="UploadStreamBase"/> implementation representing a stream that prepares and sends data as URL-encoded form data in an HTTP request.</description></item>
|
||||
/// <item><term><see cref="Upload.Forms.MultipartFormDataStream"/></term><description>An <see cref="UploadStreamBase"/> based implementation of the <c>multipart/form-data</c> Content-Type. It's very memory-effective, streams are read into memory in chunks.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public Stream UploadStream;
|
||||
|
||||
/// <summary>
|
||||
/// Set to <c>false</c> if the plugin MUST NOT dispose <see cref="UploadStream"/> after the request is finished.
|
||||
/// </summary>
|
||||
public bool DisposeStream = true;
|
||||
|
||||
/// <summary>
|
||||
/// Called periodically when data sent to the server.
|
||||
/// </summary>
|
||||
public OnProgressDelegate OnUploadProgress;
|
||||
|
||||
/// <summary>
|
||||
/// This event is fired after the headers are sent to the server.
|
||||
/// </summary>
|
||||
public event OnHeadersSentDelegate OnHeadersSent
|
||||
{
|
||||
add { _onHeadersSent += value; }
|
||||
remove { _onHeadersSent -= value; }
|
||||
}
|
||||
private OnHeadersSentDelegate _onHeadersSent;
|
||||
|
||||
// <summary>
|
||||
// Whether to send an "<c>Expect: 100-continue</c>" header and value when there's content to send (<see cref="UploadSettings.UploadStream"/> != <c>null</c>).
|
||||
// By using "<c>Expect: 100-continue</c>" the server is able to respond with an error (like <c>401-unauthorized</c>, <c>405-method not allowed</c>, etc.) or redirect before the client sends the whole payload.
|
||||
// </summary>
|
||||
// <remarks>
|
||||
// More details can be found here:
|
||||
// <list type="bullet">
|
||||
// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9110#name-expect">RFC-9110 - Expect header</see></description></item>
|
||||
// <item><description><see href="https://daniel.haxx.se/blog/2020/02/27/expect-tweaks-in-curl/">EXPECT: TWEAKS IN CURL (by Daniel Stenberg)</see></description></item>
|
||||
// </list>
|
||||
// </remarks>
|
||||
//public bool SendExpect100Continue = true;
|
||||
|
||||
// False by default, set to true only when "Expect: 100-continue" sent out.
|
||||
//internal bool Expect100Continue = false;
|
||||
|
||||
//internal void ResetExpects() => this.SendExpect100Continue = this.Expect100Continue = false;
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Called every time the request is sent out (redirected or retried).
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> being prepared.</param>
|
||||
/// <param name="dispatchHeadersSentCallback"><c>true</c> if the <see cref="OnHeadersSent"/> can be fired.</param>
|
||||
public virtual void SetupRequest(HTTPRequest request, bool dispatchHeadersSentCallback)
|
||||
{
|
||||
if (isDisposed)
|
||||
throw new ObjectDisposedException(nameof(UploadSettings));
|
||||
|
||||
if (this.UploadStream is UploadStreamBase upStream)
|
||||
upStream.BeforeSendHeaders(request);
|
||||
|
||||
// Decide on whether append an "expect: 100-continue" or not.
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-expect
|
||||
/*
|
||||
if (this.SendExpect100Continue && this.UploadStream != null)
|
||||
{
|
||||
request.AddHeader("expect", "100-continue");
|
||||
|
||||
this.Expect100Continue = true;
|
||||
}
|
||||
else
|
||||
request.RemoveHeader("expect");
|
||||
*/
|
||||
|
||||
if (dispatchHeadersSentCallback)
|
||||
{
|
||||
// Call the callback on the unity main thread
|
||||
if (HTTPUpdateDelegator.Instance.IsMainThread())
|
||||
call_onBeforeHeaderSend(request);
|
||||
else
|
||||
new RunOnceOnMainThread(() => call_onBeforeHeaderSend(request), request.Context)
|
||||
.Subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
protected void call_onBeforeHeaderSend(HTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onHeadersSent?.Invoke(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(UploadSettings), nameof(OnHeadersSent), ex, request.Context);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (this.DisposeStream)
|
||||
{
|
||||
var stream = this.UploadStream;
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
this.UploadStream?.Dispose();
|
||||
this.UploadStream = null;
|
||||
|
||||
isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources used by the UploadSettings instance.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Settings/UploadSettings.cs.meta
Normal file
18
Runtime/HTTP/Request/Settings/UploadSettings.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cf0fd5a0cd00cc40b9f5fb9f79956d4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Settings/UploadSettings.cs
|
||||
uploadId: 783279
|
||||
8
Runtime/HTTP/Request/Timings.meta
Normal file
8
Runtime/HTTP/Request/Timings.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 389dca70c7a1046429edd02f627fdb07
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
220
Runtime/HTTP/Request/Timings/TimingCollector.cs
Normal file
220
Runtime/HTTP/Request/Timings/TimingCollector.cs
Normal file
@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
|
||||
namespace Best.HTTP.Request.Timings
|
||||
{
|
||||
struct PartialEvent
|
||||
{
|
||||
public string EventName;
|
||||
public DateTime StartedAt;
|
||||
|
||||
public PartialEvent(string eventName, DateTime startedAt)
|
||||
{
|
||||
this.EventName = eventName;
|
||||
this.StartedAt = startedAt;
|
||||
}
|
||||
|
||||
public bool IsSet() => !string.IsNullOrEmpty(EventName) && StartedAt != DateTime.MinValue;
|
||||
|
||||
public override string ToString() => $"[PartialEvent '{EventName}', {StartedAt.ToString("hh:mm:ss.fffffff")}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to store, calculate and manage request related events and theirs duration, referenced by <see cref="HTTPRequest.Timing"/> field.
|
||||
/// </summary>
|
||||
public sealed class TimingCollector
|
||||
{
|
||||
public HTTPRequest ParentRequest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When the TimingCollector instance created.
|
||||
/// </summary>
|
||||
public DateTime Created { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the closing Finish event is sent.
|
||||
/// </summary>
|
||||
public DateTime Finished { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of added events.
|
||||
/// </summary>
|
||||
public List<TimingEvent> Events { get; private set; }
|
||||
|
||||
private PartialEvent _partialEvent;
|
||||
|
||||
internal TimingCollector(HTTPRequest parentRequest)
|
||||
{
|
||||
this.ParentRequest = parentRequest;
|
||||
this.Created = DateTime.UtcNow;
|
||||
this.Finished = DateTime.MinValue;
|
||||
this._partialEvent = new PartialEvent(TimingEventNames.Initial, this.Created);
|
||||
}
|
||||
|
||||
public void StartNext(string eventName) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.ParentRequest, new TimingEventInfo(this.ParentRequest, TimingEvents.StartNext, eventName)));
|
||||
|
||||
internal void AddEvent(TimingEventInfo timingEvent)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(TimingCollector), $"AddEvent({timingEvent}, {this._partialEvent})", ParentRequest.Context);
|
||||
|
||||
switch(timingEvent.Event)
|
||||
{
|
||||
case TimingEvents.StartNext:
|
||||
if (this._partialEvent.IsSet())
|
||||
{
|
||||
// If it's the same event name as the last one, merge the two event by not doing anything now.
|
||||
if (timingEvent.Name.Equals(this._partialEvent.EventName, StringComparison.OrdinalIgnoreCase))
|
||||
break;
|
||||
|
||||
AddEvent(this._partialEvent.EventName, this._partialEvent.StartedAt, timingEvent.Time - this._partialEvent.StartedAt);
|
||||
}
|
||||
|
||||
if (this.Finished != DateTime.MinValue)
|
||||
AddEvent(timingEvent.Name, timingEvent.Time, timingEvent.Time - this.Events[^1].Start);
|
||||
else if (timingEvent.Name != null)
|
||||
this._partialEvent = new PartialEvent(timingEvent.Name, timingEvent.Time);
|
||||
break;
|
||||
|
||||
case TimingEvents.Finish:
|
||||
AddEvent(this._partialEvent.EventName, this._partialEvent.StartedAt, timingEvent.Time - this._partialEvent.StartedAt);
|
||||
|
||||
this._partialEvent = default;
|
||||
var now = DateTime.UtcNow;
|
||||
AddEvent(TimingEventNames.Finished, now, now - this.Created);
|
||||
|
||||
this.Finished = now;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(TimingCollector), this.ToString(),ParentRequest.Context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the event happened and for how long.
|
||||
/// </summary>
|
||||
internal void AddEvent(string name, DateTime when, TimeSpan duration)
|
||||
{
|
||||
if (this.Events == null)
|
||||
this.Events = new List<TimingEvent>(16);
|
||||
|
||||
// Events can have zero ms durations, this block would set the same duration as the previous one,
|
||||
// and that's clearly wrong.
|
||||
/*if (duration == TimeSpan.Zero)
|
||||
{
|
||||
DateTime prevEventAt = this.Created;
|
||||
if (this.Events.Count > 0)
|
||||
prevEventAt = this.Events[this.Events.Count - 1].Start;
|
||||
duration = when - prevEventAt;
|
||||
}*/
|
||||
|
||||
//if (name != TimingEventNames.Queued)
|
||||
//{
|
||||
// int idx = this.Events.FindIndex(e => e.Name.Equals(name));
|
||||
// if (idx != -1)
|
||||
// HTTPManager.Logger.Warning(nameof(TimingCollector), $"'{name}' already present at idx '{idx}'({this.Events[idx].Start.ToString("hh:mm:ss.fffffff")}) new event time: {when.ToString("hh:mm:ss.fffffff")}! Current timing table: {this.ToString()}", this.ParentRequest.Context);
|
||||
//}
|
||||
|
||||
this.Events.Add(new TimingEvent(name, when, duration));
|
||||
}
|
||||
|
||||
public TimingEvent FindFirst(string name)
|
||||
{
|
||||
if (this.Events == null)
|
||||
return TimingEvent.Empty;
|
||||
|
||||
for (int i = 0; i < this.Events.Count; ++i)
|
||||
{
|
||||
if (this.Events[i].Name == name)
|
||||
return this.Events[i];
|
||||
}
|
||||
|
||||
return TimingEvent.Empty;
|
||||
}
|
||||
|
||||
public TimingEvent FindLast(string name)
|
||||
{
|
||||
if (this.Events == null)
|
||||
return TimingEvent.Empty;
|
||||
|
||||
for (int i = this.Events.Count - 1; i >= 0; --i)
|
||||
{
|
||||
if (this.Events[i].Name == name)
|
||||
return this.Events[i];
|
||||
}
|
||||
|
||||
return TimingEvent.Empty;
|
||||
}
|
||||
|
||||
public TimingEvent FindNext(TimingEvent relativeTo, string name)
|
||||
{
|
||||
int from = 0;
|
||||
|
||||
if (relativeTo != TimingEvent.Empty)
|
||||
{
|
||||
for (int i = 0; i < this.Events.Count; i++)
|
||||
{
|
||||
var ev = this.Events[i];
|
||||
|
||||
if (relativeTo.Equals(ev))
|
||||
{
|
||||
from = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = from; i < this.Events.Count; i++)
|
||||
{
|
||||
var ev = this.Events[i];
|
||||
|
||||
if (ev.Name == name)
|
||||
return ev;
|
||||
}
|
||||
|
||||
return TimingEvent.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = StringBuilderPool.Get(0);
|
||||
|
||||
sb.Append("{\"Created\": \"");
|
||||
sb.Append(this.Created.ToString("yyyy-MM-dd hh:mm:ss.fffffff"));
|
||||
|
||||
sb.Append("\",\"Finished\": \"");
|
||||
sb.Append(this.Finished.ToString("yyyy-MM-dd hh:mm:ss.fffffff"));
|
||||
|
||||
if (this.Events != null)
|
||||
{
|
||||
sb.Append("\", \"Events\": ");
|
||||
sb.Append('[');
|
||||
|
||||
for (int i = 0; i < this.Events.Count; ++i)
|
||||
{
|
||||
var @event = this.Events[i];
|
||||
|
||||
sb.Append("{\"Name\": \"");
|
||||
sb.Append(@event.Name);
|
||||
sb.Append("\", \"Duration\": \"");
|
||||
sb.Append(@event.Duration);
|
||||
sb.Append("\"}");
|
||||
|
||||
if (i < this.Events.Count - 1)
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
sb.Append(']');
|
||||
}
|
||||
|
||||
sb.Append('}');
|
||||
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Timings/TimingCollector.cs.meta
Normal file
18
Runtime/HTTP/Request/Timings/TimingCollector.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d71df0a057c2faa46a145bf71a3c2f1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Timings/TimingCollector.cs
|
||||
uploadId: 783279
|
||||
73
Runtime/HTTP/Request/Timings/TimingEvent.cs
Normal file
73
Runtime/HTTP/Request/Timings/TimingEvent.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Request.Timings
|
||||
{
|
||||
/// <summary>
|
||||
/// Struct to hold information about one timing event recorded for a <see cref="HTTPRequest"/>. Timing events are managed by the <see cref="TimingCollector"/>.
|
||||
/// </summary>
|
||||
public struct TimingEvent : IEquatable<TimingEvent>
|
||||
{
|
||||
public static readonly TimingEvent Empty = new TimingEvent(null, TimeSpan.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Name of the event
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the event.
|
||||
/// </summary>
|
||||
public readonly TimeSpan Duration;
|
||||
|
||||
/// <summary>
|
||||
/// When the event started.
|
||||
/// </summary>
|
||||
public readonly DateTime Start;
|
||||
|
||||
public TimingEvent(string name, TimeSpan duration)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Duration = duration;
|
||||
this.Start = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public TimingEvent(string name, DateTime when, TimeSpan duration)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Start = when;
|
||||
this.Duration = duration;
|
||||
}
|
||||
|
||||
public TimeSpan CalculateDuration(TimingEvent tEvent)
|
||||
{
|
||||
if (this.Start < tEvent.Start)
|
||||
return tEvent.Start - this.Start;
|
||||
|
||||
return this.Start - tEvent.Start;
|
||||
}
|
||||
|
||||
public bool Equals(TimingEvent other)
|
||||
{
|
||||
return this.Name == other.Name &&
|
||||
this.Duration == other.Duration &&
|
||||
this.Start == other.Start;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is TimingEvent)
|
||||
return this.Equals((TimingEvent)obj);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (this.Name != null ? this.Name.GetHashCode() : 0) ^ this.Duration.GetHashCode() ^ this.Start.GetHashCode();
|
||||
|
||||
public static bool operator ==(TimingEvent lhs, TimingEvent rhs) => lhs.Equals(rhs);
|
||||
|
||||
public static bool operator !=(TimingEvent lhs, TimingEvent rhs) => !lhs.Equals(rhs);
|
||||
|
||||
public override string ToString() => $"['{this.Name}': {this.Duration} ({this.Start.ToString("hh:mm:ss.fffffff")})]";
|
||||
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Timings/TimingEvent.cs.meta
Normal file
18
Runtime/HTTP/Request/Timings/TimingEvent.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0660f6d607bdf3c4595e0544ca50dc5b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Timings/TimingEvent.cs
|
||||
uploadId: 783279
|
||||
39
Runtime/HTTP/Request/Timings/TimingEventInfo.cs
Normal file
39
Runtime/HTTP/Request/Timings/TimingEventInfo.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Request.Timings
|
||||
{
|
||||
public enum TimingEvents
|
||||
{
|
||||
StartNext,
|
||||
Finish
|
||||
}
|
||||
|
||||
public readonly struct TimingEventInfo
|
||||
{
|
||||
public readonly HTTPRequest SourceRequest;
|
||||
public readonly TimingEvents Event;
|
||||
|
||||
public readonly string Name;
|
||||
public readonly DateTime Time;
|
||||
|
||||
public TimingEventInfo(HTTPRequest parentRequest, TimingEvents timingEvent)
|
||||
{
|
||||
this.SourceRequest = parentRequest;
|
||||
this.Event = timingEvent;
|
||||
|
||||
this.Name = null;
|
||||
this.Time = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public TimingEventInfo(HTTPRequest parentRequest, TimingEvents timingEvent, string eventName)
|
||||
{
|
||||
this.SourceRequest = parentRequest;
|
||||
this.Event = timingEvent;
|
||||
|
||||
this.Name = eventName;
|
||||
this.Time = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{Event} \"{Name}\", Time: \"{Time.ToString("hh:mm:ss.fffffff")}\", Source: {SourceRequest.Context.Hash}]";
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Timings/TimingEventInfo.cs.meta
Normal file
18
Runtime/HTTP/Request/Timings/TimingEventInfo.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c72c6b44e87158408cc72cb04fc43ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Timings/TimingEventInfo.cs
|
||||
uploadId: 783279
|
||||
23
Runtime/HTTP/Request/Timings/TimingEventNames.cs
Normal file
23
Runtime/HTTP/Request/Timings/TimingEventNames.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace Best.HTTP.Request.Timings
|
||||
{
|
||||
public static class TimingEventNames
|
||||
{
|
||||
public const string Initial = "Initial";
|
||||
public const string Queued = "Queued";
|
||||
//public const string Queued_For_Redirection = "Queued for redirection";
|
||||
public const string ProxyDetection = "Proxy Detection";
|
||||
public const string DNS_Lookup = "DNS Lookup";
|
||||
public const string TCP_Connection = "TCP Connection";
|
||||
public const string Proxy_Negotiation = "Proxy Negotiation";
|
||||
public const string TLS_Negotiation = "TLS Negotiation";
|
||||
public const string Request_Sent = "Request Sent";
|
||||
public const string Waiting_TTFB = "Waiting (TTFB)";
|
||||
public const string Headers = "Headers";
|
||||
//public const string Loading_From_Cache = "Loading from Cache";
|
||||
//public const string Writing_To_Cache = "Writing to Cache";
|
||||
public const string Response_Received = "Response Received";
|
||||
//public const string Queued_For_Disptach = "Queued for Dispatch";
|
||||
public const string Finished = "Finished in";
|
||||
public const string Callback = "Callback";
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Timings/TimingEventNames.cs.meta
Normal file
18
Runtime/HTTP/Request/Timings/TimingEventNames.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0efad4dc5671b18459733670a6241006
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Timings/TimingEventNames.cs
|
||||
uploadId: 783279
|
||||
8
Runtime/HTTP/Request/Upload.meta
Normal file
8
Runtime/HTTP/Request/Upload.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e29c95165d45184d883ca5f901223f8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
23
Runtime/HTTP/Request/Upload/BodyLengths.cs
Normal file
23
Runtime/HTTP/Request/Upload/BodyLengths.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace Best.HTTP.Request.Upload
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides constants representing different, special body lengths for HTTP requests with upload streams.
|
||||
/// </summary>
|
||||
public static class BodyLengths
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="UploadStreamBase"/>'s length is unknown and the plugin have to send data with '<c>chunked</c>' transfer-encoding.
|
||||
/// </summary>
|
||||
public const long UnknownWithChunkedTransferEncoding = -2;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="UploadStreamBase"/>'s length is unknown and the plugin have to send data as-is, without any encoding.
|
||||
/// </summary>
|
||||
public const long UnknownRaw = -1;
|
||||
|
||||
/// <summary>
|
||||
/// No content to send.
|
||||
/// </summary>
|
||||
public const long NoBody = 0;
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Upload/BodyLengths.cs.meta
Normal file
18
Runtime/HTTP/Request/Upload/BodyLengths.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 517183824f3e02745ba784c260724b60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Upload/BodyLengths.cs
|
||||
uploadId: 783279
|
||||
178
Runtime/HTTP/Request/Upload/DynamicUploadStream.cs
Normal file
178
Runtime/HTTP/Request/Upload/DynamicUploadStream.cs
Normal file
@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
namespace Best.HTTP.Request.Upload
|
||||
{
|
||||
/// <summary>
|
||||
/// A specialized upload stream designed to handle data that's generated on-the-fly or periodically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation is designed to handle scenarios where data may not always be immediately available for upload.
|
||||
/// The request will remain active until the <see cref="Complete"/> method is invoked, ensuring that data can continue to be fed into the stream even if it's temporarily empty during a Read operation.
|
||||
/// </remarks>
|
||||
public sealed class DynamicUploadStream : UploadStreamBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the length of the upload stream.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation returns a constant value of <c>-1</c>, indicating that the length of the data to be uploaded is unknown. When the processing connection encounters this value, it should utilize chunked uploading to handle the data transfer.
|
||||
/// </remarks>
|
||||
/// <value>The constant value of <c>-1</c>, representing unknown length.</value>
|
||||
public override long Length
|
||||
=> BodyLengths.UnknownWithChunkedTransferEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of data currently buffered and ready for upload.
|
||||
/// </summary>
|
||||
/// <value>The length of buffered data in bytes.</value>
|
||||
public long BufferedLength
|
||||
=> this._bufferedLength;
|
||||
|
||||
private long _bufferedLength;
|
||||
private bool _isCompleted;
|
||||
private ConcurrentQueue<BufferSegment> _chunks = new ConcurrentQueue<BufferSegment>();
|
||||
private BufferSegment _current;
|
||||
private string _contentType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the DynamicUploadStream class with an optional content type.
|
||||
/// </summary>
|
||||
/// <param name="contentType">The MIME type of the content to be uploaded. Defaults to "<c>application/octet-stream</c>" if not specified.</param>
|
||||
/// <remarks>
|
||||
/// This constructor allows the caller to specify the content type of the data to be uploaded. If not provided, it defaults to a general binary data type.
|
||||
/// </remarks>
|
||||
public DynamicUploadStream(string contentType = "application/octet-stream")
|
||||
=> this._contentType = contentType;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the necessary headers before sending the request.
|
||||
/// </summary>
|
||||
public override void BeforeSendHeaders(HTTPRequest request)
|
||||
=> request.SetHeader("content-type", this._contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the stream before the request body is sent.
|
||||
/// </summary>
|
||||
public override void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler)
|
||||
=> base.BeforeSendBody(request, threadSignaler);
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the stream to be uploaded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned value indicates the state of the stream:
|
||||
/// <list type="bullet">
|
||||
/// <item><term>-1</term><description>More data is expected in the future, but isn't currently available. When new data is ready, the IThreadSignaler must be notified.</description></item>
|
||||
/// <item><term>0</term><description>The stream has been closed and no more data will be provided.</description></item>
|
||||
/// <item><description>Otherwise it returns with the number bytes copied to the buffer.</description></item>
|
||||
/// </list>
|
||||
/// Note: A zero return value can come after a -1 return, indicating a transition from waiting to completion.
|
||||
/// </remarks>
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int readCount = 0;
|
||||
|
||||
while (readCount < count && (_current != BufferSegment.Empty || _chunks.TryDequeue(out _current)))
|
||||
{
|
||||
int copyCount = Math.Min(count - readCount, _current.Count);
|
||||
|
||||
Array.Copy(_current.Data, _current.Offset, buffer, offset, copyCount);
|
||||
|
||||
readCount += copyCount;
|
||||
offset += copyCount;
|
||||
|
||||
if (_current.Offset + copyCount >= _current.Count)
|
||||
{
|
||||
BufferPool.Release(_current);
|
||||
_current = BufferSegment.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
_current = _current.Slice(_current.Offset + copyCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._isCompleted && readCount == 0)
|
||||
return UploadReadConstants.WaitForMore;
|
||||
|
||||
Interlocked.Add(ref this._bufferedLength, -readCount);
|
||||
|
||||
return readCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes data to the stream, making it available for upload.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// After writing data to the stream using this method, the connection is signaled that data is available to send.
|
||||
/// </remarks>
|
||||
/// <param name="buffer">The array of unsigned bytes from which to copy count bytes to the current stream.</param>
|
||||
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
|
||||
/// <param name="count">The number of bytes to be written to the current stream.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to write after the stream has been marked as complete.</exception>
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
if (this._isCompleted)
|
||||
throw new InvalidOperationException("Complete() already called on the stream!");
|
||||
|
||||
var localCopy = BufferPool.Get(count, true, base.Signaler?.Context);
|
||||
Array.Copy(buffer, 0, localCopy, offset, count);
|
||||
|
||||
Write(localCopy.AsBuffer(count));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a segment of data to the stream, making it available for upload.
|
||||
/// </summary>
|
||||
/// <param name="segment">A segment of data to be written to the stream.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown when trying to write after the stream has been marked as complete.</exception>
|
||||
/// <remarks>
|
||||
/// After writing a segment to the stream using this method, the connection is signaled that data is available to send.
|
||||
/// </remarks>
|
||||
public void Write(BufferSegment segment)
|
||||
{
|
||||
if (this._isCompleted)
|
||||
throw new InvalidOperationException("Complete() already called on the stream!");
|
||||
|
||||
if (segment.Data == null)
|
||||
return;
|
||||
|
||||
this._chunks.Enqueue(segment);
|
||||
Interlocked.Add(ref this._bufferedLength, segment.Count);
|
||||
|
||||
this.Signaler?.SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the stream as complete, signaling that no more data will be added.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All remaining buffered data will be sent to the server.
|
||||
/// </remarks>
|
||||
public void Complete()
|
||||
{
|
||||
if (this._isCompleted)
|
||||
throw new InvalidOperationException("Complete() already called on the stream!");
|
||||
|
||||
this._isCompleted = true;
|
||||
base.Signaler?.SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => true;
|
||||
public override long Position { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
|
||||
public override long Seek(long offset, System.IO.SeekOrigin origin) => throw new System.NotImplementedException();
|
||||
public override void SetLength(long value) => throw new System.NotImplementedException();
|
||||
public override void Flush() { }
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Upload/DynamicUploadStream.cs.meta
Normal file
18
Runtime/HTTP/Request/Upload/DynamicUploadStream.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebb54a5ce9460664f826e2ff8b5ae38f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Upload/DynamicUploadStream.cs
|
||||
uploadId: 783279
|
||||
8
Runtime/HTTP/Request/Upload/Forms.meta
Normal file
8
Runtime/HTTP/Request/Upload/Forms.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e43c3ab19497cb84089ea5d4aefa8591
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
271
Runtime/HTTP/Request/Upload/Forms/MultipartFormDataStream.cs
Normal file
271
Runtime/HTTP/Request/Upload/Forms/MultipartFormDataStream.cs
Normal file
@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
|
||||
namespace Best.HTTP.Request.Upload.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="UploadStreamBase"/> based implementation of the <c>multipart/form-data</c> Content-Type. It's very memory-effective, streams are read into memory in chunks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Less than zero(<c>-1</c>) value </term>
|
||||
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Zero (<c>0</c>)</term>
|
||||
/// <description> means that the stream is closed, no more data can be expected.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// A zero value to signal stream closure can follow a less than zero value.</para>
|
||||
/// </remarks>
|
||||
public sealed class MultipartFormDataStream : UploadStreamBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the length of this multipart/form-data stream.
|
||||
/// </summary>
|
||||
public override long Length { get => this._length; }
|
||||
private long _length;
|
||||
|
||||
/// <summary>
|
||||
/// A random boundary generated in the constructor.
|
||||
/// </summary>
|
||||
private string boundary;
|
||||
|
||||
private Queue<StreamList> fields = new Queue<StreamList>(1);
|
||||
private StreamList currentField;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MultipartFormDataStream class.
|
||||
/// </summary>
|
||||
public MultipartFormDataStream()
|
||||
{
|
||||
var hash = new Hash128();
|
||||
hash.Append(this.GetHashCode());
|
||||
|
||||
this.boundary = $"com.Tivadar.Best.HTTP.boundary.{hash}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MultipartFormDataStream class with a custom boundary.
|
||||
/// </summary>
|
||||
public MultipartFormDataStream(string boundary)
|
||||
{
|
||||
this.boundary = boundary;
|
||||
}
|
||||
|
||||
public override void BeforeSendHeaders(HTTPRequest request)
|
||||
{
|
||||
request.SetHeader("Content-Type", $"multipart/form-data; boundary=\"{this.boundary}\"");
|
||||
|
||||
var boundaryStream = new BufferPoolMemoryStream();
|
||||
boundaryStream.WriteLine("--" + this.boundary + "--");
|
||||
boundaryStream.Position = 0;
|
||||
|
||||
this.fields.Enqueue(new StreamList(boundaryStream));
|
||||
|
||||
if (this._length >= 0)
|
||||
this._length += boundaryStream.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a textual field to the multipart/form-data stream.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="value">The textual value of the field.</param>
|
||||
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
|
||||
public MultipartFormDataStream AddField(string fieldName, string value)
|
||||
=> AddField(fieldName, value, System.Text.Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a textual field to the multipart/form-data stream.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="value">The textual value of the field.</param>
|
||||
/// <param name="encoding">The encoding to use for the value.</param>
|
||||
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
|
||||
public MultipartFormDataStream AddField(string fieldName, string value, System.Text.Encoding encoding)
|
||||
{
|
||||
var enc = encoding ?? System.Text.Encoding.UTF8;
|
||||
var byteCount = enc.GetByteCount(value);
|
||||
var buffer = BufferPool.Get(byteCount, true);
|
||||
var stream = new BufferPoolMemoryStream();
|
||||
|
||||
enc.GetBytes(value, 0, value.Length, buffer, 0);
|
||||
|
||||
stream.Write(buffer, 0, byteCount);
|
||||
|
||||
stream.Position = 0;
|
||||
|
||||
string mime = encoding != null ? "text/plain; charset=" + encoding.WebName : null;
|
||||
return AddStreamField(fieldName, stream, null, mime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stream field to the multipart/form-data stream.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="data">The data containing the field data.</param>
|
||||
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
|
||||
public MultipartFormDataStream AddField(string fieldName, byte[] data)
|
||||
=> AddStreamField(fieldName, new MemoryStream(data));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stream field to the multipart/form-data stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing the field data.</param>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
|
||||
public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream)
|
||||
=> AddStreamField(fieldName, stream, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stream field to the multipart/form-data stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing the field data.</param>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="fileName">The name of the file, if applicable.</param>
|
||||
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
|
||||
public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream, string fileName)
|
||||
=> AddStreamField(fieldName, stream, fileName, null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stream field to the multipart/form-data stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing the field data.</param>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="fileName">The name of the file, if applicable.</param>
|
||||
/// <param name="mimeType">The MIME type of the content.</param>
|
||||
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
|
||||
public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream, string fileName, string mimeType)
|
||||
{
|
||||
var header = new BufferPoolMemoryStream();
|
||||
header.WriteLine("--" + this.boundary);
|
||||
header.WriteLine("Content-Disposition: form-data; name=\"" + fieldName + "\"" + (!string.IsNullOrEmpty(fileName) ? "; filename=\"" + fileName + "\"" : string.Empty));
|
||||
|
||||
// Set up Content-Type head for the form.
|
||||
if (!string.IsNullOrEmpty(mimeType))
|
||||
header.WriteLine("Content-Type: " + mimeType);
|
||||
|
||||
header.WriteLine();
|
||||
header.Position = 0;
|
||||
|
||||
var footer = new BufferPoolMemoryStream();
|
||||
footer.Write(EOL, 0, EOL.Length);
|
||||
footer.Position = 0;
|
||||
|
||||
// all wrapped streams going to be disposed by the StreamList wrapper.
|
||||
var wrapper = new StreamList(header, stream, footer);
|
||||
|
||||
try
|
||||
{
|
||||
if (this._length >= 0)
|
||||
this._length += wrapper.Length;
|
||||
}
|
||||
catch
|
||||
{
|
||||
this._length = -1;
|
||||
}
|
||||
|
||||
this.fields.Enqueue(wrapper);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the final boundary to the multipart/form-data stream before sending the request body.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <param name="threadSignaler">The thread signaler for handling asynchronous operations.</param>
|
||||
public override void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler)
|
||||
{
|
||||
base.BeforeSendBody(request, threadSignaler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads data from the multipart/form-data stream into the provided buffer.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to read data into.</param>
|
||||
/// <param name="offset">The starting offset in the buffer.</param>
|
||||
/// <param name="length">The maximum number of bytes to read.</param>
|
||||
/// <returns>The number of bytes read into the buffer.</returns>
|
||||
public override int Read(byte[] buffer, int offset, int length)
|
||||
{
|
||||
if (this.currentField == null && this.fields.Count == 0)
|
||||
return -1;
|
||||
|
||||
if (this.currentField == null && this.fields.Count > 0)
|
||||
this.currentField = this.fields.Dequeue();
|
||||
|
||||
int readCount = 0;
|
||||
|
||||
do
|
||||
{
|
||||
// read from the current stream
|
||||
int count = this.currentField.Read(buffer, offset + readCount, length - readCount);
|
||||
|
||||
if (count > 0)
|
||||
readCount += count;
|
||||
else
|
||||
{
|
||||
// if the current field's stream is empty, go for the next one.
|
||||
|
||||
// dispose the current one first
|
||||
try
|
||||
{
|
||||
this.currentField.Dispose();
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
// no more fields/streams? exit
|
||||
if (this.fields.Count == 0)
|
||||
break;
|
||||
|
||||
// grab the next one
|
||||
this.currentField = this.fields.Dequeue();
|
||||
}
|
||||
|
||||
// exit when we reach the length goal, or there's no more streams to read from
|
||||
} while (readCount < length && this.fields.Count > 0);
|
||||
|
||||
return readCount;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (fields != null)
|
||||
{
|
||||
foreach (var field in fields)
|
||||
field.Dispose();
|
||||
fields.Clear();
|
||||
fields = null;
|
||||
}
|
||||
|
||||
currentField?.Dispose();
|
||||
currentField = null;
|
||||
}
|
||||
|
||||
public override bool CanRead { get { return true; } }
|
||||
public override bool CanSeek { get { return false; } }
|
||||
public override bool CanWrite { get { return false; } }
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
public override void Flush() { }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d54866f088b8c154db3325a2771b58af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Upload/Forms/MultipartFormDataStream.cs
|
||||
uploadId: 783279
|
||||
158
Runtime/HTTP/Request/Upload/Forms/UrlEncodedStream.cs
Normal file
158
Runtime/HTTP/Request/Upload/Forms/UrlEncodedStream.cs
Normal file
@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
|
||||
namespace Best.HTTP.Request.Upload.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Readonly struct to hold key -> value pairs, where the value is either textual or binary.
|
||||
/// </summary>
|
||||
readonly struct FormField
|
||||
{
|
||||
public readonly string Key;
|
||||
public readonly string TextValue;
|
||||
public readonly byte[] BinaryValue;
|
||||
|
||||
public FormField(string key, string textValue)
|
||||
{
|
||||
this.Key = key;
|
||||
this.TextValue = textValue;
|
||||
this.BinaryValue = null;
|
||||
}
|
||||
|
||||
public FormField(string key, byte[] binaryValue)
|
||||
{
|
||||
this.Key = key;
|
||||
this.TextValue = null;
|
||||
this.BinaryValue = binaryValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="UploadStreamBase"/> implementation representing a stream that prepares and sends data as URL-encoded form data in an HTTP request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This stream is used to send data as URL-encoded form data in an HTTP request. It sets the <c>"Content-Type"</c> header to <c>"application/x-www-form-urlencoded"</c>.
|
||||
/// URL-encoded form data is typically used for submitting form data to a web server. It is commonly used in HTTP POST requests to send data to a server, such as submitting HTML form data.</para>
|
||||
///
|
||||
/// <para>The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Less than zero(<c>-1</c>) value </term>
|
||||
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Zero (<c>0</c>)</term>
|
||||
/// <description> means that the stream is closed, no more data can be expected.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// A zero value to signal stream closure can follow a less than zero value.</para>
|
||||
/// <para>While it's possible, it's not advised to send binary data url-encoded!</para>
|
||||
/// </remarks>
|
||||
public sealed class UrlEncodedStream : UploadStreamBase
|
||||
{
|
||||
private const int EscapeTreshold = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the stream.
|
||||
/// </summary>
|
||||
public override long Length { get => this._memoryStream.Length; }
|
||||
|
||||
private MemoryStream _memoryStream;
|
||||
|
||||
/// <summary>
|
||||
/// A list that holds the form's fields.
|
||||
/// </summary>
|
||||
private List<FormField> _fields = new List<FormField>();
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the HTTP request by adding the <c>"Content-Type"</c> header as <c>"application/x-www-form-urlencoded"</c>.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
public override void BeforeSendHeaders(HTTPRequest request)
|
||||
{
|
||||
request.SetHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
StringBuilder sb = StringBuilderPool.Get(_fields.Count * 4);
|
||||
|
||||
// Create a "field1=value1&field2=value2" formatted string
|
||||
for (int i = 0; i < _fields.Count; ++i)
|
||||
{
|
||||
var field = _fields[i];
|
||||
|
||||
if (i > 0)
|
||||
sb.Append("&");
|
||||
|
||||
sb.Append(EscapeString(field.Key));
|
||||
sb.Append("=");
|
||||
|
||||
if (!string.IsNullOrEmpty(field.TextValue) || field.BinaryValue == null)
|
||||
sb.Append(EscapeString(field.TextValue));
|
||||
else
|
||||
// If forced to this form type with binary data, we will create a base64 encoded string from it.
|
||||
sb.Append(Convert.ToBase64String(field.BinaryValue, 0, field.BinaryValue.Length));
|
||||
}
|
||||
|
||||
this._memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(StringBuilderPool.ReleaseAndGrab(sb)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds binary data to the form. It is not advised to send binary data with an URL-encoded form due to the conversion cost of binary to text conversion.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the field.</param>
|
||||
/// <param name="content">The binary data content.</param>
|
||||
/// <returns>The UrlEncodedStream instance for method chaining.</returns>
|
||||
public UrlEncodedStream AddBinaryData(string fieldName, byte[] content)
|
||||
{
|
||||
_fields.Add(new FormField(fieldName, content));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public UrlEncodedStream AddField(string fieldName, string value)
|
||||
{
|
||||
_fields.Add(new FormField(fieldName, value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => this._memoryStream.Read(buffer, offset, count);
|
||||
|
||||
private static string EscapeString(string originalString)
|
||||
{
|
||||
if (originalString.Length < EscapeTreshold)
|
||||
return Uri.EscapeDataString(originalString);
|
||||
else
|
||||
{
|
||||
int loops = originalString.Length / EscapeTreshold;
|
||||
StringBuilder sb = StringBuilderPool.Get(loops); //new StringBuilder(loops);
|
||||
|
||||
for (int i = 0; i <= loops; i++)
|
||||
sb.Append(i < loops ?
|
||||
Uri.EscapeDataString(originalString.Substring(EscapeTreshold * i, EscapeTreshold)) :
|
||||
Uri.EscapeDataString(originalString.Substring(EscapeTreshold * i)));
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
this._memoryStream?.Dispose();
|
||||
this._memoryStream = null;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
|
||||
public override void Flush() => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Upload/Forms/UrlEncodedStream.cs.meta
Normal file
18
Runtime/HTTP/Request/Upload/Forms/UrlEncodedStream.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a92cdbf082f89b42b5db8ba2a373498
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Upload/Forms/UrlEncodedStream.cs
|
||||
uploadId: 783279
|
||||
101
Runtime/HTTP/Request/Upload/JSonDataStream.cs
Normal file
101
Runtime/HTTP/Request/Upload/JSonDataStream.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Request.Upload
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="UploadStreamBase"/> implementation to convert and upload the object as JSON data. It sets the <c>"Content-Type"</c> header to <c>"application/json; charset=utf-8"</c>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object to be converted to JSON.</typeparam>
|
||||
/// <remarks>
|
||||
/// <para>This stream keeps a reference to the object until the preparation in <see cref="BeforeSendHeaders"/>. This means, changes to the object after passing it to the constructor will be reflected in the sent data too.</para>
|
||||
/// <para>The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Less than zero(<c>-1</c>) value </term>
|
||||
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Zero (<c>0</c>)</term>
|
||||
/// <description> means that the stream is closed, no more data can be expected.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// A zero value to signal stream closure can follow a less than zero value.</para>
|
||||
/// </remarks>
|
||||
public sealed class JSonDataStream<T> : UploadStreamBase
|
||||
{
|
||||
public override long Length { get => this._innerStream.Length; }
|
||||
|
||||
private BufferPoolMemoryStream _innerStream;
|
||||
private T _objToJson;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JSonDataStream{T}"/> class with the specified object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to be converted to JSON and uploaded.</param>
|
||||
public JSonDataStream(T obj) => this._objToJson = obj;
|
||||
|
||||
/// <summary>
|
||||
/// Called before sending out the request's headers. It sets the <c>"Content-Type"</c> header to <c>"application/json; charset=utf-8"</c>.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
public override void BeforeSendHeaders(HTTPRequest request)
|
||||
{
|
||||
request.SetHeader("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
if (this._innerStream != null)
|
||||
{
|
||||
this._innerStream.Position = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var json = Best.HTTP.JSON.LitJson.JsonMapper.ToJson(this._objToJson);
|
||||
this._objToJson = default;
|
||||
|
||||
var byteLength = Encoding.UTF8.GetByteCount(json);
|
||||
var buffer = BufferPool.Get(byteLength, true);
|
||||
Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
|
||||
|
||||
this._innerStream = new BufferPoolMemoryStream(byteLength);
|
||||
this._innerStream.Write(buffer, 0, byteLength);
|
||||
this._innerStream.Position = 0;
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
|
||||
/// </summary>
|
||||
/// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and ( <paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
|
||||
/// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
|
||||
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
|
||||
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
|
||||
public override int Read(byte[] buffer, int offset, int count) => this._innerStream.Read(buffer, offset, count);
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources used by the <see cref="JSonDataStream{T}"/> and optionally releases the managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
this._objToJson = default;
|
||||
this._innerStream?.Dispose();
|
||||
this._innerStream = null;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public override void Flush() { }
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
|
||||
public override void SetLength(long value) { throw new NotImplementedException(); }
|
||||
public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); }
|
||||
}
|
||||
}
|
||||
18
Runtime/HTTP/Request/Upload/JSonDataStream.cs.meta
Normal file
18
Runtime/HTTP/Request/Upload/JSonDataStream.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4843451c7126126409eefb9945ddd4a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Upload/JSonDataStream.cs
|
||||
uploadId: 783279
|
||||
84
Runtime/HTTP/Request/Upload/UploadStreamBase.cs
Normal file
84
Runtime/HTTP/Request/Upload/UploadStreamBase.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
|
||||
namespace Best.HTTP.Request.Upload
|
||||
{
|
||||
// [Request Creation] --> [Request Queued] --> [UploadStream.SetupRequestHeaders call] --> [Send Request Headers] --> [UploadStream.PrepareToSend call] --> [UploadStream.Read to Send Request Body] -> [Dispose UploadStream]
|
||||
|
||||
public static class UploadReadConstants
|
||||
{
|
||||
public static int WaitForMore = -1;
|
||||
public static int Completed = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstract class to serve as a base for non-conventional streams used in HTTP requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Less than zero(<c>-1</c>)</term>
|
||||
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Zero (<c>0</c>)</term>
|
||||
/// <description> means that the stream is closed, no more data can be expected.</description>
|
||||
/// </item>
|
||||
/// <item><description>Otherwise it must return with the number bytes copied to the buffer.</description></item>
|
||||
/// </list>
|
||||
/// A zero value to signal stream closure can follow a less than zero value.
|
||||
/// </remarks>
|
||||
public abstract class UploadStreamBase : System.IO.Stream
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IThreadSignaler"/> object for signaling when new data is available.
|
||||
/// </summary>
|
||||
public IThreadSignaler Signaler { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes that the stream will upload.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The return value of Length is treated specially in the plugin:
|
||||
/// <list type="bullet">
|
||||
/// <item><term>-2</term><description>The stream's length is unknown and the plugin have to send data <c>with 'chunked' transfer-encoding</c>.</description></item>
|
||||
/// <item><term>-1</term><description>The stream's length is unknown and the plugin have to send data <c>as-is, without any encoding</c>.</description></item>
|
||||
/// <item><term>0</term><description>No content to send. The content-length header will contain zero (<c>0</c>).</description></item>
|
||||
/// <item><term>>0</term><description>Length of the content is known, will be sent <c>as-is, without any encoding</c>. The content-length header will contain zero (<c>0</c>).</description></item>
|
||||
/// </list>
|
||||
/// Constants for the first three points can be found in <see cref="Best.HTTP.Request.Upload.BodyLengths"/>.
|
||||
/// </remarks>
|
||||
public override long Length => throw new System.NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Called before sending out the request's headers. Perform content processing to calculate the final length if possible.
|
||||
/// In this function the implementor can set headers and other parameters to the request.
|
||||
/// </summary>
|
||||
/// <remarks>Typically called on a thread.</remarks>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> associated with the stream.</param>
|
||||
public abstract void BeforeSendHeaders(HTTPRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Called just before sending out the request's body, and saves the <see cref="IThreadSignaler"/> for signaling when new data is available.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTPRequest associated with the stream.</param>
|
||||
/// <param name="threadSignaler">The <see cref="IThreadSignaler"/> object to be used for signaling.</param>
|
||||
/// <remarks>Typically called on a separate thread.</remarks>
|
||||
|
||||
/// <summary>
|
||||
/// Called just before sending out the request's body, saves the <see cref="IThreadSignaler"/> that can be used for signaling when new data is available.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTPRequest associated with the stream.</param>
|
||||
/// <param name="threadSignaler">The <see cref="IThreadSignaler"/> object to be used for signaling.</param>
|
||||
/// <remarks>Typically called on a separate thread.</remarks>
|
||||
public virtual void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler) => this.Signaler = threadSignaler;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
this.Signaler = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
18
Runtime/HTTP/Request/Upload/UploadStreamBase.cs.meta
Normal file
18
Runtime/HTTP/Request/Upload/UploadStreamBase.cs.meta
Normal file
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5585905c4d10eba4ca0fe7d011d06b71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 267636
|
||||
packageName: Best HTTP
|
||||
packageVersion: 3.0.17
|
||||
assetPath: Packages/com.tivadar.best.http/Runtime/HTTP/Request/Upload/UploadStreamBase.cs
|
||||
uploadId: 783279
|
||||
Reference in New Issue
Block a user