add all
This commit is contained in:
8
Packages/com.tivadar.best.http/Runtime/HTTP/Caching.meta
Normal file
8
Packages/com.tivadar.best.http/Runtime/HTTP/Caching.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f98735e2cba6f5f4aa3fa3f53d864a85
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder struct for constructing an instance of the HTTPCache class with optional configuration options and callbacks.
|
||||
/// </summary>
|
||||
public struct HTTPCacheBuilder
|
||||
{
|
||||
private HTTPCacheOptions _options;
|
||||
private OnBeforeBeginCacheDelegate _callback;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration options for the HTTP cache.
|
||||
/// </summary>
|
||||
/// <param name="options">The <see cref="HTTPCacheOptions"/> containing cache configuration settings.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheBuilder WithOptions(HTTPCacheOptions options)
|
||||
{
|
||||
this._options = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration options for the HTTP cache using an <see cref="HTTPCacheOptionsBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="optionsBuilder">An <see cref="HTTPCacheOptionsBuilder"/> for building cache configuration settings.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheBuilder WithOptions(HTTPCacheOptionsBuilder optionsBuilder)
|
||||
{
|
||||
this._options = optionsBuilder.Build();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a callback delegate to be executed before caching of an entity begins.
|
||||
/// </summary>
|
||||
/// <param name="callback">The delegate to be executed before caching starts.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheBuilder WithBeforeBeginCacheCallback(OnBeforeBeginCacheDelegate callback)
|
||||
{
|
||||
this._callback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns an instance of the <see cref="HTTPCache"/> with the specified configuration options and callback delegate.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="HTTPCache"/> instance configured with the specified options and callback.</returns>
|
||||
public HTTPCache Build()
|
||||
=> new HTTPCache(this._options) { OnBeforeBeginCache = this._callback };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A builder struct for constructing an instance of <see cref="HTTPCacheOptions"/> with optional configuration settings.
|
||||
/// </summary>
|
||||
public struct HTTPCacheOptionsBuilder
|
||||
{
|
||||
private HTTPCacheOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum cache size for the HTTP cache.
|
||||
/// </summary>
|
||||
/// <param name="maxCacheSize">The maximum size, in bytes, that the cache can reach.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheOptionsBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheOptionsBuilder WithMaxCacheSize(ulong maxCacheSize)
|
||||
{
|
||||
this._options = this._options ?? new HTTPCacheOptions();
|
||||
this._options.MaxCacheSize = maxCacheSize;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum duration for which cached entries will be retained.
|
||||
/// By default all entities (even stalled ones) are kept cached until they are evicted to make room for new, fresh ones.
|
||||
/// </summary>
|
||||
/// <param name="olderThan">The maximum age for cached entries to be retained.</param>
|
||||
/// <returns>The current <see cref="HTTPCacheOptionsBuilder"/> instance for method chaining.</returns>
|
||||
public HTTPCacheOptionsBuilder WithDeleteOlderThen(TimeSpan olderThan)
|
||||
{
|
||||
this._options = this._options ?? new HTTPCacheOptions();
|
||||
this._options.DeleteOlder = olderThan;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and returns an instance of <see cref="HTTPCacheOptions"/> with the specified configuration settings.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="HTTPCacheOptions"/> instance configured with the specified settings.</returns>
|
||||
public HTTPCacheOptions Build()
|
||||
=> this._options ?? new HTTPCacheOptions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 114a3d036d69b7c4ca732b2f6a48b292
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1090
Packages/com.tivadar.best.http/Runtime/HTTP/Caching/HTTPCache.cs
Normal file
1090
Packages/com.tivadar.best.http/Runtime/HTTP/Caching/HTTPCache.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 202cffdfebb3b8e4ba485c44e5f5b1ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a writer for caching HTTP response content.
|
||||
/// </summary>
|
||||
public class HTTPCacheContentWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the parent HTTPCache instance associated with this content writer.
|
||||
/// </summary>
|
||||
public HTTPCache Cache { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash identifying the resource. If <see cref="Write(BufferSegment)"/> fails, it becomes an invalid one.
|
||||
/// </summary>
|
||||
public Hash128 Hash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected length of the content. Has a non-zero value only when the server is sending a "content-length" header.
|
||||
/// </summary>
|
||||
public ulong ExpectedLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes written to the cache.
|
||||
/// </summary>
|
||||
public ulong ProcessedLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Context of this cache writer used for logging.
|
||||
/// </summary>
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Underlying stream the download bytes are written into.
|
||||
/// </summary>
|
||||
private Stream _contentStream;
|
||||
|
||||
internal HTTPCacheContentWriter(HTTPCache cache, Hash128 hash, Stream contentStream, ulong expectedLength, LoggingContext loggingContext)
|
||||
{
|
||||
this.Cache = cache;
|
||||
this.Hash = hash;
|
||||
this._contentStream = contentStream;
|
||||
this.ExpectedLength = expectedLength;
|
||||
this.Context = loggingContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes content to the underlying stream.
|
||||
/// </summary>
|
||||
/// <param name="segment"><see cref="BufferSegment"/> holding a reference to the data and containing information about the offset and count of the valid range of data.</param>
|
||||
public void Write(BufferSegment segment)
|
||||
{
|
||||
if (!this.Hash.isValid)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
this._contentStream?.Write(segment.Data, segment.Offset, segment.Count);
|
||||
this.ProcessedLength += (ulong)segment.Count;
|
||||
|
||||
if (this.ProcessedLength > this.ExpectedLength)
|
||||
{
|
||||
if (!this.Cache.IsThereEnoughSpaceAfterMaintain(this.ProcessedLength, this.Context))
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPCacheContentWriter), $"Not enough space({this.ProcessedLength:N0}) in cache({this.Cache.CacheSize:N0}), even after Maintain!", this.Context);
|
||||
|
||||
this.Cache?.EndCache(this, false, this.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheContentWriter), $"{nameof(Write)}({segment}): {ex}", this.Context);
|
||||
|
||||
// EndCache will call Close, we don't have to in this catch block
|
||||
this.Cache?.EndCache(this, false, this.Context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the underlying stream and invalidate the hash.
|
||||
/// </summary>
|
||||
internal void Close()
|
||||
{
|
||||
this._contentStream?.Close();
|
||||
this._contentStream = null;
|
||||
|
||||
// this will set an invalid cache, further HTTPCaching.EndCache calls will return early.
|
||||
this.Hash = new Hash128();
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{nameof(HTTPCacheContentWriter)} {Hash} {ProcessedLength:N0}]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f1a3db4ad5927624182f465b44a2f2ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,733 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Databases;
|
||||
using Best.HTTP.Shared.Databases.Indexing;
|
||||
using Best.HTTP.Shared.Databases.Indexing.Comparers;
|
||||
using Best.HTTP.Shared.Databases.MetadataIndexFinders;
|
||||
using Best.HTTP.Shared.Databases.Utils;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
struct v128View
|
||||
{
|
||||
public ulong low;
|
||||
public ulong high;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible lock-states a cache-content can be in.
|
||||
/// </summary>
|
||||
public enum LockTypes : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No reads or writes are happening on the cached content.
|
||||
/// </summary>
|
||||
Unlocked,
|
||||
|
||||
/// <summary>
|
||||
/// There's one writer operating on the cached content. No other writes or reads allowed while this lock is held on the content.
|
||||
/// </summary>
|
||||
Write,
|
||||
|
||||
/// <summary>
|
||||
/// There's at least one read operation happening on the cached content. No writes allowed while this lock is held on the content.
|
||||
/// </summary>
|
||||
Read
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata stored for every cached content. It contains only limited data about the content to help early cache decision making and cache management.
|
||||
/// </summary>
|
||||
internal class CacheMetadata : Metadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique hash of the cached content, generated by <see cref="HTTPCache.CalculateHash(HTTPMethods, Uri)"/>.
|
||||
/// </summary>
|
||||
public UnityEngine.Hash128 Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the stored content in bytes.
|
||||
/// </summary>
|
||||
public ulong ContentLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the last time the content is accessed. Also initialized when the initial download completes.
|
||||
/// </summary>
|
||||
public DateTime LastAccessTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What kind of lock the content is currently in.
|
||||
/// </summary>
|
||||
public LockTypes Lock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of readers.
|
||||
/// </summary>
|
||||
public int ReadLockCount { get; set; }
|
||||
|
||||
public unsafe override void SaveTo(Stream stream)
|
||||
{
|
||||
base.SaveTo(stream);
|
||||
|
||||
var hash = this.Hash;
|
||||
v128View view = *(v128View*)&hash;
|
||||
|
||||
stream.EncodeUnsignedVariableByteInteger(view.low);
|
||||
stream.EncodeUnsignedVariableByteInteger(view.high);
|
||||
stream.EncodeUnsignedVariableByteInteger(ContentLength);
|
||||
stream.EncodeSignedVariableByteInteger(LastAccessTime.ToBinary() >> CacheMetadataContentParser.PrecisionShift);
|
||||
|
||||
// Only Write locks should persist as Reads doesn't alter the cached content
|
||||
if (this.Lock == LockTypes.Write)
|
||||
stream.EncodeUnsignedVariableByteInteger((byte)this.Lock);
|
||||
else
|
||||
stream.EncodeUnsignedVariableByteInteger((byte)LockTypes.Unlocked);
|
||||
}
|
||||
|
||||
public unsafe override void LoadFrom(Stream stream)
|
||||
{
|
||||
base.LoadFrom(stream);
|
||||
|
||||
var hash = default(v128View);
|
||||
hash.low = stream.DecodeUnsignedVariableByteInteger();
|
||||
hash.high = stream.DecodeUnsignedVariableByteInteger();
|
||||
|
||||
this.Hash = *(UnityEngine.Hash128*)&hash;
|
||||
|
||||
this.ContentLength = stream.DecodeUnsignedVariableByteInteger();
|
||||
this.LastAccessTime = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << CacheMetadataContentParser.PrecisionShift);
|
||||
|
||||
this.Lock = (LockTypes)stream.DecodeUnsignedVariableByteInteger();
|
||||
}
|
||||
|
||||
public override string ToString() => $"[Metadata {Hash}, {ContentLength:N0}, {Lock}, {ReadLockCount}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible caching flags that a `Cache-Control` header can send.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CacheFlags : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No special treatment required.
|
||||
/// </summary>
|
||||
None = 0x00,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the entity must be revalidated with the server or can be serverd directly from the cache without touching the server when the content is considered stale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-must-revalidate"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
MustRevalidate = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// If it's true, the client always have to revalidate the cached content when it's stale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
NoCache = 0x02
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached content associated with a <see cref="CacheMetadata"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This is NOT the cached content received from the server! It's for storing caching values to decide on how the content can be used.</remarks>
|
||||
internal sealed class CacheMetadataContent
|
||||
{
|
||||
/// <summary>
|
||||
/// ETag of the entity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string ETag;
|
||||
|
||||
/// <summary>
|
||||
/// LastModified date of the entity. Use ToString("r") to convert it to the format defined in RFC 1123.
|
||||
/// </summary>
|
||||
public DateTime LastModified = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// When the cache will expire.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-expires"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public DateTime Expires = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// The age that came with the response
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-age"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint Age;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum how long the entry should served from the cache without revalidation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age-2"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint MaxAge;
|
||||
|
||||
/// <summary>
|
||||
/// The Date that came with the response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public DateTime Date = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// It's a grace period to serve staled content without revalidation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc5861.html#section-3"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint StaleWhileRevalidate;
|
||||
|
||||
/// <summary>
|
||||
/// Allows the client to serve stale content if the server responds with an 5xx error.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc5861.html#section-4"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public uint StaleIfError;
|
||||
|
||||
/// <summary>
|
||||
/// bool values packed into one single flag.
|
||||
/// </summary>
|
||||
public CacheFlags Flags = CacheFlags.None;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the clock at the time of the request that resulted in the stored response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3-3.8"/></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public DateTime RequestTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// The value of the clock at the time the response was received.
|
||||
/// </summary>
|
||||
public DateTime ResponseTime = DateTime.MinValue;
|
||||
|
||||
public CacheMetadataContent()
|
||||
{
|
||||
}
|
||||
|
||||
internal void From(Dictionary<string, List<string>> headers)
|
||||
{
|
||||
this.ETag = headers.GetFirstHeaderValue("ETag").ToStr(this.ETag ?? string.Empty);
|
||||
this.Expires = headers.GetFirstHeaderValue("Expires").ToDateTime(this.Expires);
|
||||
if (this.Expires < DateTime.UtcNow)
|
||||
this.Expires = DateTime.MinValue;
|
||||
|
||||
this.LastModified = headers.GetFirstHeaderValue("Last-Modified").ToDateTime(DateTime.MinValue);
|
||||
this.Age = headers.GetFirstHeaderValue("Age").ToUInt32(this.Age);
|
||||
this.Date = headers.GetFirstHeaderValue("Date").ToDateTime(this.Date);
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1-4
|
||||
// When there is more than one value present for a given directive
|
||||
// (e.g., two Expires header field lines or multiple Cache-Control: max-age directives),
|
||||
// either the first occurrence should be used or the response should be considered stale.
|
||||
var cacheControl = headers.GetFirstHeaderValue("cache-control");
|
||||
if (!string.IsNullOrEmpty(cacheControl))
|
||||
{
|
||||
HeaderParser parser = new HeaderParser(cacheControl);
|
||||
|
||||
if (parser.Values != null)
|
||||
{
|
||||
this.Flags = CacheFlags.None;
|
||||
|
||||
for (int i = 0; i < parser.Values.Count; ++i)
|
||||
{
|
||||
var kvp = parser.Values[i];
|
||||
|
||||
switch (kvp.Key.ToLowerInvariant())
|
||||
{
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-max-age-2
|
||||
case "max-age":
|
||||
if (kvp.HasValue)
|
||||
{
|
||||
// Some cache proxies will return float values
|
||||
double maxAge;
|
||||
if (double.TryParse(kvp.Value, out maxAge) && maxAge >= 0)
|
||||
this.MaxAge = (uint)maxAge;
|
||||
else
|
||||
this.MaxAge = 0;
|
||||
}
|
||||
else
|
||||
this.MaxAge = 0;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3-8
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#cache-response-directive.s-maxage
|
||||
// If a response includes a Cache-Control header field with the max-age directive (Section 5.2.2.1), a recipient MUST ignore the Expires header field.
|
||||
this.Expires = DateTime.MinValue;
|
||||
break;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-must-revalidate
|
||||
case "must-revalidate": this.Flags |= CacheFlags.MustRevalidate; break;
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2
|
||||
case "no-cache": this.Flags |= CacheFlags.NoCache; break;
|
||||
// https://www.rfc-editor.org/rfc/rfc5861.html#section-3
|
||||
case "stale-while-revalidate": this.StaleWhileRevalidate = kvp.HasValue ? kvp.Value.ToUInt32(0) : 0; break;
|
||||
// https://www.rfc-editor.org/rfc/rfc5861.html#section-4
|
||||
case "stale-if-error": this.StaleIfError = kvp.HasValue ? kvp.Value.ToUInt32(0) : 0; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CacheMetadataContentParser : IDiskContentParser<CacheMetadataContent>
|
||||
{
|
||||
public const int PrecisionShift = 24;
|
||||
|
||||
public void Encode(Stream stream, CacheMetadataContent content)
|
||||
{
|
||||
stream.EncodeUnsignedVariableByteInteger(content.MaxAge);
|
||||
stream.WriteLengthPrefixedString(content.ETag);
|
||||
stream.EncodeSignedVariableByteInteger(content.LastModified.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeSignedVariableByteInteger(content.Expires.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeUnsignedVariableByteInteger(content.Age);
|
||||
stream.EncodeSignedVariableByteInteger(content.Date.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeUnsignedVariableByteInteger((byte)content.Flags);
|
||||
stream.EncodeUnsignedVariableByteInteger(content.StaleWhileRevalidate);
|
||||
stream.EncodeUnsignedVariableByteInteger(content.StaleIfError);
|
||||
stream.EncodeSignedVariableByteInteger(content.RequestTime.ToBinary() >> PrecisionShift);
|
||||
stream.EncodeSignedVariableByteInteger(content.ResponseTime.ToBinary() >> PrecisionShift);
|
||||
}
|
||||
|
||||
public CacheMetadataContent Parse(Stream stream, int length)
|
||||
{
|
||||
CacheMetadataContent content = new CacheMetadataContent();
|
||||
|
||||
content.MaxAge = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.ETag = stream.ReadLengthPrefixedString();
|
||||
content.LastModified = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.Expires = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.Age = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.Date = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.Flags = (CacheFlags)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.StaleWhileRevalidate = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.StaleIfError = (uint)stream.DecodeUnsignedVariableByteInteger();
|
||||
content.RequestTime = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
content.ResponseTime = DateTime.FromBinary(stream.DecodeSignedVariableByteInteger() << PrecisionShift);
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CacheMetadataIndexingService : IndexingService<CacheMetadataContent, CacheMetadata>
|
||||
{
|
||||
private AVLTree<UnityEngine.Hash128, int> index_Hash = new AVLTree<UnityEngine.Hash128, int>(new Hash128Comparer());
|
||||
|
||||
public override void Index(CacheMetadata metadata)
|
||||
{
|
||||
base.Index(metadata);
|
||||
this.index_Hash.Add(metadata.Hash, metadata.Index);
|
||||
}
|
||||
|
||||
public override void Remove(CacheMetadata metadata)
|
||||
{
|
||||
base.Remove(metadata);
|
||||
this.index_Hash.Remove(metadata.Hash);
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
this.index_Hash.Clear();
|
||||
}
|
||||
|
||||
public override IEnumerable<int> GetOptimizedIndexes() => this.index_Hash.WalkHorizontal();
|
||||
|
||||
public bool ContainsHash(UnityEngine.Hash128 hash) => this.index_Hash.ContainsKey(hash);
|
||||
|
||||
public List<int> FindByHash(UnityEngine.Hash128 hash) => this.index_Hash.Find(hash);
|
||||
}
|
||||
|
||||
internal sealed class CacheMetadataService : MetadataService<CacheMetadata, CacheMetadataContent>
|
||||
{
|
||||
public CacheMetadataService(IndexingService<CacheMetadataContent, CacheMetadata> indexingService, IEmptyMetadataIndexFinder<CacheMetadata> emptyMetadataIndexFinder)
|
||||
: base(indexingService, emptyMetadataIndexFinder)
|
||||
{
|
||||
}
|
||||
|
||||
public override CacheMetadata CreateFrom(Stream stream)
|
||||
{
|
||||
return base.CreateFrom(stream);
|
||||
}
|
||||
|
||||
public CacheMetadata Create(UnityEngine.Hash128 hash, CacheMetadataContent value, int filePos, int length)
|
||||
{
|
||||
var result = base.CreateDefault(value, filePos, length, (content, metadata) => {
|
||||
metadata.Hash = hash;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CacheDatabaseOptions : DatabaseOptions
|
||||
{
|
||||
public CacheDatabaseOptions() : base("CacheDatabase")
|
||||
{
|
||||
base.UseHashFile = false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HTTPCacheDatabase : Database<CacheMetadataContent, CacheMetadata, CacheMetadataIndexingService, CacheMetadataService>
|
||||
{
|
||||
public HTTPCacheDatabase(string directory)
|
||||
: this(directory, new CacheDatabaseOptions(), new CacheMetadataIndexingService())
|
||||
{
|
||||
}
|
||||
|
||||
private HTTPCacheDatabase(string directory,
|
||||
DatabaseOptions options,
|
||||
CacheMetadataIndexingService indexingService)
|
||||
: base(directory, options, indexingService, new CacheMetadataContentParser(), new CacheMetadataService(indexingService, new FindDeletedMetadataIndexFinder<CacheMetadata>()))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CacheMetadataContent FindByHashAndUpdateRequestTime(UnityEngine.Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(FindByHashAndUpdateRequestTime)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return default;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var (content, metadata) = FindContentAndMetadata(hash);
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
content.RequestTime = DateTime.UtcNow;
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public bool TryAcquireWriteLock(Hash128 hash, Dictionary<string, List<string>> headers, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireWriteLock)}({hash}, {headers?.Count})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return false;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
// FindMetadata filters out logically deleted entries, what we need here because we want to load it too.
|
||||
var metadata = FindMetadata(hash);
|
||||
CacheMetadataContent content = null;
|
||||
if (metadata != null)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireWriteLock)} - Metadata found: {metadata}", context);
|
||||
|
||||
if (metadata.Lock != LockTypes.Unlocked)
|
||||
return false;
|
||||
metadata.Lock = LockTypes.Write;
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
content = this.FromMetadata(metadata);
|
||||
content.From(headers);
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireWriteLock)} - Creating new DB entry", context);
|
||||
|
||||
content = new CacheMetadataContent();
|
||||
content.RequestTime = DateTime.UtcNow;
|
||||
content.From(headers);
|
||||
|
||||
(int filePos, int length) = this.DiskManager.Append(content);
|
||||
metadata = this.MetadataService.Create(hash, content, filePos, length);
|
||||
|
||||
metadata.Lock = LockTypes.Write;
|
||||
}
|
||||
|
||||
FlagDirty(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Update(Hash128 hash, Dictionary<string, List<string>> headers, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(Update)}({hash}, {headers?.Count})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return false;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var (content, metadata) = FindContentAndMetadata(hash);
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
content.From(headers);
|
||||
content.ResponseTime = DateTime.UtcNow;
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
|
||||
return content != null;
|
||||
}
|
||||
|
||||
public void ReleaseWriteLock(Hash128 hash, ulong length, bool onlyLockRelease, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ReleaseWriteLock)}({hash}, {length:N0}, {onlyLockRelease})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var (content, metadata) = FindContentAndMetadata(hash);
|
||||
if (content == null)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheDatabase), $"{nameof(ReleaseWriteLock)} - Couldn't find content!", context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.Lock != LockTypes.Write)
|
||||
HTTPManager.Logger.Error(nameof(HTTPCacheDatabase), $"{nameof(ReleaseWriteLock)} - Is NOT Write Locked! {metadata}", context);
|
||||
|
||||
metadata.Lock = LockTypes.Unlocked;
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
if (!onlyLockRelease)
|
||||
{
|
||||
metadata.ContentLength = length;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
metadata.LastAccessTime = now;
|
||||
content.ResponseTime = now;
|
||||
}
|
||||
|
||||
UpdateMetadataAndContent(metadata, content);
|
||||
}
|
||||
|
||||
FlagDirty(1);
|
||||
}
|
||||
|
||||
public bool TryAcquireReadLock(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireReadLock)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return false;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var metadata = FindMetadata(hash);
|
||||
if (metadata == null)
|
||||
return false;
|
||||
|
||||
if (metadata.Lock == LockTypes.Write)
|
||||
return false;
|
||||
|
||||
metadata.Lock = LockTypes.Read;
|
||||
// we are behind a write lock, it's safe to increment it like this
|
||||
metadata.ReadLockCount++;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(TryAcquireReadLock)} - {metadata}", context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ReleaseReadLock(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var metadata = FindMetadata(hash);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - Couldn't find metadata!", context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.Lock != LockTypes.Read)
|
||||
HTTPManager.Logger.Warning(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - Is NOT Locked!", context);
|
||||
|
||||
if (metadata.ReadLockCount == 0)
|
||||
HTTPManager.Logger.Error(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - ReadLockCount already zero!", context);
|
||||
|
||||
if (--metadata.ReadLockCount == 0)
|
||||
metadata.Lock = LockTypes.Unlocked;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ReleaseReadLock)} - {metadata}", context);
|
||||
}
|
||||
|
||||
internal ulong Delete(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(Delete)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return 0;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
// Don't use FindMetadata, because it would return null for a logically deleted metadata
|
||||
// so DeleteMetadata wouldn't be called!
|
||||
var byHash = this.IndexingService.FindByHash(hash);
|
||||
if (byHash == null || byHash.Count == 0)
|
||||
return 0;
|
||||
|
||||
var metadata = this.MetadataService.Metadatas[byHash[0]];
|
||||
|
||||
if (metadata == null)
|
||||
return 0;
|
||||
|
||||
var contentLength = metadata.ContentLength;
|
||||
|
||||
base.DeleteMetadata(metadata);
|
||||
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public void EnterWriteLock(LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(EnterWriteLock)}()", context);
|
||||
this.rwlock.EnterWriteLock();
|
||||
}
|
||||
|
||||
public void ExitWriteLock(LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(ExitWriteLock)}()", context);
|
||||
this.rwlock.ExitWriteLock();
|
||||
}
|
||||
|
||||
public void UpdateLastAccessTime(Hash128 hash, LoggingContext context)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPCacheDatabase), $"{nameof(UpdateLastAccessTime)}({hash})", context);
|
||||
|
||||
if (!hash.isValid)
|
||||
return;
|
||||
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
|
||||
var metadata = FindMetadata(hash);
|
||||
if (metadata != null)
|
||||
{
|
||||
metadata.LastAccessTime = DateTime.UtcNow;
|
||||
FlagDirty(1);
|
||||
}
|
||||
}
|
||||
|
||||
private CacheMetadata FindMetadata(Hash128 hash)
|
||||
{
|
||||
var byHash = this.IndexingService.FindByHash(hash);
|
||||
if (byHash == null || byHash.Count == 0)
|
||||
return null;
|
||||
|
||||
var metadata = this.MetadataService.Metadatas[byHash[0]];
|
||||
|
||||
if (metadata != null && metadata.IsDeleted)
|
||||
return null;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public (CacheMetadataContent, CacheMetadata) FindContentAndMetadataLocked(Hash128 hash)
|
||||
{
|
||||
using var _ = new WriteLock(this.rwlock);
|
||||
return FindContentAndMetadata(hash);
|
||||
}
|
||||
|
||||
private (CacheMetadataContent, CacheMetadata) FindContentAndMetadata(Hash128 hash)
|
||||
{
|
||||
var byHash = this.IndexingService.FindByHash(hash);
|
||||
if (byHash == null || byHash.Count == 0)
|
||||
return (null, null);
|
||||
|
||||
var metadata = this.MetadataService.Metadatas[byHash[0]];
|
||||
if (metadata != null && metadata.IsDeleted)
|
||||
return (null, null);
|
||||
|
||||
var content = this.FromMetadataIndex(metadata.Index);
|
||||
return (content, metadata);
|
||||
}
|
||||
|
||||
private void UpdateMetadataAndContent(Metadata metadata, CacheMetadataContent content)
|
||||
{
|
||||
this.DiskManager.SaveChanged(metadata, content);
|
||||
|
||||
FlagDirty(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dfaadcb592425e458ee6e074d4135c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Caching
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the configuration options for the HTTP cache.
|
||||
/// </summary>
|
||||
public sealed class HTTPCacheOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum duration for which cached entries will be retained.
|
||||
/// </summary>
|
||||
public TimeSpan DeleteOlder { get; internal set; } = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum size, in bytes, that the cache can reach.
|
||||
/// </summary>
|
||||
public ulong MaxCacheSize { get; internal set; } = 512 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HTTPCacheOptions"/> class with default settings.
|
||||
/// </summary>
|
||||
public HTTPCacheOptions()
|
||||
{
|
||||
// Default constructor with no arguments.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HTTPCacheOptions"/> class with custom settings.
|
||||
/// </summary>
|
||||
/// <param name="deleteOlder">The maximum age for cached entries to be retained.</param>
|
||||
/// <param name="maxCacheSize">The maximum size, in bytes, that the cache can reach.</param>
|
||||
public HTTPCacheOptions(TimeSpan deleteOlder, ulong maxCacheSize)
|
||||
{
|
||||
this.DeleteOlder = deleteOlder;
|
||||
this.MaxCacheSize = maxCacheSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d178cd9aca1ad2e45bbf4112596025e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.tivadar.best.http/Runtime/HTTP/Cookies.meta
Normal file
8
Packages/com.tivadar.best.http/Runtime/HTTP/Cookies.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc9fe3cd5cd50b844b941acec67f5e60
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
417
Packages/com.tivadar.best.http/Runtime/HTTP/Cookies/Cookie.cs
Normal file
417
Packages/com.tivadar.best.http/Runtime/HTTP/Cookies/Cookie.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Best.HTTP.Cookies
|
||||
{
|
||||
/// <summary>
|
||||
/// The Cookie implementation based on <see href="http://tools.ietf.org/html/rfc6265">RFC-6265</see>.
|
||||
/// </summary>
|
||||
public sealed class Cookie : IComparable<Cookie>, IEquatable<Cookie>
|
||||
{
|
||||
private const int Version = 1;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// The name of the cookie.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value of the cookie.
|
||||
/// </summary>
|
||||
public string Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Date when the Cookie is registered.
|
||||
/// </summary>
|
||||
public DateTime Date { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this Cookie last used in a request.
|
||||
/// </summary>
|
||||
public DateTime LastAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Expires attribute indicates the maximum lifetime of the cookie, represented as the date and time at which the cookie expires.
|
||||
/// The user agent is not required to retain the cookie until the specified date has passed.
|
||||
/// In fact, user agents often evict cookies due to memory pressure or privacy concerns.
|
||||
/// </summary>
|
||||
public DateTime Expires { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Max-Age attribute indicates the maximum lifetime of the cookie, represented as the number of seconds until the cookie expires.
|
||||
/// The user agent is not required to retain the cookie for the specified duration.
|
||||
/// In fact, user agents often evict cookies due to memory pressure or privacy concerns.
|
||||
/// </summary>
|
||||
public long MaxAge { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If a cookie has neither the Max-Age nor the Expires attribute, the user agent will retain the cookie until "the current session is over".
|
||||
/// </summary>
|
||||
public bool IsSession { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Domain attribute specifies those hosts to which the cookie will be sent.
|
||||
/// For example, if the value of the Domain attribute is "example.com", the user agent will include the cookie
|
||||
/// in the Cookie header when making HTTP requests to example.com, www.example.com, and www.corp.example.com.
|
||||
/// If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.
|
||||
/// </summary>
|
||||
public string Domain { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The scope of each cookie is limited to a set of paths, controlled by the Path attribute.
|
||||
/// If the server omits the Path attribute, the user agent will use the "directory" of the request-uri's path component as the default value.
|
||||
/// </summary>
|
||||
public string Path { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Secure attribute limits the scope of the cookie to "secure" channels (where "secure" is defined by the user agent).
|
||||
/// When a cookie has the Secure attribute, the user agent will include the cookie in an HTTP request only if the request is
|
||||
/// transmitted over a secure channel (typically HTTP over Transport Layer Security (TLS)).
|
||||
/// </summary>
|
||||
public bool IsSecure { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HttpOnly attribute limits the scope of the cookie to HTTP requests.
|
||||
/// In particular, the attribute instructs the user agent to omit the cookie when providing access to
|
||||
/// cookies via "non-HTTP" APIs (such as a web browser API that exposes cookies to scripts).
|
||||
/// </summary>
|
||||
public bool IsHttpOnly { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// SameSite prevents the browser from sending this cookie along with cross-site requests.
|
||||
/// The main goal is mitigate the risk of cross-origin information leakage.
|
||||
/// It also provides some protection against cross-site request forgery attacks.
|
||||
/// Possible values for the flag are lax or strict.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://web.dev/samesite-cookies-explained/">SameSite cookies explained</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string SameSite { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Constructors
|
||||
|
||||
public Cookie(string name, string value)
|
||||
: this(name, value, "/", string.Empty)
|
||||
{ }
|
||||
|
||||
public Cookie(string name, string value, string path)
|
||||
: this(name, value, path, string.Empty)
|
||||
{ }
|
||||
|
||||
public Cookie(string name, string value, string path, string domain)
|
||||
: this() // call the parameter-less constructor to set default values
|
||||
{
|
||||
this.Name = name;
|
||||
this.Value = value;
|
||||
this.Path = path;
|
||||
this.Domain = domain;
|
||||
}
|
||||
|
||||
public Cookie(Uri uri, string name, string value, DateTime expires, bool isSession = true)
|
||||
: this(name, value, uri.AbsolutePath, uri.Host)
|
||||
{
|
||||
this.Expires = expires;
|
||||
this.IsSession = isSession;
|
||||
this.Date = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public Cookie(Uri uri, string name, string value, long maxAge = -1, bool isSession = true)
|
||||
: this(name, value, uri.AbsolutePath, uri.Host)
|
||||
{
|
||||
this.MaxAge = maxAge;
|
||||
this.IsSession = isSession;
|
||||
this.Date = DateTime.UtcNow;
|
||||
this.SameSite = "none";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal Cookie()
|
||||
{
|
||||
// If a cookie has neither the Max-Age nor the Expires attribute, the user agent will retain the cookie
|
||||
// until "the current session is over" (as defined by the user agent).
|
||||
IsSession = true;
|
||||
MaxAge = -1;
|
||||
LastAccess = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public string ToHeaderValue() => string.Concat(this.Name, "=", this.Value);
|
||||
|
||||
public bool WillExpireInTheFuture()
|
||||
{
|
||||
// No Expires or Max-Age value sent from the server, we will fake the return value so we will not delete a freshly received cookie
|
||||
if (IsSession)
|
||||
return true;
|
||||
|
||||
// If a cookie has both the Max-Age and the Expires attribute, the Max-Age attribute has precedence and controls the expiration date of the cookie.
|
||||
return MaxAge != -1 ?
|
||||
Math.Max(0, (long)(DateTime.UtcNow - Date).TotalSeconds) < MaxAge :
|
||||
Expires > DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guess the storage size of the cookie.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public uint GuessSize()
|
||||
{
|
||||
return (uint)((this.Name != null ? this.Name.Length * sizeof(char) : 0) +
|
||||
(this.Value != null ? this.Value.Length * sizeof(char) : 0) +
|
||||
(this.Domain != null ? this.Domain.Length * sizeof(char) : 0) +
|
||||
(this.Path != null ? this.Path.Length * sizeof(char) : 0) +
|
||||
(this.SameSite != null ? this.SameSite.Length * sizeof(char) : 0) +
|
||||
(sizeof(long) * 4) +
|
||||
(sizeof(bool) * 3));
|
||||
}
|
||||
|
||||
public static Cookie Parse(string header, Uri defaultDomain, LoggingContext context)
|
||||
{
|
||||
Cookie cookie = new Cookie();
|
||||
try
|
||||
{
|
||||
var kvps = ParseCookieHeader(header);
|
||||
|
||||
foreach (var kvp in kvps)
|
||||
{
|
||||
switch (kvp.Key.ToLowerInvariant())
|
||||
{
|
||||
case "path":
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
|
||||
// If the attribute-value is empty or if the first character of the attribute-value is not %x2F ("/"):
|
||||
// Let cookie-path be the default-path.
|
||||
cookie.Path = string.IsNullOrEmpty(kvp.Value) || !kvp.Value.StartsWith("/") ? "/" : cookie.Path = kvp.Value;
|
||||
break;
|
||||
|
||||
case "domain":
|
||||
// If the attribute-value is empty, the behavior is undefined. However, the user agent SHOULD ignore the cookie-av entirely.
|
||||
if (string.IsNullOrEmpty(kvp.Value))
|
||||
return null;
|
||||
|
||||
// If the first character of the attribute-value string is %x2E ("."):
|
||||
// Let cookie-domain be the attribute-value without the leading %x2E (".") character.
|
||||
cookie.Domain = kvp.Value.StartsWith(".") ? kvp.Value.Substring(1) : kvp.Value;
|
||||
break;
|
||||
|
||||
case "expires":
|
||||
cookie.Expires = kvp.Value.ToDateTime(DateTime.FromBinary(0));
|
||||
cookie.IsSession = false;
|
||||
break;
|
||||
|
||||
case "max-age":
|
||||
cookie.MaxAge = kvp.Value.ToInt64(-1);
|
||||
cookie.IsSession = false;
|
||||
break;
|
||||
|
||||
case "secure":
|
||||
cookie.IsSecure = true;
|
||||
break;
|
||||
|
||||
case "httponly":
|
||||
cookie.IsHttpOnly = true;
|
||||
break;
|
||||
|
||||
case "samesite":
|
||||
cookie.SameSite = kvp.Value;
|
||||
break;
|
||||
|
||||
default:
|
||||
// check whether name is already set to avoid overwriting it with a non-listed setting
|
||||
if (string.IsNullOrEmpty(cookie.Name))
|
||||
{
|
||||
cookie.Name = kvp.Key;
|
||||
cookie.Value = kvp.Value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Some user agents provide users the option of preventing persistent storage of cookies across sessions.
|
||||
// When configured thusly, user agents MUST treat all received cookies as if the persistent-flag were set to false.
|
||||
if (CookieJar.IsSessionOverride)
|
||||
cookie.IsSession = true;
|
||||
|
||||
// http://tools.ietf.org/html/rfc6265#section-4.1.2.3
|
||||
// WARNING: Some existing user agents treat an absent Domain attribute as if the Domain attribute were present and contained the current host name.
|
||||
// For example, if example.com returns a Set-Cookie header without a Domain attribute, these user agents will erroneously send the cookie to www.example.com as well.
|
||||
if (string.IsNullOrEmpty(cookie.Domain))
|
||||
cookie.Domain = defaultDomain.Host;
|
||||
|
||||
// http://tools.ietf.org/html/rfc6265#section-5.3 section 7:
|
||||
// If the cookie-attribute-list contains an attribute with an attribute-name of "Path",
|
||||
// set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path".
|
||||
// __Otherwise, set the cookie's path to the default-path of the request-uri.__
|
||||
if (string.IsNullOrEmpty(cookie.Path))
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
|
||||
// 1. Let uri-path be the path portion of the request-uri if such a portion exists
|
||||
cookie.Path = defaultDomain.AbsolutePath;
|
||||
|
||||
// 2. If the uri-path is empty or if the first character of the uri-path is not a % x2F("/") character,
|
||||
// output % x2F("/") and skip the remaining steps.
|
||||
// Uri's AbsolutePath always emits at least a '/' character.
|
||||
|
||||
// 3. If the uri-path contains no more than one %x2F ("/") character,
|
||||
// output % x2F("/") and skip the remaining step.
|
||||
|
||||
int slashCount = 1;
|
||||
int lastSlashIdx = 0;
|
||||
for (int i = 1; i < cookie.Path.Length; i++)
|
||||
{
|
||||
if (cookie.Path[i] == '/')
|
||||
{
|
||||
slashCount++;
|
||||
lastSlashIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (slashCount == 1)
|
||||
{
|
||||
cookie.Path = "/";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 4. Output the characters of the uri-path from the first character up to,
|
||||
// but not including, the right-most % x2F("/").
|
||||
|
||||
cookie.Path = cookie.Path.Substring(0, lastSlashIdx);
|
||||
}
|
||||
}
|
||||
|
||||
cookie.Date = cookie.LastAccess = DateTime.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Warning("Cookie", "Parse - Couldn't parse header: " + header + " exception: " + ex.ToString() + " " + ex.StackTrace, context);
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
#region Save & Load
|
||||
|
||||
internal void SaveTo(BinaryWriter stream)
|
||||
{
|
||||
stream.Write(Version);
|
||||
stream.Write(Name ?? string.Empty);
|
||||
stream.Write(Value ?? string.Empty);
|
||||
stream.Write(Date.ToBinary());
|
||||
stream.Write(LastAccess.ToBinary());
|
||||
stream.Write(Expires.ToBinary());
|
||||
stream.Write(MaxAge);
|
||||
stream.Write(IsSession);
|
||||
stream.Write(Domain ?? string.Empty);
|
||||
stream.Write(Path ?? string.Empty);
|
||||
stream.Write(IsSecure);
|
||||
stream.Write(IsHttpOnly);
|
||||
}
|
||||
|
||||
internal void LoadFrom(BinaryReader stream)
|
||||
{
|
||||
/*int version = */stream.ReadInt32();
|
||||
this.Name = stream.ReadString();
|
||||
this.Value = stream.ReadString();
|
||||
this.Date = DateTime.FromBinary(stream.ReadInt64());
|
||||
this.LastAccess = DateTime.FromBinary(stream.ReadInt64());
|
||||
this.Expires = DateTime.FromBinary(stream.ReadInt64());
|
||||
this.MaxAge = stream.ReadInt64();
|
||||
this.IsSession = stream.ReadBoolean();
|
||||
this.Domain = stream.ReadString();
|
||||
this.Path = stream.ReadString();
|
||||
this.IsSecure = stream.ReadBoolean();
|
||||
this.IsHttpOnly = stream.ReadBoolean();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overrides and new Equals function
|
||||
|
||||
public override string ToString()
|
||||
=> $"[Cookie '{this.Name}' = '{this.Value}', {this.IsSession}, '{this.Path}']";
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return false;
|
||||
|
||||
return this.Equals(obj as Cookie);
|
||||
}
|
||||
|
||||
public bool Equals(Cookie cookie)
|
||||
{
|
||||
if (cookie == null)
|
||||
return false;
|
||||
|
||||
if (Object.ReferenceEquals(this, cookie))
|
||||
return true;
|
||||
|
||||
return this.Name.Equals(cookie.Name, StringComparison.Ordinal) &&
|
||||
((this.Domain == null && cookie.Domain == null) || this.Domain.Equals(cookie.Domain, StringComparison.Ordinal)) &&
|
||||
((this.Path == null && cookie.Path == null) || this.Path.Equals(cookie.Path, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.ToString().GetHashCode();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Functions
|
||||
|
||||
private static string ReadValue(string str, ref int pos)
|
||||
{
|
||||
string result = string.Empty;
|
||||
if (str == null)
|
||||
return result;
|
||||
|
||||
return str.Read(ref pos, ';');
|
||||
}
|
||||
|
||||
private static List<HeaderValue> ParseCookieHeader(string str)
|
||||
{
|
||||
List<HeaderValue> result = new List<HeaderValue>();
|
||||
|
||||
if (str == null)
|
||||
return result;
|
||||
|
||||
int idx = 0;
|
||||
|
||||
// process the rest of the text
|
||||
while (idx < str.Length)
|
||||
{
|
||||
// Read key
|
||||
string key = str.Read(ref idx, (ch) => ch != '=' && ch != ';').Trim();
|
||||
HeaderValue qp = new HeaderValue(key);
|
||||
|
||||
if (idx < str.Length && str[idx - 1] == '=')
|
||||
qp.Value = ReadValue(str, ref idx);
|
||||
|
||||
result.Add(qp);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable<Cookie> implementation
|
||||
|
||||
public int CompareTo(Cookie other)
|
||||
{
|
||||
return this.LastAccess.CompareTo(other.LastAccess);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e66d7566eee7c6245bb8a838fe4b3286
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
677
Packages/com.tivadar.best.http/Runtime/HTTP/Cookies/CookieJar.cs
Normal file
677
Packages/com.tivadar.best.http/Runtime/HTTP/Cookies/CookieJar.cs
Normal file
@@ -0,0 +1,677 @@
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.FileSystem;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Best.HTTP.Cookies
|
||||
{
|
||||
/// <summary>
|
||||
/// The Cookie Jar implementation based on <see href="http://tools.ietf.org/html/rfc6265">RFC 6265</see>.
|
||||
/// </summary>
|
||||
public static class CookieJar
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum size of the Cookie Jar in bytes. It's default value is 10485760 (10 MB).
|
||||
/// </summary>
|
||||
public static uint MaximumSize { get; set; } = 10 * 1024 * 1024;
|
||||
|
||||
// Version of the cookie store. It may be used in a future version for maintaining compatibility.
|
||||
private const int Version = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if File apis are supported.
|
||||
/// </summary>
|
||||
public static bool IsSavingSupported
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsSupportCheckDone)
|
||||
return _isSavingSupported;
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
_isSavingSupported = false;
|
||||
#else
|
||||
HTTPManager.IOService.DirectoryExists(HTTPManager.GetRootSaveFolder());
|
||||
_isSavingSupported = true;
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
_isSavingSupported = false;
|
||||
|
||||
HTTPManager.Logger.Warning(nameof(CookieJar), "Cookie saving and loading disabled!");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSupportCheckDone = true;
|
||||
}
|
||||
|
||||
return _isSavingSupported;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plugin will delete cookies that are accessed this threshold ago. Its default value is 7 days.
|
||||
/// </summary>
|
||||
public static TimeSpan AccessThreshold = TimeSpan.FromDays(7);
|
||||
|
||||
/// <summary>
|
||||
/// If this property is set to <c>true</c>, then new cookies treated as session cookies and these cookies are not saved to disk. Its default value is <c>false</c>.
|
||||
/// </summary>
|
||||
public static bool IsSessionOverride = false;
|
||||
|
||||
/// <summary>
|
||||
/// Enabled or disables storing and handling of cookies. If set to false, <see cref="HTTPRequest">HTTPRequests</see> aren't searched for cookies and no cookies will be set for <see cref="HTTPResponse"/>s.
|
||||
/// </summary>
|
||||
public static bool IsEnabled = true;
|
||||
|
||||
#region Privates
|
||||
|
||||
/// <summary>
|
||||
/// List of the Cookies
|
||||
/// </summary>
|
||||
private static List<Cookie> Cookies = new List<Cookie>();
|
||||
private static string CookieFolder { get; set; }
|
||||
private static string LibraryPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Synchronization object for thread safety.
|
||||
/// </summary>
|
||||
private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
|
||||
|
||||
private static bool _isSavingSupported;
|
||||
private static bool IsSupportCheckDone;
|
||||
|
||||
private static bool Loaded;
|
||||
private static RunOnceOnMainThread _saveLibraryRunner = new RunOnceOnMainThread(Persist, null);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Functions
|
||||
|
||||
internal static void SetupFolder()
|
||||
{
|
||||
if (!CookieJar.IsSavingSupported)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(CookieFolder) || string.IsNullOrEmpty(LibraryPath))
|
||||
{
|
||||
CookieFolder = System.IO.Path.Combine(HTTPManager.GetRootSaveFolder(), "Cookies");
|
||||
LibraryPath = System.IO.Path.Combine(CookieFolder, "Library");
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(SetupFolder)}() - {nameof(LibraryPath)}: '{LibraryPath}'");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will set or update all cookies from the response object.
|
||||
/// </summary>
|
||||
internal static bool SetFromRequest(HTTPResponse response)
|
||||
{
|
||||
if (response == null || !IsEnabled)
|
||||
return false;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(SetFromRequest)}({response})", response.Context);
|
||||
|
||||
List<Cookie> newCookies = new List<Cookie>();
|
||||
var setCookieHeaders = response.GetHeaderValues("set-cookie");
|
||||
|
||||
// No cookies. :'(
|
||||
if (setCookieHeaders == null)
|
||||
return false;
|
||||
|
||||
foreach (var cookieHeader in setCookieHeaders)
|
||||
{
|
||||
Cookie cookie = Cookie.Parse(cookieHeader, response.Request.CurrentUri, response.Request.Context);
|
||||
if (cookie != null)
|
||||
{
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
int idx;
|
||||
var old = Find(cookie, out idx);
|
||||
|
||||
// if no value for the cookie or already expired then the server asked us to delete the cookie
|
||||
bool expired = string.IsNullOrEmpty(cookie.Value) || !cookie.WillExpireInTheFuture();
|
||||
|
||||
if (!expired)
|
||||
{
|
||||
// no old cookie, add it straight to the list
|
||||
if (old == null)
|
||||
{
|
||||
Cookies.Add(cookie);
|
||||
|
||||
newCookies.Add(cookie);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(SetFromRequest)}({response}) - new cookie: {cookie}", response.Context);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the creation-time of the newly created cookie to match the creation-time of the old-cookie.
|
||||
cookie.Date = old.Date;
|
||||
Cookies[idx] = cookie;
|
||||
|
||||
newCookies.Add(cookie);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(SetFromRequest)}({response}) - refresh cookie: {cookie}", response.Context);
|
||||
}
|
||||
}
|
||||
else if (idx != -1) // delete the cookie
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(SetFromRequest)}({response}) - deleting cookie: {Cookies[idx]}", response.Context);
|
||||
|
||||
Cookies.RemoveAt(idx);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (HTTPManager.Logger.IsDiagnostic)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(CookieJar), $"{nameof(SetFromRequest)}({response})", ex, response.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_saveLibraryRunner.Subscribe();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void SetupRequest(HTTPRequest request)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(SetFromRequest)}({request})", request.Context);
|
||||
|
||||
// Cookies
|
||||
// User added cookies are sent even when IsCookiesEnabled is set to false
|
||||
List<Cookie> cookies = CookieJar.Get(request.CurrentUri);
|
||||
|
||||
// http://tools.ietf.org/html/rfc6265#section-5.4
|
||||
// -When the user agent generates an HTTP request, the user agent MUST NOT attach more than one Cookie header field.
|
||||
if (cookies != null && cookies.Count > 0)
|
||||
{
|
||||
// Room for improvement:
|
||||
// 2. The user agent SHOULD sort the cookie-list in the following order:
|
||||
// * Cookies with longer paths are listed before cookies with shorter paths.
|
||||
// * Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times.
|
||||
|
||||
bool first = true;
|
||||
string cookieStr = string.Empty;
|
||||
|
||||
bool isSecureProtocolInUse = HTTPProtocolFactory.IsSecureProtocol(request.CurrentUri);
|
||||
|
||||
foreach (var cookie in cookies)
|
||||
if (!cookie.IsSecure || (cookie.IsSecure && isSecureProtocolInUse))
|
||||
{
|
||||
if (!first)
|
||||
cookieStr += "; ";
|
||||
else
|
||||
first = false;
|
||||
|
||||
cookieStr += cookie.ToHeaderValue();
|
||||
|
||||
// 3. Update the last-access-time of each cookie in the cookie-list to the current date and time.
|
||||
cookie.LastAccess = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cookieStr))
|
||||
request.SetHeader("Cookie", cookieStr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all expired or 'old' cookies, and will keep the sum size of cookies under the given size.
|
||||
/// </summary>
|
||||
internal static void Maintain(bool sendEvent)
|
||||
{
|
||||
// It's not the same as in the rfc:
|
||||
// http://tools.ietf.org/html/rfc6265#section-5.3
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Maintain)}({sendEvent})");
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
uint size = 0;
|
||||
|
||||
for (int i = 0; i < Cookies.Count;)
|
||||
{
|
||||
var cookie = Cookies[i];
|
||||
|
||||
// Remove expired or not used cookies
|
||||
if (!cookie.WillExpireInTheFuture() || (cookie.LastAccess + AccessThreshold) < DateTime.UtcNow)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Maintain)}({sendEvent}) - removing expired cookie: {cookie}");
|
||||
|
||||
Cookies.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!cookie.IsSession)
|
||||
size += cookie.GuessSize();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (size > MaximumSize)
|
||||
{
|
||||
Cookies.Sort();
|
||||
|
||||
while (size > MaximumSize && Cookies.Count > 0)
|
||||
{
|
||||
var cookie = Cookies[0];
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Maintain)}({sendEvent}) - removing cookie: {cookie}");
|
||||
|
||||
Cookies.RemoveAt(0);
|
||||
|
||||
size -= cookie.GuessSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
if (sendEvent)
|
||||
_saveLibraryRunner.Subscribe();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the Cookie Jar to a file.
|
||||
/// </summary>
|
||||
/// <remarks>Not implemented under Unity WebPlayer</remarks>
|
||||
internal static void Persist()
|
||||
{
|
||||
if (!IsSavingSupported)
|
||||
return;
|
||||
|
||||
if (!Loaded)
|
||||
return;
|
||||
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Persist)}()");
|
||||
|
||||
// Delete any expired cookie
|
||||
Maintain(false);
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (!HTTPManager.IOService.DirectoryExists(CookieFolder))
|
||||
HTTPManager.IOService.DirectoryCreate(CookieFolder);
|
||||
|
||||
using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.Create))
|
||||
using (var bw = new System.IO.BinaryWriter(fs))
|
||||
{
|
||||
bw.Write(Version);
|
||||
|
||||
// Count how many non-session cookies we have
|
||||
int count = 0;
|
||||
foreach (var cookie in Cookies)
|
||||
if (!cookie.IsSession)
|
||||
count++;
|
||||
|
||||
bw.Write(count);
|
||||
|
||||
// Save only the persistable cookies
|
||||
foreach (var cookie in Cookies)
|
||||
if (!cookie.IsSession)
|
||||
cookie.SaveTo(bw);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load previously persisted cookie library from the file.
|
||||
/// </summary>
|
||||
internal static void Load()
|
||||
{
|
||||
if (!IsSavingSupported)
|
||||
return;
|
||||
|
||||
if (Loaded)
|
||||
return;
|
||||
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Load)}()");
|
||||
|
||||
SetupFolder();
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
Cookies.Clear();
|
||||
|
||||
if (!HTTPManager.IOService.DirectoryExists(CookieFolder))
|
||||
HTTPManager.IOService.DirectoryCreate(CookieFolder);
|
||||
|
||||
if (!HTTPManager.IOService.FileExists(LibraryPath))
|
||||
return;
|
||||
|
||||
using (var fs = HTTPManager.IOService.CreateFileStream(LibraryPath, FileStreamModes.OpenRead))
|
||||
using (var br = new System.IO.BinaryReader(fs))
|
||||
{
|
||||
/*int version = */br.ReadInt32();
|
||||
int cookieCount = br.ReadInt32();
|
||||
|
||||
for (int i = 0; i < cookieCount; ++i)
|
||||
{
|
||||
Cookie cookie = new Cookie();
|
||||
cookie.LoadFrom(br);
|
||||
|
||||
if (cookie.WillExpireInTheFuture())
|
||||
Cookies.Add(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Cookies.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Loaded = true;
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Functions
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Cookies that corresponds to the given Uri.
|
||||
/// </summary>
|
||||
public static List<Cookie> Get(Uri uri)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Get)}({uri})");
|
||||
|
||||
Load();
|
||||
|
||||
rwLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
List<Cookie> result = null;
|
||||
|
||||
for (int i = 0; i < Cookies.Count; ++i)
|
||||
{
|
||||
Cookie cookie = Cookies[i];
|
||||
|
||||
if (cookie == null)
|
||||
continue;
|
||||
|
||||
bool willExpireInTheFuture = cookie.WillExpireInTheFuture();
|
||||
bool domainMatch = uri.Host.IndexOf(cookie.Domain) != -1 || string.Format("{0}:{1}", uri.Host, uri.Port).IndexOf(cookie.Domain) != -1;
|
||||
|
||||
if (willExpireInTheFuture && domainMatch)
|
||||
{
|
||||
string requestPath = uri.AbsolutePath;
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
|
||||
// A request-path path-matches a given cookie-path if at least one of
|
||||
// the following conditions holds:
|
||||
// o The cookie-path and the request-path are identical.
|
||||
bool exactMatch = cookie.Path.Equals(requestPath, StringComparison.Ordinal);
|
||||
|
||||
// o The cookie-path is a prefix of the request-path, and the last
|
||||
// character of the cookie-path is %x2F ("/").
|
||||
bool prefixMatch = cookie.Path[cookie.Path.Length - 1] == '/' && requestPath.StartsWith(cookie.Path, StringComparison.Ordinal);
|
||||
|
||||
// o The cookie-path is a prefix of the request-path, and the first
|
||||
// character of the request-path that is not included in the cookie-
|
||||
// path is a %x2F ("/") character.
|
||||
bool prefixMatch2 = requestPath.Length > cookie.Path.Length &&
|
||||
requestPath.StartsWith(cookie.Path, StringComparison.Ordinal) &&
|
||||
requestPath[cookie.Path.Length] == '/';
|
||||
|
||||
if (exactMatch || prefixMatch || prefixMatch2)
|
||||
{
|
||||
if (result == null)
|
||||
result = new List<Cookie>();
|
||||
|
||||
result.Add(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will add a new, or overwrite an old cookie if already exists.
|
||||
/// </summary>
|
||||
public static void Set(Uri uri, Cookie cookie)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Set)}({uri}, {cookie})");
|
||||
|
||||
cookie.Domain = uri.Host;
|
||||
cookie.Path = uri.AbsolutePath;
|
||||
|
||||
Set(cookie);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will add a new, or overwrite an old cookie if already exists.
|
||||
/// </summary>
|
||||
public static void Set(Cookie cookie)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Set)}({cookie})");
|
||||
|
||||
Load();
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
int idx;
|
||||
Find(cookie, out idx);
|
||||
|
||||
if (idx >= 0)
|
||||
Cookies[idx] = cookie;
|
||||
else
|
||||
Cookies.Add(cookie);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_saveLibraryRunner.Subscribe();
|
||||
}
|
||||
|
||||
public static List<Cookie> GetAll()
|
||||
{
|
||||
Load();
|
||||
|
||||
return Cookies;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all cookies from the Jar.
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Clear)}()");
|
||||
|
||||
Load();
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
Cookies.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
Persist();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes cookies that older than the given parameter.
|
||||
/// </summary>
|
||||
public static void Clear(TimeSpan olderThan)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Clear)}({olderThan})");
|
||||
|
||||
Load();
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < Cookies.Count;)
|
||||
{
|
||||
var cookie = Cookies[i];
|
||||
|
||||
// Remove expired or not used cookies
|
||||
if (!cookie.WillExpireInTheFuture() || (cookie.Date + olderThan) < DateTime.UtcNow)
|
||||
Cookies.RemoveAt(i);
|
||||
else
|
||||
i++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
Persist();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes cookies that matches to the given domain.
|
||||
/// </summary>
|
||||
public static void Clear(string domain)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Clear)}({domain})");
|
||||
|
||||
Load();
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < Cookies.Count;)
|
||||
{
|
||||
var cookie = Cookies[i];
|
||||
|
||||
// Remove expired or not used cookies
|
||||
if (!cookie.WillExpireInTheFuture() || cookie.Domain.IndexOf(domain) != -1)
|
||||
Cookies.RemoveAt(i);
|
||||
else
|
||||
i++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
Persist();
|
||||
}
|
||||
|
||||
public static void Remove(Uri uri, string name)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(CookieJar), $"{nameof(Remove)}({uri}, {name})");
|
||||
|
||||
Load();
|
||||
|
||||
rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < Cookies.Count;)
|
||||
{
|
||||
var cookie = Cookies[i];
|
||||
|
||||
if (cookie.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && uri.Host.IndexOf(cookie.Domain) != -1)
|
||||
Cookies.RemoveAt(i);
|
||||
else
|
||||
i++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_saveLibraryRunner.Subscribe();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helper Functions
|
||||
|
||||
/// <summary>
|
||||
/// Find and return a Cookie and his index in the list.
|
||||
/// </summary>
|
||||
private static Cookie Find(Cookie cookie, out int idx)
|
||||
{
|
||||
for (int i = 0; i < Cookies.Count; ++i)
|
||||
{
|
||||
Cookie c = Cookies[i];
|
||||
|
||||
if (c.Equals(cookie))
|
||||
{
|
||||
idx = i;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
idx = -1;
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0788a5d8bbeea264a815fa46ec0012cc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
127
Packages/com.tivadar.best.http/Runtime/HTTP/HTTPMethods.cs
Normal file
127
Packages/com.tivadar.best.http/Runtime/HTTP/HTTPMethods.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
namespace Best.HTTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the HTTP methods used in HTTP requests.
|
||||
/// </summary>
|
||||
public enum HTTPMethods : int
|
||||
{
|
||||
/// <summary>
|
||||
/// The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.
|
||||
/// If the Request-URI refers to a data-producing process, it is the produced data which shall be returned as the
|
||||
/// entity in the response and not the source text of the process, unless that text happens to be the output of the process.
|
||||
/// </summary>
|
||||
Get,
|
||||
|
||||
/// <summary>
|
||||
/// The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response.
|
||||
/// The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request.
|
||||
/// This method can be used for obtaining metainformation about the entity implied by the request without transferring the entity-body itself.
|
||||
/// This method is often used for testing hypertext links for validity, accessibility, and recent modification.
|
||||
/// </summary>
|
||||
Head,
|
||||
|
||||
/// <summary>
|
||||
/// The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line.
|
||||
/// POST is designed to allow a uniform method to cover the following functions:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Annotation of existing resources;</description></item>
|
||||
/// <item><description>Posting a message to a bulletin board, newsgroup, mailing list, or similar group of articles;</description></item>
|
||||
/// <item><description>Providing a block of data, such as the result of submitting a form, to a data-handling process;</description></item>
|
||||
/// <item><description>Extending a database through an append operation.</description></item>
|
||||
/// </list>
|
||||
/// The actual function performed by the POST method is determined by the server and is usually dependent on the Request-URI.
|
||||
/// The posted entity is subordinate to that URI in the same way that a file is subordinate to a directory containing it,
|
||||
/// a news article is subordinate to a newsgroup to which it is posted, or a record is subordinate to a database.
|
||||
/// The action performed by the POST method might not result in a resource that can be identified by a URI. In this case,
|
||||
/// either 200 (OK) or 204 (No Content) is the appropriate response status, depending on whether or not the response includes an entity that describes the result.
|
||||
/// </summary>
|
||||
Post,
|
||||
|
||||
/// <summary>
|
||||
/// The PUT method requests that the enclosed entity be stored under the supplied Request-URI.
|
||||
/// If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server.
|
||||
/// If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent,
|
||||
/// the origin server can create the resource with that URI. If a new resource is created, the origin server MUST inform the user agent via the 201 (Created) response.
|
||||
/// If an existing resource is modified, either the 200 (OK) or 204 (No Content) response codes SHOULD be sent to indicate successful completion of the request.
|
||||
/// If the resource could not be created or modified with the Request-URI, an appropriate error response SHOULD be given that reflects the nature of the problem.
|
||||
/// The recipient of the entity MUST NOT ignore any Content-* (e.g. Content-Range) headers that it does not understand or implement and MUST return a 501 (Not Implemented) response in such cases.
|
||||
/// </summary>
|
||||
Put,
|
||||
|
||||
/// <summary>
|
||||
/// The DELETE method requests that the origin server delete the resource identified by the Request-URI. This method MAY be overridden by human intervention (or other means) on the origin server.
|
||||
/// The client cannot be guaranteed that the operation has been carried out, even if the status code returned from the origin server indicates that the action has been completed successfully.
|
||||
/// However, the server SHOULD NOT indicate success unless, at the time the response is given, it intends to delete the resource or move it to an inaccessible location.
|
||||
/// A successful response SHOULD be 200 (OK) if the response includes an entity describing the status, 202 (Accepted) if the action has not yet been enacted, or 204 (No Content)
|
||||
/// if the action has been enacted but the response does not include an entity.
|
||||
/// </summary>
|
||||
Delete,
|
||||
|
||||
/// <summary>
|
||||
/// The PATCH method requests that a set of changes described in the request entity be applied to the resource identified by the Request-URI.
|
||||
/// The set of changes is represented in a format called a "patchdocument" identified by a media type. If the Request-URI does not point to an existing resource,
|
||||
/// the server MAY create a new resource, depending on the patch document type (whether it can logically modify a null resource) and permissions, etc.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="http://tools.ietf.org/html/rfc5789">RFC-5789</see></description></item>
|
||||
/// <item><description><see href="http://restcookbook.com/HTTP%20Methods/patch/">When should we use the PATCH HTTP method?</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Patch,
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP TRACE method is used to perform a message loop-back test along the path to the target resource.
|
||||
/// </summary>
|
||||
Trace,
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP MERGE method is used to apply modifications to an existing resource.
|
||||
/// The MERGE HTTP method is not as commonly used as other methods like GET, POST, or PUT.
|
||||
/// It's often used in specific WebDAV (Web Distributed Authoring and Versioning) scenarios.
|
||||
/// </summary>
|
||||
Merge,
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP OPTIONS method requests permitted communication options for a given URL or server.
|
||||
/// A client can specify a URL with this method, or an asterisk (*) to refer to the entire server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS">Mozilla Developer Networks - OPTIONS</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Options,
|
||||
|
||||
/// <summary>
|
||||
/// The CONNECT method is primarily used in the context of HTTP proxies to establish a network connection through the proxy to a target host.
|
||||
/// It is used in the context of the HTTP CONNECT tunneling method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://datatracker.ietf.org/doc/html/rfc2616#section-9.9">RFC-2616</see></description></item>
|
||||
/// <item><description><see href="https://tools.ietf.org/html/rfc8441">RFC-8441</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Connect,
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP QUERY method is used to retrieve data based on a query parameter or search criteria.
|
||||
/// The QUERY method is not a standard HTTP method, and its usage may vary depending on the specific application or API you are working with.
|
||||
/// It might be used for querying data or resources with specific parameters.
|
||||
/// Details about the QUERY method would depend on the API or service you are interacting with.
|
||||
/// You should refer to the documentation or specifications provided by the API/service provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://horovits.medium.com/http-s-new-method-for-data-apis-http-query-1ff71e6f73f3">HTTP's New Method For Data APIs: HTTP QUERY</see></description></item>
|
||||
/// <item><description><see href="https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/">The HTTP QUERY Method (Draft)</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Query
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d27fd3bdca345a47b10b03334d3cd91
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
55
Packages/com.tivadar.best.http/Runtime/HTTP/HTTPRange.cs
Normal file
55
Packages/com.tivadar.best.http/Runtime/HTTP/HTTPRange.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace Best.HTTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an HTTP range that specifies the byte range of a response content, received as an answer for a range-request.
|
||||
/// </summary>
|
||||
public sealed class HTTPRange
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the position of the first byte in the range that the server sent.
|
||||
/// </summary>
|
||||
public long FirstBytePos { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the position of the last byte in the range that the server sent.
|
||||
/// </summary>
|
||||
public long LastBytePos { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total length of the full entity-body on the server. Returns -1 if this length is unknown or difficult to determine.
|
||||
/// </summary>
|
||||
public long ContentLength { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the HTTP range is valid.
|
||||
/// </summary>
|
||||
public bool IsValid { get; private set; }
|
||||
|
||||
internal HTTPRange()
|
||||
{
|
||||
this.ContentLength = -1;
|
||||
this.IsValid = false;
|
||||
}
|
||||
|
||||
internal HTTPRange(int contentLength)
|
||||
{
|
||||
this.ContentLength = contentLength;
|
||||
this.IsValid = false;
|
||||
}
|
||||
|
||||
internal HTTPRange(long firstBytePosition, long lastBytePosition, long contentLength)
|
||||
{
|
||||
this.FirstBytePos = firstBytePosition;
|
||||
this.LastBytePos = lastBytePosition;
|
||||
this.ContentLength = contentLength;
|
||||
|
||||
// A byte-content-range-spec with a byte-range-resp-spec whose last-byte-pos value is less than its first-byte-pos value, or whose instance-length value is less than or equal to its last-byte-pos value, is invalid.
|
||||
this.IsValid = this.FirstBytePos <= this.LastBytePos && this.ContentLength > this.LastBytePos;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}-{1}/{2} (valid: {3})", FirstBytePos, LastBytePos, ContentLength, IsValid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40f7ef056ce0ae149a73c281f34b2c18
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
680
Packages/com.tivadar.best.http/Runtime/HTTP/HTTPRequest.cs
Normal file
680
Packages/com.tivadar.best.http/Runtime/HTTP/HTTPRequest.cs
Normal file
@@ -0,0 +1,680 @@
|
||||
using Best.HTTP.Cookies;
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Authenticators;
|
||||
using Best.HTTP.Request.Settings;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Request.Upload;
|
||||
using Best.HTTP.Response.Decompression;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Best.HTTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for a callback function that is called after the request is fully processed.
|
||||
/// </summary>
|
||||
public delegate void OnRequestFinishedDelegate(HTTPRequest req, HTTPResponse resp);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for enumerating headers during request preparation.
|
||||
/// </summary>
|
||||
/// <param name="header">The header name.</param>
|
||||
/// <param name="values">A list of header values.</param>
|
||||
public delegate void OnHeaderEnumerationDelegate(string header, List<string> values);
|
||||
|
||||
/// <summary>
|
||||
/// Represents an HTTP request that allows you to send HTTP requests to remote servers and receive responses asynchronously.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><term>Asynchronous HTTP requests</term><description>Utilize a Task-based API for performing HTTP requests asynchronously.</description></item>
|
||||
/// <item><term>Unity coroutine support</term><description>Seamlessly integrate with Unity's coroutine system for coroutine-based request handling.</description></item>
|
||||
/// <item><term>HTTP method support</term><description>Support for various HTTP methods including GET, POST, PUT, DELETE, and more.</description></item>
|
||||
/// <item><term>Compression and decompression</term><description>Automatic request and response compression and decompression for efficient data transfer.</description></item>
|
||||
/// <item><term>Timing information</term><description>Collect detailed timing information about the request for performance analysis.</description></item>
|
||||
/// <item><term>Upload and download support</term><description>Support for uploading and downloading files with progress tracking.</description></item>
|
||||
/// <item><term>Customizable</term><description>Extensive options for customizing request headers, handling cookies, and more.</description></item>
|
||||
/// <item><term>Redirection handling</term><description>Automatic handling of request redirections for a seamless experience.</description></item>
|
||||
/// <item><term>Proxy server support</term><description>Ability to route requests through proxy servers for enhanced privacy and security.</description></item>
|
||||
/// <item><term>Authentication</term><description>Automatic authentication handling using authenticators for secure communication.</description></item>
|
||||
/// <item><term>Cancellation support</term><description>Ability to cancel requests to prevent further processing and release resources.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class HTTPRequest : IEnumerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Get">HTTP GET</see> request with the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <returns>An HTTPRequest instance for the GET request.</returns>
|
||||
public static HTTPRequest CreateGet(string url) => new HTTPRequest(url);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Get">HTTP GET</see> request with the specified URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <returns>An HTTPRequest instance for the GET request.</returns>
|
||||
public static HTTPRequest CreateGet(Uri uri) => new HTTPRequest(uri);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Get">HTTP GET</see> request with the specified URL and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
/// <returns>An HTTPRequest instance for the GET request.</returns>
|
||||
public static HTTPRequest CreateGet(string url, OnRequestFinishedDelegate callback) => new HTTPRequest(url, callback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Get">HTTP GET</see> request with the specified URI and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
/// <returns>An HTTPRequest instance for the GET request.</returns>
|
||||
public static HTTPRequest CreateGet(Uri uri, OnRequestFinishedDelegate callback) => new HTTPRequest(uri, callback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Post">HTTP POST</see> request with the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <returns>An HTTPRequest instance for the POST request.</returns>
|
||||
public static HTTPRequest CreatePost(string url) => new HTTPRequest(url, HTTPMethods.Post);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Post">HTTP POST</see> request with the specified URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <returns>An HTTPRequest instance for the POST request.</returns>
|
||||
public static HTTPRequest CreatePost(Uri uri) => new HTTPRequest(uri, HTTPMethods.Post);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Post">HTTP POST</see> request with the specified URL and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
/// <returns>An HTTPRequest instance for the POST request.</returns>
|
||||
public static HTTPRequest CreatePost(string url, OnRequestFinishedDelegate callback) => new HTTPRequest(url, HTTPMethods.Post, callback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Post">HTTP POST</see> request with the specified URI and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
/// <returns>An HTTPRequest instance for the POST request.</returns>
|
||||
public static HTTPRequest CreatePost(Uri uri, OnRequestFinishedDelegate callback) => new HTTPRequest(uri, HTTPMethods.Post, callback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Put">HTTP PUT</see> request with the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <returns>An HTTPRequest instance for the PUT request.</returns>
|
||||
public static HTTPRequest CreatePut(string url) => new HTTPRequest(url, HTTPMethods.Put);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Put">HTTP PUT</see> request with the specified URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <returns>An HTTPRequest instance for the PUT request.</returns>
|
||||
public static HTTPRequest CreatePut(Uri uri) => new HTTPRequest(uri, HTTPMethods.Put);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Put">HTTP PUT</see> request with the specified URL and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
/// <returns>An HTTPRequest instance for the PUT request.</returns>
|
||||
public static HTTPRequest CreatePut(string url, OnRequestFinishedDelegate callback) => new HTTPRequest(url, HTTPMethods.Put, callback);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="HTTPMethods.Put">HTTP PUT</see> request with the specified URI and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
/// <returns>An HTTPRequest instance for the PUT request.</returns>
|
||||
public static HTTPRequest CreatePut(Uri uri, OnRequestFinishedDelegate callback) => new HTTPRequest(uri, HTTPMethods.Put, callback);
|
||||
|
||||
/// <summary>
|
||||
/// Cached uppercase values to save some cpu cycles and GC alloc per request.
|
||||
/// </summary>
|
||||
public static readonly string[] MethodNames = {
|
||||
HTTPMethods.Get.ToString().ToUpper(),
|
||||
HTTPMethods.Head.ToString().ToUpper(),
|
||||
HTTPMethods.Post.ToString().ToUpper(),
|
||||
HTTPMethods.Put.ToString().ToUpper(),
|
||||
HTTPMethods.Delete.ToString().ToUpper(),
|
||||
HTTPMethods.Patch.ToString().ToUpper(),
|
||||
HTTPMethods.Trace.ToString().ToUpper(),
|
||||
HTTPMethods.Merge.ToString().ToUpper(),
|
||||
HTTPMethods.Options.ToString().ToUpper(),
|
||||
HTTPMethods.Connect.ToString().ToUpper(),
|
||||
HTTPMethods.Query.ToString().ToUpper()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The method that how we want to process our request the server.
|
||||
/// </summary>
|
||||
public HTTPMethods MethodType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original request's Uri.
|
||||
/// </summary>
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If redirected it contains the RedirectUri.
|
||||
/// </summary>
|
||||
public Uri CurrentUri { get { return this.RedirectSettings.IsRedirected ? this.RedirectSettings.RedirectUri : Uri; } }
|
||||
|
||||
/// <summary>
|
||||
/// A host-key that can be used to find the right host-variant for the request.
|
||||
/// </summary>
|
||||
public HostKey CurrentHostKey { get => HostKey.From(this); }
|
||||
|
||||
/// <summary>
|
||||
/// The response received from the server.
|
||||
/// </summary>
|
||||
/// <remarks>If an exception occurred during reading of the response stream or can't connect to the server, this will be null!</remarks>
|
||||
public HTTPResponse Response { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Download related options and settings.
|
||||
/// </summary>
|
||||
public DownloadSettings DownloadSettings = new DownloadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Upload related options and settings.
|
||||
/// </summary>
|
||||
public UploadSettings UploadSettings = new UploadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Timeout settings for the request.
|
||||
/// </summary>
|
||||
public TimeoutSettings TimeoutSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Retry settings for the request.
|
||||
/// </summary>
|
||||
public RetrySettings RetrySettings;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy settings for the request.
|
||||
/// </summary>
|
||||
public ProxySettings ProxySettings;
|
||||
|
||||
/// <summary>
|
||||
/// Redirect settings for the request.
|
||||
/// </summary>
|
||||
public RedirectSettings RedirectSettings { get; private set; } = new RedirectSettings(10);
|
||||
|
||||
/// <summary>
|
||||
/// The callback function that will be called after the request is fully processed.
|
||||
/// </summary>
|
||||
public OnRequestFinishedDelegate Callback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if <see cref="Abort"/> is called on this request.
|
||||
/// </summary>
|
||||
public bool IsCancellationRequested { get => this.CancellationTokenSource != null ? this.CancellationTokenSource.IsCancellationRequested : true; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cancellation token source for this request.
|
||||
/// </summary>
|
||||
internal System.Threading.CancellationTokenSource CancellationTokenSource { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Action called when <see cref="Abort"/> function is invoked.
|
||||
/// </summary>
|
||||
public Action<HTTPRequest> OnCancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Stores any exception that occurs during processing of the request or response.
|
||||
/// </summary>
|
||||
/// <remarks>This property if for debugging purposes as <see href="https://github.com/Benedicht/BestHTTP-Issues/issues/174">seen here</see>!</remarks>
|
||||
public Exception Exception { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any user-object that can be passed with the request.
|
||||
/// </summary>
|
||||
public object Tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current state of this request.
|
||||
/// </summary>
|
||||
public HTTPRequestStates State
|
||||
{
|
||||
get { return this._state; }
|
||||
internal set
|
||||
{
|
||||
// In a case where the request is aborted its state is set to a >= Finished state then,
|
||||
// on another thread the reqest processing will fail too queuing up a >= Finished state again.
|
||||
if (this._state >= HTTPRequestStates.Finished && value >= HTTPRequestStates.Finished)
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPRequest), $"State.Set({this._state} => {value})", this.Context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPRequest), $"State.Set({this._state} => {value})", this.Context);
|
||||
|
||||
this._state = value;
|
||||
}
|
||||
}
|
||||
private volatile HTTPRequestStates _state;
|
||||
|
||||
/// <summary>
|
||||
/// Timing information about the request.
|
||||
/// </summary>
|
||||
public TimingCollector Timing { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// An IAuthenticator implementation that can be used to authenticate the request.
|
||||
/// </summary>
|
||||
/// <remarks>Out-of-the-box included authenticators are <see cref="CredentialAuthenticator"/> and <see cref="BearerTokenAuthenticator"/>.</remarks>
|
||||
public IAuthenticator Authenticator;
|
||||
|
||||
#if UNITY_WEBGL
|
||||
/// <summary>
|
||||
/// Its value will be set to the XmlHTTPRequest's withCredentials field, required to send 3rd party cookies with the request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials">Mozilla Developer Networks - XMLHttpRequest.withCredentials</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public bool WithCredentials { get; set; }
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Logging context of the request.
|
||||
/// </summary>
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
public Dictionary<string, List<string>> Headers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP GET request with the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
public HTTPRequest(string url)
|
||||
:this(new Uri(url)) {}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP GET request with the specified URL and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
public HTTPRequest(string url, OnRequestFinishedDelegate callback)
|
||||
: this(new Uri(url), callback) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP GET request with the specified URL and HTTP method type.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <param name="methodType">The HTTP method type for the request (e.g., GET, POST, PUT).</param>
|
||||
public HTTPRequest(string url, HTTPMethods methodType)
|
||||
: this(new Uri(url), methodType) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP request with the specified URL, HTTP method type, and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the request.</param>
|
||||
/// <param name="methodType">The HTTP method type for the request (e.g., GET, POST, PUT).</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
public HTTPRequest(string url, HTTPMethods methodType, OnRequestFinishedDelegate callback)
|
||||
: this(new Uri(url), methodType, callback) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP GET request with the specified URI.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
public HTTPRequest(Uri uri)
|
||||
: this(uri, HTTPMethods.Get, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP GET request with the specified URI and registers a callback function to be called
|
||||
/// when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
public HTTPRequest(Uri uri, OnRequestFinishedDelegate callback)
|
||||
: this(uri, HTTPMethods.Get, callback)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP request with the specified URI and HTTP method type.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <param name="methodType">The HTTP method type for the request (e.g., GET, POST, PUT).</param>
|
||||
public HTTPRequest(Uri uri, HTTPMethods methodType)
|
||||
: this(uri, methodType, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an HTTP request with the specified URI, HTTP method type, and registers a callback function
|
||||
/// to be called when the request is fully processed.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the request.</param>
|
||||
/// <param name="methodType">The HTTP method type for the request (e.g., GET, POST, PUT).</param>
|
||||
/// <param name="callback">A callback function to be called when the request is finished.</param>
|
||||
public HTTPRequest(Uri uri, HTTPMethods methodType, OnRequestFinishedDelegate callback)
|
||||
{
|
||||
this.Uri = uri;
|
||||
this.MethodType = methodType;
|
||||
|
||||
this.TimeoutSettings = new TimeoutSettings(this);
|
||||
this.ProxySettings = new ProxySettings() { Proxy = HTTPManager.Proxy };
|
||||
this.RetrySettings = new RetrySettings(methodType == HTTPMethods.Get ? 1 : 0);
|
||||
|
||||
this.Callback = callback;
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
// Just because cookies are enabled, it doesn't justify creating XHR with WithCredentials == 1.
|
||||
//this.WithCredentials = this.CookieSettings.IsCookiesEnabled;
|
||||
#endif
|
||||
|
||||
this.Context = new LoggingContext(this);
|
||||
this.Timing = new TimingCollector(this);
|
||||
|
||||
this.CancellationTokenSource = new System.Threading.CancellationTokenSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a header-value pair to the Headers. Use it to add custom headers to the request.
|
||||
/// </summary>
|
||||
/// <example>AddHeader("User-Agent', "FooBar 1.0")</example>
|
||||
public void AddHeader(string name, string value) => this.Headers = Headers.AddHeader(name, value);
|
||||
|
||||
/// <summary>
|
||||
/// For the given header name, removes any previously added values and sets the given one.
|
||||
/// </summary>
|
||||
public void SetHeader(string name, string value) => this.Headers = this.Headers.SetHeader(name, value);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified header and all of its associated values. Returns <c>true</c>, if the header found and succesfully removed.
|
||||
/// </summary>
|
||||
public bool RemoveHeader(string name) => Headers.RemoveHeader(name);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if the given head name is already in the <see cref="Headers"/>.
|
||||
/// </summary>
|
||||
public bool HasHeader(string name) => Headers.HasHeader(name);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first header or <c>null</c> for the given header name.
|
||||
/// </summary>
|
||||
public string GetFirstHeaderValue(string name) => Headers.GetFirstHeaderValue(name);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all header values for the given header or <c>null</c>.
|
||||
/// </summary>
|
||||
public List<string> GetHeaderValues(string name) => Headers.GetHeaderValues(name);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all headers.
|
||||
/// </summary>
|
||||
public void RemoveHeaders() => Headers.RemoveHeaders();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Range header to download the content from the given byte position. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
|
||||
/// </summary>
|
||||
/// <param name="firstBytePos">Start position of the download.</param>
|
||||
public void SetRangeHeader(long firstBytePos)
|
||||
{
|
||||
SetHeader("Range", string.Format("bytes={0}-", firstBytePos));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Range header to download the content from the given byte position to the given last position. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
|
||||
/// </summary>
|
||||
/// <param name="firstBytePos">Start position of the download.</param>
|
||||
/// <param name="lastBytePos">The end position of the download.</param>
|
||||
public void SetRangeHeader(long firstBytePos, long lastBytePos)
|
||||
{
|
||||
SetHeader("Range", string.Format("bytes={0}-{1}", firstBytePos, lastBytePos));
|
||||
}
|
||||
|
||||
internal void RemoveUnsafeHeaders()
|
||||
{
|
||||
// https://www.rfc-editor.org/rfc/rfc9110.html#name-redirection-3xx
|
||||
/* 2. Remove header fields that were automatically generated by the implementation, replacing them with updated values as appropriate to the new request. This includes:
|
||||
1. Connection-specific header fields (see Section 7.6.1),
|
||||
2. Header fields specific to the client's proxy configuration, including (but not limited to) Proxy-Authorization,
|
||||
3. Origin-specific header fields (if any), including (but not limited to) Host,
|
||||
4. Validating header fields that were added by the implementation's cache (e.g., If-None-Match, If-Modified-Since), and
|
||||
5. Resource-specific header fields, including (but not limited to) Referer, Origin, Authorization, and Cookie.
|
||||
3. Consider removing header fields that were not automatically generated by the implementation
|
||||
(i.e., those present in the request because they were added by the calling context) where there are security implications;
|
||||
this includes but is not limited to Authorization and Cookie.
|
||||
* */
|
||||
|
||||
// 2.1
|
||||
RemoveHeader("Connection");
|
||||
RemoveHeader("Proxy-Connection");
|
||||
RemoveHeader("Keep-Alive");
|
||||
RemoveHeader("TE");
|
||||
RemoveHeader("Transfer-Encoding");
|
||||
RemoveHeader("Upgrade");
|
||||
|
||||
// 2.2
|
||||
RemoveHeader("Proxy-Authorization");
|
||||
|
||||
// 2.3
|
||||
RemoveHeader("Host");
|
||||
|
||||
// 2.4
|
||||
RemoveHeader("If-None-Match");
|
||||
RemoveHeader("If-Modified-Since");
|
||||
|
||||
// 2.5 & 3
|
||||
RemoveHeader("Referer");
|
||||
RemoveHeader("Origin");
|
||||
RemoveHeader("Authorization");
|
||||
RemoveHeader("Cookie");
|
||||
|
||||
RemoveHeader("Accept-Encoding");
|
||||
|
||||
RemoveHeader("Content-Length");
|
||||
}
|
||||
|
||||
internal void Prepare()
|
||||
{
|
||||
// Upload settings
|
||||
this.UploadSettings?.SetupRequest(this, true);
|
||||
}
|
||||
|
||||
public void EnumerateHeaders(OnHeaderEnumerationDelegate callback, bool callBeforeSendCallback)
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
if (!HasHeader("Host"))
|
||||
{
|
||||
if (CurrentUri.Port == 80 || CurrentUri.Port == 443)
|
||||
SetHeader("Host", CurrentUri.Host);
|
||||
else
|
||||
SetHeader("Host", CurrentUri.Authority);
|
||||
}
|
||||
|
||||
DecompressorFactory.SetupHeaders(this);
|
||||
|
||||
if (!DownloadSettings.DisableCache)
|
||||
HTTPManager.LocalCache?.SetupValidationHeaders(this);
|
||||
|
||||
var hostSettings = HTTPManager.PerHostSettings.Get(this.CurrentUri.Host);
|
||||
|
||||
// Websocket would be very, very sad if its "connection: upgrade" header would be overwritten!
|
||||
if (!HasHeader("Connection"))
|
||||
AddHeader("Connection", hostSettings.HTTP1ConnectionSettings.TryToReuseConnections ? "Keep-Alive, TE" : "Close, TE");
|
||||
|
||||
if (hostSettings.HTTP1ConnectionSettings.TryToReuseConnections /*&& !HasHeader("Keep-Alive")*/)
|
||||
{
|
||||
// Send the server a slightly larger value to make sure it's not going to close sooner than the client
|
||||
int seconds = (int)Math.Ceiling(hostSettings.HTTP1ConnectionSettings.MaxConnectionIdleTime.TotalSeconds + 1);
|
||||
|
||||
AddHeader("Keep-Alive", "timeout=" + seconds);
|
||||
}
|
||||
|
||||
if (!HasHeader("TE"))
|
||||
AddHeader("TE", "identity");
|
||||
|
||||
if (!string.IsNullOrEmpty(HTTPManager.UserAgent) && !HasHeader("User-Agent"))
|
||||
AddHeader("User-Agent", HTTPManager.UserAgent);
|
||||
#endif
|
||||
long contentLength = -1;
|
||||
|
||||
if (this.UploadSettings.UploadStream == null)
|
||||
{
|
||||
contentLength = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
contentLength = this.UploadSettings.UploadStream.Length;
|
||||
|
||||
if (contentLength == BodyLengths.UnknownWithChunkedTransferEncoding)
|
||||
SetHeader("Transfer-Encoding", "chunked");
|
||||
|
||||
if (!HasHeader("Content-Type"))
|
||||
SetHeader("Content-Type", "application/octet-stream");
|
||||
}
|
||||
|
||||
// Always set the Content-Length header if possible
|
||||
// http://tools.ietf.org/html/rfc2616#section-4.4 : For compatibility with HTTP/1.0 applications, HTTP/1.1 requests containing a message-body MUST include a valid Content-Length header field unless the server is known to be HTTP/1.1 compliant.
|
||||
// 2018.06.03: Changed the condition so that content-length header will be included for zero length too.
|
||||
// 2022.05.25: Don't send a Content-Length (: 0) header if there's an Upgrade header. Upgrade is set for websocket, and it might be not true that the client doesn't send any bytes.
|
||||
if (contentLength >= BodyLengths.NoBody && !HasHeader("Content-Length") && !HasHeader("Upgrade"))
|
||||
SetHeader("Content-Length", contentLength.ToString());
|
||||
|
||||
// Server authentication
|
||||
this.Authenticator?.SetupRequest(this);
|
||||
|
||||
// Cookies
|
||||
//this.CookieSettings?.SetupRequest(this);
|
||||
CookieJar.SetupRequest(this);
|
||||
|
||||
// Write out the headers to the stream
|
||||
if (callback != null && this.Headers != null)
|
||||
foreach (var kvp in this.Headers)
|
||||
callback(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts processing the request.
|
||||
/// </summary>
|
||||
public HTTPRequest Send()
|
||||
{
|
||||
// TODO: Are we really want to 'reset' the token source? Two problems i see:
|
||||
// 1.) User code will not know about this change
|
||||
// 2.) We might dispose the source while the DNS and TCP queries are running and checking the source request's Token.
|
||||
//if (this.IsRedirected)
|
||||
//{
|
||||
// this.CancellationTokenSource?.Dispose();
|
||||
// this.CancellationTokenSource = new System.Threading.CancellationTokenSource();
|
||||
//}
|
||||
this.Exception = null;
|
||||
|
||||
return HTTPManager.SendRequest(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any further processing of the HTTP request.
|
||||
/// </summary>
|
||||
public void Abort()
|
||||
{
|
||||
HTTPManager.Logger.Verbose("HTTPRequest", $"Abort({this.State})", this.Context);
|
||||
|
||||
if (this.State >= HTTPRequestStates.Finished || this.CancellationTokenSource == null)
|
||||
return;
|
||||
|
||||
//this.IsCancellationRequested = true;
|
||||
this.CancellationTokenSource.Cancel();
|
||||
|
||||
// There's a race-condition here too, another thread might set it too.
|
||||
// In this case, both state going to be queued up that we have to handle in RequestEvents.cs.
|
||||
if (this.TimeoutSettings.IsTimedOut(HTTPManager.CurrentFrameDateTime))
|
||||
{
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this, this.TimeoutSettings.IsConnectTimedOut(HTTPManager.CurrentFrameDateTime) ? HTTPRequestStates.ConnectionTimedOut : HTTPRequestStates.TimedOut, null));
|
||||
}
|
||||
else
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this, HTTPRequestStates.Aborted, null));
|
||||
|
||||
if (this.OnCancellationRequested != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.OnCancellationRequested(this);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the request for a state where switching MethodType is possible.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
RemoveHeaders();
|
||||
|
||||
this.RedirectSettings.Reset();
|
||||
this.Exception = null;
|
||||
this.CancellationTokenSource?.Dispose();
|
||||
this.CancellationTokenSource = new System.Threading.CancellationTokenSource();
|
||||
|
||||
this.UploadSettings?.Dispose();
|
||||
}
|
||||
|
||||
#region System.Collections.IEnumerator implementation
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerator.Current"/> implementation, required for <see cref="UnityEngine.Coroutine"/> support.
|
||||
/// </summary>
|
||||
public object Current { get { return null; } }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerator.MoveNext"/> implementation, required for <see cref="UnityEngine.Coroutine"/> support.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the request isn't finished yet.</returns>
|
||||
public bool MoveNext() => this.State < HTTPRequestStates.Finished;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerator.MoveNext"/> implementation throwing <see cref="NotImplementedException"/>, required for <see cref="UnityEngine.Coroutine"/> support.
|
||||
/// </summary>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public void Reset() => throw new NotImplementedException();
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of resources used by the HTTPRequest instance.
|
||||
/// </summary>
|
||||
internal void Dispose()
|
||||
{
|
||||
RequestEventHelper.UnregisterRequest(this);
|
||||
this.UploadSettings?.Dispose();
|
||||
this.Response?.Dispose();
|
||||
|
||||
this.CancellationTokenSource?.Dispose();
|
||||
this.CancellationTokenSource = null;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[HTTPRequest {this.State}, {this.Context.Hash}, {this.CurrentUri}, {this.CurrentHostKey}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c94d72de19ffc743a80c081f0a0a83e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,409 @@
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
#if WITH_UNITASK
|
||||
using Cysharp.Threading.Tasks;
|
||||
#endif
|
||||
|
||||
namespace Best.HTTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an exception thrown during or as a result of a Task-based asynchronous HTTP operations.
|
||||
/// </summary>
|
||||
public sealed class AsyncHTTPException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the status code of the server's response.
|
||||
/// </summary>
|
||||
public readonly int StatusCode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content sent by the server. This is usually an error page for 4xx or 5xx responses.
|
||||
/// </summary>
|
||||
public readonly string Content;
|
||||
|
||||
public AsyncHTTPException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AsyncHTTPException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public AsyncHTTPException(int statusCode, string message, string content)
|
||||
: base(message)
|
||||
{
|
||||
this.StatusCode = statusCode;
|
||||
this.Content = content;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("StatusCode: {0}, Message: {1}, Content: {2}, StackTrace: {3}", this.StatusCode, this.Message, this.Content, this.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A collection of extension methods for working with HTTP requests asynchronously using <see cref="Task{TResult}"/>.
|
||||
/// </summary>
|
||||
public static class HTTPRequestAsyncExtensions
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
/// <summary>
|
||||
/// Asynchronously sends an HTTP request and retrieves the response as an <see cref="AssetBundle"/>.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> to send.</param>
|
||||
/// <param name="token">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A Task that represents the asynchronous operation. The Task will complete with the retrieved AssetBundle
|
||||
/// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception.
|
||||
/// </returns>
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<AssetBundle> GetAssetBundleAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#else
|
||||
public static Task<AssetBundle> GetAssetBundleAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#endif
|
||||
{
|
||||
return CreateTask<AssetBundle>(request, token,
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
async
|
||||
#endif
|
||||
(req, resp, tcs) =>
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
if (resp.IsSuccess)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bundleLoadOp = AssetBundle.LoadFromMemoryAsync(resp.Data);
|
||||
|
||||
#if !UNITY_2023_1_OR_NEWER
|
||||
HTTPUpdateDelegator.Instance.StartCoroutine(BundleLoader(bundleLoadOp, tcs));
|
||||
#else
|
||||
await Awaitable.FromAsyncOperation(bundleLoadOp, token);
|
||||
|
||||
tcs.TrySetResult(bundleLoadOp.assetBundle);
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
GenericResponseHandler<AssetBundle>(req, resp, tcs);
|
||||
break;
|
||||
|
||||
default:
|
||||
GenericResponseHandler<AssetBundle>(req, resp, tcs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#if !UNITY_2023_1_OR_NEWER
|
||||
#if WITH_UNITASK
|
||||
static System.Collections.IEnumerator BundleLoader(AssetBundleCreateRequest req, UniTaskCompletionSource<AssetBundle> tcs)
|
||||
#else
|
||||
static System.Collections.IEnumerator BundleLoader(AssetBundleCreateRequest req, TaskCompletionSource<AssetBundle> tcs)
|
||||
#endif
|
||||
{
|
||||
yield return req;
|
||||
|
||||
tcs.TrySetResult(req.assetBundle);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sends an HTTP request and retrieves the raw <see cref="HTTPResponse"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is particularly useful when you want to access the raw response without any specific processing
|
||||
/// like converting the data into a string, texture, or other formats. It provides flexibility in handling
|
||||
/// the response for custom or advanced use cases.
|
||||
/// </remarks>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> to send.</param>
|
||||
/// <param name="token">An optional <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="Task{TResult}"/> that represents the asynchronous operation. The value of TResult is the raw <see cref="HTTPResponse"/>.
|
||||
/// If the request completes successfully, the task will return the HTTPResponse. If there's an error during the request or if
|
||||
/// the request gets canceled, the task will throw an exception, which can be caught and processed by the calling method.
|
||||
/// </returns>
|
||||
/// <exception cref="AsyncHTTPException">Thrown if there's an error in the request or if the server returns an error status code.</exception>
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<HTTPResponse> GetHTTPResponseAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#else
|
||||
public static Task<HTTPResponse> GetHTTPResponseAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#endif
|
||||
{
|
||||
return CreateTask<HTTPResponse>(request, token, (req, resp, tcs) =>
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
tcs.TrySetResult(resp);
|
||||
break;
|
||||
|
||||
default:
|
||||
GenericResponseHandler<HTTPResponse>(req, resp, tcs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sends an <see cref="HTTPRequest"/> and retrieves the response content as a <c>string</c>.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> to send.</param>
|
||||
/// <param name="token">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A Task that represents the asynchronous operation. The Task will complete with the retrieved <c>string</c> content
|
||||
/// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception.
|
||||
/// </returns>
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<string> GetAsStringAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#else
|
||||
public static Task<string> GetAsStringAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#endif
|
||||
{
|
||||
return CreateTask<string>(request, token, (req, resp, tcs) =>
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
if (resp.IsSuccess)
|
||||
tcs.TrySetResult(resp.DataAsText);
|
||||
else
|
||||
GenericResponseHandler<string>(req, resp, tcs);
|
||||
break;
|
||||
|
||||
default:
|
||||
GenericResponseHandler<string>(req, resp, tcs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#if !UNITY_SERVER
|
||||
/// <summary>
|
||||
/// Asynchronously sends an <see cref="HTTPRequest"/> and retrieves the response content as a <see cref="Texture2D"/>.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> to send.</param>
|
||||
/// <param name="token">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A Task that represents the asynchronous operation. The Task will complete with the retrieved <see cref="Texture2D"/>
|
||||
/// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception.
|
||||
/// </returns>
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<Texture2D> GetAsTexture2DAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#else
|
||||
public static Task<Texture2D> GetAsTexture2DAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#endif
|
||||
{
|
||||
return CreateTask<Texture2D>(request, token, (req, resp, tcs) =>
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
if (resp.IsSuccess)
|
||||
tcs.TrySetResult(resp.DataAsTexture2D);
|
||||
|
||||
else
|
||||
GenericResponseHandler<Texture2D>(req, resp, tcs);
|
||||
break;
|
||||
|
||||
default:
|
||||
GenericResponseHandler<Texture2D>(req, resp, tcs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sends an <see cref="HTTPRequest"/> and retrieves the response content as a <c>byte[]</c>.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> to send.</param>
|
||||
/// <param name="token">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A Task that represents the asynchronous operation. The Task will complete with the retrieved <c>byte[]</c>
|
||||
/// if the request succeeds. If the request fails or is canceled, the Task will complete with an exception.
|
||||
/// </returns>
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<byte[]> GetRawDataAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#else
|
||||
public static Task<byte[]> GetRawDataAsync(this HTTPRequest request, CancellationToken token = default)
|
||||
#endif
|
||||
{
|
||||
return CreateTask<byte[]>(request, token, (req, resp, tcs) =>
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
if (resp.IsSuccess)
|
||||
tcs.TrySetResult(resp.Data);
|
||||
else
|
||||
GenericResponseHandler<byte[]>(req, resp, tcs);
|
||||
break;
|
||||
|
||||
default:
|
||||
GenericResponseHandler<byte[]>(req, resp, tcs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sends an <see cref="HTTPRequest"/> and deserializes the response content into an object of type T using JSON deserialization.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize the JSON content into.</typeparam>
|
||||
/// <param name="request">The <see cref="HTTPRequest"/> to send.</param>
|
||||
/// <param name="token">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A Task that represents the asynchronous operation. The Task will complete with the deserialized object
|
||||
/// if the request succeeds and the response content can be deserialized. If the request fails, is canceled, or
|
||||
/// the response cannot be deserialized, the Task will complete with an exception.
|
||||
/// </returns>
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<T> GetFromJsonResultAsync<T>(this HTTPRequest request, CancellationToken token = default)
|
||||
#else
|
||||
public static Task<T> GetFromJsonResultAsync<T>(this HTTPRequest request, CancellationToken token = default)
|
||||
#endif
|
||||
{
|
||||
return HTTPRequestAsyncExtensions.CreateTask<T>(request, token, (req, resp, tcs) =>
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
if (resp.IsSuccess)
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.TrySetResult(Best.HTTP.JSON.LitJson.JsonMapper.ToObject<T>(resp.DataAsText));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
GenericResponseHandler<T>(req, resp, tcs);
|
||||
break;
|
||||
|
||||
default:
|
||||
GenericResponseHandler<T>(req, resp, tcs);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
#if WITH_UNITASK
|
||||
public static UniTask<T> CreateTask<T>(HTTPRequest request, CancellationToken token, Action<HTTPRequest, HTTPResponse, UniTaskCompletionSource<T>> callback)
|
||||
#else
|
||||
public static Task<T> CreateTask<T>(HTTPRequest request, CancellationToken token, Action<HTTPRequest, HTTPResponse, TaskCompletionSource<T>> callback)
|
||||
#endif
|
||||
{
|
||||
HTTPManager.Setup();
|
||||
|
||||
#if WITH_UNITASK
|
||||
var tcs = new UniTaskCompletionSource<T>();
|
||||
#else
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
#endif
|
||||
|
||||
request.Callback = (req, resp) =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
tcs.TrySetCanceled();
|
||||
else
|
||||
callback(req, resp, tcs);
|
||||
};
|
||||
|
||||
if (token.CanBeCanceled)
|
||||
token.Register((state) => (state as HTTPRequest)?.Abort(), request);
|
||||
|
||||
if (request.State == HTTPRequestStates.Initial)
|
||||
request.Send();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
#if WITH_UNITASK
|
||||
public static void GenericResponseHandler<T>(HTTPRequest req, HTTPResponse resp, UniTaskCompletionSource<T> tcs)
|
||||
#else
|
||||
public static void GenericResponseHandler<T>(HTTPRequest req, HTTPResponse resp, TaskCompletionSource<T> tcs)
|
||||
#endif
|
||||
{
|
||||
switch (req.State)
|
||||
{
|
||||
// The request finished without any problem.
|
||||
case HTTPRequestStates.Finished:
|
||||
if (!resp.IsSuccess)
|
||||
tcs.TrySetException(CreateException($"Request finished Successfully, but the server sent an error ({resp.StatusCode} - '{resp.Message}').", resp));
|
||||
break;
|
||||
|
||||
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
|
||||
case HTTPRequestStates.Error:
|
||||
Log(req, $"Request Finished with Error! {req.Exception?.Message} - {req.Exception?.StackTrace}");
|
||||
|
||||
tcs.TrySetException(CreateException("No Exception", null, req.Exception));
|
||||
break;
|
||||
|
||||
// The request aborted, initiated by the user.
|
||||
case HTTPRequestStates.Aborted:
|
||||
Log(req, "Request Aborted!");
|
||||
|
||||
tcs.TrySetCanceled();
|
||||
break;
|
||||
|
||||
// Connecting to the server is timed out.
|
||||
case HTTPRequestStates.ConnectionTimedOut:
|
||||
Log(req, "Connection Timed Out!");
|
||||
|
||||
tcs.TrySetException(CreateException("Connection Timed Out!"));
|
||||
break;
|
||||
|
||||
// The request didn't finished in the given time.
|
||||
case HTTPRequestStates.TimedOut:
|
||||
Log(req, "Processing the request Timed Out!");
|
||||
|
||||
tcs.TrySetException(CreateException("Processing the request Timed Out!"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public static void Log(HTTPRequest request, string str)
|
||||
{
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPRequestAsyncExtensions), str, request.Context);
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
public static Exception CreateException(string errorMessage, HTTPResponse resp = null, Exception ex = null)
|
||||
{
|
||||
if (resp != null)
|
||||
return new AsyncHTTPException(resp.StatusCode, resp.Message, resp.DataAsText);
|
||||
else if (ex != null)
|
||||
return new AsyncHTTPException(ex.Message, ex);
|
||||
else
|
||||
return new AsyncHTTPException(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 156b3c3f28098b8499aaa2fa550ef45c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible logical states of a HTTTPRequest object.
|
||||
/// </summary>
|
||||
public enum HTTPRequestStates : int
|
||||
{
|
||||
/// <summary>
|
||||
/// Initial status of a request. No callback will be called with this status.
|
||||
/// </summary>
|
||||
Initial,
|
||||
|
||||
/// <summary>
|
||||
/// The request queued for processing.
|
||||
/// </summary>
|
||||
Queued,
|
||||
|
||||
/// <summary>
|
||||
/// Processing of the request started. In this state the client will send the request, and parse the response. No callback will be called with this status.
|
||||
/// </summary>
|
||||
Processing,
|
||||
|
||||
/// <summary>
|
||||
/// The request finished without problem. Parsing the response done, the result can be used. The user defined callback will be called with a valid response object. The request’s Exception property will be null.
|
||||
/// </summary>
|
||||
Finished,
|
||||
|
||||
/// <summary>
|
||||
/// The request finished with an unexpected error. The user defined callback will be called with a null response object. The request's Exception property may contain more info about the error, but it can be null.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The request aborted by the client(HTTPRequest’s Abort() function). The user defined callback will be called with a null response. The request’s Exception property will be null.
|
||||
/// </summary>
|
||||
Aborted,
|
||||
|
||||
/// <summary>
|
||||
/// Connecting to the server timed out. The user defined callback will be called with a null response. The request’s Exception property will be null.
|
||||
/// </summary>
|
||||
ConnectionTimedOut,
|
||||
|
||||
/// <summary>
|
||||
/// The request didn't finished in the given time. The user defined callback will be called with a null response. The request’s Exception property will be null.
|
||||
/// </summary>
|
||||
TimedOut
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90ac4a07ce090bb4196db8a200211974
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.tivadar.best.http/Runtime/HTTP/Hosts.meta
Normal file
8
Packages/com.tivadar.best.http/Runtime/HTTP/Hosts.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad5bafb2bc9d90c4a8e2f9524d0bcc0d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a54f9a32a9b66a4a8de064ca99cfe52
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract base class for concrete connection implementation (HTTP/1, HTTP/2, WebGL, File).
|
||||
/// </summary>
|
||||
public abstract class ConnectionBase : IDisposable
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// The address of the server that this connection is bound to.
|
||||
/// </summary>
|
||||
public HostKey HostKey { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The state of this connection.
|
||||
/// </summary>
|
||||
public HTTPConnectionStates State { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the State is HTTPConnectionStates.Processing, then it holds a HTTPRequest instance. Otherwise it's null.
|
||||
/// </summary>
|
||||
public HTTPRequest CurrentRequest { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// How much the connection kept alive after its last request processing.
|
||||
/// </summary>
|
||||
public virtual TimeSpan KeepAliveTime { get; protected set; }
|
||||
|
||||
public virtual bool CanProcessMultiple { get { return false; } }
|
||||
|
||||
/// <summary>
|
||||
/// Number of assigned requests to process.
|
||||
/// </summary>
|
||||
public virtual int AssignedRequests { get { return this.State != HTTPConnectionStates.Initial && this.State != HTTPConnectionStates.Free ? 1 : 0; } }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of assignable requests.
|
||||
/// </summary>
|
||||
public virtual int MaxAssignedRequests { get; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// When we start to process the current request. It's set after the connection is established.
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; protected set; }
|
||||
|
||||
public Uri LastProcessedUri { get; protected set; }
|
||||
|
||||
public DateTime LastProcessTime { get; protected set; }
|
||||
|
||||
public LoggingContext Context { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Privates
|
||||
|
||||
protected bool IsThreaded;
|
||||
|
||||
#endregion
|
||||
|
||||
public ConnectionBase(HostKey hostKey)
|
||||
:this(hostKey, true)
|
||||
{}
|
||||
|
||||
public ConnectionBase(HostKey hostKey, bool threaded)
|
||||
{
|
||||
this.HostKey = hostKey;
|
||||
|
||||
this.State = HTTPConnectionStates.Initial;
|
||||
this.LastProcessTime = HTTPManager.CurrentFrameDateTime; // DateTime.UtcNow;
|
||||
|
||||
// By default we assume an HTTP/1 connection, but in the HTTP-Over-TCP-Connection the request handlers will decide its value.
|
||||
this.KeepAliveTime = HTTPManager.PerHostSettings.Get(this.HostKey.Host).HTTP1ConnectionSettings.MaxConnectionIdleTime;
|
||||
|
||||
this.IsThreaded = threaded;
|
||||
|
||||
this.Context = new LoggingContext(this);
|
||||
this.Context.Add("HostKey", this.HostKey.ToString());
|
||||
}
|
||||
|
||||
internal virtual void Process(HTTPRequest request)
|
||||
{
|
||||
if (State == HTTPConnectionStates.Processing)
|
||||
throw new Exception("Connection already processing a request! " + this.ToString());
|
||||
|
||||
this.State = HTTPConnectionStates.Processing;
|
||||
|
||||
this.CurrentRequest = request;
|
||||
this.LastProcessedUri = this.CurrentRequest.CurrentUri;
|
||||
|
||||
if (IsThreaded)
|
||||
{
|
||||
ThreadedRunner.RunLongLiving(ThreadFunc);
|
||||
}
|
||||
else
|
||||
ThreadFunc();
|
||||
}
|
||||
|
||||
protected virtual void ThreadFunc()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ShutdownTypes ShutdownType { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plugin shuts down immediately.
|
||||
/// </summary>
|
||||
public virtual void Shutdown(ShutdownTypes type)
|
||||
{
|
||||
this.ShutdownType = type;
|
||||
}
|
||||
|
||||
#region Dispose Pattern
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
|
||||
~ConnectionBase()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}:{1}]", this.Context.Hash, this.HostKey.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 060e50add8ab92b429eba096140dbc83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
public enum ConnectionEvents
|
||||
{
|
||||
StateChange,
|
||||
ProtocolSupport
|
||||
}
|
||||
|
||||
public readonly struct ConnectionEventInfo
|
||||
{
|
||||
public readonly ConnectionBase Source;
|
||||
|
||||
public readonly ConnectionEvents Event;
|
||||
|
||||
public readonly HTTPConnectionStates State;
|
||||
|
||||
public readonly HostProtocolSupport ProtocolSupport;
|
||||
|
||||
public readonly HTTPRequest Request;
|
||||
|
||||
public readonly HTTPRequestStates RequestState;
|
||||
|
||||
public ConnectionEventInfo(ConnectionBase sourceConn, ConnectionEvents @event)
|
||||
{
|
||||
this.Source = sourceConn;
|
||||
this.Event = @event;
|
||||
|
||||
this.State = HTTPConnectionStates.Initial;
|
||||
|
||||
this.ProtocolSupport = HostProtocolSupport.Unknown;
|
||||
|
||||
this.Request = null;
|
||||
this.RequestState = HTTPRequestStates.Initial;
|
||||
}
|
||||
|
||||
public ConnectionEventInfo(ConnectionBase sourceConn, HTTPConnectionStates newState)
|
||||
{
|
||||
this.Source = sourceConn;
|
||||
|
||||
this.Event = ConnectionEvents.StateChange;
|
||||
|
||||
this.State = newState;
|
||||
|
||||
this.ProtocolSupport = HostProtocolSupport.Unknown;
|
||||
|
||||
this.Request = null;
|
||||
this.RequestState = HTTPRequestStates.Initial;
|
||||
}
|
||||
|
||||
public ConnectionEventInfo(ConnectionBase sourceConn, HostProtocolSupport protocolSupport)
|
||||
{
|
||||
this.Source = sourceConn;
|
||||
this.Event = ConnectionEvents.ProtocolSupport;
|
||||
|
||||
this.State = HTTPConnectionStates.Initial;
|
||||
|
||||
this.ProtocolSupport = protocolSupport;
|
||||
|
||||
this.Request = null;
|
||||
this.RequestState = HTTPRequestStates.Initial;
|
||||
}
|
||||
|
||||
public ConnectionEventInfo(ConnectionBase sourceConn, HTTPRequest request)
|
||||
{
|
||||
this.Source = sourceConn;
|
||||
|
||||
this.Event = ConnectionEvents.StateChange;
|
||||
|
||||
this.State = HTTPConnectionStates.ClosedResendRequest;
|
||||
|
||||
this.ProtocolSupport = HostProtocolSupport.Unknown;
|
||||
|
||||
this.Request = request;
|
||||
this.RequestState = HTTPRequestStates.Initial;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
switch(this.Event)
|
||||
{
|
||||
case ConnectionEvents.StateChange: return $"[ConnectionEventInfo Source: {this.Source.ToString()} To State: {this.State}]";
|
||||
case ConnectionEvents.ProtocolSupport: return $"[ConnectionEventInfo Source: {this.Source.ToString()} ProtocolSupport: {this.ProtocolSupport}]";
|
||||
default: return string.Format("[ConnectionEventInfo SourceConnection: {0}, Event: {1}, State: {2}, ProtocolSupport: {3}]", this.Source.ToString(), this.Event, this.State, this.ProtocolSupport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionEventHelper
|
||||
{
|
||||
private static ConcurrentQueue<ConnectionEventInfo> connectionEventQueue = new ConcurrentQueue<ConnectionEventInfo>();
|
||||
|
||||
#pragma warning disable 0649
|
||||
public static Action<ConnectionEventInfo> OnEvent;
|
||||
#pragma warning restore
|
||||
|
||||
public static void EnqueueConnectionEvent(ConnectionEventInfo @event)
|
||||
{
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Information("ConnectionEventHelper", "Enqueue " + @event.ToString(), @event.Source.Context);
|
||||
|
||||
connectionEventQueue.Enqueue(@event);
|
||||
}
|
||||
|
||||
internal static void Clear()
|
||||
{
|
||||
connectionEventQueue.Clear();
|
||||
}
|
||||
|
||||
internal static void ProcessQueue()
|
||||
{
|
||||
ConnectionEventInfo connectionEvent;
|
||||
while (connectionEventQueue.TryDequeue(out connectionEvent))
|
||||
{
|
||||
//if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
// HTTPManager.Logger.Information("ConnectionEventHelper", "Processing connection event: " + connectionEvent.ToString(), connectionEvent.Source.Context);
|
||||
|
||||
if (OnEvent != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnEvent(connectionEvent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("ConnectionEventHelper", "ProcessQueue", ex, connectionEvent.Source.Context);
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionEvent.Source.LastProcessedUri == null)
|
||||
{
|
||||
HTTPManager.Logger.Information("ConnectionEventHelper", String.Format("Ignoring ConnectionEventInfo({0}) because its LastProcessedUri is null!", connectionEvent.ToString()), connectionEvent.Source.Context);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (connectionEvent.Event)
|
||||
{
|
||||
case ConnectionEvents.StateChange:
|
||||
HandleConnectionStateChange(connectionEvent);
|
||||
break;
|
||||
|
||||
case ConnectionEvents.ProtocolSupport:
|
||||
HostManager.GetHostVariant(connectionEvent.Source)
|
||||
.AddProtocol(connectionEvent.ProtocolSupport);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleConnectionStateChange(ConnectionEventInfo @event)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connection = @event.Source;
|
||||
|
||||
switch (@event.State)
|
||||
{
|
||||
case HTTPConnectionStates.Recycle:
|
||||
HostManager.GetHostVariant(connection)
|
||||
.RecycleConnection(connection)
|
||||
.TryToSendQueuedRequests();
|
||||
|
||||
break;
|
||||
|
||||
case HTTPConnectionStates.WaitForProtocolShutdown:
|
||||
HostManager.GetHostVariant(connection)
|
||||
.RemoveConnection(connection, @event.State);
|
||||
break;
|
||||
|
||||
case HTTPConnectionStates.Closed:
|
||||
case HTTPConnectionStates.ClosedResendRequest:
|
||||
// in case of ClosedResendRequest
|
||||
if (@event.Request != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(@event.Request, RequestEvents.Resend));
|
||||
|
||||
if (connection.LastProcessedUri == null)
|
||||
UnityEngine.Debug.LogError($"{connection} - LastProcessedUri is null!");
|
||||
|
||||
HostManager.GetHostVariant(connection)
|
||||
.RemoveConnection(connection, @event.State)
|
||||
.TryToSendQueuedRequests();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("ConnectionEvents", $"HandleConnectionStateChange ({@event.State})", ex, @event.Source.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 050e6e85c91a93849bade99924033025
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,310 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Caching;
|
||||
using Best.HTTP.Cookies;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
using static Best.HTTP.Response.HTTPStatusCodes;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/draft-thomson-hybi-http-timeout-03
|
||||
/// Test servers: http://tools.ietf.org/ http://nginx.org/
|
||||
/// </summary>
|
||||
public sealed class KeepAliveHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// A host sets the value of the "timeout" parameter to the time that the host will allow an idle connection to remain open before it is closed. A connection is idle if no data is sent or received by a host.
|
||||
/// </summary>
|
||||
public TimeSpan TimeOut { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The "max" parameter has been used to indicate the maximum number of requests that would be made on the connection.This parameter is deprecated.Any limit on requests can be enforced by sending "Connection: close" and closing the connection.
|
||||
/// </summary>
|
||||
public int MaxRequests { get; private set; }
|
||||
|
||||
public void Parse(List<string> headerValues)
|
||||
{
|
||||
HeaderParser parser = new HeaderParser(headerValues[0]);
|
||||
HeaderValue value;
|
||||
|
||||
this.TimeOut = TimeSpan.MaxValue;
|
||||
this.MaxRequests = int.MaxValue;
|
||||
|
||||
if (parser.TryGet("timeout", out value) && value.HasValue)
|
||||
{
|
||||
int intValue = 0;
|
||||
if (int.TryParse(value.Value, out intValue) && intValue > 1)
|
||||
this.TimeOut = TimeSpan.FromSeconds(intValue - 1);
|
||||
}
|
||||
|
||||
if (parser.TryGet("max", out value) && value.HasValue)
|
||||
{
|
||||
int intValue = 0;
|
||||
if (int.TryParse("max", out intValue))
|
||||
this.MaxRequests = intValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static helper class to handle cases where the plugin has to do additional logic based on the received response. These are like connection management, handling redirections, loading from local cache, authentication challanges, etc.
|
||||
/// </summary>
|
||||
public static class ConnectionHelper
|
||||
{
|
||||
public static void ResendRequestAndCloseConnection(ConnectionBase connection, HTTPRequest request)
|
||||
{
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(connection, request));
|
||||
}
|
||||
|
||||
public static void EnqueueEvents(ConnectionBase connection, HTTPConnectionStates connectionState, HTTPRequest request, HTTPRequestStates requestState, Exception error)
|
||||
{
|
||||
// SetState
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, requestState, error));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(connection, connectionState));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the whole response received
|
||||
/// </summary>
|
||||
public static Exception HandleResponse(HTTPRequest request,
|
||||
out bool resendRequest,
|
||||
out HTTPConnectionStates proposedConnectionState,
|
||||
ref KeepAliveHeader keepAlive,
|
||||
LoggingContext loggingContext)
|
||||
{
|
||||
resendRequest = false;
|
||||
proposedConnectionState = HTTPConnectionStates.Recycle;
|
||||
|
||||
var resp = request.Response;
|
||||
var currentUri = request.CurrentUri;
|
||||
|
||||
if (resp == null)
|
||||
return null;
|
||||
|
||||
// Try to store cookies before we do anything else, as we may remove the response deleting the cookies as well.
|
||||
CookieJar.SetFromRequest(resp);
|
||||
|
||||
switch (resp.StatusCode)
|
||||
{
|
||||
// Not authorized
|
||||
// https://www.rfc-editor.org/rfc/rfc9110.html#name-www-authenticate
|
||||
case Unauthorized:
|
||||
if (request.Authenticator != null)
|
||||
resendRequest = request.Authenticator.HandleChallange(request, resp);
|
||||
|
||||
goto default;
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
case ProxyAuthenticationRequired:
|
||||
if (request.ProxySettings == null)
|
||||
goto default;
|
||||
|
||||
resendRequest = request.ProxySettings.Handle407(request);
|
||||
|
||||
goto default;
|
||||
#endif
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-417-expectation-failed
|
||||
case ExpectationFailed: // expectation failed
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#section-10.1.1-11.4
|
||||
// A client that receives a 417 (Expectation Failed) status code in response to a request
|
||||
// containing a 100-continue expectation SHOULD repeat that request without a 100-continue expectation,
|
||||
// since the 417 response merely indicates that the response chain does not support expectations (e.g., it passes through an HTTP/1.0 server).
|
||||
|
||||
//request.UploadSettings.ResetExpects();
|
||||
//resendRequest = true;
|
||||
break;
|
||||
|
||||
// Redirected
|
||||
case MovedPermanently: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
|
||||
case Found: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3
|
||||
case SeeOther:
|
||||
case TemporaryRedirect: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.8
|
||||
case PermanentRedirect: // http://tools.ietf.org/html/rfc7238
|
||||
{
|
||||
if (request.RedirectSettings.RedirectCount >= request.RedirectSettings.MaxRedirects)
|
||||
goto default;
|
||||
request.RedirectSettings.RedirectCount++;
|
||||
|
||||
string location = resp.GetFirstHeaderValue("location");
|
||||
if (!string.IsNullOrEmpty(location))
|
||||
{
|
||||
Uri redirectUri = ConnectionHelper.GetRedirectUri(request, location);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(ConnectionHelper), $"Redirected to Location: '{location}' redirectUri: '{redirectUri}'", loggingContext);
|
||||
|
||||
if (redirectUri == request.CurrentUri)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(ConnectionHelper), "Redirected to the same location!", loggingContext);
|
||||
goto default;
|
||||
}
|
||||
|
||||
// Let the user to take some control over the redirection
|
||||
if (!request.RedirectSettings.CallOnBeforeRedirection(request, resp, redirectUri))
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(ConnectionHelper), "OnBeforeRedirection returned False", loggingContext);
|
||||
goto default;
|
||||
}
|
||||
|
||||
if (!request.CurrentUri.Host.Equals(redirectUri.Host, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
//DNSCache.Prefetch(redirectUri.Host);
|
||||
Shared.PlatformSupport.Network.DNS.Cache.DNSCache.Query(new Shared.PlatformSupport.Network.DNS.Cache.DNSQueryParameters(redirectUri) { Context = loggingContext });
|
||||
#endif
|
||||
|
||||
// Remove unsafe headers when redirected to an other host.
|
||||
// Just like for https://www.rfc-editor.org/rfc/rfc9110#name-redirection-3xx
|
||||
request.RemoveUnsafeHeaders();
|
||||
}
|
||||
|
||||
// Set the Referer header to the last Uri.
|
||||
request.SetHeader("Referer", request.CurrentUri.ToString());
|
||||
|
||||
// Set the new Uri, the CurrentUri will return this while the IsRedirected property is true
|
||||
request.RedirectSettings.RedirectUri = redirectUri;
|
||||
|
||||
request.RedirectSettings.IsRedirected = true;
|
||||
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
return new Exception($"Got redirect status({resp.StatusCode}) without 'location' header!");
|
||||
|
||||
goto default;
|
||||
}
|
||||
|
||||
case NotModified:
|
||||
if (request.DownloadSettings.DisableCache || HTTPManager.LocalCache == null)
|
||||
break;
|
||||
|
||||
var hash = HTTPCache.CalculateHash(request.MethodType, request.CurrentUri);
|
||||
if (HTTPManager.LocalCache.RefreshHeaders(hash, resp.Headers, request.Context))
|
||||
{
|
||||
HTTPManager.LocalCache.Redirect(request, hash);
|
||||
resendRequest = true;
|
||||
}
|
||||
break;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc5861.html#section-4
|
||||
// In this context, an error is any situation that would result in a
|
||||
// 500, 502, 503, or 504 HTTP response status code being returned.
|
||||
case var statusCode when statusCode == InternalServerError || (statusCode >= BadGateway && statusCode <= GatewayTimeout):
|
||||
if (HTTPManager.LocalCache != null)
|
||||
{
|
||||
hash = HTTPCache.CalculateHash(request.MethodType, request.CurrentUri);
|
||||
if (HTTPManager.LocalCache.CanServeWithoutValidation(hash, ErrorTypeForValidation.ServerError, request.Context))
|
||||
{
|
||||
HTTPManager.LocalCache.Redirect(request, hash);
|
||||
resendRequest = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// If we have a response and the server telling us that it closed the connection after the message sent to us, then
|
||||
// we will close the connection too.
|
||||
bool closeByServer = resp.HasHeaderWithValue("connection", "close") ||
|
||||
resp.HasHeaderWithValue("proxy-connection", "close");
|
||||
|
||||
bool tryToKeepAlive = !string.IsNullOrEmpty(currentUri.Host) && HTTPManager.PerHostSettings.Get(currentUri.Host).HTTP1ConnectionSettings.TryToReuseConnections;
|
||||
bool closeByClient = !tryToKeepAlive;
|
||||
|
||||
// BugFix: We MUST NOT close the underlying connection when we just upgraded (switched) protocols!
|
||||
// - Explanation: Setting TryToReuseConnections to false would otherwise force the connection to close for websocket connections too!
|
||||
if ((closeByServer || closeByClient) && resp.StatusCode != SwitchingProtocols)
|
||||
{
|
||||
proposedConnectionState = HTTPConnectionStates.Closed;
|
||||
}
|
||||
else if (resp != null)
|
||||
{
|
||||
var keepAliveheaderValues = resp.GetHeaderValues("keep-alive");
|
||||
if (keepAliveheaderValues != null && keepAliveheaderValues.Count > 0)
|
||||
{
|
||||
if (keepAlive == null)
|
||||
keepAlive = new KeepAliveHeader();
|
||||
keepAlive.Parse(keepAliveheaderValues);
|
||||
}
|
||||
}
|
||||
|
||||
// Null out the response here instead of the redirected cases (301, 302, 307, 308)
|
||||
// because response might have a Connection: Close header that we would miss to process.
|
||||
// If Connection: Close is present, the server is closing the connection and we would
|
||||
// reuse that closed connection.
|
||||
if (resendRequest)
|
||||
{
|
||||
HTTPManager.Logger.Verbose(nameof(ConnectionHelper), "HandleResponse - discarding response", request.Response?.Context ?? loggingContext);
|
||||
|
||||
request.Response?.Dispose();
|
||||
// Discard the redirect response, we don't need it any more
|
||||
request.Response = null;
|
||||
|
||||
if (proposedConnectionState == HTTPConnectionStates.Closed)
|
||||
proposedConnectionState = HTTPConnectionStates.ClosedResendRequest;
|
||||
}
|
||||
|
||||
if (!resendRequest && proposedConnectionState < HTTPConnectionStates.Closed && resp.IsUpgraded)
|
||||
proposedConnectionState = HTTPConnectionStates.WaitForProtocolShutdown;
|
||||
else
|
||||
{
|
||||
// Do nothing here, the what we are timing for is decived in the caller, outside code.
|
||||
//request.Timing.Finish(TimingEventNames.Response_Received);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Uri GetRedirectUri(HTTPRequest request, string location)
|
||||
{
|
||||
Uri result = null;
|
||||
try
|
||||
{
|
||||
result = new Uri(location);
|
||||
|
||||
if (result.IsFile || result.AbsolutePath == location)
|
||||
result = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Sometimes the server sends back only the path and query component of the new uri
|
||||
result = null;
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
var baseURL = request.CurrentUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
|
||||
|
||||
if (!location.StartsWith("/"))
|
||||
{
|
||||
var segments = request.CurrentUri.Segments;
|
||||
segments[segments.Length - 1] = location;
|
||||
|
||||
location = String.Join(string.Empty, segments);
|
||||
if (location.StartsWith("//"))
|
||||
location = location.Substring(1);
|
||||
}
|
||||
|
||||
bool endsWithSlash = baseURL[baseURL.Length - 1] == '/';
|
||||
bool startsWithSlash = location[0] == '/';
|
||||
if (endsWithSlash && startsWithSlash)
|
||||
result = new Uri(baseURL + location.Substring(1));
|
||||
else if (!endsWithSlash && !startsWithSlash)
|
||||
result = new Uri(baseURL + '/' + location);
|
||||
else
|
||||
result = new Uri(baseURL + location);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58178ddebb70e5445954339197663a91
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf1d243d47a66dd42af3d2c418120e33
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Caching;
|
||||
using Best.HTTP.Hosts.Connections.HTTP1;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.FileSystem;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.File
|
||||
{
|
||||
internal sealed class FileConnection : ConnectionBase, IContentConsumer, IDownloadContentBufferAvailable
|
||||
{
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
PeekableHTTP1Response _response;
|
||||
NonblockingUnderlyingStream _stream;
|
||||
|
||||
UnityEngine.Hash128 _cacheHash;
|
||||
|
||||
public FileConnection(HostKey hostKey)
|
||||
: base(hostKey)
|
||||
{ }
|
||||
|
||||
protected override void ThreadFunc()
|
||||
{
|
||||
this.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
||||
|
||||
this.Context.Remove("Request");
|
||||
this.Context.Add("Request", this.CurrentRequest.Context);
|
||||
|
||||
bool isFromLocalCache = this.CurrentRequest.CurrentUri.Host.Equals(HTTPCache.CacheHostName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (this._response == null)
|
||||
this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, isFromLocalCache, this);
|
||||
this._response.Context.Add(nameof(FileConnection), this.Context);
|
||||
|
||||
StreamList stream = new StreamList();
|
||||
try
|
||||
{
|
||||
var headers = new BufferPoolMemoryStream();
|
||||
stream.AppendStream(headers);
|
||||
|
||||
headers.WriteLine("HTTP/1.1 200 Ok");
|
||||
|
||||
System.IO.Stream contentStream = null;
|
||||
|
||||
if (isFromLocalCache)
|
||||
{
|
||||
var hashStr = this.CurrentRequest.CurrentUri.AbsolutePath.Substring(1);
|
||||
var hash = UnityEngine.Hash128.Parse(hashStr);
|
||||
|
||||
// BeginReadContent tries to acquire a read lock on the content and returns null if couldn't.
|
||||
contentStream = HTTPManager.LocalCache?.BeginReadContent(hash, this.Context);
|
||||
if (contentStream == null)
|
||||
throw new HTTPCacheAcquireLockException($"Coulnd't acquire read lock on cached entity.");
|
||||
|
||||
this._cacheHash = hash;
|
||||
|
||||
headers.WriteLine($"BestHTTP-Origin: cachefile({hashStr})");
|
||||
|
||||
stream.AppendStream(HTTPManager.IOService.CreateFileStream(HTTPManager.LocalCache.GetHeaderPathFromHash(hash), FileStreamModes.OpenRead));
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.WriteLine($"BestHTTP-Origin: file");
|
||||
headers.WriteLine("Content-Type: application/octet-stream");
|
||||
|
||||
contentStream = HTTPManager.IOService.CreateFileStream(this.CurrentRequest.CurrentUri.LocalPath, FileStreamModes.OpenRead);
|
||||
}
|
||||
|
||||
headers.WriteLine($"Content-Length: {contentStream.Length.ToString()}");
|
||||
if (!isFromLocalCache)
|
||||
headers.WriteLine();
|
||||
|
||||
headers.Seek(0, System.IO.SeekOrigin.Begin);
|
||||
|
||||
stream.AppendStream(contentStream);
|
||||
|
||||
this.CurrentRequest.TimeoutSettings.SetProcessing(DateTime.UtcNow);
|
||||
|
||||
this._stream = new NonblockingUnderlyingStream(stream, 1024 * 1024, this.Context);
|
||||
this._stream.SetTwoWayBinding(this);
|
||||
this._stream.BeginReceive();
|
||||
|
||||
this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
FinishedProcessing(ex);
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
|
||||
{
|
||||
//HTTPManager.Logger.Verbose(nameof(FileConnection), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
|
||||
|
||||
// Here we should trigger somehow the read stream and that should call OnContent(IPeekableContentProvider provider, PeekableStream peekable)
|
||||
// to go the regular route.
|
||||
|
||||
if (this._response != null)
|
||||
OnContent();
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider)
|
||||
{
|
||||
this.ContentProvider = contentProvider;
|
||||
}
|
||||
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
this._response.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
}
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(FileConnection), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
|
||||
|
||||
// If the consumer still have a request: error it and close the connection
|
||||
if (this.CurrentRequest != null && this._response != null)
|
||||
{
|
||||
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
||||
}
|
||||
else // If no current request: close the connection
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
public void OnError(Exception e)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(FileConnection), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
|
||||
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
private void OnCancellationRequested(HTTPRequest req)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(FileConnection), "OnCancellationRequested()", this.Context);
|
||||
|
||||
Interlocked.Exchange(ref this._response, null);
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
this._stream.Dispose();
|
||||
}
|
||||
|
||||
void FinishedProcessing(Exception ex)
|
||||
{
|
||||
// Warning: FinishedProcessing might be called from different threads in parallel:
|
||||
// - send thread triggered by a write failure
|
||||
// - read thread oncontent/OnError/OnConnectionClosed
|
||||
|
||||
var resp = Interlocked.Exchange(ref this._response, null);
|
||||
if (resp == null)
|
||||
return;
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);
|
||||
|
||||
HTTPManager.LocalCache?.EndReadContent(this._cacheHash, this.Context);
|
||||
this._cacheHash = new UnityEngine.Hash128();
|
||||
|
||||
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
||||
this._stream?.Unbind();
|
||||
this._stream?.Dispose();
|
||||
this._stream = null;
|
||||
|
||||
var req = this.CurrentRequest;
|
||||
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
bool resendRequest = false;
|
||||
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
||||
HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
|
||||
Exception error = ex;
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Timeout is a non-retryable error
|
||||
if (ex is TimeoutException)
|
||||
{
|
||||
error = null;
|
||||
requestState = HTTPRequestStates.TimedOut;
|
||||
}
|
||||
else if (ex is HTTPCacheAcquireLockException)
|
||||
{
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
|
||||
{
|
||||
req.RetrySettings.Retries++;
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestState = HTTPRequestStates.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
|
||||
connectionState = HTTPConnectionStates.Closed;
|
||||
|
||||
resp.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// After HandleResponse connectionState can have the following values:
|
||||
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
|
||||
// - Closed: server sent an connection: close header.
|
||||
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
|
||||
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
|
||||
|
||||
KeepAliveHeader keepAlive = null;
|
||||
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
|
||||
connectionState = HTTPConnectionStates.Recycle;
|
||||
|
||||
if (!resendRequest && resp.IsUpgraded)
|
||||
requestState = HTTPRequestStates.Processing;
|
||||
}
|
||||
|
||||
req.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
|
||||
|
||||
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
|
||||
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
|
||||
{
|
||||
ConnectionHelper.ResendRequestAndCloseConnection(this, req);
|
||||
}
|
||||
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
||||
{
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise set the request's then the connection's state
|
||||
ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 589a909d7aa31c64a936621b85c9fe1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce034ab1cdf3f3244a7e21c4251fcccf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP1
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const byte CR = 13;
|
||||
public const byte LF = 10;
|
||||
|
||||
public static readonly byte[] EOL = { Constants.CR, Constants.LF };
|
||||
|
||||
public static readonly byte[] HeaderValueSeparator = { (byte)':', (byte)' ' };
|
||||
|
||||
// expect: 100-continue
|
||||
//public static readonly byte[] Expect100Continue = { (byte)'e', (byte)'x', (byte)'p', (byte)'e', (byte)'c', (byte)'t', (byte)':', (byte)' ', (byte)'1', (byte)'0', (byte)'0', (byte)'-', (byte)'c', (byte)'o', (byte)'n', (byte)'t', (byte)'i', (byte)'n', (byte)'u', (byte)'e', CR, LF };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7fa489dfa1912f42974e02a6cf10e9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,605 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Request.Upload;
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP1
|
||||
{
|
||||
public sealed class HTTP1ContentConsumer : IHTTPRequestHandler, IContentConsumer, IDownloadContentBufferAvailable, IThreadSignaler
|
||||
{
|
||||
public ShutdownTypes ShutdownType { get; private set; }
|
||||
|
||||
public KeepAliveHeader KeepAlive { get { return this._keepAlive; } }
|
||||
private KeepAliveHeader _keepAlive;
|
||||
|
||||
public bool CanProcessMultiple { get { return false; } }
|
||||
|
||||
/// <summary>
|
||||
/// Number of assigned requests to process.
|
||||
/// </summary>
|
||||
public int AssignedRequests => this.conn.CurrentRequest == null ? 0 : 1;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of assignable requests
|
||||
/// </summary>
|
||||
public int MaxAssignedRequests => 1;
|
||||
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
private readonly HTTPOverTCPConnection conn;
|
||||
private PeekableHTTP1Response _response;
|
||||
private int _isAlreadyProcessingContent;
|
||||
private WriteOnlyBufferedStream _upStream;
|
||||
|
||||
private int _isSendingContent;
|
||||
private int _hasMoreData;
|
||||
private bool _isContentSendingFinished;
|
||||
private int _acceptContentSignals;
|
||||
|
||||
// Initialize the progress report variables
|
||||
private long _uploaded = 0;
|
||||
|
||||
public HTTP1ContentConsumer(HTTPOverTCPConnection conn)
|
||||
{
|
||||
this.Context = new LoggingContext(this);
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
public void RunHandler()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "Started processing request", this.Context);
|
||||
|
||||
//ThreadedRunner.SetThreadName("Best.HTTP1 Write");
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(now))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.conn.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
// create the response before we would send out the request, because sending out might cause an exception
|
||||
// and the response is used for decision making in the FinishedProcessing call.
|
||||
if (this._response == null)
|
||||
this.conn.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.conn.CurrentRequest, false, this);
|
||||
|
||||
// Write the request to the stream
|
||||
this.conn.CurrentRequest.TimeoutSettings.SetProcessing(now);
|
||||
|
||||
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Request_Sent);
|
||||
|
||||
SendOutTo(this.conn.CurrentRequest, this.conn.TopStream);
|
||||
|
||||
this.conn.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendOutTo(HTTPRequest request, System.IO.Stream stream)
|
||||
{
|
||||
request.Prepare();
|
||||
|
||||
string requestPathAndQuery =
|
||||
request.ProxySettings.HasProxyFor(request.CurrentUri) ?
|
||||
request.ProxySettings.Proxy.GetRequestPath(request.CurrentUri) :
|
||||
request.CurrentUri.GetRequestPathAndQueryURL();
|
||||
|
||||
string requestLine = string.Format("{0} {1} HTTP/1.1", HTTPRequest.MethodNames[(byte)request.MethodType], requestPathAndQuery);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HTTPRequest", string.Format("Sending request: '{0}'", requestLine), request.Context);
|
||||
|
||||
// Create a buffer stream that will not close 'stream' when disposed or closed.
|
||||
// buffersize should be larger than UploadChunkSize as it might be used for uploading user data and
|
||||
// it should have enough room for UploadChunkSize data and additional chunk information.
|
||||
using (WriteOnlyBufferedStream bufferStream = new WriteOnlyBufferedStream(stream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context))
|
||||
{
|
||||
var requestLineBytes = requestLine.GetASCIIBytes();
|
||||
bufferStream.WriteBufferSegment(requestLineBytes);
|
||||
bufferStream.WriteArray(EOL);
|
||||
|
||||
BufferPool.Release(requestLineBytes);
|
||||
|
||||
// Write headers to the buffer
|
||||
request.EnumerateHeaders((header, values) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(header) || values == null)
|
||||
return;
|
||||
|
||||
//var headerName = string.Concat(header, ": ").GetASCIIBytes();
|
||||
var headerName = header.GetASCIIBytes();
|
||||
|
||||
for (int i = 0; i < values.Count; ++i)
|
||||
{
|
||||
if (string.IsNullOrEmpty(values[i]))
|
||||
{
|
||||
HTTPManager.Logger.Warning("HTTPRequest", string.Format("Null/empty value for header: {0}", header), request.Context);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Verbose("HTTPRequest", $"Header - '{header}': '{values[i]}'", request.Context);
|
||||
|
||||
var valueBytes = values[i].GetASCIIBytes();
|
||||
|
||||
bufferStream.WriteBufferSegment(headerName);
|
||||
bufferStream.WriteArray(HeaderValueSeparator);
|
||||
bufferStream.WriteBufferSegment(valueBytes);
|
||||
bufferStream.WriteArray(EOL);
|
||||
|
||||
BufferPool.Release(valueBytes);
|
||||
}
|
||||
|
||||
BufferPool.Release(headerName);
|
||||
}, /*callBeforeSendCallback:*/ true);
|
||||
|
||||
bufferStream.WriteArray(EOL);
|
||||
|
||||
// Send body content
|
||||
if (this.conn.CurrentRequest.UploadSettings.UploadStream is Request.Upload.UploadStreamBase upStream)
|
||||
upStream.BeforeSendBody(this.conn.CurrentRequest, this);
|
||||
|
||||
Interlocked.Exchange(ref this._isSendingContent, 1);
|
||||
Interlocked.Exchange(ref this._acceptContentSignals, 1);
|
||||
|
||||
SendContent(bufferStream);
|
||||
} // bufferStream.Dispose
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("HTTPRequest", "Sent out '" + requestLine + "'", this.Context);
|
||||
}
|
||||
|
||||
void SendContent(WriteOnlyBufferedStream streamToSend)
|
||||
{
|
||||
Interlocked.Exchange(ref this._hasMoreData, 0);
|
||||
|
||||
var request = this.conn.CurrentRequest;
|
||||
System.IO.Stream uploadStream = request.UploadSettings.UploadStream;
|
||||
|
||||
try
|
||||
{
|
||||
if (uploadStream == null)
|
||||
{
|
||||
this._isContentSendingFinished = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (streamToSend == null)
|
||||
{
|
||||
if (this._upStream == null)
|
||||
this._upStream = new WriteOnlyBufferedStream(this.conn.TopStream, (int)(request.UploadSettings.UploadChunkSize * 1.5f), request.Context);
|
||||
streamToSend = this._upStream;
|
||||
}
|
||||
long uploadLength = uploadStream.Length;
|
||||
bool isChunked = uploadLength == BodyLengths.UnknownWithChunkedTransferEncoding;
|
||||
|
||||
// Upload buffer. First we will read the data into this buffer from the UploadStream, then write this buffer to our outStream
|
||||
byte[] buffer = BufferPool.Get(this.conn.CurrentRequest.UploadSettings.UploadChunkSize, true);
|
||||
using var _ = new AutoReleaseBuffer(buffer);
|
||||
|
||||
// How many bytes was read from the UploadStream
|
||||
int count = uploadStream.Read(buffer, 0, buffer.Length);
|
||||
while (count > 0)
|
||||
{
|
||||
if (isChunked)
|
||||
{
|
||||
var countBytes = count.ToString("X").GetASCIIBytes();
|
||||
streamToSend.WriteBufferSegment(countBytes);
|
||||
streamToSend.WriteArray(EOL);
|
||||
|
||||
BufferPool.Release(countBytes);
|
||||
}
|
||||
|
||||
// write out the buffer to the wire
|
||||
streamToSend.Write(buffer, 0, count);
|
||||
|
||||
// chunk trailing EOL
|
||||
if (uploadLength < 0)
|
||||
streamToSend.WriteArray(EOL);
|
||||
|
||||
// update how many bytes are uploaded
|
||||
_uploaded += count;
|
||||
|
||||
if (this.conn.CurrentRequest.UploadSettings.OnUploadProgress != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.conn.CurrentRequest, RequestEvents.UploadProgress, _uploaded, uploadLength));
|
||||
|
||||
if (this.conn.CurrentRequest.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
count = uploadStream.Read(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
this._isContentSendingFinished = count == UploadReadConstants.Completed;
|
||||
|
||||
// All data from the stream are sent, write the 'end' chunk if necessary
|
||||
if (isChunked && this._isContentSendingFinished)
|
||||
{
|
||||
ReadOnlySpan<byte> bytes = stackalloc byte[] { (byte)'0' };
|
||||
|
||||
streamToSend.Write(bytes);
|
||||
streamToSend.WriteArray(EOL);
|
||||
streamToSend.WriteArray(EOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Make sure all remaining data will be on the wire
|
||||
streamToSend?.Flush();
|
||||
|
||||
if (this._isContentSendingFinished)
|
||||
{
|
||||
this._upStream?.Dispose();
|
||||
this._upStream = null;
|
||||
if (this.conn.CurrentRequest.UploadSettings.DisposeStream)
|
||||
uploadStream?.Dispose();
|
||||
|
||||
this.conn.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
||||
|
||||
Interlocked.Exchange(ref this._isSendingContent, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Exchange(ref this._isSendingContent, 0);
|
||||
|
||||
if (this._hasMoreData > 0)
|
||||
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
|
||||
{
|
||||
//HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
|
||||
|
||||
// TODO: Do NOT call OnContent on the Unity main thread
|
||||
if (this._response != null)
|
||||
OnContent();
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
|
||||
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
//HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnContent({peekable?.Length}, {this._response?.ReadState})", this.Context, this.conn.CurrentRequest.Context);
|
||||
try
|
||||
{
|
||||
if (this.conn.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.conn.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
this._response.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null)
|
||||
{
|
||||
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
else if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.WaitForContentSent)
|
||||
{
|
||||
(this as IThreadSignaler).SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
|
||||
|
||||
if (this._response != null &&
|
||||
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
|
||||
this._response.DeliveryMode == PeekableHTTP1Response.ContentDeliveryMode.RawUnknownLength &&
|
||||
"close".Equals(this._response.GetFirstHeaderValue("connection"), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FinishedProcessing(null);
|
||||
}
|
||||
else if (this.ContentProvider != null && this.ContentProvider.Length > 0 &&
|
||||
this._response != null &&
|
||||
this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Content &&
|
||||
this._response.DownStream != null)
|
||||
{
|
||||
// Let the stream comsume any buffered data first, and handle closure when the buffer depletes.
|
||||
// TODO: This require however that the PeekableResponse ping this HTTP1ContentConsumer when buffer space is available in the down-stream.
|
||||
// Or force-add all remaining data to the stream and see whether we finished downloading or not.
|
||||
// Problems:
|
||||
// 1.) OnContent might be already called and a call to it would be dropped. We could spin up a new thread waiting for its finish, then call it again.
|
||||
//throw new NotImplementedException();
|
||||
ThreadedRunner.RunShortLiving(() =>
|
||||
{
|
||||
SpinWait spinWait = new SpinWait();
|
||||
|
||||
while (Interlocked.CompareExchange(ref this._isAlreadyProcessingContent, 1, 0) == 1)
|
||||
spinWait.SpinOnce();
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
this._response.DownStream.EmergencyIncreaseMaxBuffered();
|
||||
this._response.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null)
|
||||
{
|
||||
if (this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
else
|
||||
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref this._isAlreadyProcessingContent, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the consumer still have a request: error it and close the connection
|
||||
if (this.conn.CurrentRequest != null && this._response != null)
|
||||
{
|
||||
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
||||
}
|
||||
else // If no current request: close the connection
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
public void OnError(Exception e)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
|
||||
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
private void OnCancellationRequested(HTTPRequest req)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTP1ContentConsumer), "OnCancellationRequested()", this.Context);
|
||||
|
||||
Interlocked.Exchange(ref this._response, null);
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
this.conn?.Streamer?.Dispose();
|
||||
}
|
||||
|
||||
void FinishedProcessing(Exception ex)
|
||||
{
|
||||
// Warning: FinishedProcessing might be called from different threads in parallel:
|
||||
// - send thread triggered by a write failure
|
||||
// - read thread oncontent/OnError/OnConnectionClosed
|
||||
|
||||
var resp = Interlocked.Exchange(ref this._response, null);
|
||||
if (resp == null)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)}({resp.ReadState}, {ex})", this.Context);
|
||||
|
||||
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
||||
//if (conn.TopStream is IPeekableContentProvider provider && provider?.Consumer == this)
|
||||
// provider.Consumer = null;
|
||||
this.ContentProvider.UnbindIf(this);
|
||||
|
||||
var req = this.conn.CurrentRequest;
|
||||
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
bool resendRequest = false;
|
||||
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
||||
HTTPConnectionStates connectionState = ex != null ? HTTPConnectionStates.Closed : HTTPConnectionStates.Recycle;
|
||||
|
||||
// We could finish the request, ignore the error.
|
||||
if (resp.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
ex = null;
|
||||
|
||||
Exception error = ex;
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Timeout is a non-retryable error
|
||||
if (ex is TimeoutException)
|
||||
{
|
||||
error = null;
|
||||
requestState = HTTPRequestStates.TimedOut;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
|
||||
{
|
||||
req.RetrySettings.Retries++;
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestState = HTTPRequestStates.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
|
||||
connectionState = HTTPConnectionStates.Closed;
|
||||
|
||||
resp.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// After HandleResponse connectionState can have the following values:
|
||||
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
|
||||
// - Closed: server sent an connection: close header.
|
||||
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
|
||||
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
|
||||
|
||||
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref this._keepAlive, this.Context);
|
||||
|
||||
if (error != null)
|
||||
requestState = HTTPRequestStates.Error;
|
||||
else if (!resendRequest && resp.IsUpgraded)
|
||||
requestState = HTTPRequestStates.Processing;
|
||||
}
|
||||
|
||||
req.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
|
||||
|
||||
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
|
||||
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
|
||||
{
|
||||
ConnectionHelper.ResendRequestAndCloseConnection(this.conn, req);
|
||||
}
|
||||
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
||||
{
|
||||
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
|
||||
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, connectionState));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resp == null || !resp.IsUpgraded)
|
||||
(conn.TopStream as IPeekableContentProvider).SwitchIf(null, this.conn as IContentConsumer);
|
||||
|
||||
// Otherwise set the request's then the connection's state
|
||||
ConnectionHelper.EnqueueEvents(this.conn, connectionState, req, requestState, error);
|
||||
}
|
||||
}
|
||||
|
||||
public void Process(HTTPRequest request)
|
||||
{
|
||||
(conn.TopStream as IPeekableContentProvider).SetTwoWayBinding(this);
|
||||
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/179
|
||||
// Toughts:
|
||||
// - Many requests, especially if they are uploading slowly, can occupy all background threads.
|
||||
// Use short-living thread when:
|
||||
// - It's a GET request
|
||||
// - It's not an upgrade request
|
||||
|
||||
bool isRequestWithoutBody = request.MethodType == HTTPMethods.Get ||
|
||||
request.MethodType == HTTPMethods.Head ||
|
||||
request.MethodType == HTTPMethods.Delete ||
|
||||
request.MethodType == HTTPMethods.Options;
|
||||
bool isUpgrade = request.HasHeader("upgrade");
|
||||
|
||||
var useShortLivingThread = HTTPManager.PerHostSettings.Get(request.CurrentHostKey).HTTP1ConnectionSettings.ForceUseThreadPool ||
|
||||
(isRequestWithoutBody && !isUpgrade);
|
||||
|
||||
if (useShortLivingThread)
|
||||
ThreadedRunner.RunShortLiving(RunHandler);
|
||||
else
|
||||
ThreadedRunner.RunLongLiving(RunHandler);
|
||||
}
|
||||
|
||||
public void Shutdown(ShutdownTypes type)
|
||||
{
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP1ContentConsumer), string.Format($"Shutdown({type})"), this.Context);
|
||||
this.ShutdownType = type;
|
||||
}
|
||||
|
||||
void IThreadSignaler.SignalThread(SignalHandlerTypes signalType)
|
||||
{
|
||||
Interlocked.Exchange(ref this._hasMoreData, 1);
|
||||
|
||||
if (this._acceptContentSignals == 1 && Interlocked.CompareExchange(ref this._isSendingContent, 1, 0) == 0)
|
||||
{
|
||||
switch (signalType)
|
||||
{
|
||||
case SignalHandlerTypes.Signal:
|
||||
ThreadedRunner.RunShortLiving(SignalThreadHandler);
|
||||
break;
|
||||
|
||||
case SignalHandlerTypes.InlineIfPossible:
|
||||
SignalThreadHandler();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalThreadHandler()
|
||||
{
|
||||
try
|
||||
{
|
||||
SendContent(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
this._upStream?.Dispose();
|
||||
this._upStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c761c8505fda1a4e9e6be9f79b74054
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,540 @@
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Response.Decompression;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
using static Best.HTTP.Response.HTTPStatusCodes;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP1
|
||||
{
|
||||
/// <summary>
|
||||
/// An HTTP 1.1 response implementation that can utilize a peekable stream.
|
||||
/// Its main entry point is the ProcessPeekable method that should be called after every chunk of data downloaded.
|
||||
/// </summary>
|
||||
public class PeekableHTTP1Response : HTTPResponse
|
||||
{
|
||||
public PeekableReadState ReadState
|
||||
{
|
||||
get => this._readState;
|
||||
private set
|
||||
{
|
||||
if (this._readState != value && HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"{this._readState} => {value}", this.Context);
|
||||
this._readState = value;
|
||||
}
|
||||
}
|
||||
private PeekableReadState _readState;
|
||||
|
||||
public bool ForceDepleteContent;
|
||||
|
||||
public enum ContentDeliveryMode
|
||||
{
|
||||
Raw,
|
||||
RawUnknownLength,
|
||||
Chunked,
|
||||
}
|
||||
|
||||
public enum PeekableReadState
|
||||
{
|
||||
StatusLine,
|
||||
Headers,
|
||||
WaitForContentSent, // when received a 100-continue
|
||||
PrepareForContent,
|
||||
ContentSetup,
|
||||
Content,
|
||||
Finished
|
||||
}
|
||||
|
||||
public ContentDeliveryMode DeliveryMode => this._deliveryMode;
|
||||
private ContentDeliveryMode _deliveryMode;
|
||||
private long _expectedLength;
|
||||
private Dictionary<string, List<string>> _newHeaders;
|
||||
|
||||
long _downloaded = 0;
|
||||
IDecompressor _decompressor = null;
|
||||
bool _compressed = false;
|
||||
bool sendProgressChanged;
|
||||
|
||||
int _chunkLength = -1;
|
||||
|
||||
enum ReadChunkedStates
|
||||
{
|
||||
ReadChunkLength,
|
||||
ReadChunk,
|
||||
ReadTrailingCRLF,
|
||||
ReadTrailingHeaders
|
||||
}
|
||||
ReadChunkedStates _readChunkedState = ReadChunkedStates.ReadChunkLength;
|
||||
|
||||
IDownloadContentBufferAvailable _bufferAvailableHandler;
|
||||
|
||||
public PeekableHTTP1Response(HTTPRequest request, bool isFromCache, IDownloadContentBufferAvailable bufferAvailableHandler)
|
||||
: base(request, isFromCache)
|
||||
{
|
||||
this._bufferAvailableHandler = bufferAvailableHandler;
|
||||
}
|
||||
|
||||
private int _isProccessing;
|
||||
|
||||
public void ProcessPeekable(PeekableContentProviderStream peekable)
|
||||
{
|
||||
// To avoid executing ProcessPeekable in parallel on two threads, do an atomic CompareExchange and return if the old value wasn't 0.
|
||||
if (Interlocked.CompareExchange(ref this._isProccessing, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"ProcessPeekable({this.ReadState}, {peekable.Length})", this.Context);
|
||||
|
||||
try
|
||||
{
|
||||
// The first call after setting it to PeekableReadState.WaitForContentSent is after the the client could send its content.
|
||||
// This also works when "If the request did not contain an Expect header field containing the 100-continue expectation,
|
||||
// the client can simply discard this interim response."
|
||||
// (https://www.rfc-editor.org/rfc/rfc9110#section-15.2.1-3)
|
||||
if (this._readState == PeekableReadState.WaitForContentSent)
|
||||
{
|
||||
this._newHeaders?.Clear();
|
||||
this.Headers?.Clear();
|
||||
|
||||
this._readState = PeekableReadState.StatusLine;
|
||||
}
|
||||
|
||||
// It's an unexpected network closure, except when we reading the content in the RawUnknownLength delivery mode.
|
||||
if (peekable == null && ReadState != PeekableReadState.Content && this._deliveryMode != ContentDeliveryMode.RawUnknownLength)
|
||||
throw new Exception("Server closed the connection unexpectedly!");
|
||||
|
||||
switch (ReadState)
|
||||
{
|
||||
case PeekableReadState.StatusLine:
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return;
|
||||
|
||||
Request.Timing.StartNext(TimingEventNames.Headers);
|
||||
|
||||
var statusLine = HTTPResponse.ReadTo(peekable, (byte)' ');
|
||||
string[] versions = statusLine.Split(new char[] { '/', '.' });
|
||||
|
||||
this.HTTPVersion = new Version(int.Parse(versions[1]), int.Parse(versions[2]));
|
||||
|
||||
int statusCode;
|
||||
string statusCodeStr = NoTrimReadTo(peekable, (byte)' ', LF);
|
||||
|
||||
if (!int.TryParse(statusCodeStr, out statusCode))
|
||||
throw new Exception($"Couldn't parse '{statusCodeStr}' as a status code!");
|
||||
|
||||
this.StatusCode = statusCode;
|
||||
|
||||
if (statusCodeStr.Length > 0 && (byte)statusCodeStr[statusCodeStr.Length - 1] != LF && (byte)statusCodeStr[statusCodeStr.Length - 1] != CR)
|
||||
this.Message = ReadTo(peekable, LF);
|
||||
else
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(PeekableHTTP1Response), "Skipping Status Message reading!", this.Context);
|
||||
|
||||
this.Message = string.Empty;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"HTTP/'{this.HTTPVersion}' '{this.StatusCode}' '{this.Message}'", this.Context);
|
||||
|
||||
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
|
||||
this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
this.ReadState = PeekableReadState.Headers;
|
||||
goto case PeekableReadState.Headers;
|
||||
|
||||
case PeekableReadState.Headers:
|
||||
ProcessReadHeaders(peekable, PeekableReadState.PrepareForContent);
|
||||
if (this.ReadState == PeekableReadState.PrepareForContent)
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
// When upgraded, we don't want to read the content here, so set the state to Finished.
|
||||
if (this.StatusCode == 101 && (HasHeaderWithValue("connection", "upgrade") || HasHeader("upgrade")) && this.Request?.DownloadSettings?.OnUpgraded != null)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), "Request Upgraded!", this.Context);
|
||||
|
||||
this.IsUpgraded = this.Request.DownloadSettings.OnUpgraded(this.Request, this, peekable);
|
||||
|
||||
if (this.IsUpgraded)
|
||||
{
|
||||
this._readState = PeekableReadState.Finished;
|
||||
goto case PeekableReadState.Finished;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If it's a 100-continue, restart reading the response after the client could send its content.
|
||||
if (this.StatusCode == Continue)
|
||||
{
|
||||
this._readState = PeekableReadState.WaitForContentSent;
|
||||
break;
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-informational-1xx
|
||||
// A 1xx response is terminated by the end of the header section; it cannot contain content or trailers.
|
||||
if ((this.StatusCode >= Continue && this.StatusCode < OK) ||
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-204-no-content
|
||||
// A 204 response is terminated by the end of the header section; it cannot contain content or trailers.
|
||||
this.StatusCode == NoContent ||
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9110#name-304-not-modified
|
||||
// A 304 response is terminated by the end of the header section; it cannot contain content or trailers.
|
||||
this.StatusCode == NotModified ||
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3
|
||||
// Responses to the HEAD request method (Section 4.3.2
|
||||
// of [RFC7231]) never include a message body because the associated
|
||||
// response header fields (e.g., Transfer-Encoding, Content-Length,
|
||||
// etc.), if present, indicate only what their values would have been if
|
||||
// the request method had been GET
|
||||
this.Request.MethodType == HTTPMethods.Head)
|
||||
{
|
||||
this._readState = PeekableReadState.Finished;
|
||||
goto case PeekableReadState.Finished;
|
||||
}
|
||||
|
||||
Request.Timing.StartNext(TimingEventNames.Response_Received);
|
||||
|
||||
// if not an upgraded response, or OnUpgraded returned false, go for the content too.
|
||||
goto case PeekableReadState.PrepareForContent;
|
||||
}
|
||||
break;
|
||||
|
||||
case PeekableReadState.PrepareForContent:
|
||||
BeginReceiveContent();
|
||||
|
||||
// A content-length header might come with chunked transfer-encoding too.
|
||||
var contentLengthHeader = GetFirstHeaderValue("content-length");
|
||||
long.TryParse(contentLengthHeader, out this._expectedLength);
|
||||
|
||||
if (HasHeaderWithValue("transfer-encoding", "chunked") && string.IsNullOrEmpty(contentLengthHeader))
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.Chunked;
|
||||
this.ReadState = PeekableReadState.ContentSetup;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.Raw;
|
||||
this.ReadState = PeekableReadState.ContentSetup;
|
||||
var contentRangeHeaders = GetHeaderValues("content-range");
|
||||
|
||||
if (contentLengthHeader == null && contentRangeHeaders == null)
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.RawUnknownLength;
|
||||
}
|
||||
else if (contentLengthHeader == null && contentRangeHeaders != null)
|
||||
{
|
||||
HTTPRange range = GetRange();
|
||||
|
||||
this._expectedLength = (range.LastBytePos - range.FirstBytePos) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(PeekableHTTP1Response), $"PrepareForContent - delivery mode selected: {this._deliveryMode}, {this._expectedLength}!", this.Context);
|
||||
|
||||
CreateDownloadStream(this._bufferAvailableHandler);
|
||||
|
||||
string encoding = IsFromCache ? null : GetFirstHeaderValue("content-encoding");
|
||||
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
this._compressed = !string.IsNullOrEmpty(encoding);
|
||||
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/183
|
||||
// If _decompressor is still null, remove the compressed flag and serve the content as-is.
|
||||
if ((this._decompressor = DecompressorFactory.GetDecompressor(encoding, this.Context)) == null)
|
||||
this._compressed = false;
|
||||
#endif
|
||||
|
||||
this.sendProgressChanged = this.Request.DownloadSettings.OnDownloadProgress != null && this.IsSuccess;
|
||||
|
||||
this.ReadState = PeekableReadState.Content;
|
||||
goto case PeekableReadState.Content;
|
||||
|
||||
case PeekableReadState.Content:
|
||||
var downStream = this.DownStream;
|
||||
if (downStream != null && downStream.MaxBuffered <= downStream.Length)
|
||||
return;
|
||||
|
||||
switch (this._deliveryMode)
|
||||
{
|
||||
case ContentDeliveryMode.Raw: ProcessReadRaw(peekable); break;
|
||||
case ContentDeliveryMode.RawUnknownLength: ProcessReadRawUnknownLength(peekable); break;
|
||||
case ContentDeliveryMode.Chunked: ProcessReadChunked(peekable); break;
|
||||
}
|
||||
|
||||
if (this.ReadState == PeekableReadState.Finished)
|
||||
goto case PeekableReadState.Finished;
|
||||
break;
|
||||
|
||||
case PeekableReadState.Finished:
|
||||
//baseRequest.Timing.StartNext(TimingEventNames.Queued_For_Disptach);
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref this._isProccessing, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsNewLinePresent(PeekableStream peekable)
|
||||
{
|
||||
peekable.BeginPeek();
|
||||
|
||||
int nextByte = peekable.PeekByte();
|
||||
while (nextByte >= 0 && nextByte != 0x0A)
|
||||
nextByte = peekable.PeekByte();
|
||||
|
||||
return nextByte == 0x0A;
|
||||
}
|
||||
|
||||
private void ProcessReadHeaders(PeekableStream peekable, PeekableReadState targetState)
|
||||
{
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
string headerName = ReadTo(peekable, (byte)':', LF);
|
||||
if (headerName == string.Empty)
|
||||
{
|
||||
this.ReadState = targetState;
|
||||
|
||||
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, this._newHeaders));
|
||||
return;
|
||||
}
|
||||
|
||||
string value = ReadTo(peekable, LF);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(PeekableHTTP1Response), $"Header - '{headerName}': '{value}'", this.Context);
|
||||
|
||||
AddHeader(headerName, value);
|
||||
|
||||
if (this._newHeaders != null)
|
||||
{
|
||||
List<string> values;
|
||||
if (!this._newHeaders.TryGetValue(headerName, out values))
|
||||
this._newHeaders.Add(headerName, values = new List<string>(1));
|
||||
|
||||
values.Add(value);
|
||||
}
|
||||
} while (IsNewLinePresent(peekable));
|
||||
}
|
||||
|
||||
private void ProcessReadRawUnknownLength(PeekableStream peekable)
|
||||
{
|
||||
if (peekable == null)
|
||||
{
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
|
||||
PostProcessContent();
|
||||
|
||||
this.ReadState = PeekableReadState.Finished;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
while (peekable.Length > 0)
|
||||
{
|
||||
var buffer = BufferPool.Get(64 * 1024, true, this.Context);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, buffer.Length);
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
}
|
||||
|
||||
private bool TryReadChunkLength(PeekableStream peekable, out int result)
|
||||
{
|
||||
result = -1;
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return false;
|
||||
|
||||
// Read until the end of line, then split the string so we will discard any optional chunk extensions
|
||||
|
||||
var buff = ReadToAsByte(peekable, LF);
|
||||
|
||||
if (buff == BufferSegment.Empty)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var chars = MemoryMarshal.Cast<byte, char>(buff.AsSpan());
|
||||
|
||||
var idx = chars.IndexOf(';');
|
||||
|
||||
if (idx == -1)
|
||||
idx = chars.Length;
|
||||
|
||||
return int.TryParse(chars.Slice(0, idx), System.Globalization.NumberStyles.AllowHexSpecifier, null, out result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BufferPool.Release(buff);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessReadChunked(PeekableStream peekable)
|
||||
{
|
||||
switch(this._readChunkedState)
|
||||
{
|
||||
case ReadChunkedStates.ReadChunkLength:
|
||||
this._readChunkedState = ReadChunkedStates.ReadChunkLength;
|
||||
|
||||
if (TryReadChunkLength(peekable, out this._chunkLength))
|
||||
{
|
||||
if (this._chunkLength == 0)
|
||||
{
|
||||
PostProcessContent();
|
||||
|
||||
if (this.Request?.DownloadSettings?.OnHeadersReceived != null)
|
||||
this._newHeaders = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
goto case ReadChunkedStates.ReadTrailingHeaders;
|
||||
}
|
||||
|
||||
goto case ReadChunkedStates.ReadChunk;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadChunk:
|
||||
this._readChunkedState = ReadChunkedStates.ReadChunk;
|
||||
|
||||
while (this._chunkLength > 0 && peekable.Length > 0)
|
||||
{
|
||||
int targetReadCount = Math.Min(Math.Min(64 * 1024, this._chunkLength), (int)peekable.Length);
|
||||
|
||||
var buffer = BufferPool.Get(targetReadCount, true, this.Context);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, targetReadCount);
|
||||
|
||||
if (readCount < 0)
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
}
|
||||
|
||||
this._chunkLength -= readCount;
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
|
||||
// Every chunk data has a trailing CRLF
|
||||
if (this._chunkLength == 0)
|
||||
goto case ReadChunkedStates.ReadTrailingCRLF;
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadTrailingCRLF:
|
||||
this._readChunkedState = ReadChunkedStates.ReadTrailingCRLF;
|
||||
|
||||
if (IsNewLinePresent(peekable))
|
||||
{
|
||||
BufferPool.Release(HTTPResponse.ReadToAsByte(peekable, LF));
|
||||
|
||||
goto case ReadChunkedStates.ReadChunkLength;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadTrailingHeaders:
|
||||
this._readChunkedState = ReadChunkedStates.ReadTrailingHeaders;
|
||||
|
||||
ProcessReadHeaders(peekable, PeekableReadState.Finished);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessReadRaw(PeekableStream peekable)
|
||||
{
|
||||
if (this.DownStream == null)
|
||||
throw new ArgumentNullException(nameof(this.DownStream));
|
||||
if (peekable == null)
|
||||
throw new ArgumentNullException(nameof(peekable));
|
||||
|
||||
while (peekable.Length > 0 && !this.DownStream.IsFull)
|
||||
{
|
||||
var buffer = BufferPool.Get(64 * 1024, true, this.Context);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (readCount < 0)
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
}
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (sendProgressChanged)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadProgress, this._downloaded, this._expectedLength));
|
||||
|
||||
if (this._downloaded >= this._expectedLength)
|
||||
{
|
||||
PostProcessContent();
|
||||
this.ReadState = PeekableReadState.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessChunk(BufferSegment chunk)
|
||||
{
|
||||
this._downloaded += chunk.Count;
|
||||
|
||||
if (this._compressed)
|
||||
{
|
||||
var (decompressed, release) = this._decompressor.Decompress(chunk, false, true, this.Context);
|
||||
if (decompressed != BufferSegment.Empty)
|
||||
FeedDownloadedContentChunk(decompressed);
|
||||
|
||||
//if (decompressed.Data != chunk.Data)
|
||||
if (release)
|
||||
BufferPool.Release(chunk);
|
||||
}
|
||||
else
|
||||
{
|
||||
FeedDownloadedContentChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessContent()
|
||||
{
|
||||
if (this._compressed)
|
||||
{
|
||||
var (decompressed, release) = this._decompressor.Decompress(BufferSegment.Empty, true, true, this.Context);
|
||||
if (decompressed != BufferSegment.Empty)
|
||||
FeedDownloadedContentChunk(decompressed);
|
||||
}
|
||||
|
||||
FinishedContentReceiving();
|
||||
|
||||
if (this._decompressor != null)
|
||||
{
|
||||
this._decompressor.Dispose();
|
||||
this._decompressor = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
this._decompressor?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52e5f71e9a1d66d4ab61f2f2eca03ff0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1d3da817905a4242998820f95474d45
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,167 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
public static class BufferHelper
|
||||
{
|
||||
public static void SetUInt16(byte[] buffer, int offset, UInt16 value)
|
||||
{
|
||||
buffer[offset + 1] = (byte)(value);
|
||||
buffer[offset + 0] = (byte)(value >> 8);
|
||||
}
|
||||
|
||||
public static void SetUInt24(byte[] buffer, int offset, UInt32 value)
|
||||
{
|
||||
buffer[offset + 2] = (byte)(value);
|
||||
buffer[offset + 1] = (byte)(value >> 8);
|
||||
buffer[offset + 0] = (byte)(value >> 16);
|
||||
}
|
||||
|
||||
public static void SetUInt24(Span<byte> buffer, int offset, UInt32 value)
|
||||
{
|
||||
buffer[offset + 2] = (byte)(value);
|
||||
buffer[offset + 1] = (byte)(value >> 8);
|
||||
buffer[offset + 0] = (byte)(value >> 16);
|
||||
}
|
||||
|
||||
public static void SetUInt31(byte[] buffer, int offset, UInt32 value)
|
||||
{
|
||||
buffer[offset + 3] = (byte)(value);
|
||||
buffer[offset + 2] = (byte)(value >> 8);
|
||||
buffer[offset + 1] = (byte)(value >> 16);
|
||||
buffer[offset + 0] = (byte)((value >> 24) & 0x7F);
|
||||
}
|
||||
|
||||
public static void SetUInt31(Span<byte> buffer, int offset, UInt32 value)
|
||||
{
|
||||
buffer[offset + 3] = (byte)(value);
|
||||
buffer[offset + 2] = (byte)(value >> 8);
|
||||
buffer[offset + 1] = (byte)(value >> 16);
|
||||
buffer[offset + 0] = (byte)((value >> 24) & 0x7F);
|
||||
}
|
||||
|
||||
public static void SetUInt32(byte[] buffer, int offset, UInt32 value)
|
||||
{
|
||||
buffer[offset + 3] = (byte)(value);
|
||||
buffer[offset + 2] = (byte)(value >> 8);
|
||||
buffer[offset + 1] = (byte)(value >> 16);
|
||||
buffer[offset + 0] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
public static void SetLong(byte[] buffer, int offset, long value)
|
||||
{
|
||||
buffer[offset + 7] = (byte)(value);
|
||||
buffer[offset + 6] = (byte)(value >> 8);
|
||||
buffer[offset + 5] = (byte)(value >> 16);
|
||||
buffer[offset + 4] = (byte)(value >> 24);
|
||||
buffer[offset + 3] = (byte)(value >> 32);
|
||||
buffer[offset + 2] = (byte)(value >> 40);
|
||||
buffer[offset + 1] = (byte)(value >> 48);
|
||||
buffer[offset + 0] = (byte)(value >> 56);
|
||||
}
|
||||
|
||||
public static byte SetBit(byte value, byte bitIdx, bool bitValue)
|
||||
{
|
||||
// bitIdx: 01234567
|
||||
return SetBit(value, bitIdx, Convert.ToByte(bitValue));
|
||||
}
|
||||
|
||||
public static byte SetBit(byte value, byte bitIdx, byte bitValue)
|
||||
{
|
||||
// bitIdx: 01234567
|
||||
|
||||
return (byte)((value ^ (value & (0x80 >> bitIdx))) | bitValue << (7 - bitIdx));
|
||||
}
|
||||
|
||||
public static byte ReadBit(byte value, byte bitIdx)
|
||||
{
|
||||
// bitIdx: 01234567
|
||||
byte mask = (byte)(0x80 >> bitIdx);
|
||||
|
||||
return (byte)((value & mask) >> (7 - bitIdx));
|
||||
}
|
||||
|
||||
public static byte ReadValue(byte value, byte fromBit, byte toBit)
|
||||
{
|
||||
// bitIdx: 01234567
|
||||
|
||||
byte result = 0;
|
||||
short idx = toBit;
|
||||
|
||||
while (idx >= fromBit)
|
||||
{
|
||||
result += (byte)(ReadBit(value, (byte)idx) << (toBit - idx));
|
||||
idx--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static UInt16 ReadUInt16(byte[] buffer, int offset)
|
||||
{
|
||||
return (UInt16)(buffer[offset + 1] | buffer[offset] << 8);
|
||||
}
|
||||
|
||||
public static UInt32 ReadUInt24(byte[] buffer, int offset)
|
||||
{
|
||||
return (UInt32)(buffer[offset + 2] |
|
||||
buffer[offset + 1] << 8 |
|
||||
buffer[offset + 0] << 16
|
||||
);
|
||||
}
|
||||
|
||||
public static UInt32 ReadUInt31(byte[] buffer, int offset)
|
||||
{
|
||||
return (UInt32)(buffer[offset + 3] |
|
||||
buffer[offset + 2] << 8 |
|
||||
buffer[offset + 1] << 16 |
|
||||
(buffer[offset] & 0x7F) << 24
|
||||
);
|
||||
}
|
||||
|
||||
public static UInt32 ReadUInt31(BufferSegment buffer, int offset)
|
||||
{
|
||||
return (UInt32)( buffer.Data[offset + 3] |
|
||||
buffer.Data[offset + 2] << 8 |
|
||||
buffer.Data[offset + 1] << 16 |
|
||||
(buffer.Data[offset] & 0x7F) << 24
|
||||
);
|
||||
}
|
||||
|
||||
public static UInt32 ReadUInt32(byte[] buffer, int offset)
|
||||
{
|
||||
return (UInt32)(buffer[offset + 3] |
|
||||
buffer[offset + 2] << 8 |
|
||||
buffer[offset + 1] << 16 |
|
||||
buffer[offset + 0] << 24
|
||||
);
|
||||
}
|
||||
|
||||
public static UInt32 ReadUInt32(BufferSegment buffer, int offset)
|
||||
{
|
||||
return (UInt32)(buffer.Data[offset + 3] |
|
||||
buffer.Data[offset + 2] << 8 |
|
||||
buffer.Data[offset + 1] << 16 |
|
||||
buffer.Data[offset + 0] << 24
|
||||
);
|
||||
}
|
||||
|
||||
public static long ReadLong(BufferSegment buffer, int offset)
|
||||
{
|
||||
return (long)buffer.Data[offset + 7] |
|
||||
(long)buffer.Data[offset + 6] << 8 |
|
||||
(long)buffer.Data[offset + 5] << 16 |
|
||||
(long)buffer.Data[offset + 4] << 24 |
|
||||
(long)buffer.Data[offset + 3] << 32 |
|
||||
(long)buffer.Data[offset + 2] << 40 |
|
||||
(long)buffer.Data[offset + 1] << 48 |
|
||||
(long)buffer.Data[offset + 0] << 56;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ddab2aecdc1bd94688535c6d3532949
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,203 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
public interface IFrameDataView : IDisposable
|
||||
{
|
||||
long Length { get; }
|
||||
long Position { get; }
|
||||
|
||||
void AddFrame(HTTP2FrameHeaderAndPayload frame);
|
||||
int ReadByte();
|
||||
int Read(byte[] buffer, int offset, int count);
|
||||
}
|
||||
|
||||
public abstract class CommonFrameView : IFrameDataView
|
||||
{
|
||||
public long Length { get; protected set; }
|
||||
public long Position { get; protected set; }
|
||||
|
||||
protected List<HTTP2FrameHeaderAndPayload> frames = new List<HTTP2FrameHeaderAndPayload>();
|
||||
protected int currentFrameIdx = -1;
|
||||
protected byte[] data;
|
||||
protected int dataOffset;
|
||||
protected int maxOffset;
|
||||
|
||||
public abstract void AddFrame(HTTP2FrameHeaderAndPayload frame);
|
||||
protected abstract long CalculateDataLengthForFrame(HTTP2FrameHeaderAndPayload frame);
|
||||
|
||||
public virtual int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (this.dataOffset >= this.maxOffset && !AdvanceFrame())
|
||||
return -1;
|
||||
|
||||
int readCount = 0;
|
||||
|
||||
while (count > 0)
|
||||
{
|
||||
long copyCount = Math.Min(count, this.maxOffset - this.dataOffset);
|
||||
|
||||
Array.Copy(this.data, this.dataOffset, buffer, offset + readCount, copyCount);
|
||||
|
||||
count -= (int)copyCount;
|
||||
readCount += (int)copyCount;
|
||||
|
||||
this.dataOffset += (int)copyCount;
|
||||
this.Position += copyCount;
|
||||
|
||||
if (this.dataOffset >= this.maxOffset && !AdvanceFrame())
|
||||
break;
|
||||
}
|
||||
|
||||
return readCount;
|
||||
}
|
||||
|
||||
public virtual int ReadByte()
|
||||
{
|
||||
if (this.dataOffset >= this.maxOffset && !AdvanceFrame())
|
||||
return -1;
|
||||
|
||||
byte data = this.data[this.dataOffset];
|
||||
this.dataOffset++;
|
||||
this.Position++;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
protected abstract bool AdvanceFrame();
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
for (int i = 0; i < this.frames.Count; ++i)
|
||||
//if (this.frames[i].Payload != null && !this.frames[i].DontUseMemPool)
|
||||
BufferPool.Release(this.frames[i].Payload);
|
||||
this.frames.Clear();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = StringBuilderPool.Get(this.frames.Count + 2);
|
||||
sb.Append("[CommonFrameView ");
|
||||
|
||||
for (int i = 0; i < this.frames.Count; ++i) {
|
||||
sb.AppendFormat("{0} Payload: {1}\n", this.frames[i], this.frames[i].PayloadAsHex());
|
||||
}
|
||||
|
||||
sb.Append("]");
|
||||
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HeaderFrameView : CommonFrameView
|
||||
{
|
||||
public override void AddFrame(HTTP2FrameHeaderAndPayload frame)
|
||||
{
|
||||
if (frame.Type != HTTP2FrameTypes.HEADERS && frame.Type != HTTP2FrameTypes.CONTINUATION)
|
||||
throw new ArgumentException("HeaderFrameView - Unexpected frame type: " + frame.Type);
|
||||
|
||||
this.frames.Add(frame);
|
||||
this.Length += CalculateDataLengthForFrame(frame);
|
||||
|
||||
if (this.currentFrameIdx == -1)
|
||||
AdvanceFrame();
|
||||
}
|
||||
|
||||
protected override long CalculateDataLengthForFrame(HTTP2FrameHeaderAndPayload frame)
|
||||
{
|
||||
switch (frame.Type)
|
||||
{
|
||||
case HTTP2FrameTypes.HEADERS:
|
||||
return HTTP2FrameHelper.ReadHeadersFrame(frame).HeaderBlockFragment.Count;
|
||||
|
||||
case HTTP2FrameTypes.CONTINUATION:
|
||||
return frame.Payload.Count;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected override bool AdvanceFrame()
|
||||
{
|
||||
if (this.currentFrameIdx >= this.frames.Count - 1)
|
||||
return false;
|
||||
|
||||
this.currentFrameIdx++;
|
||||
HTTP2FrameHeaderAndPayload frame = this.frames[this.currentFrameIdx];
|
||||
|
||||
this.data = frame.Payload.Data;
|
||||
|
||||
switch (frame.Type)
|
||||
{
|
||||
case HTTP2FrameTypes.HEADERS:
|
||||
var header = HTTP2FrameHelper.ReadHeadersFrame(frame);
|
||||
this.dataOffset = header.HeaderBlockFragment.Offset;
|
||||
this.maxOffset = this.dataOffset + header.HeaderBlockFragment.Count;
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.CONTINUATION:
|
||||
this.dataOffset = 0;
|
||||
this.maxOffset = frame.Payload.Count;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FramesAsStreamView : Stream
|
||||
{
|
||||
public override bool CanRead { get { return true; } }
|
||||
public override bool CanSeek { get { return false; } }
|
||||
public override bool CanWrite { get { return false; } }
|
||||
public override long Length { get { return this.view.Length; } }
|
||||
public override long Position { get { return this.view.Position; } set { throw new NotSupportedException(); } }
|
||||
|
||||
private IFrameDataView view;
|
||||
|
||||
public FramesAsStreamView(IFrameDataView view)
|
||||
{
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public void AddFrame(HTTP2FrameHeaderAndPayload frame)
|
||||
{
|
||||
this.view.AddFrame(frame);
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
return this.view.ReadByte();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return this.view.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
this.view.Dispose();
|
||||
}
|
||||
|
||||
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(); }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.view.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3117e531a50d6f74faedada4b320cebf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,815 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
public sealed class HPACKEncoder
|
||||
{
|
||||
private HTTP2SettingsManager settingsRegistry;
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#encoding.context
|
||||
// When used for bidirectional communication, such as in HTTP, the encoding and decoding dynamic tables
|
||||
// maintained by an endpoint are completely independent, i.e., the request and response dynamic tables are separate.
|
||||
private HeaderTable requestTable;
|
||||
private HeaderTable responseTable;
|
||||
|
||||
private LoggingContext _context;
|
||||
|
||||
public HPACKEncoder(LoggingContext context, HTTP2SettingsManager registry)
|
||||
{
|
||||
this._context = context;
|
||||
this.settingsRegistry = registry;
|
||||
|
||||
// I'm unsure what settings (local or remote) we should use for these two tables!
|
||||
this.requestTable = new HeaderTable(this.settingsRegistry.MySettings);
|
||||
this.responseTable = new HeaderTable(this.settingsRegistry.RemoteSettings);
|
||||
}
|
||||
|
||||
public void Encode(HTTP2Stream context, HTTPRequest request, Queue<HTTP2FrameHeaderAndPayload> to, UInt32 streamId)
|
||||
{
|
||||
// Add usage of SETTINGS_MAX_HEADER_LIST_SIZE to be able to create a header and one or more continuation fragments
|
||||
// (https://httpwg.org/specs/rfc7540.html#SettingValues)
|
||||
|
||||
using (BufferPoolMemoryStream bufferStream = new BufferPoolMemoryStream(request.UploadSettings.UploadChunkSize))
|
||||
{
|
||||
request.Prepare();
|
||||
|
||||
// add :method
|
||||
switch (request.MethodType)
|
||||
{
|
||||
case HTTPMethods.Get: WriteIndexedHeaderField(bufferStream, 2); break;
|
||||
case HTTPMethods.Post: WriteIndexedHeaderField(bufferStream, 3); break;
|
||||
default:
|
||||
WriteLiteralHeaderFieldWithoutIndexing_IndexedName(bufferStream, 2, HTTPRequest.MethodNames[(int)request.MethodType]);
|
||||
break;
|
||||
}
|
||||
|
||||
// add :authority
|
||||
WriteLiteralHeaderFieldWithoutIndexing_IndexedName(bufferStream, 1, request.CurrentUri.Authority);
|
||||
|
||||
// add :scheme
|
||||
WriteIndexedHeaderField(bufferStream, 7);
|
||||
|
||||
// add :path
|
||||
WriteLiteralHeaderFieldWithoutIndexing_IndexedName(bufferStream, 4, request.CurrentUri.PathAndQuery);
|
||||
|
||||
//bool hasBody = false;
|
||||
|
||||
// add other, regular headers
|
||||
request.EnumerateHeaders((header, values) =>
|
||||
{
|
||||
if (header.Equals("connection", StringComparison.OrdinalIgnoreCase) ||
|
||||
(header.Equals("te", StringComparison.OrdinalIgnoreCase) && !values.Contains("trailers") && values.Count <= 1) ||
|
||||
header.Equals("host", StringComparison.OrdinalIgnoreCase) ||
|
||||
header.Equals("keep-alive", StringComparison.OrdinalIgnoreCase) ||
|
||||
header.StartsWith("proxy-", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
if (header.Equals("content-length", StringComparison.OrdinalIgnoreCase) && values.Count == 1 && int.TryParse(values[0], out int contentLength) && contentLength <= 0 && !context.ParentHandler.ConnectionSettings.ForceSendContentLengthHeader)
|
||||
return;
|
||||
|
||||
//if (!hasBody)
|
||||
// hasBody = header.Equals("content-length", StringComparison.OrdinalIgnoreCase) && int.Parse(values[0]) > 0;
|
||||
|
||||
// https://httpwg.org/specs/rfc7540.html#HttpSequence
|
||||
// The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
|
||||
if (header.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// error!
|
||||
return;
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc7540.html#HttpHeaders
|
||||
// Just as in HTTP/1.x, header field names are strings of ASCII characters that are compared in a case-insensitive fashion.
|
||||
// However, header field names MUST be converted to lowercase prior to their encoding in HTTP/2.
|
||||
// A request or response containing uppercase header field names MUST be treated as malformed
|
||||
if (header.Any(Char.IsUpper))
|
||||
header = header.ToLowerInvariant();
|
||||
|
||||
for (int i = 0; i < values.Count; ++i)
|
||||
{
|
||||
WriteHeader(bufferStream, header, values[i]);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] - Encode - Header({1}/{2}): '{3}': '{4}'", context.Id, i + 1, values.Count, header, values[i]), this._context);
|
||||
}
|
||||
}, true);
|
||||
|
||||
CreateHeaderFrames(to,
|
||||
streamId,
|
||||
bufferStream.ToArray(true, this._context),
|
||||
(UInt32)bufferStream.Length,
|
||||
request.UploadSettings.UploadStream != null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Decode(HTTP2Stream context, Stream stream, List<KeyValuePair<string, string>> to)
|
||||
{
|
||||
int headerType = stream.ReadByte();
|
||||
while (headerType != -1)
|
||||
{
|
||||
byte firstDataByte = (byte)headerType;
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#indexed.header.representation
|
||||
if (BufferHelper.ReadBit(firstDataByte, 0) == 1)
|
||||
{
|
||||
var header = ReadIndexedHeader(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - IndexedHeader: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
to.Add(header);
|
||||
}
|
||||
else if (BufferHelper.ReadValue(firstDataByte, 0, 1) == 1)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
|
||||
|
||||
if (BufferHelper.ReadValue(firstDataByte, 2, 7) == 0)
|
||||
{
|
||||
// Literal Header Field with Incremental Indexing — New Name
|
||||
var header = ReadLiteralHeaderFieldWithIncrementalIndexing_NewName(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldWithIncrementalIndexing_NewName: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
this.responseTable.Add(header);
|
||||
to.Add(header);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Literal Header Field with Incremental Indexing — Indexed Name
|
||||
var header = ReadLiteralHeaderFieldWithIncrementalIndexing_IndexedName(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldWithIncrementalIndexing_IndexedName: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
this.responseTable.Add(header);
|
||||
to.Add(header);
|
||||
}
|
||||
}
|
||||
else if (BufferHelper.ReadValue(firstDataByte, 0, 3) == 0)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
|
||||
|
||||
if (BufferHelper.ReadValue(firstDataByte, 4, 7) == 0)
|
||||
{
|
||||
// Literal Header Field without Indexing — New Name
|
||||
var header = ReadLiteralHeaderFieldwithoutIndexing_NewName(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldwithoutIndexing_NewName: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
to.Add(header);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Literal Header Field without Indexing — Indexed Name
|
||||
var header = ReadLiteralHeaderFieldwithoutIndexing_IndexedName(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldwithoutIndexing_IndexedName: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
to.Add(header);
|
||||
}
|
||||
}
|
||||
else if (BufferHelper.ReadValue(firstDataByte, 0, 3) == 1)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
|
||||
|
||||
if (BufferHelper.ReadValue(firstDataByte, 4, 7) == 0)
|
||||
{
|
||||
// Literal Header Field Never Indexed — New Name
|
||||
var header = ReadLiteralHeaderFieldNeverIndexed_NewName(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldNeverIndexed_NewName: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
to.Add(header);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Literal Header Field Never Indexed — Indexed Name
|
||||
var header = ReadLiteralHeaderFieldNeverIndexed_IndexedName(firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - LiteralHeaderFieldNeverIndexed_IndexedName: {1}", context.Id, header.ToString()), this._context);
|
||||
|
||||
to.Add(header);
|
||||
}
|
||||
}
|
||||
else if (BufferHelper.ReadValue(firstDataByte, 0, 2) == 1)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#encoding.context.update
|
||||
|
||||
UInt32 newMaxSize = DecodeInteger(5, firstDataByte, stream);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HPACKEncoder", string.Format("[{0}] Decode - Dynamic Table Size Update: {1}", context.Id, newMaxSize), this._context);
|
||||
|
||||
//this.settingsRegistry[HTTP2Settings.HEADER_TABLE_SIZE] = (UInt16)newMaxSize;
|
||||
this.responseTable.MaxDynamicTableSize = (UInt16)newMaxSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ERROR
|
||||
}
|
||||
|
||||
headerType = stream.ReadByte();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadIndexedHeader(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#indexed.header.representation
|
||||
|
||||
UInt32 index = DecodeInteger(7, firstByte, stream);
|
||||
return this.responseTable.GetHeader(index);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadLiteralHeaderFieldWithIncrementalIndexing_IndexedName(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
|
||||
|
||||
UInt32 keyIndex = DecodeInteger(6, firstByte, stream);
|
||||
|
||||
string header = this.responseTable.GetKey(keyIndex);
|
||||
string value = DecodeString(stream);
|
||||
|
||||
return new KeyValuePair<string, string>(header, value);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadLiteralHeaderFieldWithIncrementalIndexing_NewName(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
|
||||
|
||||
string header = DecodeString(stream);
|
||||
string value = DecodeString(stream);
|
||||
|
||||
return new KeyValuePair<string, string>(header, value);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadLiteralHeaderFieldwithoutIndexing_IndexedName(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
|
||||
|
||||
UInt32 index = DecodeInteger(4, firstByte, stream);
|
||||
string header = this.responseTable.GetKey(index);
|
||||
string value = DecodeString(stream);
|
||||
|
||||
return new KeyValuePair<string, string>(header, value);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadLiteralHeaderFieldwithoutIndexing_NewName(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
|
||||
|
||||
string header = DecodeString(stream);
|
||||
string value = DecodeString(stream);
|
||||
|
||||
return new KeyValuePair<string, string>(header, value);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadLiteralHeaderFieldNeverIndexed_IndexedName(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
|
||||
|
||||
UInt32 index = DecodeInteger(4, firstByte, stream);
|
||||
string header = this.responseTable.GetKey(index);
|
||||
string value = DecodeString(stream);
|
||||
|
||||
return new KeyValuePair<string, string>(header, value);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, string> ReadLiteralHeaderFieldNeverIndexed_NewName(byte firstByte, Stream stream)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
|
||||
|
||||
string header = DecodeString(stream);
|
||||
string value = DecodeString(stream);
|
||||
|
||||
return new KeyValuePair<string, string>(header, value);
|
||||
}
|
||||
|
||||
private string DecodeString(Stream stream)
|
||||
{
|
||||
byte start = (byte)stream.ReadByte();
|
||||
bool rawString = BufferHelper.ReadBit(start, 0) == 0;
|
||||
UInt32 stringLength = DecodeInteger(7, start, stream);
|
||||
|
||||
if (stringLength == 0)
|
||||
return string.Empty;
|
||||
|
||||
if (rawString)
|
||||
{
|
||||
byte[] buffer = BufferPool.Get(stringLength, true);
|
||||
|
||||
stream.Read(buffer, 0, (int)stringLength);
|
||||
|
||||
var result = System.Text.Encoding.UTF8.GetString(buffer, 0, (int)stringLength);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var node = HuffmanEncoder.GetRoot();
|
||||
byte currentByte = (byte)stream.ReadByte();
|
||||
byte bitIdx = 0; // 0..7
|
||||
|
||||
using (BufferPoolMemoryStream bufferStream = new BufferPoolMemoryStream((int)(stringLength * 1.5f)))
|
||||
{
|
||||
do
|
||||
{
|
||||
byte bitValue = BufferHelper.ReadBit(currentByte, bitIdx);
|
||||
|
||||
if (++bitIdx > 7)
|
||||
{
|
||||
stringLength--;
|
||||
|
||||
if (stringLength > 0)
|
||||
{
|
||||
bitIdx = 0;
|
||||
currentByte = (byte)stream.ReadByte();
|
||||
}
|
||||
}
|
||||
|
||||
node = HuffmanEncoder.GetNext(node, bitValue);
|
||||
|
||||
if (node.Value != 0)
|
||||
{
|
||||
if (node.Value != HuffmanEncoder.EOS)
|
||||
bufferStream.WriteByte((byte)node.Value);
|
||||
|
||||
node = HuffmanEncoder.GetRoot();
|
||||
}
|
||||
} while (stringLength > 0);
|
||||
|
||||
byte[] buffer = bufferStream.GetBuffer();
|
||||
|
||||
string result = System.Text.Encoding.UTF8.GetString(buffer, 0, (int)bufferStream.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateHeaderFrames(Queue<HTTP2FrameHeaderAndPayload> to, UInt32 streamId, byte[] dataToSend, UInt32 payloadLength, bool hasBody)
|
||||
{
|
||||
UInt32 maxFrameSize = this.settingsRegistry.RemoteSettings[HTTP2Settings.MAX_FRAME_SIZE];
|
||||
|
||||
// Only one headers frame
|
||||
if (payloadLength <= maxFrameSize)
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload frameHeader = new HTTP2FrameHeaderAndPayload();
|
||||
frameHeader.Type = HTTP2FrameTypes.HEADERS;
|
||||
frameHeader.StreamId = streamId;
|
||||
frameHeader.Flags = (byte)(HTTP2HeadersFlags.END_HEADERS);
|
||||
|
||||
if (!hasBody)
|
||||
frameHeader.Flags |= (byte)(HTTP2HeadersFlags.END_STREAM);
|
||||
|
||||
frameHeader.Payload = dataToSend.AsBuffer((int)payloadLength);
|
||||
|
||||
to.Enqueue(frameHeader);
|
||||
}
|
||||
else
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload frameHeader = new HTTP2FrameHeaderAndPayload();
|
||||
frameHeader.Type = HTTP2FrameTypes.HEADERS;
|
||||
frameHeader.StreamId = streamId;
|
||||
|
||||
frameHeader.Payload = dataToSend.AsBuffer((int)maxFrameSize);
|
||||
frameHeader.DontUseMemPool = true;
|
||||
|
||||
if (!hasBody)
|
||||
frameHeader.Flags = (byte)(HTTP2HeadersFlags.END_STREAM);
|
||||
|
||||
to.Enqueue(frameHeader);
|
||||
|
||||
UInt32 offset = maxFrameSize;
|
||||
while (offset < payloadLength)
|
||||
{
|
||||
frameHeader = new HTTP2FrameHeaderAndPayload();
|
||||
frameHeader.Type = HTTP2FrameTypes.CONTINUATION;
|
||||
frameHeader.StreamId = streamId;
|
||||
|
||||
frameHeader.Payload = dataToSend.AsBuffer((int)offset, (int)maxFrameSize);
|
||||
|
||||
offset += maxFrameSize;
|
||||
|
||||
if (offset >= payloadLength)
|
||||
{
|
||||
frameHeader.Flags = (byte)(HTTP2ContinuationFlags.END_HEADERS);
|
||||
// last sent continuation fragment will release back the payload buffer
|
||||
frameHeader.DontUseMemPool = false;
|
||||
}
|
||||
else
|
||||
frameHeader.DontUseMemPool = true;
|
||||
|
||||
to.Enqueue(frameHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteHeader(Stream stream, string header, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#header.representation
|
||||
|
||||
KeyValuePair<UInt32, UInt32> index = this.requestTable.GetIndex(header, value);
|
||||
|
||||
if (index.Key == 0 && index.Value == 0)
|
||||
{
|
||||
WriteLiteralHeaderFieldWithIncrementalIndexing_NewName(stream, header, value);
|
||||
this.requestTable.Add(new KeyValuePair<string, string>(header, value));
|
||||
}
|
||||
else if (index.Key != 0 && index.Value == 0)
|
||||
{
|
||||
WriteLiteralHeaderFieldWithIncrementalIndexing_IndexedName(stream, index.Key, value);
|
||||
this.requestTable.Add(new KeyValuePair<string, string>(header, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteIndexedHeaderField(stream, index.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteIndexedHeaderField(Stream stream, UInt32 index)
|
||||
{
|
||||
byte requiredBytes = RequiredBytesToEncodeInteger(index, 7);
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[0] = 0x80;
|
||||
EncodeInteger(index, 7, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteLiteralHeaderFieldWithIncrementalIndexing_IndexedName(Stream stream, UInt32 index, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
|
||||
|
||||
UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 6) +
|
||||
RequiredBytesToEncodeString(value);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[0] = 0x40;
|
||||
EncodeInteger(index, 6, buffer, ref offset);
|
||||
EncodeString(value, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteLiteralHeaderFieldWithIncrementalIndexing_NewName(Stream stream, string header, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.with.incremental.indexing
|
||||
|
||||
UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[offset++] = 0x40;
|
||||
EncodeString(header, buffer, ref offset);
|
||||
EncodeString(value, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteLiteralHeaderFieldWithoutIndexing_IndexedName(Stream stream, UInt32 index, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
|
||||
|
||||
UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 4) + RequiredBytesToEncodeString(value);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[0] = 0;
|
||||
EncodeInteger(index, 4, buffer, ref offset);
|
||||
EncodeString(value, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteLiteralHeaderFieldWithoutIndexing_NewName(Stream stream, string header, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.without.indexing
|
||||
|
||||
UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[offset++] = 0;
|
||||
EncodeString(header, buffer, ref offset);
|
||||
EncodeString(value, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteLiteralHeaderFieldNeverIndexed_IndexedName(Stream stream, UInt32 index, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
|
||||
|
||||
UInt32 requiredBytes = RequiredBytesToEncodeInteger(index, 4) + RequiredBytesToEncodeString(value);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[0] = 0x10;
|
||||
EncodeInteger(index, 4, buffer, ref offset);
|
||||
EncodeString(value, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteLiteralHeaderFieldNeverIndexed_NewName(Stream stream, string header, string value)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#literal.header.never.indexed
|
||||
|
||||
UInt32 requiredBytes = 1 + RequiredBytesToEncodeString(header) + RequiredBytesToEncodeString(value);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[offset++] = 0x10;
|
||||
EncodeString(header, buffer, ref offset);
|
||||
EncodeString(value, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static void WriteDynamicTableSizeUpdate(Stream stream, UInt16 maxSize)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#encoding.context.update
|
||||
|
||||
UInt32 requiredBytes = RequiredBytesToEncodeInteger(maxSize, 5);
|
||||
|
||||
byte[] buffer = BufferPool.Get(requiredBytes, true);
|
||||
UInt32 offset = 0;
|
||||
|
||||
buffer[offset] = 0x20;
|
||||
EncodeInteger(maxSize, 5, buffer, ref offset);
|
||||
|
||||
stream.Write(buffer, 0, (int)offset);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
|
||||
private static UInt32 RequiredBytesToEncodeString(string str)
|
||||
{
|
||||
uint requiredBytesForRawStr = RequiredBytesToEncodeRawString(str);
|
||||
uint requiredBytesForHuffman = RequiredBytesToEncodeStringWithHuffman(str);
|
||||
requiredBytesForHuffman += RequiredBytesToEncodeInteger(requiredBytesForHuffman, 7);
|
||||
|
||||
return Math.Min(requiredBytesForRawStr, requiredBytesForHuffman);
|
||||
}
|
||||
|
||||
private static void EncodeString(string str, byte[] buffer, ref UInt32 offset)
|
||||
{
|
||||
uint requiredBytesForRawStr = RequiredBytesToEncodeRawString(str);
|
||||
uint requiredBytesForHuffman = RequiredBytesToEncodeStringWithHuffman(str);
|
||||
|
||||
// if using huffman encoding would produce the same length, we choose raw encoding instead as it requires
|
||||
// less CPU cicles
|
||||
if (requiredBytesForRawStr <= requiredBytesForHuffman + RequiredBytesToEncodeInteger(requiredBytesForHuffman, 7))
|
||||
EncodeRawStringTo(str, buffer, ref offset);
|
||||
else
|
||||
EncodeStringWithHuffman(str, requiredBytesForHuffman, buffer, ref offset);
|
||||
}
|
||||
|
||||
// This calculates only the length of the compressed string,
|
||||
// additional header length must be calculated using the value returned by this function
|
||||
private static UInt32 RequiredBytesToEncodeStringWithHuffman(string str)
|
||||
{
|
||||
int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str);
|
||||
byte[] strBytes = BufferPool.Get(requiredBytesForStr, true);
|
||||
|
||||
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, strBytes, 0);
|
||||
|
||||
UInt32 requiredBits = 0;
|
||||
|
||||
for (int i = 0; i < requiredBytesForStr; ++i)
|
||||
requiredBits += HuffmanEncoder.GetEntryForCodePoint(strBytes[i]).Bits;
|
||||
|
||||
BufferPool.Release(strBytes);
|
||||
|
||||
return (UInt32)((requiredBits / 8) + ((requiredBits % 8) == 0 ? 0 : 1));
|
||||
}
|
||||
|
||||
private static void EncodeStringWithHuffman(string str, UInt32 encodedLength, byte[] buffer, ref UInt32 offset)
|
||||
{
|
||||
int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str);
|
||||
byte[] strBytes = BufferPool.Get(requiredBytesForStr, true);
|
||||
|
||||
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, strBytes, 0);
|
||||
|
||||
// 0. bit: huffman flag
|
||||
buffer[offset] = 0x80;
|
||||
|
||||
// 1..7+ bit: length
|
||||
EncodeInteger(encodedLength, 7, buffer, ref offset);
|
||||
|
||||
byte bufferBitIdx = 0;
|
||||
|
||||
for (int i = 0; i < requiredBytesForStr; ++i)
|
||||
AddCodePointToBuffer(HuffmanEncoder.GetEntryForCodePoint(strBytes[i]), buffer, ref offset, ref bufferBitIdx);
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#string.literal.representation
|
||||
// As the Huffman-encoded data doesn't always end at an octet boundary, some padding is inserted after it,
|
||||
// up to the next octet boundary. To prevent this padding from being misinterpreted as part of the string literal,
|
||||
// the most significant bits of the code corresponding to the EOS (end-of-string) symbol are used.
|
||||
if (bufferBitIdx != 0)
|
||||
AddCodePointToBuffer(HuffmanEncoder.GetEntryForCodePoint(256), buffer, ref offset, ref bufferBitIdx, true);
|
||||
|
||||
BufferPool.Release(strBytes);
|
||||
}
|
||||
|
||||
private static void AddCodePointToBuffer(HuffmanTableEntry code, byte[] buffer, ref UInt32 offset, ref byte bufferBitIdx, bool finishOnBoundary = false)
|
||||
{
|
||||
for (byte codeBitIdx = 1; codeBitIdx <= code.Bits; ++codeBitIdx)
|
||||
{
|
||||
byte bit = code.GetBitAtIdx(codeBitIdx);
|
||||
buffer[offset] = BufferHelper.SetBit(buffer[offset], bufferBitIdx, bit);
|
||||
|
||||
// octet boundary reached, proceed to the next octet
|
||||
if (++bufferBitIdx == 8)
|
||||
{
|
||||
if (++offset < buffer.Length)
|
||||
buffer[offset] = 0;
|
||||
|
||||
if (finishOnBoundary)
|
||||
return;
|
||||
|
||||
bufferBitIdx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UInt32 RequiredBytesToEncodeRawString(string str)
|
||||
{
|
||||
int requiredBytesForStr = System.Text.Encoding.UTF8.GetByteCount(str);
|
||||
int requiredBytesForLengthPrefix = RequiredBytesToEncodeInteger((UInt32)requiredBytesForStr, 7);
|
||||
|
||||
return (UInt32)(requiredBytesForStr + requiredBytesForLengthPrefix);
|
||||
}
|
||||
|
||||
// This method encodes a string without huffman encoding
|
||||
private static void EncodeRawStringTo(string str, byte[] buffer, ref UInt32 offset)
|
||||
{
|
||||
uint requiredBytesForStr = (uint)System.Text.Encoding.UTF8.GetByteCount(str);
|
||||
int requiredBytesForLengthPrefix = RequiredBytesToEncodeInteger((UInt32)requiredBytesForStr, 7);
|
||||
|
||||
UInt32 originalOffset = offset;
|
||||
buffer[offset] = 0;
|
||||
EncodeInteger(requiredBytesForStr, 7, buffer, ref offset);
|
||||
|
||||
// Zero out the huffman flag
|
||||
buffer[originalOffset] = BufferHelper.SetBit(buffer[originalOffset], 0, false);
|
||||
|
||||
if (offset != originalOffset + requiredBytesForLengthPrefix)
|
||||
throw new Exception(string.Format("offset({0}) != originalOffset({1}) + requiredBytesForLengthPrefix({1})", offset, originalOffset, requiredBytesForLengthPrefix));
|
||||
|
||||
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, (int)offset);
|
||||
offset += requiredBytesForStr;
|
||||
}
|
||||
|
||||
private static byte RequiredBytesToEncodeInteger(UInt32 value, byte N)
|
||||
{
|
||||
UInt32 maxValue = (1u << N) - 1;
|
||||
byte count = 0;
|
||||
|
||||
// If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix.
|
||||
if (value < maxValue)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2^N-1
|
||||
count++;
|
||||
value -= maxValue;
|
||||
|
||||
while (value >= 0x80)
|
||||
{
|
||||
// The most significant bit of each octet is used as a continuation flag: its value is set to 1 except for the last octet in the list.
|
||||
count++;
|
||||
value = value / 0x80;
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#integer.representation
|
||||
private static void EncodeInteger(UInt32 value, byte N, byte[] buffer, ref UInt32 offset)
|
||||
{
|
||||
// 2^N - 1
|
||||
UInt32 maxValue = (1u << N) - 1;
|
||||
|
||||
// If the integer value is small enough, i.e., strictly less than 2^N-1, it is encoded within the N-bit prefix.
|
||||
if (value < maxValue)
|
||||
{
|
||||
buffer[offset++] |= (byte)value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, all the bits of the prefix are set to 1, and the value, decreased by 2^N-1
|
||||
buffer[offset++] |= (byte)(0xFF >> (8 - N));
|
||||
value -= maxValue;
|
||||
|
||||
while (value >= 0x80)
|
||||
{
|
||||
// The most significant bit of each octet is used as a continuation flag: its value is set to 1 except for the last octet in the list.
|
||||
buffer[offset++] = (byte)(0x80 | (0x7F & value));
|
||||
value = value / 0x80;
|
||||
}
|
||||
|
||||
buffer[offset++] = (byte)value;
|
||||
}
|
||||
}
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#integer.representation
|
||||
private static UInt32 DecodeInteger(byte N, byte[] buffer, ref UInt32 offset)
|
||||
{
|
||||
// The starting value is the value behind the mask of the N bits
|
||||
UInt32 value = (UInt32)(buffer[offset++] & (byte)(0xFF >> (8 - N)));
|
||||
|
||||
// All N bits are 1s ? If so, we have at least one another byte to decode
|
||||
if (value == (1u << N) - 1)
|
||||
{
|
||||
byte shift = 0;
|
||||
|
||||
do
|
||||
{
|
||||
// The most significant bit is a continuation flag, so we have to mask it out
|
||||
value += (UInt32)((buffer[offset] & 0x7F) << shift);
|
||||
shift += 7;
|
||||
} while ((buffer[offset++] & 0x80) == 0x80);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#integer.representation
|
||||
private static UInt32 DecodeInteger(byte N, byte data, Stream stream)
|
||||
{
|
||||
// The starting value is the value behind the mask of the N bits
|
||||
UInt32 value = (UInt32)(data & (byte)(0xFF >> (8 - N)));
|
||||
|
||||
// All N bits are 1s ? If so, we have at least one another byte to decode
|
||||
if (value == (1u << N) - 1)
|
||||
{
|
||||
byte shift = 0;
|
||||
|
||||
do
|
||||
{
|
||||
data = (byte)stream.ReadByte();
|
||||
|
||||
// The most significant bit is a continuation flag, so we have to mask it out
|
||||
value += (UInt32)((data & 0x7F) << shift);
|
||||
shift += 7;
|
||||
} while ((data & 0x80) == 0x80);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.requestTable.ToString() + this.responseTable.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa3995669bb00bb4b9662903873154c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,93 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings for HTTP/2 connections when the Connect protocol is available.
|
||||
/// </summary>
|
||||
public sealed class WebSocketOverHTTP2Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Set it to false to disable Websocket Over HTTP/2 (RFC 8441). It's true by default.
|
||||
/// </summary>
|
||||
public bool EnableWebSocketOverHTTP2 { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Set it to disable fallback logic from the Websocket Over HTTP/2 implementation to the 'old' HTTP/1 implementation when it fails to connect.
|
||||
/// </summary>
|
||||
public bool EnableImplementationFallback { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for HTTP/2 connections.
|
||||
/// </summary>
|
||||
public sealed class HTTP2ConnectionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// When set to false, the plugin will not try to use HTTP/2 connections.
|
||||
/// </summary>
|
||||
public bool EnableHTTP2Connections = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum size of the HPACK header table.
|
||||
/// </summary>
|
||||
public UInt32 HeaderTableSize = 4096; // Spec default: 4096
|
||||
|
||||
/// <summary>
|
||||
/// Maximum concurrent http2 stream on http2 connection will allow. Its default value is 128;
|
||||
/// </summary>
|
||||
public UInt32 MaxConcurrentStreams = 128; // Spec default: not defined
|
||||
|
||||
/// <summary>
|
||||
/// Initial window size of a http2 stream. Its default value is 65535, can be controlled through the HTTPRequest's DownloadSettings object.
|
||||
/// </summary>
|
||||
public UInt32 InitialStreamWindowSize = UInt16.MaxValue; // Spec default: 65535
|
||||
|
||||
/// <summary>
|
||||
/// Global window size of a http/2 connection. Its default value is the maximum possible value on 31 bits.
|
||||
/// </summary>
|
||||
public UInt32 InitialConnectionWindowSize = HTTP2ContentConsumer.MaxValueFor31Bits; // Spec default: 65535
|
||||
|
||||
/// <summary>
|
||||
/// Maximum size of a http2 frame.
|
||||
/// </summary>
|
||||
public UInt32 MaxFrameSize = 16384; // 16384 spec def.
|
||||
|
||||
/// <summary>
|
||||
/// Not used.
|
||||
/// </summary>
|
||||
public UInt32 MaxHeaderListSize = UInt32.MaxValue; // Spec default: infinite
|
||||
|
||||
/// <summary>
|
||||
/// With HTTP/2 only one connection will be open so we can keep it open longer as we hope it will be reused more.
|
||||
/// </summary>
|
||||
public TimeSpan MaxIdleTime = TimeSpan.FromSeconds(120);
|
||||
|
||||
/// <summary>
|
||||
/// Time between two ping messages.
|
||||
/// </summary>
|
||||
public TimeSpan PingFrequency = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Timeout to receive a ping acknowledgement from the server. If no ack received in this time the connection will be treated as broken.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout = TimeSpan.FromSeconds(3);
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to enable RFC 8441 "Bootstrapping WebSockets with HTTP/2" (https://tools.ietf.org/html/rfc8441).
|
||||
/// </summary>
|
||||
public bool EnableConnectProtocol = false;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for WebSockets over HTTP/2 (RFC 8441)
|
||||
/// </summary>
|
||||
public WebSocketOverHTTP2Settings WebSocketOverHTTP2Settings = new WebSocketOverHTTP2Settings();
|
||||
|
||||
/// <summary>
|
||||
/// When set to <c>true</c> the plugin will send a <c>Content-Length</c> header, even if it has a <c>0</c> value.
|
||||
/// </summary>
|
||||
public bool ForceSendContentLengthHeader { get; set; } = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40bfeb8809240f346a1a067b7fed0f3c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,709 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
#define ENABLE_LOGGING
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
public delegate HTTP2Stream CustomHTTP2StreamFactory(HTTPRequest request, uint streamId, HTTP2ContentConsumer parentHandler, HTTP2SettingsManager registry, HPACKEncoder hpackEncoder);
|
||||
|
||||
public sealed class HTTP2ContentConsumer : IHTTPRequestHandler, IContentConsumer, IThreadSignaler
|
||||
{
|
||||
public KeepAliveHeader KeepAlive { get { return null; } }
|
||||
|
||||
public bool CanProcessMultiple { get { return !this.SentGoAwayFrame && this.isRunning && this._maxAssignedRequests > 1; } }
|
||||
|
||||
public int AssignedRequests => this._assignedRequest;
|
||||
private int _assignedRequest;
|
||||
|
||||
public int MaxAssignedRequests => this._maxAssignedRequests;
|
||||
private int _maxAssignedRequests = 1;
|
||||
|
||||
public const UInt32 MaxValueFor31Bits = 0xFFFFFFFF >> 1;
|
||||
|
||||
public double Latency { get; private set; }
|
||||
|
||||
public HTTP2SettingsManager settings;
|
||||
public HPACKEncoder HPACKEncoder;
|
||||
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
public HTTP2ConnectionSettings ConnectionSettings { get; private set; }
|
||||
|
||||
private DateTime lastPingSent = DateTime.MinValue;
|
||||
private int waitingForPingAck = 0;
|
||||
private DateTime lastDataFrameReceived = DateTime.MinValue;
|
||||
|
||||
public static int RTTBufferCapacity = 5;
|
||||
private CircularBuffer<double> rtts = new CircularBuffer<double>(RTTBufferCapacity);
|
||||
|
||||
private volatile bool isRunning;
|
||||
|
||||
private AutoResetEvent newFrameSignal = new AutoResetEvent(false);
|
||||
|
||||
private ConcurrentQueue<HTTPRequest> requestQueue = new ConcurrentQueue<HTTPRequest>();
|
||||
|
||||
private List<HTTP2Stream> clientInitiatedStreams = new List<HTTP2Stream>();
|
||||
|
||||
private ConcurrentQueue<HTTP2FrameHeaderAndPayload> newFrames = new ConcurrentQueue<HTTP2FrameHeaderAndPayload>();
|
||||
|
||||
private List<HTTP2FrameHeaderAndPayload> outgoingFrames = new List<HTTP2FrameHeaderAndPayload>();
|
||||
|
||||
private UInt32 remoteWindow;
|
||||
private DateTime lastInteraction;
|
||||
private DateTime goAwaySentAt = DateTime.MaxValue;
|
||||
private bool SentGoAwayFrame { get => this.goAwaySentAt != DateTime.MaxValue; }
|
||||
|
||||
private HTTPOverTCPConnection conn;
|
||||
|
||||
private TimeSpan MaxGoAwayWaitTime { get { return !this.SentGoAwayFrame ? TimeSpan.MaxValue : TimeSpan.FromMilliseconds(Math.Max(this.Latency * 2.5, 1500)); } }
|
||||
|
||||
// https://httpwg.org/specs/rfc7540.html#StreamIdentifiers
|
||||
// Streams initiated by a client MUST use odd-numbered stream identifiers
|
||||
// With an initial value of -1, the first client initiated stream's id going to be 1.
|
||||
private long LastStreamId = -1;
|
||||
|
||||
public HTTP2ContentConsumer(HTTPOverTCPConnection conn)
|
||||
{
|
||||
this.Context = new LoggingContext(this);
|
||||
this.Context.Add("Parent", conn.Context);
|
||||
|
||||
this.conn = conn;
|
||||
this.isRunning = true;
|
||||
|
||||
this.ConnectionSettings = HTTPManager.PerHostSettings.Get(conn.HostKey).HTTP2ConnectionSettings;
|
||||
this.settings = new HTTP2SettingsManager(this.Context, this.ConnectionSettings);
|
||||
|
||||
Process(this.conn.CurrentRequest);
|
||||
}
|
||||
|
||||
public void Process(HTTPRequest request)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Process request called", this.Context);
|
||||
#endif
|
||||
|
||||
request.TimeoutSettings.SetProcessing(this.lastInteraction = DateTime.UtcNow);
|
||||
|
||||
Interlocked.Increment(ref this._assignedRequest);
|
||||
|
||||
this.requestQueue.Enqueue(request);
|
||||
SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
|
||||
public void SignalThread(SignalHandlerTypes signalType) => this.newFrameSignal?.Set();
|
||||
|
||||
public void RunHandler()
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Processing thread up and running!", this.Context);
|
||||
#endif
|
||||
|
||||
ThreadedRunner.SetThreadName("Best.HTTP2 Process");
|
||||
|
||||
string abortWithMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
bool atLeastOneStreamHasAFrameToSend = true;
|
||||
|
||||
this.HPACKEncoder = new HPACKEncoder(this.Context, this.settings);
|
||||
|
||||
// https://httpwg.org/specs/rfc7540.html#InitialWindowSize
|
||||
// The connection flow-control window is also 65,535 octets.
|
||||
this.remoteWindow = this.settings.RemoteSettings[HTTP2Settings.INITIAL_WINDOW_SIZE];
|
||||
|
||||
// we want to pack as many data as we can in one tcp segment, but setting the buffer's size too high
|
||||
// we might keep data too long and send them in bursts instead of in a steady stream.
|
||||
// Keeping it too low might result in a full tcp segment and one with very low payload
|
||||
// Is it possible that one full tcp segment sized buffer would be the best, or multiple of it.
|
||||
// It would keep the network busy without any fragments. The ethernet layer has a maximum of 1500 bytes,
|
||||
// but there's two layers of 20 byte headers each, so as a theoretical maximum it's 1500-20-20 bytes.
|
||||
// On the other hand, if the buffer is small (1-2), that means that for larger data, we have to do a lot
|
||||
// of system calls, in that case a larger buffer might be better. Still, if we are not cpu bound,
|
||||
// a well saturated network might serve us better.
|
||||
using (WriteOnlyBufferedStream bufferedStream = new WriteOnlyBufferedStream(this.conn.TopStream, 1024 * 1024 /*1500 - 20 - 20*/, this.Context))
|
||||
{
|
||||
// The client connection preface starts with a sequence of 24 octets
|
||||
// Connection preface starts with the string PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n).
|
||||
ReadOnlySpan<byte> MAGIC = stackalloc byte[24] { 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a };
|
||||
bufferedStream.Write(MAGIC);
|
||||
|
||||
// This sequence MUST be followed by a SETTINGS frame (Section 6.5), which MAY be empty.
|
||||
// The client sends the client connection preface immediately upon receipt of a
|
||||
// 101 (Switching Protocols) response (indicating a successful upgrade)
|
||||
// or as the first application data octets of a TLS connection
|
||||
|
||||
this.settings.InitiatedMySettings[HTTP2Settings.INITIAL_WINDOW_SIZE] = this.ConnectionSettings.InitialStreamWindowSize;
|
||||
this.settings.InitiatedMySettings[HTTP2Settings.MAX_CONCURRENT_STREAMS] = this.ConnectionSettings.MaxConcurrentStreams;
|
||||
this.settings.InitiatedMySettings[HTTP2Settings.ENABLE_CONNECT_PROTOCOL] = (uint)(this.ConnectionSettings.EnableConnectProtocol ? 1 : 0);
|
||||
this.settings.InitiatedMySettings[HTTP2Settings.ENABLE_PUSH] = 0;
|
||||
this.settings.SendChanges(this.outgoingFrames);
|
||||
this.settings.RemoteSettings.OnSettingChangedEvent += OnRemoteSettingChanged;
|
||||
|
||||
// The default window size for the whole connection is 65535 bytes,
|
||||
// but we want to set it to the maximum possible value.
|
||||
Int64 initialConnectionWindowSize = this.ConnectionSettings.InitialConnectionWindowSize;
|
||||
|
||||
// yandex.ru returns with an FLOW_CONTROL_ERROR (3) error when the plugin tries to set the connection window to 2^31 - 1
|
||||
// and works only with a maximum value of 2^31 - 10Mib (10 * 1024 * 1024).
|
||||
if (initialConnectionWindowSize == HTTP2ContentConsumer.MaxValueFor31Bits)
|
||||
initialConnectionWindowSize -= 10 * 1024 * 1024;
|
||||
|
||||
if (initialConnectionWindowSize > 65535)
|
||||
{
|
||||
Int64 initialConnectionWindowSizeDiff = initialConnectionWindowSize - 65535;
|
||||
if (initialConnectionWindowSizeDiff > 0)
|
||||
this.outgoingFrames.Add(HTTP2FrameHelper.CreateWindowUpdateFrame(0, (UInt32)initialConnectionWindowSizeDiff, this.Context));
|
||||
}
|
||||
|
||||
initialConnectionWindowSize -= 65535;
|
||||
|
||||
// local, per-connection window
|
||||
long localConnectionWindow = initialConnectionWindowSize;
|
||||
UInt32 updateConnectionWindowAt = (UInt32)(localConnectionWindow / 2);
|
||||
|
||||
while (this.isRunning)
|
||||
{
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
if (!atLeastOneStreamHasAFrameToSend)
|
||||
{
|
||||
// buffered stream will call flush automatically if its internal buffer is full.
|
||||
// But we have to make it sure that we flush remaining data before we go to sleep.
|
||||
bufferedStream.Flush();
|
||||
|
||||
// Wait until we have to send the next ping, OR a new frame is received on the read thread.
|
||||
// lastPingSent Now lastPingSent+frequency lastPingSent+Ping timeout
|
||||
//----|---------------------|---------------|----------------------|----------------------|------------|
|
||||
// lastInteraction lastInteraction + MaxIdleTime
|
||||
|
||||
var sendPingAt = this.lastPingSent + this.ConnectionSettings.PingFrequency;
|
||||
var timeoutAt = this.waitingForPingAck != 0 ? this.lastPingSent + this.ConnectionSettings.Timeout : DateTime.MaxValue;
|
||||
|
||||
// sendPingAt can be in the past if Timeout is larger than PingFrequency
|
||||
var nextPingInteraction = sendPingAt < timeoutAt && sendPingAt >= now ? sendPingAt : timeoutAt;
|
||||
|
||||
var disconnectByIdleAt = this.lastInteraction + this.ConnectionSettings.MaxIdleTime;
|
||||
|
||||
var nextDueClientInteractionAt = nextPingInteraction < disconnectByIdleAt ? nextPingInteraction : disconnectByIdleAt;
|
||||
int wait = (int)(nextDueClientInteractionAt - now).TotalMilliseconds;
|
||||
|
||||
wait = (int)Math.Min(wait, this.MaxGoAwayWaitTime.TotalMilliseconds);
|
||||
|
||||
TimeSpan nextStreamInteraction = TimeSpan.MaxValue;
|
||||
for (int i = 0; i < this.clientInitiatedStreams.Count; i++)
|
||||
{
|
||||
var streamInteraction = this.clientInitiatedStreams[i].NextInteraction;
|
||||
if (streamInteraction < nextStreamInteraction)
|
||||
nextStreamInteraction = streamInteraction;
|
||||
}
|
||||
|
||||
wait = (int)Math.Min(wait, nextStreamInteraction.TotalMilliseconds);
|
||||
wait = (int)Math.Min(wait, 1000);
|
||||
|
||||
if (wait >= 1)
|
||||
{
|
||||
//if (HTTPManager.Logger.Level <= Logger.Loglevels.All)
|
||||
// HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), string.Format("Sleeping for {0:N0}ms", wait), this.Context);
|
||||
this.newFrameSignal.WaitOne(wait);
|
||||
|
||||
now = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't send a new ping until a pong isn't received for the last one
|
||||
if (now - this.lastPingSent >= this.ConnectionSettings.PingFrequency && Interlocked.CompareExchange(ref this.waitingForPingAck, 1, 0) == 0)
|
||||
{
|
||||
this.lastPingSent = now;
|
||||
|
||||
var frame = HTTP2FrameHelper.CreatePingFrame(HTTP2PingFlags.None, this.Context);
|
||||
BufferHelper.SetLong(frame.Payload.Data, 0, now.Ticks);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), $"PING frame created with payload: {frame.Payload.Slice(0, 8)}", this.Context);
|
||||
#endif
|
||||
|
||||
this.outgoingFrames.Add(frame);
|
||||
}
|
||||
|
||||
// Process received frames
|
||||
HTTP2FrameHeaderAndPayload header;
|
||||
while (this.newFrames.TryDequeue(out header))
|
||||
{
|
||||
if (header.StreamId > 0)
|
||||
{
|
||||
switch (header.Type)
|
||||
{
|
||||
case HTTP2FrameTypes.DATA:
|
||||
localConnectionWindow -= header.Payload.Count;
|
||||
lastDataFrameReceived = now;
|
||||
break;
|
||||
}
|
||||
|
||||
HTTP2Stream http2Stream = FindStreamById(header.StreamId);
|
||||
|
||||
// Add frame to the stream, so it can process it when its Process function is called
|
||||
if (http2Stream != null)
|
||||
{
|
||||
http2Stream.AddFrame(header, this.outgoingFrames);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Error? It's possible that we closed and removed the stream while the server was in the middle of sending frames
|
||||
#if ENABLE_LOGGING
|
||||
if (HTTPManager.Logger.Level == Loglevels.All)
|
||||
HTTPManager.Logger.Warning(nameof(HTTP2ContentConsumer), $"Can't deliver frame: {header}, because no stream could be found for its Id!", this.Context);
|
||||
#endif
|
||||
|
||||
BufferPool.Release(header.Payload);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (header.Type)
|
||||
{
|
||||
case HTTP2FrameTypes.SETTINGS:
|
||||
this.settings.Process(header, this.outgoingFrames);
|
||||
|
||||
Interlocked.Exchange(ref this._maxAssignedRequests,
|
||||
(int)Math.Min(this.ConnectionSettings.MaxConcurrentStreams,
|
||||
this.settings.RemoteSettings[HTTP2Settings.MAX_CONCURRENT_STREAMS]));
|
||||
|
||||
/*
|
||||
PluginEventHelper.EnqueuePluginEvent(
|
||||
new PluginEventInfo(PluginEvents.HTTP2ConnectProtocol,
|
||||
new HTTP2ConnectProtocolInfo(this.conn.HostKey,
|
||||
this.settings.MySettings[HTTP2Settings.ENABLE_CONNECT_PROTOCOL] == 1 && this.settings.RemoteSettings[HTTP2Settings.ENABLE_CONNECT_PROTOCOL] == 1)));
|
||||
*/
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.PING:
|
||||
var pingFrame = HTTP2FrameHelper.ReadPingFrame(header);
|
||||
|
||||
if ((pingFrame.Flags & HTTP2PingFlags.ACK) != 0)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref this.waitingForPingAck, 0, 1) == 0)
|
||||
break; // waitingForPingAck was 0 == aren't expecting a ping ack!
|
||||
|
||||
// it was an ack, payload must contain what we sent
|
||||
|
||||
var ticks = BufferHelper.ReadLong(pingFrame.OpaqueData, 0);
|
||||
|
||||
// the difference between the current time and the time when the ping message is sent
|
||||
TimeSpan diff = TimeSpan.FromTicks(now.Ticks - ticks);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (diff.TotalSeconds > 10 || diff.TotalSeconds < 0)
|
||||
HTTPManager.Logger.Warning(nameof(HTTP2ContentConsumer), $"Pong received with weird diff: {diff}! Payload: {pingFrame.OpaqueData}", this.Context);
|
||||
#endif
|
||||
|
||||
// add it to the buffer
|
||||
this.rtts.Add(diff.TotalMilliseconds);
|
||||
|
||||
// and calculate the new latency
|
||||
this.Latency = CalculateLatency();
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP2ContentConsumer), string.Format("Latency: {0:F2}ms, RTT buffer: {1}", this.Latency, this.rtts.ToString()), this.Context);
|
||||
#endif
|
||||
}
|
||||
else if ((pingFrame.Flags & HTTP2PingFlags.ACK) == 0)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#PING
|
||||
// if it wasn't an ack for our ping, we have to send one
|
||||
|
||||
var frame = HTTP2FrameHelper.CreatePingFrame(HTTP2PingFlags.ACK, this.Context);
|
||||
Array.Copy(pingFrame.OpaqueData.Data, 0, frame.Payload.Data, 0, pingFrame.OpaqueData.Count);
|
||||
|
||||
this.outgoingFrames.Insert(0, frame);
|
||||
}
|
||||
|
||||
BufferPool.Release(pingFrame.OpaqueData);
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.WINDOW_UPDATE:
|
||||
var windowUpdateFrame = HTTP2FrameHelper.ReadWindowUpdateFrame(header);
|
||||
this.remoteWindow += windowUpdateFrame.WindowSizeIncrement;
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.GOAWAY:
|
||||
// parse the frame, so we can print out detailed information
|
||||
HTTP2GoAwayFrame goAwayFrame = HTTP2FrameHelper.ReadGoAwayFrame(header);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Received GOAWAY frame: " + goAwayFrame.ToString(), this.Context);
|
||||
#endif
|
||||
|
||||
abortWithMessage = string.Format("Server closing the connection! Error code: {0} ({1}) Additonal Debug Data: {2}",
|
||||
goAwayFrame.Error, goAwayFrame.ErrorCode, goAwayFrame.AdditionalDebugData);
|
||||
|
||||
for (int i = 0; i < this.clientInitiatedStreams.Count; ++i)
|
||||
this.clientInitiatedStreams[i].Abort(abortWithMessage);
|
||||
this.clientInitiatedStreams.Clear();
|
||||
|
||||
// set the running flag to false, so the thread can exit
|
||||
this.isRunning = false;
|
||||
|
||||
BufferPool.Release(goAwayFrame.AdditionalDebugData);
|
||||
|
||||
//this.conn.State = HTTPConnectionStates.Closed;
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.ALT_SVC:
|
||||
//HTTP2AltSVCFrame altSvcFrame = HTTP2FrameHelper.ReadAltSvcFrame(header);
|
||||
|
||||
// Implement
|
||||
//HTTPManager.EnqueuePluginEvent(new PluginEventInfo(PluginEvents.AltSvcHeader, new AltSvcEventInfo(altSvcFrame.Origin, ))
|
||||
break;
|
||||
}
|
||||
|
||||
if (header.Payload != null)
|
||||
BufferPool.Release(header.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
// If no pong received in a (configurable) reasonable time, treat the connection broken
|
||||
if (this.waitingForPingAck != 0)
|
||||
{
|
||||
// Even if we weren't received a ping ack, check for data frames.
|
||||
// If data is flowing in, we can say that the connection is still healthy.
|
||||
var lastPingDiff = now - this.lastPingSent;
|
||||
var lastDataDiff = now - this.lastDataFrameReceived;
|
||||
|
||||
if(lastPingDiff >= this.ConnectionSettings.Timeout && lastDataDiff >= this.ConnectionSettings.Timeout)
|
||||
throw new TimeoutException("Ping ACK isn't received in time!");
|
||||
}
|
||||
|
||||
// pre-test stream count to lock only when truly needed.
|
||||
if (this.clientInitiatedStreams.Count < _maxAssignedRequests && this.isRunning)
|
||||
{
|
||||
// grab requests from queue
|
||||
HTTPRequest request;
|
||||
while (this.clientInitiatedStreams.Count < _maxAssignedRequests && this.requestQueue.TryDequeue(out request))
|
||||
{
|
||||
HTTP2Stream newStream = null;
|
||||
|
||||
if (request.Tag is CustomHTTP2StreamFactory factory)
|
||||
{
|
||||
newStream = factory(request, (UInt32)Interlocked.Add(ref LastStreamId, 2), this, this.settings, this.HPACKEncoder);
|
||||
}
|
||||
else
|
||||
{
|
||||
newStream = new HTTP2Stream((UInt32)Interlocked.Add(ref LastStreamId, 2), this, this.settings, this.HPACKEncoder);
|
||||
}
|
||||
|
||||
newStream.Assign(request);
|
||||
this.clientInitiatedStreams.Add(newStream);
|
||||
}
|
||||
}
|
||||
|
||||
// send any settings changes
|
||||
this.settings.SendChanges(this.outgoingFrames);
|
||||
|
||||
atLeastOneStreamHasAFrameToSend = false;
|
||||
|
||||
// process other streams
|
||||
for (int i = 0; i < this.clientInitiatedStreams.Count; ++i)
|
||||
{
|
||||
var stream = this.clientInitiatedStreams[i];
|
||||
stream.Process(this.outgoingFrames);
|
||||
|
||||
// remove closed, empty streams (not enough to check the closed flag, a closed stream still can contain frames to send)
|
||||
if (stream.State == HTTP2StreamStates.Closed && !stream.HasFrameToSend)
|
||||
{
|
||||
this.clientInitiatedStreams.RemoveAt(i--);
|
||||
stream.Removed();
|
||||
|
||||
Interlocked.Decrement(ref this._assignedRequest);
|
||||
}
|
||||
|
||||
atLeastOneStreamHasAFrameToSend |= stream.HasFrameToSend;
|
||||
|
||||
this.lastInteraction = now;
|
||||
}
|
||||
|
||||
// If we encounter a data frame that too large for the current remote window, we have to stop
|
||||
// sending all data frames as we could send smaller data frames before the large ones.
|
||||
// Room for improvement: An improvement would be here to stop data frame sending per-stream.
|
||||
bool haltDataSending = false;
|
||||
|
||||
if (this.ShutdownType == ShutdownTypes.Running && !this.SentGoAwayFrame && now - this.lastInteraction >= this.ConnectionSettings.MaxIdleTime)
|
||||
{
|
||||
this.lastInteraction = now;
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Reached idle time, sending GoAway frame!", this.Context);
|
||||
#endif
|
||||
this.outgoingFrames.Add(HTTP2FrameHelper.CreateGoAwayFrame(0, HTTP2ErrorCodes.NO_ERROR, this.Context));
|
||||
this.goAwaySentAt = now;
|
||||
}
|
||||
|
||||
// https://httpwg.org/specs/rfc7540.html#GOAWAY
|
||||
// Endpoints SHOULD always send a GOAWAY frame before closing a connection so that the remote peer can know whether a stream has been partially processed or not.
|
||||
if (this.ShutdownType == ShutdownTypes.Gentle && !this.SentGoAwayFrame)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Connection abort requested, sending GoAway frame!", this.Context);
|
||||
#endif
|
||||
|
||||
this.outgoingFrames.Clear();
|
||||
this.outgoingFrames.Add(HTTP2FrameHelper.CreateGoAwayFrame(0, HTTP2ErrorCodes.NO_ERROR, this.Context));
|
||||
this.goAwaySentAt = now;
|
||||
}
|
||||
|
||||
if (this.isRunning && this.SentGoAwayFrame && now - goAwaySentAt >= this.MaxGoAwayWaitTime)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "No GoAway frame received back. Really quitting now!", this.Context);
|
||||
#endif
|
||||
this.isRunning = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (localConnectionWindow < updateConnectionWindowAt)
|
||||
{
|
||||
UInt32 diff = (UInt32)(initialConnectionWindowSize - localConnectionWindow);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), $"Updating local connection window by {diff:N0} ({initialConnectionWindowSize:N0} - {localConnectionWindow:N0})", this.Context);
|
||||
#endif
|
||||
|
||||
this.outgoingFrames.Add(HTTP2FrameHelper.CreateWindowUpdateFrame(0, diff, this.Context));
|
||||
localConnectionWindow = initialConnectionWindowSize;
|
||||
}
|
||||
|
||||
// Go through all the collected frames and send them.
|
||||
for (int i = 0; i < this.outgoingFrames.Count; ++i)
|
||||
{
|
||||
var frame = this.outgoingFrames[i];
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (HTTPManager.Logger.IsDiagnostic && frame.Type != HTTP2FrameTypes.DATA /*&& frame.Type != HTTP2FrameTypes.PING*/)
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Sending frame: " + frame.ToString(), this.Context);
|
||||
#endif
|
||||
|
||||
// post process frames
|
||||
switch (frame.Type)
|
||||
{
|
||||
case HTTP2FrameTypes.DATA:
|
||||
if (haltDataSending)
|
||||
continue;
|
||||
|
||||
// if the tracked remoteWindow is smaller than the frame's payload, we stop sending
|
||||
// data frames until we receive window-update frames
|
||||
if (frame.Payload.Count > this.remoteWindow)
|
||||
{
|
||||
haltDataSending = true;
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Warning(nameof(HTTP2ContentConsumer), string.Format("Data sending halted for this round. Remote Window: {0:N0}, frame: {1}", this.remoteWindow, frame.ToString()), this.Context);
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.outgoingFrames.RemoveAt(i--);
|
||||
|
||||
static void SendHeader(WriteOnlyBufferedStream stream,
|
||||
HTTP2FrameHeaderAndPayload headerAndPayload)
|
||||
{
|
||||
Span<byte> frameData = stackalloc byte[9];
|
||||
HTTP2FrameHelper.HeaderAsBinary(headerAndPayload, frameData);
|
||||
stream.Write(frameData);
|
||||
}
|
||||
|
||||
SendHeader(bufferedStream, frame);
|
||||
|
||||
if (frame.Payload.Count > 0)
|
||||
{
|
||||
bufferedStream.Write(frame.Payload.Data, frame.Payload.Offset, frame.Payload.Count);
|
||||
|
||||
if (!frame.DontUseMemPool)
|
||||
BufferPool.Release(frame.Payload);
|
||||
}
|
||||
|
||||
if (frame.Type == HTTP2FrameTypes.DATA)
|
||||
this.remoteWindow -= (uint)frame.Payload.Count;
|
||||
}
|
||||
|
||||
bufferedStream.Flush();
|
||||
} // while (this.isRunning)
|
||||
|
||||
bufferedStream.Flush();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
abortWithMessage = ex.ToString();
|
||||
// Log out the exception if it's a non-expected one.
|
||||
if (this.ShutdownType == ShutdownTypes.Running && this.isRunning && !this.SentGoAwayFrame && !HTTPManager.IsQuitting)
|
||||
HTTPManager.Logger.Exception(nameof(HTTP2ContentConsumer), "Sender thread", ex, this.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isRunning = false;
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), $"Sender thread closing - cleaning up remaining requests({this.clientInitiatedStreams.Count})...", this.Context);
|
||||
#endif
|
||||
|
||||
if (string.IsNullOrEmpty(abortWithMessage))
|
||||
abortWithMessage = "Connection closed unexpectedly";
|
||||
|
||||
for (int i = 0; i < this.clientInitiatedStreams.Count; ++i)
|
||||
this.clientInitiatedStreams[i].Abort(abortWithMessage);
|
||||
this.clientInitiatedStreams.Clear();
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Sender thread closing", this.Context);
|
||||
#endif
|
||||
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this.conn, HTTPConnectionStates.Closed));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemoteSettingChanged(HTTP2SettingsRegistry registry, HTTP2Settings setting, uint oldValue, uint newValue)
|
||||
{
|
||||
switch (setting)
|
||||
{
|
||||
case HTTP2Settings.INITIAL_WINDOW_SIZE:
|
||||
this.remoteWindow = newValue - (oldValue - this.remoteWindow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (this.isRunning && HTTP2FrameHelper.CanReadFullFrame(this.ContentProvider))
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload header = HTTP2FrameHelper.ReadHeader(this.ContentProvider, this.Context);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (HTTPManager.Logger.IsDiagnostic /*&& header.Type != HTTP2FrameTypes.DATA /*&& header.Type != HTTP2FrameTypes.PING*/)
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "New frame received: " + header.ToString(), this.Context);
|
||||
#endif
|
||||
|
||||
// Add the new frame to the queue. Processing it on the write thread gives us the advantage that
|
||||
// we don't have to deal with too much locking.
|
||||
this.newFrames.Enqueue(header);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(HTTP2ContentConsumer), "", ex, this.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// ping write thread to process the new frame
|
||||
this.newFrameSignal?.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Verbose(nameof(HTTP2ContentConsumer), $"{nameof(OnConnectionClosed)}({this.isRunning})", this.Context);
|
||||
#endif
|
||||
this.isRunning = false;
|
||||
this.newFrameSignal?.Set();
|
||||
}
|
||||
|
||||
public void OnError(Exception ex)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Exception(nameof(HTTP2ContentConsumer), $"{nameof(OnError)}({this.isRunning}, {ex})", ex, this.Context);
|
||||
#endif
|
||||
this.isRunning = false;
|
||||
this.newFrameSignal?.Set();
|
||||
}
|
||||
|
||||
private double CalculateLatency()
|
||||
{
|
||||
if (this.rtts.Count == 0)
|
||||
return 0;
|
||||
|
||||
double sumLatency = 0;
|
||||
for (int i = 0; i < this.rtts.Count; ++i)
|
||||
sumLatency += this.rtts[i];
|
||||
|
||||
return sumLatency / this.rtts.Count;
|
||||
}
|
||||
|
||||
HTTP2Stream FindStreamById(UInt32 streamId)
|
||||
{
|
||||
for (int i = 0; i < this.clientInitiatedStreams.Count; ++i)
|
||||
{
|
||||
var stream = this.clientInitiatedStreams[i];
|
||||
if (stream.Id == streamId)
|
||||
return stream;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ShutdownTypes ShutdownType { get; private set; }
|
||||
|
||||
public void Shutdown(ShutdownTypes type)
|
||||
{
|
||||
this.ShutdownType = type;
|
||||
|
||||
switch (this.ShutdownType)
|
||||
{
|
||||
case ShutdownTypes.Gentle:
|
||||
this.newFrameSignal.Set();
|
||||
break;
|
||||
|
||||
case ShutdownTypes.Immediate:
|
||||
this.conn?.TopStream?.Dispose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), "Dispose", this.Context);
|
||||
#endif
|
||||
|
||||
while (this.newFrames.TryDequeue(out var frame))
|
||||
BufferPool.Release(frame.Payload);
|
||||
|
||||
foreach (var frame in this.outgoingFrames)
|
||||
BufferPool.Release(frame.Payload);
|
||||
this.outgoingFrames.Clear();
|
||||
|
||||
HTTPRequest request = null;
|
||||
while (this.requestQueue.TryDequeue(out request))
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information(nameof(HTTP2ContentConsumer), string.Format("Dispose - Request '{0}' IsCancellationRequested: {1}", request.CurrentUri.ToString(), request.IsCancellationRequested.ToString()), this.Context);
|
||||
#endif
|
||||
RequestEventHelper.EnqueueRequestEvent(request.IsCancellationRequested ? new RequestEventInfo(request, HTTPRequestStates.Aborted, null) : new RequestEventInfo(request, RequestEvents.Resend));
|
||||
}
|
||||
|
||||
this.newFrameSignal?.Close();
|
||||
this.newFrameSignal = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e555764ed1609a4bb57371203918367
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,430 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#ErrorCodes
|
||||
public enum HTTP2ErrorCodes
|
||||
{
|
||||
NO_ERROR = 0x00,
|
||||
PROTOCOL_ERROR = 0x01,
|
||||
INTERNAL_ERROR = 0x02,
|
||||
FLOW_CONTROL_ERROR = 0x03,
|
||||
SETTINGS_TIMEOUT = 0x04,
|
||||
STREAM_CLOSED = 0x05,
|
||||
FRAME_SIZE_ERROR = 0x06,
|
||||
REFUSED_STREAM = 0x07,
|
||||
CANCEL = 0x08,
|
||||
COMPRESSION_ERROR = 0x09,
|
||||
CONNECT_ERROR = 0x0A,
|
||||
ENHANCE_YOUR_CALM = 0x0B,
|
||||
INADEQUATE_SECURITY = 0x0C,
|
||||
HTTP_1_1_REQUIRED = 0x0D
|
||||
}
|
||||
|
||||
public static class HTTP2FrameHelper
|
||||
{
|
||||
public static HTTP2ContinuationFrame ReadContinuationFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#CONTINUATION
|
||||
|
||||
HTTP2ContinuationFrame frame = new HTTP2ContinuationFrame(header);
|
||||
|
||||
frame.HeaderBlockFragment = header.Payload;
|
||||
header.Payload = BufferSegment.Empty;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2WindowUpdateFrame ReadWindowUpdateFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#WINDOW_UPDATE
|
||||
|
||||
HTTP2WindowUpdateFrame frame = new HTTP2WindowUpdateFrame(header);
|
||||
|
||||
frame.ReservedBit = BufferHelper.ReadBit(header.Payload.Data[0], 0);
|
||||
frame.WindowSizeIncrement = BufferHelper.ReadUInt31(header.Payload, 0);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2GoAwayFrame ReadGoAwayFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#GOAWAY
|
||||
// str id error
|
||||
// | 0, 1, 2, 3 | 4, 5, 6, 7 | ...
|
||||
|
||||
HTTP2GoAwayFrame frame = new HTTP2GoAwayFrame(header);
|
||||
|
||||
frame.ReservedBit = BufferHelper.ReadBit(header.Payload.Data[0], 0);
|
||||
frame.LastStreamId = BufferHelper.ReadUInt31(header.Payload, 0);
|
||||
frame.ErrorCode = BufferHelper.ReadUInt32(header.Payload, 4);
|
||||
|
||||
var additionalDebugDataLength = header.Payload.Count - 8;
|
||||
if (additionalDebugDataLength > 0)
|
||||
{
|
||||
var buff = BufferPool.Get(additionalDebugDataLength, true);
|
||||
Array.Copy(header.Payload.Data, 8, buff, 0, additionalDebugDataLength);
|
||||
|
||||
frame.AdditionalDebugData = buff.AsBuffer(additionalDebugDataLength);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2PingFrame ReadPingFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#PING
|
||||
|
||||
HTTP2PingFrame frame = new HTTP2PingFrame(header);
|
||||
|
||||
Array.Copy(header.Payload.Data, 0, frame.OpaqueData.Data, 0, frame.OpaqueData.Count);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2PushPromiseFrame ReadPush_PromiseFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#PUSH_PROMISE
|
||||
|
||||
HTTP2PushPromiseFrame frame = new HTTP2PushPromiseFrame(header);
|
||||
int HeaderBlockFragmentLength = header.Payload.Count - 4; // PromisedStreamId
|
||||
|
||||
bool isPadded = (frame.Flags & HTTP2PushPromiseFlags.PADDED) != 0;
|
||||
if (isPadded)
|
||||
{
|
||||
frame.PadLength = header.Payload.Data[0];
|
||||
|
||||
HeaderBlockFragmentLength -= 1 + (frame.PadLength ?? 0);
|
||||
}
|
||||
|
||||
frame.ReservedBit = BufferHelper.ReadBit(header.Payload.Data[1], 0);
|
||||
frame.PromisedStreamId = BufferHelper.ReadUInt31(header.Payload, 1);
|
||||
|
||||
var HeaderBlockFragmentIdx = isPadded ? 5 : 4;
|
||||
frame.HeaderBlockFragment = header.Payload.Slice(HeaderBlockFragmentIdx, HeaderBlockFragmentLength);
|
||||
header.Payload = BufferSegment.Empty;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2RSTStreamFrame ReadRST_StreamFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#RST_STREAM
|
||||
|
||||
HTTP2RSTStreamFrame frame = new HTTP2RSTStreamFrame(header);
|
||||
frame.ErrorCode = BufferHelper.ReadUInt32(header.Payload, 0);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2PriorityFrame ReadPriorityFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#PRIORITY
|
||||
|
||||
if (header.Payload.Count != 5)
|
||||
{
|
||||
//throw FRAME_SIZE_ERROR
|
||||
}
|
||||
|
||||
HTTP2PriorityFrame frame = new HTTP2PriorityFrame(header);
|
||||
|
||||
frame.IsExclusive = BufferHelper.ReadBit(header.Payload.Data[0], 0);
|
||||
frame.StreamDependency = BufferHelper.ReadUInt31(header.Payload, 0);
|
||||
frame.Weight = header.Payload.Data[4];
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2HeadersFrame ReadHeadersFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#HEADERS
|
||||
|
||||
HTTP2HeadersFrame frame = new HTTP2HeadersFrame(header);
|
||||
var HeaderBlockFragmentLength = header.Payload.Count;
|
||||
|
||||
bool isPadded = (frame.Flags & HTTP2HeadersFlags.PADDED) != 0;
|
||||
bool isPriority = (frame.Flags & HTTP2HeadersFlags.PRIORITY) != 0;
|
||||
|
||||
int payloadIdx = 0;
|
||||
|
||||
if (isPadded)
|
||||
{
|
||||
frame.PadLength = header.Payload.Data[payloadIdx++];
|
||||
|
||||
int subLength = 1 + (frame.PadLength ?? 0);
|
||||
if (subLength <= HeaderBlockFragmentLength)
|
||||
HeaderBlockFragmentLength -= subLength;
|
||||
//else
|
||||
// throw PROTOCOL_ERROR;
|
||||
}
|
||||
|
||||
if (isPriority)
|
||||
{
|
||||
frame.IsExclusive = BufferHelper.ReadBit(header.Payload.Data[payloadIdx], 0);
|
||||
frame.StreamDependency = BufferHelper.ReadUInt31(header.Payload, payloadIdx);
|
||||
payloadIdx += 4;
|
||||
frame.Weight = header.Payload.Data[payloadIdx++];
|
||||
|
||||
int subLength = 5;
|
||||
if (subLength <= HeaderBlockFragmentLength)
|
||||
HeaderBlockFragmentLength -= subLength;
|
||||
//else
|
||||
// throw PROTOCOL_ERROR;
|
||||
}
|
||||
|
||||
var HeaderBlockFragmentIdx = payloadIdx;
|
||||
frame.HeaderBlockFragment = header.Payload.Slice(HeaderBlockFragmentIdx, HeaderBlockFragmentLength);
|
||||
|
||||
header.Payload = BufferSegment.Empty;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2DataFrame ReadDataFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#DATA
|
||||
|
||||
HTTP2DataFrame frame = new HTTP2DataFrame(header);
|
||||
|
||||
var DataLength = header.Payload.Count;
|
||||
|
||||
bool isPadded = (frame.Flags & HTTP2DataFlags.PADDED) != 0;
|
||||
if (isPadded)
|
||||
{
|
||||
frame.PadLength = header.Payload.Data[0];
|
||||
|
||||
int subLength = 1 + (frame.PadLength ?? 0);
|
||||
if (subLength <= DataLength)
|
||||
DataLength -= subLength;
|
||||
//else
|
||||
// throw PROTOCOL_ERROR;
|
||||
}
|
||||
|
||||
var DataIdx = isPadded ? 1 : 0;
|
||||
|
||||
frame.Data = header.Payload.Slice(DataIdx, DataLength);
|
||||
header.Payload = BufferSegment.Empty;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2AltSVCFrame ReadAltSvcFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
HTTP2AltSVCFrame frame = new HTTP2AltSVCFrame(header);
|
||||
|
||||
// Implement
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static void StreamRead(Stream stream, byte[] buffer, int offset, uint count)
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
uint sumRead = 0;
|
||||
do
|
||||
{
|
||||
int readCount = (int)(count - sumRead);
|
||||
int streamReadCount = stream.Read(buffer, (int)(offset + sumRead), readCount);
|
||||
if (streamReadCount <= 0 && readCount > 0)
|
||||
throw new Exception("TCP Stream closed!");
|
||||
sumRead += (uint)streamReadCount;
|
||||
} while (sumRead < count);
|
||||
}
|
||||
|
||||
public static void StreamRead(Stream stream, BufferSegment buffer)
|
||||
{
|
||||
if (buffer.Count == 0)
|
||||
return;
|
||||
|
||||
uint sumRead = 0;
|
||||
do
|
||||
{
|
||||
int readCount = (int)(buffer.Count - sumRead);
|
||||
int streamReadCount = stream.Read(buffer.Data, (int)(buffer.Offset + sumRead), readCount);
|
||||
if (streamReadCount <= 0 && readCount > 0)
|
||||
throw new Exception("TCP Stream closed!");
|
||||
sumRead += (uint)streamReadCount;
|
||||
} while (sumRead < buffer.Count);
|
||||
}
|
||||
|
||||
public static void HeaderAsBinary(HTTP2FrameHeaderAndPayload header, Span<byte> buffer)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#FrameHeader
|
||||
|
||||
BufferHelper.SetUInt24(buffer, 0, (uint)header.Payload.Count);
|
||||
buffer[3] = (byte)header.Type;
|
||||
buffer[4] = header.Flags;
|
||||
BufferHelper.SetUInt31(buffer, 5, header.StreamId);
|
||||
}
|
||||
|
||||
public unsafe static bool CanReadFullFrame(PeekableStream stream)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#FrameHeader
|
||||
|
||||
// A frame without any payload is 9 bytes
|
||||
if (stream.Length < 9)
|
||||
return false;
|
||||
|
||||
stream.BeginPeek();
|
||||
|
||||
// First 3 bytes are the payload length
|
||||
var rawLength = stackalloc byte[3];
|
||||
rawLength[0] = (byte)stream.PeekByte();
|
||||
rawLength[1] = (byte)stream.PeekByte();
|
||||
rawLength[2] = (byte)stream.PeekByte();
|
||||
|
||||
var payloadLength = (UInt32)(rawLength[2] | rawLength[1] << 8 | rawLength[0] << 16);
|
||||
|
||||
return stream.Length >= (9 + payloadLength);
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload ReadHeader(Stream stream, LoggingContext context)
|
||||
{
|
||||
byte[] buffer = BufferPool.Get(9, true, context);
|
||||
using var _ = buffer.AsAutoRelease();
|
||||
|
||||
StreamRead(stream, buffer, 0, 9);
|
||||
|
||||
HTTP2FrameHeaderAndPayload header = new HTTP2FrameHeaderAndPayload();
|
||||
|
||||
var PayloadLength = (int)BufferHelper.ReadUInt24(buffer, 0);
|
||||
header.Type = (HTTP2FrameTypes)buffer[3];
|
||||
header.Flags = buffer[4];
|
||||
header.StreamId = BufferHelper.ReadUInt31(buffer, 5);
|
||||
|
||||
header.Payload = BufferPool.Get(PayloadLength, true, context).AsBuffer(PayloadLength);
|
||||
|
||||
try
|
||||
{
|
||||
StreamRead(stream, header.Payload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
BufferPool.Release(header.Payload);
|
||||
throw;
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
public static HTTP2SettingsFrame ReadSettings(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
HTTP2SettingsFrame frame = new HTTP2SettingsFrame(header);
|
||||
|
||||
if (header.Payload.Count > 0)
|
||||
{
|
||||
int kvpCount = (int)(header.Payload.Count / 6);
|
||||
|
||||
frame.Settings = new List<KeyValuePair<HTTP2Settings, uint>>(kvpCount);
|
||||
for (int i = 0; i < kvpCount; ++i)
|
||||
{
|
||||
HTTP2Settings key = (HTTP2Settings)BufferHelper.ReadUInt16(header.Payload.Data, i * 6);
|
||||
UInt32 value = BufferHelper.ReadUInt32(header.Payload, (i * 6) + 2);
|
||||
|
||||
frame.Settings.Add(new KeyValuePair<HTTP2Settings, uint>(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload CreateACKSettingsFrame()
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.SETTINGS;
|
||||
frame.Flags = (byte)HTTP2SettingsFlags.ACK;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload CreateSettingsFrame(List<KeyValuePair<HTTP2Settings, UInt32>> settings, LoggingContext context)
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.SETTINGS;
|
||||
frame.Flags = 0;
|
||||
var PayloadLength = settings.Count * 6;
|
||||
|
||||
frame.Payload = BufferPool.Get(PayloadLength, true, context).AsBuffer(PayloadLength);
|
||||
|
||||
for (int i = 0; i < settings.Count; ++i)
|
||||
{
|
||||
BufferHelper.SetUInt16(frame.Payload.Data, i * 6, (UInt16)settings[i].Key);
|
||||
BufferHelper.SetUInt32(frame.Payload.Data, (i * 6) + 2, settings[i].Value);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload CreatePingFrame(HTTP2PingFlags flags, LoggingContext context)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#PING
|
||||
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.PING;
|
||||
frame.Flags = (byte)flags;
|
||||
frame.StreamId = 0;
|
||||
frame.Payload = BufferPool.Get(8, true, context).AsBuffer(8);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload CreateWindowUpdateFrame(UInt32 streamId, UInt32 windowSizeIncrement, LoggingContext context)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#WINDOW_UPDATE
|
||||
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.WINDOW_UPDATE;
|
||||
frame.Flags = 0;
|
||||
frame.StreamId = streamId;
|
||||
frame.Payload = BufferPool.Get(4, true, context).AsBuffer(4);
|
||||
|
||||
BufferHelper.SetBit(0, 0, 0);
|
||||
BufferHelper.SetUInt31(frame.Payload.Data, 0, windowSizeIncrement);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload CreateGoAwayFrame(UInt32 lastStreamId, HTTP2ErrorCodes error, LoggingContext context)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#GOAWAY
|
||||
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.GOAWAY;
|
||||
frame.Flags = 0;
|
||||
frame.StreamId = 0;
|
||||
frame.Payload = BufferPool.Get(8, true, context).AsBuffer(8);
|
||||
|
||||
BufferHelper.SetUInt31(frame.Payload.Data, 0, lastStreamId);
|
||||
BufferHelper.SetUInt31(frame.Payload.Data, 4, (UInt32)error);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static HTTP2FrameHeaderAndPayload CreateRSTFrame(UInt32 streamId, HTTP2ErrorCodes errorCode, LoggingContext context)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#RST_STREAM
|
||||
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.RST_STREAM;
|
||||
frame.Flags = 0;
|
||||
frame.StreamId = streamId;
|
||||
frame.Payload = BufferPool.Get(4, true, context).AsBuffer(4);
|
||||
|
||||
BufferHelper.SetUInt32(frame.Payload.Data, 0, (UInt32)errorCode);
|
||||
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed90f479cb36204458007c8c20c99490
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,392 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#iana-frames
|
||||
public enum HTTP2FrameTypes : byte
|
||||
{
|
||||
DATA = 0x00,
|
||||
HEADERS = 0x01,
|
||||
PRIORITY = 0x02,
|
||||
RST_STREAM = 0x03,
|
||||
SETTINGS = 0x04,
|
||||
PUSH_PROMISE = 0x05,
|
||||
PING = 0x06,
|
||||
GOAWAY = 0x07,
|
||||
WINDOW_UPDATE = 0x08,
|
||||
CONTINUATION = 0x09,
|
||||
|
||||
// https://tools.ietf.org/html/rfc7838#section-4
|
||||
ALT_SVC = 0x0A
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HTTP2DataFlags : byte
|
||||
{
|
||||
None = 0x00,
|
||||
END_STREAM = 0x01,
|
||||
PADDED = 0x08,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HTTP2HeadersFlags : byte
|
||||
{
|
||||
None = 0x00,
|
||||
END_STREAM = 0x01,
|
||||
END_HEADERS = 0x04,
|
||||
PADDED = 0x08,
|
||||
PRIORITY = 0x20,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HTTP2SettingsFlags : byte
|
||||
{
|
||||
None = 0x00,
|
||||
ACK = 0x01,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HTTP2PushPromiseFlags : byte
|
||||
{
|
||||
None = 0x00,
|
||||
END_HEADERS = 0x04,
|
||||
PADDED = 0x08,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HTTP2PingFlags : byte
|
||||
{
|
||||
None = 0x00,
|
||||
ACK = 0x01,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum HTTP2ContinuationFlags : byte
|
||||
{
|
||||
None = 0x00,
|
||||
END_HEADERS = 0x04,
|
||||
}
|
||||
|
||||
public struct HTTP2FrameHeaderAndPayload
|
||||
{
|
||||
public HTTP2FrameTypes Type;
|
||||
public byte Flags;
|
||||
public UInt32 StreamId;
|
||||
|
||||
//public byte[] Payload;
|
||||
public BufferSegment Payload;
|
||||
|
||||
//public UInt32 PayloadOffset;
|
||||
//public UInt32 PayloadLength;
|
||||
|
||||
public bool DontUseMemPool;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[HTTP2FrameHeaderAndPayload Type: {Type}, Flags: {Flags.ToBinaryStr()}, StreamId: {StreamId}, DontUseMemPool: {DontUseMemPool}, Payload: {Payload}]";
|
||||
}
|
||||
|
||||
public string PayloadAsHex() => this.Payload.ToString();
|
||||
}
|
||||
|
||||
public struct HTTP2SettingsFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
public HTTP2SettingsFlags Flags { get { return (HTTP2SettingsFlags)this.Header.Flags; } }
|
||||
public List<KeyValuePair<HTTP2Settings, UInt32>> Settings;
|
||||
|
||||
public HTTP2SettingsFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.Settings = null;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string settings = null;
|
||||
if (this.Settings != null)
|
||||
{
|
||||
System.Text.StringBuilder sb = StringBuilderPool.Get(this.Settings.Count + 2);
|
||||
|
||||
sb.Append("[");
|
||||
foreach (var kvp in this.Settings)
|
||||
sb.AppendFormat("[{0}: {1}]", kvp.Key, kvp.Value);
|
||||
sb.Append("]");
|
||||
|
||||
settings = StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
|
||||
return string.Format("[HTTP2SettingsFrame Header: {0}, Flags: {1}, Settings: {2}]", this.Header.ToString(), this.Flags, settings ?? "Empty");
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2DataFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
public HTTP2DataFlags Flags { get { return (HTTP2DataFlags)this.Header.Flags; } }
|
||||
|
||||
public byte? PadLength;
|
||||
|
||||
public BufferSegment Data;
|
||||
|
||||
public HTTP2DataFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.PadLength = null;
|
||||
this.Data = BufferSegment.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2DataFrame Header: {0}, Flags: {1}, PadLength: {2}, Data: {3}]",
|
||||
this.Header.ToString(),
|
||||
this.Flags,
|
||||
this.PadLength == null ? ":Empty" : this.PadLength.Value.ToString(),
|
||||
this.Data.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2HeadersFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
public HTTP2HeadersFlags Flags { get { return (HTTP2HeadersFlags)this.Header.Flags; } }
|
||||
|
||||
public byte? PadLength;
|
||||
public byte? IsExclusive;
|
||||
public UInt32? StreamDependency;
|
||||
public byte? Weight;
|
||||
|
||||
public BufferSegment HeaderBlockFragment;
|
||||
|
||||
public HTTP2HeadersFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.PadLength = null;
|
||||
this.IsExclusive = null;
|
||||
this.StreamDependency = null;
|
||||
this.Weight = null;
|
||||
this.HeaderBlockFragment = BufferSegment.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2HeadersFrame Header: {0}, Flags: {1}, PadLength: {2}, IsExclusive: {3}, StreamDependency: {4}, Weight: {5}, HeaderBlockFragmentLength: {6}]",
|
||||
this.Header.ToString(),
|
||||
this.Flags,
|
||||
this.PadLength == null ? ":Empty" : this.PadLength.Value.ToString(),
|
||||
this.IsExclusive == null ? "Empty" : this.IsExclusive.Value.ToString(),
|
||||
this.StreamDependency == null ? "Empty" : this.StreamDependency.Value.ToString(),
|
||||
this.Weight == null ? "Empty" : this.Weight.Value.ToString(),
|
||||
this.HeaderBlockFragment.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2PriorityFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
|
||||
public byte IsExclusive;
|
||||
public UInt32 StreamDependency;
|
||||
public byte Weight;
|
||||
|
||||
public HTTP2PriorityFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.IsExclusive = 0;
|
||||
this.StreamDependency = 0;
|
||||
this.Weight = 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2PriorityFrame Header: {0}, IsExclusive: {1}, StreamDependency: {2}, Weight: {3}]",
|
||||
this.Header.ToString(), this.IsExclusive, this.StreamDependency, this.Weight);
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2RSTStreamFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
|
||||
public UInt32 ErrorCode;
|
||||
public HTTP2ErrorCodes Error { get { return (HTTP2ErrorCodes)this.ErrorCode; } }
|
||||
|
||||
public HTTP2RSTStreamFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.ErrorCode = 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2RST_StreamFrame Header: {0}, Error: {1}({2})]", this.Header.ToString(), this.Error, this.ErrorCode);
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2PushPromiseFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
public HTTP2PushPromiseFlags Flags { get { return (HTTP2PushPromiseFlags)this.Header.Flags; } }
|
||||
|
||||
public byte? PadLength;
|
||||
public byte ReservedBit;
|
||||
public UInt32 PromisedStreamId;
|
||||
|
||||
public BufferSegment HeaderBlockFragment;
|
||||
//public UInt32 HeaderBlockFragmentIdx;
|
||||
//public byte[] HeaderBlockFragment;
|
||||
//public UInt32 HeaderBlockFragmentLength;
|
||||
|
||||
public HTTP2PushPromiseFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.PadLength = null;
|
||||
this.ReservedBit = 0;
|
||||
this.PromisedStreamId = 0;
|
||||
//this.HeaderBlockFragmentIdx = 0;
|
||||
//this.HeaderBlockFragment = null;
|
||||
//this.HeaderBlockFragmentLength = 0;
|
||||
this.HeaderBlockFragment = BufferSegment.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2Push_PromiseFrame Header: {0}, Flags: {1}, PadLength: {2}, ReservedBit: {3}, PromisedStreamId: {4}, HeaderBlockFragmentLength: {5}]",
|
||||
this.Header.ToString(),
|
||||
this.Flags,
|
||||
this.PadLength == null ? "Empty" : this.PadLength.Value.ToString(),
|
||||
this.ReservedBit,
|
||||
this.PromisedStreamId,
|
||||
this.HeaderBlockFragment);
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2PingFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
public HTTP2PingFlags Flags { get { return (HTTP2PingFlags)this.Header.Flags; } }
|
||||
|
||||
//public readonly byte[] OpaqueData;
|
||||
//public readonly byte OpaqueDataLength;
|
||||
public readonly BufferSegment OpaqueData;
|
||||
|
||||
public HTTP2PingFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.OpaqueData = BufferPool.Get(8, true).AsBuffer(8);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2PingFrame Header: {0}, Flags: {1}, OpaqueData: {2}]",
|
||||
this.Header.ToString(),
|
||||
this.Flags,
|
||||
this.OpaqueData);
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2GoAwayFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
public HTTP2ErrorCodes Error { get { return (HTTP2ErrorCodes)this.ErrorCode; } }
|
||||
|
||||
public byte ReservedBit;
|
||||
public UInt32 LastStreamId;
|
||||
public UInt32 ErrorCode;
|
||||
|
||||
public BufferSegment AdditionalDebugData;
|
||||
//public byte[] AdditionalDebugData;
|
||||
//public UInt32 AdditionalDebugDataLength;
|
||||
|
||||
public HTTP2GoAwayFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.ReservedBit = 0;
|
||||
this.LastStreamId = 0;
|
||||
this.ErrorCode = 0;
|
||||
this.AdditionalDebugData = BufferSegment.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2GoAwayFrame Header: {0}, ReservedBit: {1}, LastStreamId: {2}, Error: {3}({4}), AdditionalDebugData: {5}]",
|
||||
this.Header.ToString(),
|
||||
this.ReservedBit,
|
||||
this.LastStreamId,
|
||||
this.Error,
|
||||
this.ErrorCode,
|
||||
this.AdditionalDebugData.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2WindowUpdateFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
|
||||
public byte ReservedBit;
|
||||
public UInt32 WindowSizeIncrement;
|
||||
|
||||
public HTTP2WindowUpdateFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.ReservedBit = 0;
|
||||
this.WindowSizeIncrement = 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2WindowUpdateFrame Header: {0}, ReservedBit: {1}, WindowSizeIncrement: {2}]",
|
||||
this.Header.ToString(), this.ReservedBit, this.WindowSizeIncrement);
|
||||
}
|
||||
}
|
||||
|
||||
public struct HTTP2ContinuationFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
|
||||
public HTTP2ContinuationFlags Flags { get { return (HTTP2ContinuationFlags)this.Header.Flags; } }
|
||||
|
||||
public BufferSegment HeaderBlockFragment;
|
||||
|
||||
public HTTP2ContinuationFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.HeaderBlockFragment = BufferSegment.Empty;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[HTTP2ContinuationFrame Header: {0}, Flags: {1}, HeaderBlockFragment: {2}]",
|
||||
this.Header.ToString(),
|
||||
this.Flags,
|
||||
this.HeaderBlockFragment);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/rfc7838#section-4
|
||||
/// </summary>
|
||||
public struct HTTP2AltSVCFrame
|
||||
{
|
||||
public readonly HTTP2FrameHeaderAndPayload Header;
|
||||
|
||||
public string Origin;
|
||||
public string AltSvcFieldValue;
|
||||
|
||||
public HTTP2AltSVCFrame(HTTP2FrameHeaderAndPayload header)
|
||||
{
|
||||
this.Header = header;
|
||||
this.Origin = null;
|
||||
this.AltSvcFieldValue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b1b3f5f21e2324b9a6513e8d458167
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,160 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Response.Decompression;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
public sealed class HTTP2Response : HTTPResponse
|
||||
{
|
||||
// For progress report
|
||||
public long ExpectedContentLength { get; private set; }
|
||||
|
||||
private string contentEncoding = null;
|
||||
|
||||
bool isPrepared;
|
||||
private IDecompressor _decompressor;
|
||||
|
||||
public HTTP2Response(HTTPRequest request, bool isFromCache)
|
||||
: base(request, isFromCache)
|
||||
{
|
||||
this.HTTPVersion = new Version(2, 0);
|
||||
}
|
||||
|
||||
internal void AddHeaders(List<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
this.ExpectedContentLength = -1;
|
||||
Dictionary<string, List<string>> newHeaders = this.Request.DownloadSettings.OnHeadersReceived != null ? new Dictionary<string, List<string>>() : null;
|
||||
|
||||
for (int i = 0; i < headers.Count; ++i)
|
||||
{
|
||||
KeyValuePair<string, string> header = headers[i];
|
||||
|
||||
if (header.Key.Equals(":status", StringComparison.Ordinal))
|
||||
{
|
||||
base.StatusCode = int.Parse(header.Value);
|
||||
base.Message = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.contentEncoding) && header.Key.Equals("content-encoding", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
this.contentEncoding = header.Value;
|
||||
}
|
||||
else if (base.Request.DownloadSettings.OnDownloadProgress != null && header.Key.Equals("content-length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
long contentLength;
|
||||
if (long.TryParse(header.Value, out contentLength))
|
||||
this.ExpectedContentLength = contentLength;
|
||||
else
|
||||
HTTPManager.Logger.Information("HTTP2Response", string.Format("AddHeaders - Can't parse Content-Length as an int: '{0}'", header.Value), this.Context);
|
||||
}
|
||||
|
||||
base.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (newHeaders != null)
|
||||
{
|
||||
List<string> values;
|
||||
if (!newHeaders.TryGetValue(header.Key, out values))
|
||||
newHeaders.Add(header.Key, values = new List<string>(1));
|
||||
|
||||
values.Add(header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ExpectedContentLength == -1 && base.Request.DownloadSettings.OnDownloadProgress != null)
|
||||
HTTPManager.Logger.Information("HTTP2Response", "AddHeaders - No Content-Length header found!", this.Context);
|
||||
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, newHeaders));
|
||||
}
|
||||
|
||||
internal void Prepare(IDownloadContentBufferAvailable bufferAvailable)
|
||||
{
|
||||
if (!this.isPrepared)
|
||||
{
|
||||
this.isPrepared = true;
|
||||
|
||||
CreateDownloadStream(bufferAvailable);
|
||||
|
||||
base.BeginReceiveContent();
|
||||
}
|
||||
}
|
||||
|
||||
internal void ProcessData(BufferSegment payload)
|
||||
{
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/183
|
||||
// If _decompressor is still null, remove the content encoding value and serve the content as-is.
|
||||
if (!string.IsNullOrEmpty(this.contentEncoding) && this._decompressor == null)
|
||||
{
|
||||
if ((this._decompressor = DecompressorFactory.GetDecompressor(this.contentEncoding, this.Context)) == null)
|
||||
this.contentEncoding = null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(this.contentEncoding))
|
||||
{
|
||||
BufferSegment result = BufferSegment.Empty;
|
||||
bool release;
|
||||
try
|
||||
{
|
||||
(result, release) = this._decompressor.Decompress(payload, false, true, this.Context);
|
||||
}
|
||||
catch
|
||||
{
|
||||
BufferPool.Release(payload);
|
||||
throw;
|
||||
}
|
||||
if (release)
|
||||
BufferPool.Release(payload);
|
||||
|
||||
base.FeedDownloadedContentChunk(result);
|
||||
}
|
||||
else
|
||||
base.FeedDownloadedContentChunk(payload);
|
||||
}
|
||||
|
||||
internal void FinishProcessData()
|
||||
{
|
||||
if (this._decompressor != null)
|
||||
{
|
||||
var (decompressed, _) = this._decompressor.Decompress(BufferSegment.Empty, true, true, this.Context);
|
||||
if (decompressed != BufferSegment.Empty)
|
||||
base.FeedDownloadedContentChunk(decompressed);
|
||||
|
||||
this._decompressor.Dispose();
|
||||
this._decompressor = null;
|
||||
}
|
||||
|
||||
base.FinishedContentReceiving();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (this._decompressor != null)
|
||||
{
|
||||
// In some cases, the request is aborted and the decompressor left in an incomplete state.
|
||||
// Closing it might cause an exception that we don't care about.
|
||||
try
|
||||
{
|
||||
this._decompressor.Dispose();
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
|
||||
this._decompressor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe46645d97549c146a8c38f1297a75ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,331 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
/// <summary>
|
||||
/// <see href="https://httpwg.org/specs/rfc7540.html#iana-settings">Settings Registry</see>
|
||||
/// </summary>
|
||||
public enum HTTP2Settings : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows the sender to inform the remote endpoint of the maximum size of the
|
||||
/// header compression table used to decode header blocks, in octets.
|
||||
/// The encoder can select any size equal to or less than this value
|
||||
/// by using signaling specific to the header compression format inside a header block (see [COMPRESSION]).
|
||||
/// The initial value is 4,096 octets.
|
||||
/// </summary>
|
||||
HEADER_TABLE_SIZE = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// This setting can be used to disable server push (Section 8.2).
|
||||
/// An endpoint MUST NOT send a PUSH_PROMISE frame if it receives this parameter set to a value of 0.
|
||||
/// An endpoint that has both set this parameter to 0 and had it acknowledged MUST treat the receipt of a
|
||||
/// PUSH_PROMISE frame as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
|
||||
///
|
||||
/// The initial value is 1, which indicates that server push is permitted.
|
||||
/// Any value other than 0 or 1 MUST be treated as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
|
||||
/// </summary>
|
||||
ENABLE_PUSH = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the maximum number of concurrent streams that the sender will allow. This limit is directional:
|
||||
/// it applies to the number of streams that the sender permits the receiver to create.
|
||||
/// Initially, there is no limit to this value. It is recommended that this value be no smaller than 100,
|
||||
/// so as to not unnecessarily limit parallelism.
|
||||
///
|
||||
/// A value of 0 for SETTINGS_MAX_CONCURRENT_STREAMS SHOULD NOT be treated as special by endpoints.
|
||||
/// A zero value does prevent the creation of new streams;
|
||||
/// however, this can also happen for any limit that is exhausted with active streams.
|
||||
/// Servers SHOULD only set a zero value for short durations; if a server does not wish to accept requests,
|
||||
/// closing the connection is more appropriate.
|
||||
/// </summary>
|
||||
MAX_CONCURRENT_STREAMS = 0x03,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the sender's initial window size (in octets) for stream-level flow control.
|
||||
/// The initial value is 2^16-1 (65,535) octets.
|
||||
///
|
||||
/// This setting affects the window size of all streams (see Section 6.9.2).
|
||||
///
|
||||
/// Values above the maximum flow-control window size of 2^31-1 MUST be treated as a connection error
|
||||
/// (Section 5.4.1) of type FLOW_CONTROL_ERROR.
|
||||
/// </summary>
|
||||
INITIAL_WINDOW_SIZE = 0x04,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the size of the largest frame payload that the sender is willing to receive, in octets.
|
||||
///
|
||||
/// The initial value is 2^14 (16,384) octets.
|
||||
/// The value advertised by an endpoint MUST be between this initial value and the maximum allowed frame size
|
||||
/// (2^24-1 or 16,777,215 octets), inclusive.
|
||||
/// Values outside this range MUST be treated as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
|
||||
/// </summary>
|
||||
MAX_FRAME_SIZE = 0x05,
|
||||
|
||||
/// <summary>
|
||||
/// This advisory setting informs a peer of the maximum size of header list that the sender is prepared to accept, in octets.
|
||||
/// The value is based on the uncompressed size of header fields,
|
||||
/// including the length of the name and value in octets plus an overhead of 32 octets for each header field.
|
||||
///
|
||||
/// For any given request, a lower limit than what is advertised MAY be enforced. The initial value of this setting is unlimited.
|
||||
/// </summary>
|
||||
MAX_HEADER_LIST_SIZE = 0x06,
|
||||
|
||||
RESERVED = 0x07,
|
||||
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/rfc8441
|
||||
/// Upon receipt of SETTINGS_ENABLE_CONNECT_PROTOCOL with a value of 1, a client MAY use the Extended CONNECT as defined in this document when creating new streams.
|
||||
/// Receipt of this parameter by a server does not have any impact.
|
||||
///
|
||||
/// A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter with the value of 0 after previously sending a value of 1.
|
||||
/// </summary>
|
||||
ENABLE_CONNECT_PROTOCOL = 0x08,
|
||||
|
||||
/// <summary>
|
||||
/// Allow endpoints to omit or ignore HTTP/2 priority signals.
|
||||
/// <see href="https://www.rfc-editor.org/rfc/rfc9218.html">Extensible Prioritization Scheme for HTTP</see>
|
||||
/// </summary>
|
||||
NO_RFC7540_PRIORITIES = 0x09,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a registry for HTTP/2 settings.
|
||||
/// </summary>
|
||||
public sealed class HTTP2SettingsRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the registry is read-only.
|
||||
/// </summary>
|
||||
public bool IsReadOnly { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a setting changes.
|
||||
/// </summary>
|
||||
public Action<HTTP2SettingsRegistry, HTTP2Settings, UInt32, UInt32> OnSettingChangedEvent;
|
||||
|
||||
private UInt32[] values;
|
||||
private bool[] changeFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Indexer to get or set values based on an <see cref="HTTP2Settings"/> key.
|
||||
/// </summary>
|
||||
/// <param name="setting">The setting key.</param>
|
||||
/// <returns>The value associated with the given setting key.</returns>
|
||||
public UInt32 this[HTTP2Settings setting]
|
||||
{
|
||||
get { return this.values[(ushort)setting]; }
|
||||
|
||||
set
|
||||
{
|
||||
if (this.IsReadOnly)
|
||||
throw new NotSupportedException("It's a read-only one!");
|
||||
|
||||
ushort idx = (ushort)setting;
|
||||
|
||||
// https://httpwg.org/specs/rfc7540.html#SettingValues
|
||||
// An endpoint that receives a SETTINGS frame with any unknown or unsupported identifier MUST ignore that setting.
|
||||
if (idx == 0 || idx >= this.values.Length)
|
||||
return;
|
||||
|
||||
UInt32 oldValue = this.values[idx];
|
||||
if (oldValue != value)
|
||||
{
|
||||
this.values[idx] = value;
|
||||
this.changeFlags[idx] = true;
|
||||
IsChanged = true;
|
||||
|
||||
if (this.OnSettingChangedEvent != null)
|
||||
this.OnSettingChangedEvent(this, setting, oldValue, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any setting has changed.
|
||||
/// </summary>
|
||||
public bool IsChanged { get; private set; }
|
||||
|
||||
private HTTP2SettingsManager _parent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the HTTP2SettingsRegistry class.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent <see cref="HTTP2SettingsManager"/>.</param>
|
||||
/// <param name="readOnly">Whether this registry is read-only.</param>
|
||||
/// <param name="treatItAsAlreadyChanged">Whether to treat the registry as if a setting has already changed.</param>
|
||||
public HTTP2SettingsRegistry(HTTP2SettingsManager parent, bool readOnly, bool treatItAsAlreadyChanged)
|
||||
{
|
||||
this._parent = parent;
|
||||
|
||||
this.values = new UInt32[HTTP2SettingsManager.KnownSettingsCount];
|
||||
|
||||
this.IsReadOnly = readOnly;
|
||||
if (!this.IsReadOnly)
|
||||
this.changeFlags = new bool[HTTP2SettingsManager.KnownSettingsCount];
|
||||
|
||||
// Set default values (https://httpwg.org/specs/rfc7540.html#iana-settings)
|
||||
this.values[(UInt16)HTTP2Settings.HEADER_TABLE_SIZE] = 4096;
|
||||
this.values[(UInt16)HTTP2Settings.ENABLE_PUSH] = 1;
|
||||
this.values[(UInt16)HTTP2Settings.MAX_CONCURRENT_STREAMS] = 128;
|
||||
this.values[(UInt16)HTTP2Settings.INITIAL_WINDOW_SIZE] = 65535;
|
||||
this.values[(UInt16)HTTP2Settings.MAX_FRAME_SIZE] = 16384;
|
||||
this.values[(UInt16)HTTP2Settings.MAX_HEADER_LIST_SIZE] = UInt32.MaxValue; // infinite
|
||||
|
||||
if (this.IsChanged = treatItAsAlreadyChanged)
|
||||
{
|
||||
this.changeFlags[(UInt16)HTTP2Settings.MAX_CONCURRENT_STREAMS] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the specified settings into the current registry.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to merge.</param>
|
||||
public void Merge(List<KeyValuePair<HTTP2Settings, UInt32>> settings)
|
||||
{
|
||||
if (settings == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < settings.Count; ++i)
|
||||
{
|
||||
HTTP2Settings setting = settings[i].Key;
|
||||
UInt16 key = (UInt16)setting;
|
||||
UInt32 value = settings[i].Value;
|
||||
|
||||
if (key >= this.values.Length)
|
||||
Array.Resize<uint>(ref this.values, (int)key + 1);
|
||||
|
||||
UInt32 oldValue = this.values[key];
|
||||
this.values[key] = value;
|
||||
|
||||
if (oldValue != value && this.OnSettingChangedEvent != null)
|
||||
this.OnSettingChangedEvent(this, setting, oldValue, value);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("HTTP2SettingsRegistry", string.Format("Merge {0}({1}) = {2}", setting, key, value), this._parent.Context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges settings from another HTTP2SettingsRegistry into the current registry.
|
||||
/// </summary>
|
||||
/// <param name="from">The registry to merge settings from.</param>
|
||||
public void Merge(HTTP2SettingsRegistry from)
|
||||
{
|
||||
if (this.values != null)
|
||||
this.values = new uint[from.values.Length];
|
||||
|
||||
for (int i = 0; i < this.values.Length; ++i)
|
||||
this.values[i] = from.values[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new HTTP/2 frame based on the current registry settings.
|
||||
/// </summary>
|
||||
/// <param name="context">The logging context.</param>
|
||||
/// <returns>A new HTTP/2 frame.</returns>
|
||||
internal HTTP2FrameHeaderAndPayload CreateFrame(LoggingContext context)
|
||||
{
|
||||
List<KeyValuePair<HTTP2Settings, UInt32>> keyValuePairs = new List<KeyValuePair<HTTP2Settings, uint>>(HTTP2SettingsManager.KnownSettingsCount);
|
||||
|
||||
for (int i = 1; i < HTTP2SettingsManager.KnownSettingsCount; ++i)
|
||||
if (this.changeFlags[i])
|
||||
{
|
||||
keyValuePairs.Add(new KeyValuePair<HTTP2Settings, uint>((HTTP2Settings)i, this[(HTTP2Settings)i]));
|
||||
this.changeFlags[i] = false;
|
||||
}
|
||||
|
||||
this.IsChanged = false;
|
||||
|
||||
return HTTP2FrameHelper.CreateSettingsFrame(keyValuePairs, context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class to manager local and remote HTTP/2 settings.
|
||||
/// </summary>
|
||||
public sealed class HTTP2SettingsManager
|
||||
{
|
||||
public static readonly int KnownSettingsCount = Enum.GetNames(typeof(HTTP2Settings)).Length + 1;
|
||||
|
||||
/// <summary>
|
||||
/// This is the ACKd or default settings that we sent to the server.
|
||||
/// </summary>
|
||||
public HTTP2SettingsRegistry MySettings { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is the setting that can be changed. It will be sent to the server ASAP, and when ACKd, it will be copied
|
||||
/// to MySettings.
|
||||
/// </summary>
|
||||
public HTTP2SettingsRegistry InitiatedMySettings { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Settings of the remote peer
|
||||
/// </summary>
|
||||
public HTTP2SettingsRegistry RemoteSettings { get; private set; }
|
||||
|
||||
public DateTime SettingsChangesSentAt { get; private set; }
|
||||
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
private HTTP2ConnectionSettings _connectionSettings;
|
||||
|
||||
public HTTP2SettingsManager(LoggingContext context, HTTP2ConnectionSettings connectionSettings)
|
||||
{
|
||||
this.Context = context;
|
||||
this._connectionSettings = connectionSettings;
|
||||
|
||||
this.MySettings = new HTTP2SettingsRegistry(this, readOnly: true, treatItAsAlreadyChanged: false);
|
||||
this.InitiatedMySettings = new HTTP2SettingsRegistry(this, readOnly: false, treatItAsAlreadyChanged: true);
|
||||
this.RemoteSettings = new HTTP2SettingsRegistry(this, readOnly: true, treatItAsAlreadyChanged: false);
|
||||
this.SettingsChangesSentAt = DateTime.MinValue;
|
||||
}
|
||||
|
||||
internal void Process(HTTP2FrameHeaderAndPayload frame, List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
if (frame.Type != HTTP2FrameTypes.SETTINGS)
|
||||
return;
|
||||
|
||||
HTTP2SettingsFrame settingsFrame = HTTP2FrameHelper.ReadSettings(frame);
|
||||
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HTTP2SettingsManager", "Processing Settings frame: " + settingsFrame.ToString(), this.Context);
|
||||
|
||||
if ((settingsFrame.Flags & HTTP2SettingsFlags.ACK) == HTTP2SettingsFlags.ACK)
|
||||
{
|
||||
this.MySettings.Merge(this.InitiatedMySettings);
|
||||
this.SettingsChangesSentAt = DateTime.MinValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RemoteSettings.Merge(settingsFrame.Settings);
|
||||
outgoingFrames.Add(HTTP2FrameHelper.CreateACKSettingsFrame());
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendChanges(List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
if (this.SettingsChangesSentAt != DateTime.MinValue && DateTime.UtcNow - this.SettingsChangesSentAt >= this._connectionSettings.Timeout)
|
||||
{
|
||||
HTTPManager.Logger.Error("HTTP2SettingsManager", "No ACK received for settings frame!", this.Context);
|
||||
this.SettingsChangesSentAt = DateTime.MinValue;
|
||||
}
|
||||
|
||||
// Upon receiving a SETTINGS frame with the ACK flag set, the sender of the altered parameters can rely on the setting having been applied.
|
||||
if (!this.InitiatedMySettings.IsChanged)
|
||||
return;
|
||||
|
||||
outgoingFrames.Add(this.InitiatedMySettings.CreateFrame(this.Context));
|
||||
this.SettingsChangesSentAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9723be24bbd74214dbff4fc66a5b24bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,663 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
#define ENABLE_LOGGING
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Request.Upload;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
// https://httpwg.org/specs/rfc7540.html#StreamStates
|
||||
//
|
||||
// Idle
|
||||
// |
|
||||
// V
|
||||
// Open
|
||||
// Receive END_STREAM / | \ Send END_STREAM
|
||||
// v |R V
|
||||
// Half Closed Remote |S Half Closed Locale
|
||||
// \ |T /
|
||||
// Send END_STREAM | RST_STREAM \ | / Receive END_STREAM | RST_STREAM
|
||||
// Receive RST_STREAM \ | / Send RST_STREAM
|
||||
// V
|
||||
// Closed
|
||||
//
|
||||
// IDLE -> send headers -> OPEN -> send data -> HALF CLOSED - LOCAL -> receive headers -> receive Data -> CLOSED
|
||||
// | ^ | ^
|
||||
// +-------------------------------------+ +-----------------------------+
|
||||
// END_STREAM flag present? END_STREAM flag present?
|
||||
//
|
||||
|
||||
public enum HTTP2StreamStates
|
||||
{
|
||||
Idle,
|
||||
//ReservedLocale,
|
||||
//ReservedRemote,
|
||||
Open,
|
||||
HalfClosedLocal,
|
||||
HalfClosedRemote,
|
||||
Closed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements an HTTP/2 logical stream.
|
||||
/// </summary>
|
||||
public class HTTP2Stream : IDownloadContentBufferAvailable
|
||||
{
|
||||
public UInt32 Id { get; private set; }
|
||||
|
||||
public HTTP2StreamStates State {
|
||||
get { return this._state; }
|
||||
|
||||
protected set {
|
||||
var oldState = this._state;
|
||||
|
||||
this._state = value;
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (oldState != this._state && HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] State changed from {1} to {2}", this.Id, oldState, this._state), this.Context);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
private HTTP2StreamStates _state;
|
||||
|
||||
/// <summary>
|
||||
/// This flag is checked by the connection to decide whether to do a new processing-frame sending round before sleeping until new data arrives
|
||||
/// </summary>
|
||||
public virtual bool HasFrameToSend
|
||||
{
|
||||
get
|
||||
{
|
||||
// Don't let the connection sleep until
|
||||
return this.outgoing.Count > 0 || // we already booked at least one frame in advance
|
||||
(this.State == HTTP2StreamStates.Open && this.remoteWindow > 0 && this.lastReadCount > 0); // we are in the middle of sending request data
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Next interaction scheduled by the stream relative to *now*. Its default is TimeSpan.MaxValue == no interaction.
|
||||
/// </summary>
|
||||
public virtual TimeSpan NextInteraction { get; } = TimeSpan.MaxValue;
|
||||
|
||||
public HTTPRequest AssignedRequest { get; protected set; }
|
||||
|
||||
public LoggingContext Context { get; protected set; }
|
||||
|
||||
public HTTP2ContentConsumer ParentHandler { get; private set; }
|
||||
|
||||
protected uint downloaded;
|
||||
|
||||
protected HTTP2SettingsManager settings;
|
||||
protected HPACKEncoder encoder;
|
||||
|
||||
// Outgoing frames. The stream will send one frame per Process call, but because one step might be able to
|
||||
// generate more than one frames, we use a list.
|
||||
protected Queue<HTTP2FrameHeaderAndPayload> outgoing = new Queue<HTTP2FrameHeaderAndPayload>();
|
||||
|
||||
protected Queue<HTTP2FrameHeaderAndPayload> incomingFrames = new Queue<HTTP2FrameHeaderAndPayload>();
|
||||
|
||||
protected FramesAsStreamView headerView;
|
||||
|
||||
protected Int64 localWindow;
|
||||
protected Int64 remoteWindow;
|
||||
|
||||
protected uint windowUpdateThreshold;
|
||||
|
||||
protected UInt32 assignDataLength;
|
||||
|
||||
protected long sentData;
|
||||
protected long uploadLength;
|
||||
|
||||
protected bool isRSTFrameSent;
|
||||
protected bool isEndSTRReceived;
|
||||
|
||||
protected HTTP2Response response;
|
||||
|
||||
protected int lastReadCount;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor to create a client stream.
|
||||
/// </summary>
|
||||
public HTTP2Stream(UInt32 id, HTTP2ContentConsumer parentHandler, HTTP2SettingsManager registry, HPACKEncoder hpackEncoder)
|
||||
{
|
||||
this.Id = id;
|
||||
this.ParentHandler = parentHandler;
|
||||
this.settings = registry;
|
||||
this.encoder = hpackEncoder;
|
||||
|
||||
this.Context = new LoggingContext(this);
|
||||
this.Context.Add("id", id);
|
||||
this.Context.Add("Parent", parentHandler.Context);
|
||||
|
||||
this.remoteWindow = this.settings.RemoteSettings[HTTP2Settings.INITIAL_WINDOW_SIZE];
|
||||
this.settings.RemoteSettings.OnSettingChangedEvent += OnRemoteSettingChanged;
|
||||
|
||||
// Room for improvement: If INITIAL_WINDOW_SIZE is small (what we can consider a 'small' value?), threshold must be higher
|
||||
this.windowUpdateThreshold = (uint)(this.remoteWindow / 2);
|
||||
}
|
||||
|
||||
public virtual void Assign(HTTPRequest request)
|
||||
{
|
||||
this.Context.Add("Request", request.Context);
|
||||
|
||||
request.Timing.StartNext(TimingEventNames.Request_Sent);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] Request assigned to stream. Remote Window: {1:N0}. Uri: {2}", this.Id, this.remoteWindow, request.CurrentUri.ToString()), this.Context);
|
||||
#endif
|
||||
this.AssignedRequest = request;
|
||||
|
||||
this.downloaded = 0;
|
||||
}
|
||||
|
||||
public void Process(List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
if (this.AssignedRequest.IsCancellationRequested && !this.isRSTFrameSent)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", $"[{this.Id}] Process({this.State}) - IsCancellationRequested", this.Context);
|
||||
#endif
|
||||
|
||||
// These two are already set in HTTPRequest's Abort().
|
||||
//this.AssignedRequest.Response = null;
|
||||
//this.AssignedRequest.State = this.AssignedRequest.IsTimedOut ? HTTPRequestStates.TimedOut : HTTPRequestStates.Aborted;
|
||||
|
||||
this.outgoing.Clear();
|
||||
if (this.State != HTTP2StreamStates.Idle)
|
||||
this.outgoing.Enqueue(HTTP2FrameHelper.CreateRSTFrame(this.Id, HTTP2ErrorCodes.CANCEL, this.Context));
|
||||
|
||||
// We can close the stream if already received headers, or not even sent one
|
||||
if (this.State == HTTP2StreamStates.HalfClosedRemote || this.State == HTTP2StreamStates.HalfClosedLocal || this.State == HTTP2StreamStates.Idle)
|
||||
this.State = HTTP2StreamStates.Closed;
|
||||
|
||||
this.isRSTFrameSent = true;
|
||||
}
|
||||
|
||||
// 1.) Go through incoming frames
|
||||
ProcessIncomingFrames(outgoingFrames);
|
||||
|
||||
// 2.) Create outgoing frames based on the stream's state and the request processing state.
|
||||
ProcessState(outgoingFrames);
|
||||
|
||||
// 3.) Send one frame per Process call
|
||||
if (this.outgoing.Count > 0)
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload frame = this.outgoing.Dequeue();
|
||||
|
||||
outgoingFrames.Add(frame);
|
||||
|
||||
// If END_Stream in header or data frame is present => half closed local
|
||||
if ((frame.Type == HTTP2FrameTypes.HEADERS && (frame.Flags & (byte)HTTP2HeadersFlags.END_STREAM) != 0) ||
|
||||
(frame.Type == HTTP2FrameTypes.DATA && (frame.Flags & (byte)HTTP2DataFlags.END_STREAM) != 0))
|
||||
{
|
||||
this.State = HTTP2StreamStates.HalfClosedLocal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFrame(HTTP2FrameHeaderAndPayload frame, List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
// Room for improvement: error check for forbidden frames (like settings) and stream state
|
||||
|
||||
this.incomingFrames.Enqueue(frame);
|
||||
|
||||
ProcessIncomingFrames(outgoingFrames);
|
||||
}
|
||||
|
||||
public void Abort(string msg)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", $"[{this.Id}] Abort(\"{msg}\", {this.State}, {this.AssignedRequest.State})", this.Context);
|
||||
#endif
|
||||
|
||||
if (this.State != HTTP2StreamStates.Closed)
|
||||
{
|
||||
// TODO: Remove AssignedRequest.State checks. If the main thread has delays processing queued up state change requests,
|
||||
// the request's State can contain old information!
|
||||
|
||||
if (this.AssignedRequest.State != HTTPRequestStates.Processing)
|
||||
{
|
||||
// do nothing, its state is already set.
|
||||
}
|
||||
else if (this.AssignedRequest.IsCancellationRequested)
|
||||
{
|
||||
// These two are already set in HTTPRequest's Abort().
|
||||
//this.AssignedRequest.Response = null;
|
||||
//this.AssignedRequest.State = this.AssignedRequest.IsTimedOut ? HTTPRequestStates.TimedOut : HTTPRequestStates.Aborted;
|
||||
|
||||
this.State = HTTP2StreamStates.Closed;
|
||||
}
|
||||
else if (this.AssignedRequest.RetrySettings.Retries >= this.AssignedRequest.RetrySettings.MaxRetries || HTTPManager.IsQuitting)
|
||||
{
|
||||
this.AssignedRequest.Timing.StartNext(TimingEventNames.Queued);
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.AssignedRequest, HTTPRequestStates.Error, new Exception(msg)));
|
||||
|
||||
this.State = HTTP2StreamStates.Closed;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AssignedRequest.RetrySettings.Retries++;
|
||||
|
||||
this.AssignedRequest.Response?.Dispose();
|
||||
this.AssignedRequest.Response = null;
|
||||
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.AssignedRequest, RequestEvents.Resend));
|
||||
}
|
||||
}
|
||||
|
||||
this.Removed();
|
||||
}
|
||||
|
||||
protected void ProcessIncomingFrames(List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
while (this.incomingFrames.Count > 0)
|
||||
{
|
||||
HTTP2FrameHeaderAndPayload frame = this.incomingFrames.Dequeue();
|
||||
|
||||
if ((this.isRSTFrameSent || this.AssignedRequest.IsCancellationRequested) && frame.Type != HTTP2FrameTypes.HEADERS && frame.Type != HTTP2FrameTypes.CONTINUATION)
|
||||
{
|
||||
BufferPool.Release(frame.Payload);
|
||||
continue;
|
||||
}
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (/*HTTPManager.Logger.Level == Logger.Loglevels.All && */frame.Type != HTTP2FrameTypes.DATA && frame.Type != HTTP2FrameTypes.WINDOW_UPDATE)
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] Process - processing frame: {1}", this.Id, frame.ToString()), this.Context);
|
||||
#endif
|
||||
|
||||
switch (frame.Type)
|
||||
{
|
||||
case HTTP2FrameTypes.HEADERS:
|
||||
case HTTP2FrameTypes.CONTINUATION:
|
||||
if (this.State != HTTP2StreamStates.HalfClosedLocal && this.State != HTTP2StreamStates.Open && this.State != HTTP2StreamStates.Idle)
|
||||
{
|
||||
// ERROR!
|
||||
continue;
|
||||
}
|
||||
|
||||
// payload will be released by the view
|
||||
frame.DontUseMemPool = true;
|
||||
|
||||
if (this.headerView == null)
|
||||
{
|
||||
this.AssignedRequest.Timing.StartNext(TimingEventNames.Headers);
|
||||
|
||||
this.headerView = new FramesAsStreamView(new HeaderFrameView());
|
||||
}
|
||||
|
||||
this.headerView.AddFrame(frame);
|
||||
|
||||
// END_STREAM may arrive sooner than an END_HEADERS, so we have to store that we already received it
|
||||
if ((frame.Flags & (byte)HTTP2HeadersFlags.END_STREAM) != 0)
|
||||
this.isEndSTRReceived = true;
|
||||
|
||||
if ((frame.Flags & (byte)HTTP2HeadersFlags.END_HEADERS) != 0)
|
||||
{
|
||||
List<KeyValuePair<string, string>> headers = new List<KeyValuePair<string, string>>();
|
||||
|
||||
try
|
||||
{
|
||||
this.encoder.Decode(this, this.headerView, headers);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("HTTP2Stream", string.Format("[{0}] ProcessIncomingFrames - Header Frames: {1}, Encoder: {2}", this.Id, this.headerView.ToString(), this.encoder.ToString()), ex, this.Context);
|
||||
}
|
||||
|
||||
this.headerView.Close();
|
||||
this.headerView = null;
|
||||
|
||||
this.AssignedRequest.Timing.StartNext(TimingEventNames.Response_Received);
|
||||
|
||||
if (this.isRSTFrameSent)
|
||||
{
|
||||
this.State = HTTP2StreamStates.Closed;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.response == null)
|
||||
this.AssignedRequest.Response = this.response = new HTTP2Response(this.AssignedRequest, false);
|
||||
|
||||
this.response.AddHeaders(headers);
|
||||
|
||||
if (this.isEndSTRReceived)
|
||||
{
|
||||
// If there's any trailing header, no data frame has an END_STREAM flag
|
||||
this.response.FinishProcessData();
|
||||
|
||||
FinishRequest();
|
||||
|
||||
if (this.State == HTTP2StreamStates.HalfClosedLocal)
|
||||
this.State = HTTP2StreamStates.Closed;
|
||||
else
|
||||
this.State = HTTP2StreamStates.HalfClosedRemote;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.DATA:
|
||||
ProcessIncomingDATAFrame(ref frame);
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.WINDOW_UPDATE:
|
||||
HTTP2WindowUpdateFrame windowUpdateFrame = HTTP2FrameHelper.ReadWindowUpdateFrame(frame);
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] Received Window Update: {1:N0}, new remoteWindow: {2:N0}, initial remote window: {3:N0}, total data sent: {4:N0}", this.Id, windowUpdateFrame.WindowSizeIncrement, this.remoteWindow + windowUpdateFrame.WindowSizeIncrement, this.settings.RemoteSettings[HTTP2Settings.INITIAL_WINDOW_SIZE], this.sentData), this.Context);
|
||||
#endif
|
||||
|
||||
this.remoteWindow += windowUpdateFrame.WindowSizeIncrement;
|
||||
break;
|
||||
|
||||
case HTTP2FrameTypes.RST_STREAM:
|
||||
// https://httpwg.org/specs/rfc7540.html#RST_STREAM
|
||||
|
||||
// It's possible to receive an RST_STREAM on a closed stream. In this case, we have to ignore it.
|
||||
if (this.State == HTTP2StreamStates.Closed)
|
||||
break;
|
||||
|
||||
var rstStreamFrame = HTTP2FrameHelper.ReadRST_StreamFrame(frame);
|
||||
|
||||
//HTTPManager.Logger.Error("HTTP2Stream", string.Format("[{0}] RST Stream frame ({1}) received in state {2}!", this.Id, rstStreamFrame, this.State), this.Context);
|
||||
|
||||
Abort(string.Format("RST_STREAM frame received! Error code: {0}({1})", rstStreamFrame.Error.ToString(), rstStreamFrame.ErrorCode));
|
||||
break;
|
||||
|
||||
default:
|
||||
HTTPManager.Logger.Warning("HTTP2Stream", string.Format("[{0}] Unexpected frame ({1}, Payload: {2}) in state {3}!", this.Id, frame, frame.PayloadAsHex(), this.State), this.Context);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!frame.DontUseMemPool)
|
||||
BufferPool.Release(frame.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
|
||||
{
|
||||
// Signal the http2 thread, window update will be sent out in ProcessOpenState.
|
||||
this.ParentHandler.SignalThread(SignalHandlerTypes.Signal);
|
||||
}
|
||||
|
||||
protected virtual void ProcessIncomingDATAFrame(ref HTTP2FrameHeaderAndPayload frame)
|
||||
{
|
||||
if (this.State != HTTP2StreamStates.HalfClosedLocal && this.State != HTTP2StreamStates.Open)
|
||||
{
|
||||
// ERROR!
|
||||
return;
|
||||
}
|
||||
|
||||
HTTP2DataFrame dataFrame = HTTP2FrameHelper.ReadDataFrame(frame);
|
||||
|
||||
this.downloaded += (uint)dataFrame.Data.Count;
|
||||
|
||||
this.response.Prepare(this);
|
||||
|
||||
this.response.ProcessData(dataFrame.Data);
|
||||
frame.DontUseMemPool = true;
|
||||
|
||||
// Because of padding, frame.Payload.Count can be larger than dataFrame.Data.Count!
|
||||
// "The entire DATA frame payload is included in flow control, including the Pad Length and Padding fields if present."
|
||||
this.localWindow -= frame.Payload.Count;
|
||||
|
||||
this.isEndSTRReceived = (frame.Flags & (byte)HTTP2DataFlags.END_STREAM) != 0;
|
||||
|
||||
if (this.isEndSTRReceived)
|
||||
{
|
||||
this.response.FinishProcessData();
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] All data arrived, data length: {1:N0}", this.Id, this.downloaded), this.Context);
|
||||
#endif
|
||||
|
||||
FinishRequest();
|
||||
|
||||
if (this.State == HTTP2StreamStates.HalfClosedLocal)
|
||||
this.State = HTTP2StreamStates.Closed;
|
||||
else
|
||||
this.State = HTTP2StreamStates.HalfClosedRemote;
|
||||
}
|
||||
else if (this.AssignedRequest.DownloadSettings.OnDownloadProgress != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.AssignedRequest,
|
||||
RequestEvents.DownloadProgress,
|
||||
downloaded,
|
||||
this.response.ExpectedContentLength));
|
||||
}
|
||||
|
||||
protected void ProcessState(List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
switch (this.State)
|
||||
{
|
||||
case HTTP2StreamStates.Idle:
|
||||
// hpack encode the request's headers
|
||||
this.encoder.Encode(this, this.AssignedRequest, this.outgoing, this.Id);
|
||||
|
||||
// HTTP/2 uses DATA frames to carry message payloads.
|
||||
// The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
|
||||
|
||||
if (this.AssignedRequest.UploadSettings.UploadStream == null)
|
||||
{
|
||||
this.State = HTTP2StreamStates.HalfClosedLocal;
|
||||
//this.AssignedRequest.Timing.Finish(TimingEventNames.Request_Sent);
|
||||
this.AssignedRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.State = HTTP2StreamStates.Open;
|
||||
this.lastReadCount = 1;
|
||||
|
||||
if (this.AssignedRequest.UploadSettings.UploadStream is UploadStreamBase upStream)
|
||||
upStream.BeforeSendBody(this.AssignedRequest, this.ParentHandler);
|
||||
|
||||
if (this.AssignedRequest.UploadSettings != null && this.AssignedRequest.UploadSettings.UploadStream != null)
|
||||
this.uploadLength = this.AssignedRequest.UploadSettings.UploadStream.Length;
|
||||
}
|
||||
|
||||
// Change the initial window size to the request's DownloadSettings.ContentStreamMaxBuffered and send it to the server.
|
||||
// After this initial setup the sending out window_update frames faces two problems:
|
||||
// 1.) The local window should be bound to the Down-stream's MaxBuffered (ContentStreamMaxBuffered) and how its current length.
|
||||
// 2.) Even while the its bound to the stream's current values, when the download finishes, we still have to update the global window.
|
||||
// So, there's two options to follow:
|
||||
// 1.) Update the local window based on the stream's usage
|
||||
// 2.a) Send global window_update for every DATA frame processed/received
|
||||
|
||||
UInt32 initiatedInitialWindowSize = this.settings.InitiatedMySettings[HTTP2Settings.INITIAL_WINDOW_SIZE];
|
||||
this.localWindow = initiatedInitialWindowSize;
|
||||
|
||||
// Maximize max buffered to HTTP/2's limit
|
||||
if (this.AssignedRequest.DownloadSettings.ContentStreamMaxBuffered > HTTP2ContentConsumer.MaxValueFor31Bits)
|
||||
this.AssignedRequest.DownloadSettings.ContentStreamMaxBuffered = HTTP2ContentConsumer.MaxValueFor31Bits;
|
||||
|
||||
long localWindowDiff = this.AssignedRequest.DownloadSettings.ContentStreamMaxBuffered - this.localWindow;
|
||||
|
||||
if (localWindowDiff > 0)
|
||||
{
|
||||
this.localWindow += localWindowDiff;
|
||||
this.outgoing.Enqueue(HTTP2FrameHelper.CreateWindowUpdateFrame(this.Id, (UInt32)localWindowDiff, this.Context));
|
||||
}
|
||||
break;
|
||||
|
||||
case HTTP2StreamStates.Open:
|
||||
ProcessOpenState(outgoingFrames);
|
||||
//HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] New DATA frame created! remoteWindow: {1:N0}", this.Id, this.remoteWindow), this.Context);
|
||||
break;
|
||||
|
||||
case HTTP2StreamStates.HalfClosedLocal:
|
||||
if (this.response?.DownStream != null)
|
||||
{
|
||||
var windowIncrement = this.response.DownStream.MaxBuffered - this.localWindow - this.response.DownStream.Length;
|
||||
if (windowIncrement > 0)
|
||||
{
|
||||
this.localWindow += windowIncrement;
|
||||
outgoingFrames.Add(HTTP2FrameHelper.CreateWindowUpdateFrame(this.Id, (UInt32)windowIncrement, this.Context));
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", $"[{this.Id}] Sending window inc. update: {windowIncrement:N0}, {this.localWindow:N0}/{this.response.DownStream.MaxBuffered:N0}", this.Context);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case HTTP2StreamStates.HalfClosedRemote:
|
||||
break;
|
||||
|
||||
case HTTP2StreamStates.Closed:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ProcessOpenState(List<HTTP2FrameHeaderAndPayload> outgoingFrames)
|
||||
{
|
||||
// remote Window can be negative! See https://httpwg.org/specs/rfc7540.html#InitialWindowSize
|
||||
if (this.remoteWindow <= 0)
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] Skipping data sending as remote Window is {1}!", this.Id, this.remoteWindow), this.Context);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// This step will send one frame per ProcessOpenState call.
|
||||
|
||||
Int64 maxFrameSize = Math.Min(this.AssignedRequest.UploadSettings.UploadChunkSize, Math.Min(this.remoteWindow, this.settings.RemoteSettings[HTTP2Settings.MAX_FRAME_SIZE]));
|
||||
|
||||
HTTP2FrameHeaderAndPayload frame = new HTTP2FrameHeaderAndPayload();
|
||||
frame.Type = HTTP2FrameTypes.DATA;
|
||||
frame.StreamId = this.Id;
|
||||
|
||||
frame.Payload = BufferPool.Get(maxFrameSize, true)
|
||||
.AsBuffer((int)maxFrameSize);
|
||||
|
||||
// Expect a readCount of zero if it's end of the stream. But, to enable non-blocking scenario to wait for data, going to treat a negative value as no data.
|
||||
this.lastReadCount = this.AssignedRequest.UploadSettings.UploadStream.Read(frame.Payload.Data, 0, (int)Math.Min(maxFrameSize, int.MaxValue));
|
||||
if (this.lastReadCount <= 0)
|
||||
{
|
||||
BufferPool.Release(frame.Payload);
|
||||
|
||||
frame.Payload = BufferSegment.Empty;
|
||||
|
||||
if (this.lastReadCount < 0)
|
||||
return;
|
||||
}
|
||||
else
|
||||
frame.Payload = frame.Payload.Slice(0, this.lastReadCount);
|
||||
|
||||
frame.DontUseMemPool = false;
|
||||
|
||||
if (this.lastReadCount <= 0)
|
||||
{
|
||||
this.AssignedRequest.UploadSettings.Dispose();
|
||||
|
||||
frame.Flags = (byte)(HTTP2DataFlags.END_STREAM);
|
||||
|
||||
this.State = HTTP2StreamStates.HalfClosedLocal;
|
||||
|
||||
this.AssignedRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
||||
}
|
||||
|
||||
this.outgoing.Enqueue(frame);
|
||||
|
||||
this.remoteWindow -= frame.Payload.Count;
|
||||
|
||||
this.sentData += (uint)frame.Payload.Count;
|
||||
|
||||
if (this.AssignedRequest.UploadSettings.OnUploadProgress != null)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.AssignedRequest, RequestEvents.UploadProgress, this.sentData, uploadLength));
|
||||
}
|
||||
|
||||
protected void OnRemoteSettingChanged(HTTP2SettingsRegistry registry, HTTP2Settings setting, uint oldValue, uint newValue)
|
||||
{
|
||||
switch (setting)
|
||||
{
|
||||
case HTTP2Settings.INITIAL_WINDOW_SIZE:
|
||||
// https://httpwg.org/specs/rfc7540.html#InitialWindowSize
|
||||
// "Prior to receiving a SETTINGS frame that sets a value for SETTINGS_INITIAL_WINDOW_SIZE,
|
||||
// an endpoint can only use the default initial window size when sending flow-controlled frames."
|
||||
// "In addition to changing the flow-control window for streams that are not yet active,
|
||||
// a SETTINGS frame can alter the initial flow-control window size for streams with active flow-control windows
|
||||
// (that is, streams in the "open" or "half-closed (remote)" state). When the value of SETTINGS_INITIAL_WINDOW_SIZE changes,
|
||||
// a receiver MUST adjust the size of all stream flow-control windows that it maintains by the difference between the new value and the old value."
|
||||
|
||||
// So, if we created a stream before the remote peer's initial settings frame is received, we
|
||||
// will adjust the window size. For example: initial window size by default is 65535, if we later
|
||||
// receive a change to 1048576 (1 MB) we will increase the current remoteWindow by (1 048 576 - 65 535 =) 983 041
|
||||
|
||||
// But because initial window size in a setting frame can be smaller then the default 65535 bytes,
|
||||
// the difference can be negative:
|
||||
// "A change to SETTINGS_INITIAL_WINDOW_SIZE can cause the available space in a flow-control window to become negative.
|
||||
// A sender MUST track the negative flow-control window and MUST NOT send new flow-controlled frames
|
||||
// until it receives WINDOW_UPDATE frames that cause the flow-control window to become positive.
|
||||
|
||||
// For example, if the client sends 60 KB immediately on connection establishment
|
||||
// and the server sets the initial window size to be 16 KB, the client will recalculate
|
||||
// the available flow - control window to be - 44 KB on receipt of the SETTINGS frame.
|
||||
// The client retains a negative flow-control window until WINDOW_UPDATE frames restore the
|
||||
// window to being positive, after which the client can resume sending."
|
||||
|
||||
this.remoteWindow += newValue - oldValue;
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", string.Format("[{0}] Remote Setting's Initial Window Updated from {1:N0} to {2:N0}, diff: {3:N0}, new remoteWindow: {4:N0}, total data sent: {5:N0}", this.Id, oldValue, newValue, newValue - oldValue, this.remoteWindow, this.sentData), this.Context);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void FinishRequest()
|
||||
{
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", $"FinishRequest({this.Id})", this.Context);
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
this.AssignedRequest.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
bool resendRequest;
|
||||
HTTPConnectionStates proposedConnectionStates; // ignored
|
||||
KeepAliveHeader keepAliveHeader = null; // ignored
|
||||
|
||||
ConnectionHelper.HandleResponse(this.AssignedRequest, out resendRequest, out proposedConnectionStates, ref keepAliveHeader, this.Context);
|
||||
|
||||
if (resendRequest && !this.AssignedRequest.IsCancellationRequested)
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.AssignedRequest, RequestEvents.Resend));
|
||||
else
|
||||
{
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.AssignedRequest, HTTPRequestStates.Finished, null));
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("HTTP2Stream", "FinishRequest", ex, this.Context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Removed()
|
||||
{
|
||||
this.AssignedRequest.UploadSettings.Dispose();
|
||||
|
||||
// After receiving a RST_STREAM on a stream, the receiver MUST NOT send additional frames for that stream, with the exception of PRIORITY.
|
||||
this.outgoing.Clear();
|
||||
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/77
|
||||
// Unsubscribe from OnSettingChangedEvent to remove reference to this instance.
|
||||
this.settings.RemoteSettings.OnSettingChangedEvent -= OnRemoteSettingChanged;
|
||||
|
||||
this.headerView?.Close();
|
||||
|
||||
#if ENABLE_LOGGING
|
||||
HTTPManager.Logger.Information("HTTP2Stream", "Stream removed: " + this.Id.ToString(), this.Context);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2eadc0f2891358d4c9754796420a8e15
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,201 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Shared.PlatformSupport.Text;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
sealed class HeaderTable
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#static.table.definition
|
||||
// Valid indexes starts with 1, so there's an empty entry.
|
||||
static string[] StaticTableValues = new string[] { string.Empty, string.Empty, "GET", "POST", "/", "/index.html", "http", "https", "200", "204", "206", "304", "400", "404", "500", string.Empty, "gzip, deflate" };
|
||||
|
||||
// https://http2.github.io/http2-spec/compression.html#static.table.definition
|
||||
// Valid indexes starts with 1, so there's an empty entry.
|
||||
static string[] StaticTable = new string[62]
|
||||
{
|
||||
string.Empty,
|
||||
":authority",
|
||||
":method", // GET
|
||||
":method", // POST
|
||||
":path", // /
|
||||
":path", // index.html
|
||||
":scheme", // http
|
||||
":scheme", // https
|
||||
":status", // 200
|
||||
":status", // 204
|
||||
":status", // 206
|
||||
":status", // 304
|
||||
":status", // 400
|
||||
":status", // 404
|
||||
":status", // 500
|
||||
"accept-charset",
|
||||
"accept-encoding", // gzip, deflate
|
||||
"accept-language",
|
||||
"accept-ranges",
|
||||
"accept",
|
||||
"access-control-allow-origin",
|
||||
"age",
|
||||
"allow",
|
||||
"authorization",
|
||||
"cache-control",
|
||||
"content-disposition",
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"content-length",
|
||||
"content-location",
|
||||
"content-range",
|
||||
"content-type",
|
||||
"cookie",
|
||||
"date",
|
||||
"etag",
|
||||
"expect",
|
||||
"expires",
|
||||
"from",
|
||||
"host",
|
||||
"if-match",
|
||||
"if-modified-since",
|
||||
"if-none-match",
|
||||
"if-range",
|
||||
"if-unmodified-since",
|
||||
"last-modified",
|
||||
"link",
|
||||
"location",
|
||||
"max-forwards",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"range",
|
||||
"referer",
|
||||
"refresh",
|
||||
"retry-after",
|
||||
"server",
|
||||
"set-cookie",
|
||||
"strict-transport-security",
|
||||
"transfer-encoding",
|
||||
"user-agent",
|
||||
"vary",
|
||||
"via",
|
||||
"www-authenticate",
|
||||
};
|
||||
|
||||
public UInt32 DynamicTableSize { get; private set; }
|
||||
public UInt32 MaxDynamicTableSize {
|
||||
get { return this._maxDynamicTableSize; }
|
||||
set
|
||||
{
|
||||
this._maxDynamicTableSize = value;
|
||||
EvictEntries(0);
|
||||
}
|
||||
}
|
||||
private UInt32 _maxDynamicTableSize;
|
||||
|
||||
private List<KeyValuePair<string, string>> DynamicTable = new List<KeyValuePair<string, string>>();
|
||||
private HTTP2SettingsRegistry settingsRegistry;
|
||||
|
||||
public HeaderTable(HTTP2SettingsRegistry registry)
|
||||
{
|
||||
this.settingsRegistry = registry;
|
||||
this.MaxDynamicTableSize = this.settingsRegistry[HTTP2Settings.HEADER_TABLE_SIZE];
|
||||
}
|
||||
|
||||
public KeyValuePair<UInt32, UInt32> GetIndex(string key, string value)
|
||||
{
|
||||
for (int i = 0; i < DynamicTable.Count; ++i)
|
||||
{
|
||||
var kvp = DynamicTable[i];
|
||||
|
||||
// Exact match for both key and value
|
||||
if (kvp.Key.Equals(key, StringComparison.OrdinalIgnoreCase) && kvp.Value.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
return new KeyValuePair<UInt32, UInt32>((UInt32)(StaticTable.Length + i), (UInt32)(StaticTable.Length + i));
|
||||
}
|
||||
|
||||
KeyValuePair<UInt32, UInt32> bestMatch = new KeyValuePair<UInt32, UInt32>(0, 0);
|
||||
for (int i = 0; i < StaticTable.Length; ++i)
|
||||
{
|
||||
if (StaticTable[i].Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (i < StaticTableValues.Length && !string.IsNullOrEmpty(StaticTableValues[i]) && StaticTableValues[i].Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
return new KeyValuePair<UInt32, UInt32>((UInt32)i, (UInt32)i);
|
||||
else
|
||||
bestMatch = new KeyValuePair<UInt32, UInt32>((UInt32)i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
public string GetKey(UInt32 index)
|
||||
{
|
||||
if (index < StaticTable.Length)
|
||||
return StaticTable[index];
|
||||
|
||||
return this.DynamicTable[(int)(index - StaticTable.Length)].Key;
|
||||
}
|
||||
|
||||
public KeyValuePair<string, string> GetHeader(UInt32 index)
|
||||
{
|
||||
if (index < StaticTable.Length)
|
||||
return new KeyValuePair<string, string>(StaticTable[index],
|
||||
index < StaticTableValues.Length ? StaticTableValues[index] : null);
|
||||
|
||||
return this.DynamicTable[(int)(index - StaticTable.Length)];
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, string> header)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#calculating.table.size
|
||||
// The size of an entry is the sum of its name's length in octets (as defined in Section 5.2),
|
||||
// its value's length in octets, and 32.
|
||||
UInt32 newHeaderSize = CalculateEntrySize(header);
|
||||
|
||||
EvictEntries(newHeaderSize);
|
||||
|
||||
// If the size of the new entry is less than or equal to the maximum size, that entry is added to the table.
|
||||
// It is not an error to attempt to add an entry that is larger than the maximum size;
|
||||
// an attempt to add an entry larger than the maximum size causes the table to be
|
||||
// emptied of all existing entries and results in an empty table.
|
||||
if (this.DynamicTableSize + newHeaderSize <= this.MaxDynamicTableSize)
|
||||
{
|
||||
this.DynamicTable.Insert(0, header);
|
||||
this.DynamicTableSize += (UInt32)newHeaderSize;
|
||||
}
|
||||
}
|
||||
|
||||
private UInt32 CalculateEntrySize(KeyValuePair<string, string> entry)
|
||||
{
|
||||
return 32 + (UInt32)System.Text.Encoding.UTF8.GetByteCount(entry.Key) +
|
||||
(UInt32)System.Text.Encoding.UTF8.GetByteCount(entry.Value);
|
||||
}
|
||||
|
||||
private void EvictEntries(uint newHeaderSize)
|
||||
{
|
||||
// https://http2.github.io/http2-spec/compression.html#entry.addition
|
||||
// Before a new entry is added to the dynamic table, entries are evicted from the end of the dynamic
|
||||
// table until the size of the dynamic table is less than or equal to (maximum size - new entry size) or until the table is empty.
|
||||
while (this.DynamicTableSize + newHeaderSize > this.MaxDynamicTableSize && this.DynamicTable.Count > 0)
|
||||
{
|
||||
KeyValuePair<string, string> entry = this.DynamicTable[this.DynamicTable.Count - 1];
|
||||
this.DynamicTable.RemoveAt(this.DynamicTable.Count - 1);
|
||||
this.DynamicTableSize -= CalculateEntrySize(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
System.Text.StringBuilder sb = StringBuilderPool.Get(this.DynamicTable.Count + 3);
|
||||
sb.Append("[HeaderTable ");
|
||||
sb.AppendFormat("DynamicTable count: {0}, DynamicTableSize: {1}, MaxDynamicTableSize: {2}, ", this.DynamicTable.Count, this.DynamicTableSize, this.MaxDynamicTableSize);
|
||||
|
||||
foreach(var kvp in this.DynamicTable)
|
||||
sb.AppendFormat("\"{0}\": \"{1}\", ", kvp.Key, kvp.Value);
|
||||
|
||||
sb.Append("]");
|
||||
return StringBuilderPool.ReleaseAndGrab(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a0cd78904e5b8e4095111b9a2d439e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,933 @@
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.HTTP2
|
||||
{
|
||||
/// <summary>
|
||||
/// A pre-generated table entry in a Huffman-Tree.
|
||||
/// </summary>
|
||||
public readonly struct HuffmanTableEntry
|
||||
{
|
||||
public readonly UInt32 Code;
|
||||
public readonly byte Bits;
|
||||
|
||||
public HuffmanTableEntry(UInt32 code, byte bits)
|
||||
{
|
||||
this.Code = code;
|
||||
this.Bits = bits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It must return 0 or 1 at bit index. Indexing will be relative to the Bits representing the current code. Idx grows from left to right. Idx must be between [1..Bits].
|
||||
/// </summary>
|
||||
public byte GetBitAtIdx(byte idx)
|
||||
{
|
||||
return (byte)((this.Code >> (this.Bits - idx)) & 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[TableEntry Code: 0x{0:X}, Bits: {1}]", this.Code, this.Bits);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct HuffmanTreeNode
|
||||
{
|
||||
public readonly UInt16 Value;
|
||||
|
||||
public readonly UInt16 NextZeroIdx;
|
||||
public readonly UInt16 NextOneIdx;
|
||||
|
||||
public HuffmanTreeNode(UInt16 value, UInt16 nextZeroIdx, UInt16 nextOneIdx)
|
||||
{
|
||||
this.Value = value;
|
||||
this.NextZeroIdx = nextZeroIdx;
|
||||
this.NextOneIdx = nextOneIdx;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[TreeNode Value: {0}, NextZeroIdx: {1}, NextOneIdx: {2}]",
|
||||
this.Value, this.NextZeroIdx, this.NextOneIdx);
|
||||
}
|
||||
}
|
||||
|
||||
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstruction]
|
||||
static class HuffmanEncoder
|
||||
{
|
||||
public const UInt16 EOS = 256;
|
||||
|
||||
static readonly HuffmanTableEntry[] StaticTable = new HuffmanTableEntry[257]
|
||||
{
|
||||
new HuffmanTableEntry( 0x1ff8 , 13 ),
|
||||
new HuffmanTableEntry( 0x7fffd8 , 23 ),
|
||||
new HuffmanTableEntry( 0xfffffe2, 28 ),
|
||||
new HuffmanTableEntry( 0xfffffe3, 28 ),
|
||||
new HuffmanTableEntry( 0xfffffe4, 28 ),
|
||||
new HuffmanTableEntry( 0xfffffe5, 28 ),
|
||||
new HuffmanTableEntry( 0xfffffe6, 28 ),
|
||||
new HuffmanTableEntry( 0xfffffe7, 28 ),
|
||||
new HuffmanTableEntry( 0xfffffe8, 28 ),
|
||||
new HuffmanTableEntry( 0xffffea, 24 ),
|
||||
new HuffmanTableEntry( 0x3ffffffc , 30 ),
|
||||
new HuffmanTableEntry( 0xfffffe9 , 28 ),
|
||||
new HuffmanTableEntry( 0xfffffea , 28 ),
|
||||
new HuffmanTableEntry( 0x3ffffffd , 30 ),
|
||||
new HuffmanTableEntry( 0xfffffeb , 28 ),
|
||||
new HuffmanTableEntry( 0xfffffec , 28 ),
|
||||
new HuffmanTableEntry( 0xfffffed , 28 ),
|
||||
new HuffmanTableEntry( 0xfffffee , 28 ),
|
||||
new HuffmanTableEntry( 0xfffffef , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff0 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff1 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff2 , 28 ),
|
||||
new HuffmanTableEntry( 0x3ffffffe, 30 ),
|
||||
new HuffmanTableEntry( 0xffffff3 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff4 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff5 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff6 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff7 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff8 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffff9 , 28 ),
|
||||
new HuffmanTableEntry( 0xffffffa , 28 ),
|
||||
new HuffmanTableEntry( 0xffffffb , 28 ),
|
||||
new HuffmanTableEntry( 0x14 , 6 ),
|
||||
new HuffmanTableEntry( 0x3f8, 10 ),
|
||||
new HuffmanTableEntry( 0x3f9, 10 ),
|
||||
new HuffmanTableEntry( 0xffa, 12 ),
|
||||
new HuffmanTableEntry( 0x1ff9 , 13 ),
|
||||
new HuffmanTableEntry( 0x15 , 6 ),
|
||||
new HuffmanTableEntry( 0xf8 , 8 ),
|
||||
new HuffmanTableEntry( 0x7fa, 11 ),
|
||||
new HuffmanTableEntry( 0x3fa, 10 ),
|
||||
new HuffmanTableEntry( 0x3fb, 10 ),
|
||||
new HuffmanTableEntry( 0xf9 , 8 ),
|
||||
new HuffmanTableEntry( 0x7fb, 11 ),
|
||||
new HuffmanTableEntry( 0xfa , 8 ),
|
||||
new HuffmanTableEntry( 0x16 , 6 ),
|
||||
new HuffmanTableEntry( 0x17 , 6 ),
|
||||
new HuffmanTableEntry( 0x18 , 6 ),
|
||||
new HuffmanTableEntry( 0x0 , 5 ),
|
||||
new HuffmanTableEntry( 0x1 , 5 ),
|
||||
new HuffmanTableEntry( 0x2 , 5 ),
|
||||
new HuffmanTableEntry( 0x19 , 6 ),
|
||||
new HuffmanTableEntry( 0x1a , 6 ),
|
||||
new HuffmanTableEntry( 0x1b , 6 ),
|
||||
new HuffmanTableEntry( 0x1c , 6 ),
|
||||
new HuffmanTableEntry( 0x1d , 6 ),
|
||||
new HuffmanTableEntry( 0x1e , 6 ),
|
||||
new HuffmanTableEntry( 0x1f , 6 ),
|
||||
new HuffmanTableEntry( 0x5c , 7 ),
|
||||
new HuffmanTableEntry( 0xfb , 8 ),
|
||||
new HuffmanTableEntry( 0x7ffc , 15 ),
|
||||
new HuffmanTableEntry( 0x20 , 6 ),
|
||||
new HuffmanTableEntry( 0xffb, 12 ),
|
||||
new HuffmanTableEntry( 0x3fc, 10 ),
|
||||
new HuffmanTableEntry( 0x1ffa , 13 ),
|
||||
new HuffmanTableEntry( 0x21, 6 ),
|
||||
new HuffmanTableEntry( 0x5d, 7 ),
|
||||
new HuffmanTableEntry( 0x5e, 7 ),
|
||||
new HuffmanTableEntry( 0x5f, 7 ),
|
||||
new HuffmanTableEntry( 0x60, 7 ),
|
||||
new HuffmanTableEntry( 0x61, 7 ),
|
||||
new HuffmanTableEntry( 0x62, 7 ),
|
||||
new HuffmanTableEntry( 0x63, 7 ),
|
||||
new HuffmanTableEntry( 0x64, 7 ),
|
||||
new HuffmanTableEntry( 0x65, 7 ),
|
||||
new HuffmanTableEntry( 0x66, 7 ),
|
||||
new HuffmanTableEntry( 0x67, 7 ),
|
||||
new HuffmanTableEntry( 0x68, 7 ),
|
||||
new HuffmanTableEntry( 0x69, 7 ),
|
||||
new HuffmanTableEntry( 0x6a, 7 ),
|
||||
new HuffmanTableEntry( 0x6b, 7 ),
|
||||
new HuffmanTableEntry( 0x6c, 7 ),
|
||||
new HuffmanTableEntry( 0x6d, 7 ),
|
||||
new HuffmanTableEntry( 0x6e, 7 ),
|
||||
new HuffmanTableEntry( 0x6f, 7 ),
|
||||
new HuffmanTableEntry( 0x70, 7 ),
|
||||
new HuffmanTableEntry( 0x71, 7 ),
|
||||
new HuffmanTableEntry( 0x72, 7 ),
|
||||
new HuffmanTableEntry( 0xfc, 8 ),
|
||||
new HuffmanTableEntry( 0x73, 7 ),
|
||||
new HuffmanTableEntry( 0xfd, 8 ),
|
||||
new HuffmanTableEntry( 0x1ffb, 13 ),
|
||||
new HuffmanTableEntry( 0x7fff0, 19 ),
|
||||
new HuffmanTableEntry( 0x1ffc, 13 ),
|
||||
new HuffmanTableEntry( 0x3ffc, 14 ),
|
||||
new HuffmanTableEntry( 0x22, 6 ),
|
||||
new HuffmanTableEntry( 0x7ffd, 15 ),
|
||||
new HuffmanTableEntry( 0x3, 5 ),
|
||||
new HuffmanTableEntry( 0x23, 6 ),
|
||||
new HuffmanTableEntry( 0x4, 5 ),
|
||||
new HuffmanTableEntry( 0x24, 6 ),
|
||||
new HuffmanTableEntry( 0x5, 5 ),
|
||||
new HuffmanTableEntry( 0x25, 6 ),
|
||||
new HuffmanTableEntry( 0x26, 6 ),
|
||||
new HuffmanTableEntry( 0x27, 6 ),
|
||||
new HuffmanTableEntry( 0x6 , 5 ),
|
||||
new HuffmanTableEntry( 0x74, 7 ),
|
||||
new HuffmanTableEntry( 0x75, 7 ),
|
||||
new HuffmanTableEntry( 0x28, 6 ),
|
||||
new HuffmanTableEntry( 0x29, 6 ),
|
||||
new HuffmanTableEntry( 0x2a, 6 ),
|
||||
new HuffmanTableEntry( 0x7 , 5 ),
|
||||
new HuffmanTableEntry( 0x2b, 6 ),
|
||||
new HuffmanTableEntry( 0x76, 7 ),
|
||||
new HuffmanTableEntry( 0x2c, 6 ),
|
||||
new HuffmanTableEntry( 0x8 , 5 ),
|
||||
new HuffmanTableEntry( 0x9 , 5 ),
|
||||
new HuffmanTableEntry( 0x2d, 6 ),
|
||||
new HuffmanTableEntry( 0x77, 7 ),
|
||||
new HuffmanTableEntry( 0x78, 7 ),
|
||||
new HuffmanTableEntry( 0x79, 7 ),
|
||||
new HuffmanTableEntry( 0x7a, 7 ),
|
||||
new HuffmanTableEntry( 0x7b, 7 ),
|
||||
new HuffmanTableEntry( 0x7ffe, 15 ),
|
||||
new HuffmanTableEntry( 0x7fc, 11 ),
|
||||
new HuffmanTableEntry( 0x3ffd, 14 ),
|
||||
new HuffmanTableEntry( 0x1ffd, 13 ),
|
||||
new HuffmanTableEntry( 0xffffffc, 28 ),
|
||||
new HuffmanTableEntry( 0xfffe6 , 20 ),
|
||||
new HuffmanTableEntry( 0x3fffd2, 22 ),
|
||||
new HuffmanTableEntry( 0xfffe7 , 20 ),
|
||||
new HuffmanTableEntry( 0xfffe8 , 20 ),
|
||||
new HuffmanTableEntry( 0x3fffd3, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffd4, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffd5, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffd9, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffd6, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffda, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffdb, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffdc, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffdd, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffde, 23 ),
|
||||
new HuffmanTableEntry( 0xffffeb, 24 ),
|
||||
new HuffmanTableEntry( 0x7fffdf, 23 ),
|
||||
new HuffmanTableEntry( 0xffffec, 24 ),
|
||||
new HuffmanTableEntry( 0xffffed, 24 ),
|
||||
new HuffmanTableEntry( 0x3fffd7, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffe0, 23 ),
|
||||
new HuffmanTableEntry( 0xffffee, 24 ),
|
||||
new HuffmanTableEntry( 0x7fffe1, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffe2, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffe3, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffe4, 23 ),
|
||||
new HuffmanTableEntry( 0x1fffdc, 21 ),
|
||||
new HuffmanTableEntry( 0x3fffd8, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffe5, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffd9, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffe6, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffe7, 23 ),
|
||||
new HuffmanTableEntry( 0xffffef, 24 ),
|
||||
new HuffmanTableEntry( 0x3fffda, 22 ),
|
||||
new HuffmanTableEntry( 0x1fffdd, 21 ),
|
||||
new HuffmanTableEntry( 0xfffe9 , 20 ),
|
||||
new HuffmanTableEntry( 0x3fffdb, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffdc, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffe8, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffe9, 23 ),
|
||||
new HuffmanTableEntry( 0x1fffde, 21 ),
|
||||
new HuffmanTableEntry( 0x7fffea, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffdd, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffde, 22 ),
|
||||
new HuffmanTableEntry( 0xfffff0, 24 ),
|
||||
new HuffmanTableEntry( 0x1fffdf, 21 ),
|
||||
new HuffmanTableEntry( 0x3fffdf, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffeb, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffec, 23 ),
|
||||
new HuffmanTableEntry( 0x1fffe0, 21 ),
|
||||
new HuffmanTableEntry( 0x1fffe1, 21 ),
|
||||
new HuffmanTableEntry( 0x3fffe0, 22 ),
|
||||
new HuffmanTableEntry( 0x1fffe2, 21 ),
|
||||
new HuffmanTableEntry( 0x7fffed, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffe1, 22 ),
|
||||
new HuffmanTableEntry( 0x7fffee, 23 ),
|
||||
new HuffmanTableEntry( 0x7fffef, 23 ),
|
||||
new HuffmanTableEntry( 0xfffea , 20 ),
|
||||
new HuffmanTableEntry( 0x3fffe2, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffe3, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffe4, 22 ),
|
||||
new HuffmanTableEntry( 0x7ffff0, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffe5, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffe6, 22 ),
|
||||
new HuffmanTableEntry( 0x7ffff1, 23 ),
|
||||
new HuffmanTableEntry( 0x3ffffe0, 26 ),
|
||||
new HuffmanTableEntry( 0x3ffffe1, 26 ),
|
||||
new HuffmanTableEntry( 0xfffeb , 20 ),
|
||||
new HuffmanTableEntry( 0x7fff1 , 19 ),
|
||||
new HuffmanTableEntry( 0x3fffe7, 22 ),
|
||||
new HuffmanTableEntry( 0x7ffff2, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffe8, 22 ),
|
||||
new HuffmanTableEntry( 0x1ffffec, 25 ),
|
||||
new HuffmanTableEntry( 0x3ffffe2, 26 ),
|
||||
new HuffmanTableEntry( 0x3ffffe3, 26 ),
|
||||
new HuffmanTableEntry( 0x3ffffe4, 26 ),
|
||||
new HuffmanTableEntry( 0x7ffffde, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffdf, 27 ),
|
||||
new HuffmanTableEntry( 0x3ffffe5, 26 ),
|
||||
new HuffmanTableEntry( 0xfffff1 , 24 ),
|
||||
new HuffmanTableEntry( 0x1ffffed, 25 ),
|
||||
new HuffmanTableEntry( 0x7fff2 , 19 ),
|
||||
new HuffmanTableEntry( 0x1fffe3 , 21 ),
|
||||
new HuffmanTableEntry( 0x3ffffe6, 26 ),
|
||||
new HuffmanTableEntry( 0x7ffffe0, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffe1, 27 ),
|
||||
new HuffmanTableEntry( 0x3ffffe7, 26 ),
|
||||
new HuffmanTableEntry( 0x7ffffe2, 27 ),
|
||||
new HuffmanTableEntry( 0xfffff2 , 24 ),
|
||||
new HuffmanTableEntry( 0x1fffe4 , 21 ),
|
||||
new HuffmanTableEntry( 0x1fffe5 , 21 ),
|
||||
new HuffmanTableEntry( 0x3ffffe8, 26 ),
|
||||
new HuffmanTableEntry( 0x3ffffe9, 26 ),
|
||||
new HuffmanTableEntry( 0xffffffd, 28 ),
|
||||
new HuffmanTableEntry( 0x7ffffe3, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffe4, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffe5, 27 ),
|
||||
new HuffmanTableEntry( 0xfffec , 20 ),
|
||||
new HuffmanTableEntry( 0xfffff3, 24 ),
|
||||
new HuffmanTableEntry( 0xfffed , 20 ),
|
||||
new HuffmanTableEntry( 0x1fffe6, 21 ),
|
||||
new HuffmanTableEntry( 0x3fffe9, 22 ),
|
||||
new HuffmanTableEntry( 0x1fffe7, 21 ),
|
||||
new HuffmanTableEntry( 0x1fffe8, 21 ),
|
||||
new HuffmanTableEntry( 0x7ffff3, 23 ),
|
||||
new HuffmanTableEntry( 0x3fffea, 22 ),
|
||||
new HuffmanTableEntry( 0x3fffeb, 22 ),
|
||||
new HuffmanTableEntry( 0x1ffffee, 25 ),
|
||||
new HuffmanTableEntry( 0x1ffffef, 25 ),
|
||||
new HuffmanTableEntry( 0xfffff4 , 24 ),
|
||||
new HuffmanTableEntry( 0xfffff5 , 24 ),
|
||||
new HuffmanTableEntry( 0x3ffffea, 26 ),
|
||||
new HuffmanTableEntry( 0x7ffff4 , 23 ),
|
||||
new HuffmanTableEntry( 0x3ffffeb, 26 ),
|
||||
new HuffmanTableEntry( 0x7ffffe6, 27 ),
|
||||
new HuffmanTableEntry( 0x3ffffec, 26 ),
|
||||
new HuffmanTableEntry( 0x3ffffed, 26 ),
|
||||
new HuffmanTableEntry( 0x7ffffe7, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffe8, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffe9, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffea, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffeb, 27 ),
|
||||
new HuffmanTableEntry( 0xffffffe, 28 ),
|
||||
new HuffmanTableEntry( 0x7ffffec, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffed, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffee, 27 ),
|
||||
new HuffmanTableEntry( 0x7ffffef, 27 ),
|
||||
new HuffmanTableEntry( 0x7fffff0, 27 ),
|
||||
new HuffmanTableEntry( 0x3ffffee, 26 ),
|
||||
new HuffmanTableEntry( 0x3fffffff, 30 )
|
||||
};
|
||||
//static List<TreeNode> entries = new List<TreeNode>();
|
||||
|
||||
static readonly HuffmanTreeNode[] HuffmanTree = new HuffmanTreeNode[]
|
||||
{
|
||||
new HuffmanTreeNode ( 0, 98, 1 ),
|
||||
new HuffmanTreeNode ( 0, 151, 2 ),
|
||||
new HuffmanTreeNode ( 0, 173, 3 ),
|
||||
new HuffmanTreeNode ( 0, 204, 4 ),
|
||||
new HuffmanTreeNode ( 0, 263, 5 ),
|
||||
new HuffmanTreeNode ( 0, 113, 6 ),
|
||||
new HuffmanTreeNode ( 0, 211, 7 ),
|
||||
new HuffmanTreeNode ( 0, 104, 8 ),
|
||||
new HuffmanTreeNode ( 0, 116, 9 ),
|
||||
new HuffmanTreeNode ( 0, 108, 10 ),
|
||||
new HuffmanTreeNode ( 0, 11, 14 ),
|
||||
new HuffmanTreeNode ( 0, 12, 166 ),
|
||||
new HuffmanTreeNode ( 0, 13, 111 ),
|
||||
new HuffmanTreeNode ( 0, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 220, 15 ),
|
||||
new HuffmanTreeNode ( 0, 222, 16 ),
|
||||
new HuffmanTreeNode ( 0, 158, 17 ),
|
||||
new HuffmanTreeNode ( 0, 270, 18 ),
|
||||
new HuffmanTreeNode ( 0, 216, 19 ),
|
||||
new HuffmanTreeNode ( 0, 279, 20 ),
|
||||
new HuffmanTreeNode ( 0, 21, 27 ),
|
||||
new HuffmanTreeNode ( 0, 377, 22 ),
|
||||
new HuffmanTreeNode ( 0, 414, 23 ),
|
||||
new HuffmanTreeNode ( 0, 24, 301 ),
|
||||
new HuffmanTreeNode ( 0, 25, 298 ),
|
||||
new HuffmanTreeNode ( 0, 26, 295 ),
|
||||
new HuffmanTreeNode ( 1, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 314, 28 ),
|
||||
new HuffmanTreeNode ( 0, 50, 29 ),
|
||||
new HuffmanTreeNode ( 0, 362, 30 ),
|
||||
new HuffmanTreeNode ( 0, 403, 31 ),
|
||||
new HuffmanTreeNode ( 0, 440, 32 ),
|
||||
new HuffmanTreeNode ( 0, 33, 55 ),
|
||||
new HuffmanTreeNode ( 0, 34, 46 ),
|
||||
new HuffmanTreeNode ( 0, 35, 39 ),
|
||||
new HuffmanTreeNode ( 0, 510, 36 ),
|
||||
new HuffmanTreeNode ( 0, 37, 38 ),
|
||||
new HuffmanTreeNode ( 2, 0, 0 ),
|
||||
new HuffmanTreeNode ( 3, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 40, 43 ),
|
||||
new HuffmanTreeNode ( 0, 41, 42 ),
|
||||
new HuffmanTreeNode ( 4, 0, 0 ),
|
||||
new HuffmanTreeNode ( 5, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 44, 45 ),
|
||||
new HuffmanTreeNode ( 6, 0, 0 ),
|
||||
new HuffmanTreeNode ( 7, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 47, 67 ),
|
||||
new HuffmanTreeNode ( 0, 48, 63 ),
|
||||
new HuffmanTreeNode ( 0, 49, 62 ),
|
||||
new HuffmanTreeNode ( 8, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 396, 51 ),
|
||||
new HuffmanTreeNode ( 0, 52, 309 ),
|
||||
new HuffmanTreeNode ( 0, 486, 53 ),
|
||||
new HuffmanTreeNode ( 0, 54, 307 ),
|
||||
new HuffmanTreeNode ( 9, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 74, 56 ),
|
||||
new HuffmanTreeNode ( 0, 91, 57 ),
|
||||
new HuffmanTreeNode ( 0, 274, 58 ),
|
||||
new HuffmanTreeNode ( 0, 502, 59 ),
|
||||
new HuffmanTreeNode ( 0, 60, 81 ),
|
||||
new HuffmanTreeNode ( 0, 61, 65 ),
|
||||
new HuffmanTreeNode ( 10, 0, 0 ),
|
||||
new HuffmanTreeNode ( 11, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 64, 66 ),
|
||||
new HuffmanTreeNode ( 12, 0, 0 ),
|
||||
new HuffmanTreeNode ( 13, 0, 0 ),
|
||||
new HuffmanTreeNode ( 14, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 68, 71 ),
|
||||
new HuffmanTreeNode ( 0, 69, 70 ),
|
||||
new HuffmanTreeNode ( 15, 0, 0 ),
|
||||
new HuffmanTreeNode ( 16, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 72, 73 ),
|
||||
new HuffmanTreeNode ( 17, 0, 0 ),
|
||||
new HuffmanTreeNode ( 18, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 75, 84 ),
|
||||
new HuffmanTreeNode ( 0, 76, 79 ),
|
||||
new HuffmanTreeNode ( 0, 77, 78 ),
|
||||
new HuffmanTreeNode ( 19, 0, 0 ),
|
||||
new HuffmanTreeNode ( 20, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 80, 83 ),
|
||||
new HuffmanTreeNode ( 21, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 82, 512 ),
|
||||
new HuffmanTreeNode ( 22, 0, 0 ),
|
||||
new HuffmanTreeNode ( 23, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 85, 88 ),
|
||||
new HuffmanTreeNode ( 0, 86, 87 ),
|
||||
new HuffmanTreeNode ( 24, 0, 0 ),
|
||||
new HuffmanTreeNode ( 25, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 89, 90 ),
|
||||
new HuffmanTreeNode ( 26, 0, 0 ),
|
||||
new HuffmanTreeNode ( 27, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 92, 95 ),
|
||||
new HuffmanTreeNode ( 0, 93, 94 ),
|
||||
new HuffmanTreeNode ( 28, 0, 0 ),
|
||||
new HuffmanTreeNode ( 29, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 96, 97 ),
|
||||
new HuffmanTreeNode ( 30, 0, 0 ),
|
||||
new HuffmanTreeNode ( 31, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 133, 99 ),
|
||||
new HuffmanTreeNode ( 0, 100, 129 ),
|
||||
new HuffmanTreeNode ( 0, 258, 101 ),
|
||||
new HuffmanTreeNode ( 0, 102, 126 ),
|
||||
new HuffmanTreeNode ( 0, 103, 112 ),
|
||||
new HuffmanTreeNode ( 32, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 105, 119 ),
|
||||
new HuffmanTreeNode ( 0, 106, 107 ),
|
||||
new HuffmanTreeNode ( 33, 0, 0 ),
|
||||
new HuffmanTreeNode ( 34, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 271, 109 ),
|
||||
new HuffmanTreeNode ( 0, 110, 164 ),
|
||||
new HuffmanTreeNode ( 35, 0, 0 ),
|
||||
new HuffmanTreeNode ( 36, 0, 0 ),
|
||||
new HuffmanTreeNode ( 37, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 114, 124 ),
|
||||
new HuffmanTreeNode ( 0, 115, 122 ),
|
||||
new HuffmanTreeNode ( 38, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 165, 117 ),
|
||||
new HuffmanTreeNode ( 0, 118, 123 ),
|
||||
new HuffmanTreeNode ( 39, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 120, 121 ),
|
||||
new HuffmanTreeNode ( 40, 0, 0 ),
|
||||
new HuffmanTreeNode ( 41, 0, 0 ),
|
||||
new HuffmanTreeNode ( 42, 0, 0 ),
|
||||
new HuffmanTreeNode ( 43, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 125, 157 ),
|
||||
new HuffmanTreeNode ( 44, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 127, 128 ),
|
||||
new HuffmanTreeNode ( 45, 0, 0 ),
|
||||
new HuffmanTreeNode ( 46, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 130, 144 ),
|
||||
new HuffmanTreeNode ( 0, 131, 141 ),
|
||||
new HuffmanTreeNode ( 0, 132, 140 ),
|
||||
new HuffmanTreeNode ( 47, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 134, 229 ),
|
||||
new HuffmanTreeNode ( 0, 135, 138 ),
|
||||
new HuffmanTreeNode ( 0, 136, 137 ),
|
||||
new HuffmanTreeNode ( 48, 0, 0 ),
|
||||
new HuffmanTreeNode ( 49, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 139, 227 ),
|
||||
new HuffmanTreeNode ( 50, 0, 0 ),
|
||||
new HuffmanTreeNode ( 51, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 142, 143 ),
|
||||
new HuffmanTreeNode ( 52, 0, 0 ),
|
||||
new HuffmanTreeNode ( 53, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 145, 148 ),
|
||||
new HuffmanTreeNode ( 0, 146, 147 ),
|
||||
new HuffmanTreeNode ( 54, 0, 0 ),
|
||||
new HuffmanTreeNode ( 55, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 149, 150 ),
|
||||
new HuffmanTreeNode ( 56, 0, 0 ),
|
||||
new HuffmanTreeNode ( 57, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 160, 152 ),
|
||||
new HuffmanTreeNode ( 0, 246, 153 ),
|
||||
new HuffmanTreeNode ( 0, 256, 154 ),
|
||||
new HuffmanTreeNode ( 0, 155, 170 ),
|
||||
new HuffmanTreeNode ( 0, 156, 169 ),
|
||||
new HuffmanTreeNode ( 58, 0, 0 ),
|
||||
new HuffmanTreeNode ( 59, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 159, 226 ),
|
||||
new HuffmanTreeNode ( 60, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 161, 232 ),
|
||||
new HuffmanTreeNode ( 0, 162, 224 ),
|
||||
new HuffmanTreeNode ( 0, 163, 168 ),
|
||||
new HuffmanTreeNode ( 61, 0, 0 ),
|
||||
new HuffmanTreeNode ( 62, 0, 0 ),
|
||||
new HuffmanTreeNode ( 63, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 167, 215 ),
|
||||
new HuffmanTreeNode ( 64, 0, 0 ),
|
||||
new HuffmanTreeNode ( 65, 0, 0 ),
|
||||
new HuffmanTreeNode ( 66, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 171, 172 ),
|
||||
new HuffmanTreeNode ( 67, 0, 0 ),
|
||||
new HuffmanTreeNode ( 68, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 174, 189 ),
|
||||
new HuffmanTreeNode ( 0, 175, 182 ),
|
||||
new HuffmanTreeNode ( 0, 176, 179 ),
|
||||
new HuffmanTreeNode ( 0, 177, 178 ),
|
||||
new HuffmanTreeNode ( 69, 0, 0 ),
|
||||
new HuffmanTreeNode ( 70, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 180, 181 ),
|
||||
new HuffmanTreeNode ( 71, 0, 0 ),
|
||||
new HuffmanTreeNode ( 72, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 183, 186 ),
|
||||
new HuffmanTreeNode ( 0, 184, 185 ),
|
||||
new HuffmanTreeNode ( 73, 0, 0 ),
|
||||
new HuffmanTreeNode ( 74, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 187, 188 ),
|
||||
new HuffmanTreeNode ( 75, 0, 0 ),
|
||||
new HuffmanTreeNode ( 76, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 190, 197 ),
|
||||
new HuffmanTreeNode ( 0, 191, 194 ),
|
||||
new HuffmanTreeNode ( 0, 192, 193 ),
|
||||
new HuffmanTreeNode ( 77, 0, 0 ),
|
||||
new HuffmanTreeNode ( 78, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 195, 196 ),
|
||||
new HuffmanTreeNode ( 79, 0, 0 ),
|
||||
new HuffmanTreeNode ( 80, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 198, 201 ),
|
||||
new HuffmanTreeNode ( 0, 199, 200 ),
|
||||
new HuffmanTreeNode ( 81, 0, 0 ),
|
||||
new HuffmanTreeNode ( 82, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 202, 203 ),
|
||||
new HuffmanTreeNode ( 83, 0, 0 ),
|
||||
new HuffmanTreeNode ( 84, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 205, 242 ),
|
||||
new HuffmanTreeNode ( 0, 206, 209 ),
|
||||
new HuffmanTreeNode ( 0, 207, 208 ),
|
||||
new HuffmanTreeNode ( 85, 0, 0 ),
|
||||
new HuffmanTreeNode ( 86, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 210, 213 ),
|
||||
new HuffmanTreeNode ( 87, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 212, 214 ),
|
||||
new HuffmanTreeNode ( 88, 0, 0 ),
|
||||
new HuffmanTreeNode ( 89, 0, 0 ),
|
||||
new HuffmanTreeNode ( 90, 0, 0 ),
|
||||
new HuffmanTreeNode ( 91, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 217, 286 ),
|
||||
new HuffmanTreeNode ( 0, 218, 276 ),
|
||||
new HuffmanTreeNode ( 0, 219, 410 ),
|
||||
new HuffmanTreeNode ( 92, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 221, 273 ),
|
||||
new HuffmanTreeNode ( 93, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 223, 272 ),
|
||||
new HuffmanTreeNode ( 94, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 225, 228 ),
|
||||
new HuffmanTreeNode ( 95, 0, 0 ),
|
||||
new HuffmanTreeNode ( 96, 0, 0 ),
|
||||
new HuffmanTreeNode ( 97, 0, 0 ),
|
||||
new HuffmanTreeNode ( 98, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 230, 240 ),
|
||||
new HuffmanTreeNode ( 0, 231, 235 ),
|
||||
new HuffmanTreeNode ( 99, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 233, 237 ),
|
||||
new HuffmanTreeNode ( 0, 234, 236 ),
|
||||
new HuffmanTreeNode ( 100, 0, 0 ),
|
||||
new HuffmanTreeNode ( 101, 0, 0 ),
|
||||
new HuffmanTreeNode ( 102, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 238, 239 ),
|
||||
new HuffmanTreeNode ( 103, 0, 0 ),
|
||||
new HuffmanTreeNode ( 104, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 241, 252 ),
|
||||
new HuffmanTreeNode ( 105, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 243, 254 ),
|
||||
new HuffmanTreeNode ( 0, 244, 245 ),
|
||||
new HuffmanTreeNode ( 106, 0, 0 ),
|
||||
new HuffmanTreeNode ( 107, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 247, 250 ),
|
||||
new HuffmanTreeNode ( 0, 248, 249 ),
|
||||
new HuffmanTreeNode ( 108, 0, 0 ),
|
||||
new HuffmanTreeNode ( 109, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 251, 253 ),
|
||||
new HuffmanTreeNode ( 110, 0, 0 ),
|
||||
new HuffmanTreeNode ( 111, 0, 0 ),
|
||||
new HuffmanTreeNode ( 112, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 255, 262 ),
|
||||
new HuffmanTreeNode ( 113, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 257, 261 ),
|
||||
new HuffmanTreeNode ( 114, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 259, 260 ),
|
||||
new HuffmanTreeNode ( 115, 0, 0 ),
|
||||
new HuffmanTreeNode ( 116, 0, 0 ),
|
||||
new HuffmanTreeNode ( 117, 0, 0 ),
|
||||
new HuffmanTreeNode ( 118, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 264, 267 ),
|
||||
new HuffmanTreeNode ( 0, 265, 266 ),
|
||||
new HuffmanTreeNode ( 119, 0, 0 ),
|
||||
new HuffmanTreeNode ( 120, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 268, 269 ),
|
||||
new HuffmanTreeNode ( 121, 0, 0 ),
|
||||
new HuffmanTreeNode ( 122, 0, 0 ),
|
||||
new HuffmanTreeNode ( 123, 0, 0 ),
|
||||
new HuffmanTreeNode ( 124, 0, 0 ),
|
||||
new HuffmanTreeNode ( 125, 0, 0 ),
|
||||
new HuffmanTreeNode ( 126, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 275, 459 ),
|
||||
new HuffmanTreeNode ( 127, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 436, 277 ),
|
||||
new HuffmanTreeNode ( 0, 278, 285 ),
|
||||
new HuffmanTreeNode ( 128, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 372, 280 ),
|
||||
new HuffmanTreeNode ( 0, 281, 332 ),
|
||||
new HuffmanTreeNode ( 0, 282, 291 ),
|
||||
new HuffmanTreeNode ( 0, 473, 283 ),
|
||||
new HuffmanTreeNode ( 0, 284, 290 ),
|
||||
new HuffmanTreeNode ( 129, 0, 0 ),
|
||||
new HuffmanTreeNode ( 130, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 287, 328 ),
|
||||
new HuffmanTreeNode ( 0, 288, 388 ),
|
||||
new HuffmanTreeNode ( 0, 289, 345 ),
|
||||
new HuffmanTreeNode ( 131, 0, 0 ),
|
||||
new HuffmanTreeNode ( 132, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 292, 296 ),
|
||||
new HuffmanTreeNode ( 0, 293, 294 ),
|
||||
new HuffmanTreeNode ( 133, 0, 0 ),
|
||||
new HuffmanTreeNode ( 134, 0, 0 ),
|
||||
new HuffmanTreeNode ( 135, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 297, 313 ),
|
||||
new HuffmanTreeNode ( 136, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 299, 300 ),
|
||||
new HuffmanTreeNode ( 137, 0, 0 ),
|
||||
new HuffmanTreeNode ( 138, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 302, 305 ),
|
||||
new HuffmanTreeNode ( 0, 303, 304 ),
|
||||
new HuffmanTreeNode ( 139, 0, 0 ),
|
||||
new HuffmanTreeNode ( 140, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 306, 308 ),
|
||||
new HuffmanTreeNode ( 141, 0, 0 ),
|
||||
new HuffmanTreeNode ( 142, 0, 0 ),
|
||||
new HuffmanTreeNode ( 143, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 310, 319 ),
|
||||
new HuffmanTreeNode ( 0, 311, 312 ),
|
||||
new HuffmanTreeNode ( 144, 0, 0 ),
|
||||
new HuffmanTreeNode ( 145, 0, 0 ),
|
||||
new HuffmanTreeNode ( 146, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 315, 350 ),
|
||||
new HuffmanTreeNode ( 0, 316, 325 ),
|
||||
new HuffmanTreeNode ( 0, 317, 322 ),
|
||||
new HuffmanTreeNode ( 0, 318, 321 ),
|
||||
new HuffmanTreeNode ( 147, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 320, 341 ),
|
||||
new HuffmanTreeNode ( 148, 0, 0 ),
|
||||
new HuffmanTreeNode ( 149, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 323, 324 ),
|
||||
new HuffmanTreeNode ( 150, 0, 0 ),
|
||||
new HuffmanTreeNode ( 151, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 326, 338 ),
|
||||
new HuffmanTreeNode ( 0, 327, 336 ),
|
||||
new HuffmanTreeNode ( 152, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 465, 329 ),
|
||||
new HuffmanTreeNode ( 0, 330, 355 ),
|
||||
new HuffmanTreeNode ( 0, 331, 344 ),
|
||||
new HuffmanTreeNode ( 153, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 333, 347 ),
|
||||
new HuffmanTreeNode ( 0, 334, 342 ),
|
||||
new HuffmanTreeNode ( 0, 335, 337 ),
|
||||
new HuffmanTreeNode ( 154, 0, 0 ),
|
||||
new HuffmanTreeNode ( 155, 0, 0 ),
|
||||
new HuffmanTreeNode ( 156, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 339, 340 ),
|
||||
new HuffmanTreeNode ( 157, 0, 0 ),
|
||||
new HuffmanTreeNode ( 158, 0, 0 ),
|
||||
new HuffmanTreeNode ( 159, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 343, 346 ),
|
||||
new HuffmanTreeNode ( 160, 0, 0 ),
|
||||
new HuffmanTreeNode ( 161, 0, 0 ),
|
||||
new HuffmanTreeNode ( 162, 0, 0 ),
|
||||
new HuffmanTreeNode ( 163, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 348, 360 ),
|
||||
new HuffmanTreeNode ( 0, 349, 359 ),
|
||||
new HuffmanTreeNode ( 164, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 351, 369 ),
|
||||
new HuffmanTreeNode ( 0, 352, 357 ),
|
||||
new HuffmanTreeNode ( 0, 353, 354 ),
|
||||
new HuffmanTreeNode ( 165, 0, 0 ),
|
||||
new HuffmanTreeNode ( 166, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 356, 366 ),
|
||||
new HuffmanTreeNode ( 167, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 358, 368 ),
|
||||
new HuffmanTreeNode ( 168, 0, 0 ),
|
||||
new HuffmanTreeNode ( 169, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 361, 367 ),
|
||||
new HuffmanTreeNode ( 170, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 363, 417 ),
|
||||
new HuffmanTreeNode ( 0, 364, 449 ),
|
||||
new HuffmanTreeNode ( 0, 365, 434 ),
|
||||
new HuffmanTreeNode ( 171, 0, 0 ),
|
||||
new HuffmanTreeNode ( 172, 0, 0 ),
|
||||
new HuffmanTreeNode ( 173, 0, 0 ),
|
||||
new HuffmanTreeNode ( 174, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 370, 385 ),
|
||||
new HuffmanTreeNode ( 0, 371, 383 ),
|
||||
new HuffmanTreeNode ( 175, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 373, 451 ),
|
||||
new HuffmanTreeNode ( 0, 374, 381 ),
|
||||
new HuffmanTreeNode ( 0, 375, 376 ),
|
||||
new HuffmanTreeNode ( 176, 0, 0 ),
|
||||
new HuffmanTreeNode ( 177, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 378, 393 ),
|
||||
new HuffmanTreeNode ( 0, 379, 390 ),
|
||||
new HuffmanTreeNode ( 0, 380, 384 ),
|
||||
new HuffmanTreeNode ( 178, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 382, 437 ),
|
||||
new HuffmanTreeNode ( 179, 0, 0 ),
|
||||
new HuffmanTreeNode ( 180, 0, 0 ),
|
||||
new HuffmanTreeNode ( 181, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 386, 387 ),
|
||||
new HuffmanTreeNode ( 182, 0, 0 ),
|
||||
new HuffmanTreeNode ( 183, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 389, 409 ),
|
||||
new HuffmanTreeNode ( 184, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 391, 392 ),
|
||||
new HuffmanTreeNode ( 185, 0, 0 ),
|
||||
new HuffmanTreeNode ( 186, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 394, 400 ),
|
||||
new HuffmanTreeNode ( 0, 395, 399 ),
|
||||
new HuffmanTreeNode ( 187, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 397, 412 ),
|
||||
new HuffmanTreeNode ( 0, 398, 402 ),
|
||||
new HuffmanTreeNode ( 188, 0, 0 ),
|
||||
new HuffmanTreeNode ( 189, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 401, 411 ),
|
||||
new HuffmanTreeNode ( 190, 0, 0 ),
|
||||
new HuffmanTreeNode ( 191, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 404, 427 ),
|
||||
new HuffmanTreeNode ( 0, 405, 424 ),
|
||||
new HuffmanTreeNode ( 0, 406, 421 ),
|
||||
new HuffmanTreeNode ( 0, 407, 408 ),
|
||||
new HuffmanTreeNode ( 192, 0, 0 ),
|
||||
new HuffmanTreeNode ( 193, 0, 0 ),
|
||||
new HuffmanTreeNode ( 194, 0, 0 ),
|
||||
new HuffmanTreeNode ( 195, 0, 0 ),
|
||||
new HuffmanTreeNode ( 196, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 413, 474 ),
|
||||
new HuffmanTreeNode ( 197, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 415, 475 ),
|
||||
new HuffmanTreeNode ( 0, 416, 471 ),
|
||||
new HuffmanTreeNode ( 198, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 481, 418 ),
|
||||
new HuffmanTreeNode ( 0, 419, 478 ),
|
||||
new HuffmanTreeNode ( 0, 420, 435 ),
|
||||
new HuffmanTreeNode ( 199, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 422, 423 ),
|
||||
new HuffmanTreeNode ( 200, 0, 0 ),
|
||||
new HuffmanTreeNode ( 201, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 425, 438 ),
|
||||
new HuffmanTreeNode ( 0, 426, 433 ),
|
||||
new HuffmanTreeNode ( 202, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 455, 428 ),
|
||||
new HuffmanTreeNode ( 0, 490, 429 ),
|
||||
new HuffmanTreeNode ( 0, 511, 430 ),
|
||||
new HuffmanTreeNode ( 0, 431, 432 ),
|
||||
new HuffmanTreeNode ( 203, 0, 0 ),
|
||||
new HuffmanTreeNode ( 204, 0, 0 ),
|
||||
new HuffmanTreeNode ( 205, 0, 0 ),
|
||||
new HuffmanTreeNode ( 206, 0, 0 ),
|
||||
new HuffmanTreeNode ( 207, 0, 0 ),
|
||||
new HuffmanTreeNode ( 208, 0, 0 ),
|
||||
new HuffmanTreeNode ( 209, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 439, 446 ),
|
||||
new HuffmanTreeNode ( 210, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 441, 494 ),
|
||||
new HuffmanTreeNode ( 0, 442, 461 ),
|
||||
new HuffmanTreeNode ( 0, 443, 447 ),
|
||||
new HuffmanTreeNode ( 0, 444, 445 ),
|
||||
new HuffmanTreeNode ( 211, 0, 0 ),
|
||||
new HuffmanTreeNode ( 212, 0, 0 ),
|
||||
new HuffmanTreeNode ( 213, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 448, 460 ),
|
||||
new HuffmanTreeNode ( 214, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 450, 467 ),
|
||||
new HuffmanTreeNode ( 215, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 452, 469 ),
|
||||
new HuffmanTreeNode ( 0, 453, 454 ),
|
||||
new HuffmanTreeNode ( 216, 0, 0 ),
|
||||
new HuffmanTreeNode ( 217, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 456, 484 ),
|
||||
new HuffmanTreeNode ( 0, 457, 458 ),
|
||||
new HuffmanTreeNode ( 218, 0, 0 ),
|
||||
new HuffmanTreeNode ( 219, 0, 0 ),
|
||||
new HuffmanTreeNode ( 220, 0, 0 ),
|
||||
new HuffmanTreeNode ( 221, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 462, 488 ),
|
||||
new HuffmanTreeNode ( 0, 463, 464 ),
|
||||
new HuffmanTreeNode ( 222, 0, 0 ),
|
||||
new HuffmanTreeNode ( 223, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 466, 468 ),
|
||||
new HuffmanTreeNode ( 224, 0, 0 ),
|
||||
new HuffmanTreeNode ( 225, 0, 0 ),
|
||||
new HuffmanTreeNode ( 226, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 470, 472 ),
|
||||
new HuffmanTreeNode ( 227, 0, 0 ),
|
||||
new HuffmanTreeNode ( 228, 0, 0 ),
|
||||
new HuffmanTreeNode ( 229, 0, 0 ),
|
||||
new HuffmanTreeNode ( 230, 0, 0 ),
|
||||
new HuffmanTreeNode ( 231, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 476, 477 ),
|
||||
new HuffmanTreeNode ( 232, 0, 0 ),
|
||||
new HuffmanTreeNode ( 233, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 479, 480 ),
|
||||
new HuffmanTreeNode ( 234, 0, 0 ),
|
||||
new HuffmanTreeNode ( 235, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 482, 483 ),
|
||||
new HuffmanTreeNode ( 236, 0, 0 ),
|
||||
new HuffmanTreeNode ( 237, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 485, 487 ),
|
||||
new HuffmanTreeNode ( 238, 0, 0 ),
|
||||
new HuffmanTreeNode ( 239, 0, 0 ),
|
||||
new HuffmanTreeNode ( 240, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 489, 493 ),
|
||||
new HuffmanTreeNode ( 241, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 491, 492 ),
|
||||
new HuffmanTreeNode ( 242, 0, 0 ),
|
||||
new HuffmanTreeNode ( 243, 0, 0 ),
|
||||
new HuffmanTreeNode ( 244, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 495, 503 ),
|
||||
new HuffmanTreeNode ( 0, 496, 499 ),
|
||||
new HuffmanTreeNode ( 0, 497, 498 ),
|
||||
new HuffmanTreeNode ( 245, 0, 0 ),
|
||||
new HuffmanTreeNode ( 246, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 500, 501 ),
|
||||
new HuffmanTreeNode ( 247, 0, 0 ),
|
||||
new HuffmanTreeNode ( 248, 0, 0 ),
|
||||
new HuffmanTreeNode ( 249, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 504, 507 ),
|
||||
new HuffmanTreeNode ( 0, 505, 506 ),
|
||||
new HuffmanTreeNode ( 250, 0, 0 ),
|
||||
new HuffmanTreeNode ( 251, 0, 0 ),
|
||||
new HuffmanTreeNode ( 0, 508, 509 ),
|
||||
new HuffmanTreeNode ( 252, 0, 0 ),
|
||||
new HuffmanTreeNode ( 253, 0, 0 ),
|
||||
new HuffmanTreeNode ( 254, 0, 0 ),
|
||||
new HuffmanTreeNode ( 255, 0, 0 ),
|
||||
new HuffmanTreeNode ( 256, 0, 0 )
|
||||
};
|
||||
|
||||
//static HuffmanEncoder()
|
||||
//{
|
||||
// BuildTree();
|
||||
//}
|
||||
//
|
||||
//private static void BuildTree()
|
||||
//{
|
||||
// // Add root
|
||||
// entries.Add(new TreeNode());
|
||||
//
|
||||
// for (int i = 0; i < StaticTable.Length; ++i)
|
||||
// {
|
||||
// var tableEntry = StaticTable[i];
|
||||
// var currentNode = entries[0];
|
||||
// int currentNodeIdx = 0;
|
||||
//
|
||||
// for (byte bitIdx = 1; bitIdx <= tableEntry.Bits; bitIdx++)
|
||||
// {
|
||||
// byte bit = tableEntry.GetBitAtIdx(bitIdx);
|
||||
//
|
||||
// switch(bit)
|
||||
// {
|
||||
// case 0:
|
||||
// if (currentNode.NextZeroIdx == 0)
|
||||
// {
|
||||
// currentNode.NextZeroIdx = (UInt16)entries.Count;
|
||||
// entries[currentNodeIdx] = currentNode;
|
||||
// entries.Add(new TreeNode());
|
||||
// }
|
||||
//
|
||||
// currentNodeIdx = currentNode.NextZeroIdx;
|
||||
// currentNode = entries[currentNodeIdx];
|
||||
// break;
|
||||
//
|
||||
// case 1:
|
||||
// if (currentNode.NextOneIdx == 0)
|
||||
// {
|
||||
// currentNode.NextOneIdx = (UInt16)entries.Count;
|
||||
// entries[currentNodeIdx] = currentNode;
|
||||
// entries.Add(new TreeNode());
|
||||
// }
|
||||
//
|
||||
// currentNodeIdx = currentNode.NextOneIdx;
|
||||
// currentNode = entries[currentNodeIdx];
|
||||
// break;
|
||||
//
|
||||
// default:
|
||||
// HTTPManager.Logger.Information("HuffmanEncoder", "BuildTree - GetBitAtIdx returned with an unsupported value: " + bit);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// entries[currentNodeIdx] = new TreeNode { Value = (UInt16)i };
|
||||
//
|
||||
// //HTTPManager.Logger.Information("HuffmanEncoder", string.Format("BuildTree - {0} - Entry({1}) added to idx: {2}", i, entries[currentNodeIdx], currentNodeIdx));
|
||||
// }
|
||||
//
|
||||
// //HTTPManager.Logger.Information("HuffmanEncoder", "BuildTree - entries: " + entries.Count);
|
||||
// //for (int i = 0; i < entries.Count; ++i)
|
||||
// // HTTPManager.Logger.Information("HuffmanEncoder", string.Format("{0} - Entry : {1}", i, entries[i]));
|
||||
// System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
||||
// for (int i = 0; i < entries.Count; ++i)
|
||||
// {
|
||||
// sb.AppendFormat("new TreeNode {{ Value = {0}, NextZeroIdx = {1}, NextOneIdx = {2} }},\n", entries[i].Value, entries[i].NextZeroIdx, entries[i].NextOneIdx);
|
||||
// }
|
||||
// UnityEngine.Debug.Log(sb.ToString());
|
||||
//}
|
||||
|
||||
public static HuffmanTreeNode GetRoot()
|
||||
{
|
||||
return HuffmanTree[0];
|
||||
}
|
||||
|
||||
public static HuffmanTreeNode GetNext(HuffmanTreeNode current, byte bit)
|
||||
{
|
||||
switch(bit)
|
||||
{
|
||||
case 0:
|
||||
return HuffmanTree[current.NextZeroIdx];
|
||||
case 1:
|
||||
return HuffmanTree[current.NextOneIdx];
|
||||
}
|
||||
|
||||
throw new Exception("HuffmanEncoder - GetNext - unsupported bit: " + bit);
|
||||
}
|
||||
|
||||
public static HuffmanTableEntry GetEntryForCodePoint(UInt16 codePoint)
|
||||
{
|
||||
return StaticTable[codePoint];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: babdeee42980ac84fb76ef946256b076
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible states of a Http Connection.
|
||||
/// The ideal lifecycle of a connection that has KeepAlive is the following: Initial => [Processing => WaitForRecycle => Free] => Closed.
|
||||
/// </summary>
|
||||
public enum HTTPConnectionStates
|
||||
{
|
||||
/// <summary>
|
||||
/// This Connection instance is just created.
|
||||
/// </summary>
|
||||
Initial,
|
||||
|
||||
/// <summary>
|
||||
/// This Connection is processing a request
|
||||
/// </summary>
|
||||
Processing,
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the upgraded protocol to shut down.
|
||||
/// </summary>
|
||||
WaitForProtocolShutdown,
|
||||
|
||||
/// <summary>
|
||||
/// The Connection is finished processing the request, it's waiting now to deliver it's result.
|
||||
/// </summary>
|
||||
Recycle,
|
||||
|
||||
/// <summary>
|
||||
/// The request result's delivered, it's now up to processing again.
|
||||
/// </summary>
|
||||
Free,
|
||||
|
||||
/// <summary>
|
||||
/// If it's not a KeepAlive connection, or something happened, then we close this connection and remove from the pool.
|
||||
/// </summary>
|
||||
Closed,
|
||||
|
||||
/// <summary>
|
||||
/// Same as the Closed state, but processing this request requires resending the last processed request too.
|
||||
/// </summary>
|
||||
ClosedResendRequest
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edafbf4cb6d32074a96a4953d3991946
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,457 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
using Best.HTTP.Hosts.Connections.HTTP2;
|
||||
#endif
|
||||
|
||||
using Best.HTTP.Hosts.Connections.HTTP1;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.PlatformSupport.Threading;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
// DNS -> TCP -> [ Proxy ] -> [ BC TLS | Framework TLS ] -> (HTTP/1 | HTTP/2)
|
||||
|
||||
/// <summary>
|
||||
/// Represents and manages a connection to a server.
|
||||
/// </summary>
|
||||
public sealed class HTTPOverTCPConnection : ConnectionBase, INegotiationPeer, IContentConsumer
|
||||
{
|
||||
public PeekableContentProviderStream TopStream { get => this._negotiator.Stream; }
|
||||
public TCPStreamer Streamer { get => this._negotiator.Streamer; }
|
||||
|
||||
public IHTTPRequestHandler requestHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Number of assigned requests to process.
|
||||
/// </summary>
|
||||
public override int AssignedRequests { get => this.requestHandler != null ? this.requestHandler.AssignedRequests : base.AssignedRequests; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of assignable requests.
|
||||
/// </summary>
|
||||
public override int MaxAssignedRequests { get => this.requestHandler != null ? this.requestHandler.MaxAssignedRequests : base.MaxAssignedRequests; }
|
||||
|
||||
public override TimeSpan KeepAliveTime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.requestHandler != null && this.requestHandler.KeepAlive != null)
|
||||
{
|
||||
if (this.requestHandler.KeepAlive.MaxRequests > 0)
|
||||
{
|
||||
if (base.KeepAliveTime < this.requestHandler.KeepAlive.TimeOut)
|
||||
return base.KeepAliveTime;
|
||||
else
|
||||
return this.requestHandler.KeepAlive.TimeOut;
|
||||
}
|
||||
else
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
return base.KeepAliveTime;
|
||||
}
|
||||
|
||||
protected set
|
||||
{
|
||||
base.KeepAliveTime = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanProcessMultiple
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.requestHandler != null)
|
||||
return this.requestHandler.CanProcessMultiple;
|
||||
return base.CanProcessMultiple;
|
||||
}
|
||||
}
|
||||
|
||||
PeekableContentProviderStream IContentConsumer.ContentProvider { get; }
|
||||
|
||||
private Negotiator _negotiator;
|
||||
private NegotiationSteps _lastStep;
|
||||
|
||||
internal HTTPOverTCPConnection(HostKey hostKey)
|
||||
: base(hostKey)
|
||||
{ }
|
||||
|
||||
internal override void Process(HTTPRequest request)
|
||||
{
|
||||
this.LastProcessedUri = request.CurrentUri;
|
||||
this.CurrentRequest = request;
|
||||
this.CurrentRequest.Context.Add("Connection", this.Context.Hash);
|
||||
|
||||
this.State = HTTPConnectionStates.Processing;
|
||||
|
||||
if (this.requestHandler == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
NegotiationParameters parameters = new NegotiationParameters();
|
||||
parameters.context = this.Context;
|
||||
parameters.proxy = CurrentRequest.ProxySettings.Proxy;
|
||||
parameters.targetUri = CurrentRequest.CurrentUri;
|
||||
parameters.negotiateTLS = HTTPProtocolFactory.IsSecureProtocol(CurrentRequest.CurrentUri);
|
||||
parameters.token = CurrentRequest.CancellationTokenSource.Token;
|
||||
|
||||
//parameters.tryToKeepAlive = HTTPManager.PerHostSettings.Get(CurrentRequest.CurrentUri.Host).HTTP1ConnectionSettings.TryToReuseConnections;
|
||||
|
||||
parameters.hostSettings = HTTPManager.PerHostSettings.Get(CurrentRequest.CurrentUri.Host);
|
||||
|
||||
this._negotiator = new Negotiator(this, parameters);
|
||||
this._negotiator.Start();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(HTTPOverTCPConnection), $"Process({request})", ex, this.Context);
|
||||
TrySetErrorState(request, ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.requestHandler.Process(request);
|
||||
LastProcessTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
List<string> INegotiationPeer.GetSupportedProtocolNames(Negotiator negotiator)
|
||||
{
|
||||
List<string> protocols = new List<string>();
|
||||
|
||||
SupportedProtocols protocol = HTTPProtocolFactory.GetProtocolFromUri(negotiator.Parameters.targetUri);
|
||||
|
||||
#if !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
if (protocol == SupportedProtocols.HTTP && negotiator.Parameters.hostSettings.HTTP2ConnectionSettings.EnableHTTP2Connections)
|
||||
{
|
||||
// http/2 over tls (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
|
||||
protocols.Add(HTTPProtocolFactory.W3C_HTTP2);
|
||||
}
|
||||
#endif
|
||||
|
||||
protocols.Add(HTTPProtocolFactory.W3C_HTTP1);
|
||||
|
||||
return protocols;
|
||||
}
|
||||
|
||||
bool INegotiationPeer.MustStopAdvancingToNextStep(Negotiator negotiator, NegotiationSteps finishedStep, NegotiationSteps nextStep, Exception error)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(INegotiationPeer.MustStopAdvancingToNextStep)}({negotiator}, {finishedStep}, {nextStep}, {error})", this.Context);
|
||||
|
||||
_lastStep = finishedStep;
|
||||
|
||||
if (TrySetErrorState(CurrentRequest, error))
|
||||
return true;
|
||||
|
||||
switch (finishedStep)
|
||||
{
|
||||
case NegotiationSteps.Start:
|
||||
this.LastProcessTime = DateTime.UtcNow;
|
||||
|
||||
this.CurrentRequest.Timing.StartNext(TimingEventNames.DNS_Lookup);
|
||||
break;
|
||||
|
||||
case NegotiationSteps.DNSQuery:
|
||||
this.CurrentRequest.Timing.StartNext(TimingEventNames.TCP_Connection);
|
||||
break;
|
||||
|
||||
case NegotiationSteps.TCPRace:
|
||||
CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
||||
|
||||
switch(nextStep)
|
||||
{
|
||||
case NegotiationSteps.Proxy:
|
||||
CurrentRequest.Timing.StartNext(TimingEventNames.Proxy_Negotiation);
|
||||
break;
|
||||
case NegotiationSteps.TLSNegotiation:
|
||||
CurrentRequest.Timing.StartNext(TimingEventNames.TLS_Negotiation);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case NegotiationSteps.Proxy:
|
||||
if (nextStep == NegotiationSteps.TLSNegotiation)
|
||||
CurrentRequest.Timing.StartNext(TimingEventNames.TLS_Negotiation);
|
||||
break;
|
||||
|
||||
case NegotiationSteps.TLSNegotiation:
|
||||
break;
|
||||
|
||||
case NegotiationSteps.Finish:
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void INegotiationPeer.EvaluateProxyNegotiationFailure(Negotiator negotiator, Exception error, bool resendForAuthentication)
|
||||
{
|
||||
if (resendForAuthentication && !this.TrySetErrorState(CurrentRequest, null))
|
||||
{
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(CurrentRequest, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
else if (!this.TrySetErrorState(CurrentRequest, error))
|
||||
{
|
||||
// TODO: what?
|
||||
}
|
||||
}
|
||||
|
||||
void INegotiationPeer.OnNegotiationFailed(Negotiator negotiator, Exception error)
|
||||
{
|
||||
PreprocessRequestState(error);
|
||||
}
|
||||
|
||||
void INegotiationPeer.OnNegotiationFinished(Negotiator negotiator, PeekableContentProviderStream stream, TCPStreamer streamer, string negotiatedProtocol)
|
||||
{
|
||||
if (!PreprocessRequestState(null))
|
||||
StartWithNegotiatedProtocol(negotiatedProtocol, stream);
|
||||
}
|
||||
|
||||
private void OnCancellationRequested(HTTPRequest req)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(OnCancellationRequested)}({_lastStep}, {req}, {this._negotiator?.Peer}, {this._negotiator?.Streamer}, {this._negotiator?.Stream})", this.Context);
|
||||
|
||||
CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
this._negotiator?.OnCancellationRequested();
|
||||
|
||||
if (_lastStep < NegotiationSteps.TLSNegotiation)
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
private bool PreprocessRequestState(Exception error)
|
||||
{
|
||||
CurrentRequest.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"PreprocessRequestState({CurrentRequest}, {error})", this.Context);
|
||||
|
||||
// OnTLSNegotiated might get called _after_ the request is aborted. In this case, we must not set its State!
|
||||
// So here we have to check its State, if it's one of the Finished state (Finished, Error, etc.) we have to quit early and only enqueue a connection event.
|
||||
if (CurrentRequest.State >= HTTPRequestStates.Finished)
|
||||
{
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
return true;
|
||||
}
|
||||
|
||||
return TrySetErrorState(CurrentRequest, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an error state is set to the request and the connection is closing.
|
||||
/// </summary>
|
||||
bool TrySetErrorState(HTTPRequest request, Exception ex)
|
||||
{
|
||||
// Check wether the request is already in a finshed state.
|
||||
// For example it can happen in the following case:
|
||||
// 1.) HTTP proxy sends out a CONNECT request to the proxy
|
||||
// 2.) Request times out and RequestEventHelper.AbortRequestWhenTimedOut is called
|
||||
// 2.a) Request's state set to ConnectionTimedOut
|
||||
// 3.) Request's callback is called
|
||||
// 4.) Either the Proxy connects or fails to connect to the remote host, but one of the first call in the callbacks is TrySetErrorState,
|
||||
// where we would try to set the request's State. If we would set a different state (like Error or TimedOut) than the one we already set (ConnectionTimedOut in this specific case)
|
||||
// then a new RequestEvents.StateChange event would be queued up and resulting in a new calling the request's callback again!
|
||||
if (request.State >= HTTPRequestStates.Finished)
|
||||
{
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
request.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
ConnectionHelper.EnqueueEvents(this,
|
||||
HTTPConnectionStates.Closed,
|
||||
request,
|
||||
ex is TimeoutException ? HTTPRequestStates.ConnectionTimedOut : HTTPRequestStates.Error,
|
||||
ex is TimeoutException ? (Exception)null : ex);
|
||||
return true;
|
||||
}
|
||||
else if (request.TimeoutSettings.IsConnectTimedOut(DateTime.UtcNow))
|
||||
{
|
||||
TrySetErrorState(request, new TimeoutException("request.IsConnectTimedOut"));
|
||||
return true;
|
||||
}
|
||||
else if (request.IsCancellationRequested)
|
||||
{
|
||||
ConnectionHelper.EnqueueEvents(this, HTTPConnectionStates.Closed, request, HTTPRequestStates.Aborted, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void StartWithNegotiatedProtocol(string negotiatedProtocol, PeekableContentProviderStream stream)
|
||||
{
|
||||
this.CurrentRequest.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
if (string.IsNullOrEmpty(negotiatedProtocol))
|
||||
negotiatedProtocol = HTTPProtocolFactory.W3C_HTTP1;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"Negotiated protocol through ALPN: '{negotiatedProtocol}'", this.Context);
|
||||
|
||||
bool useShortLivingThread = false;
|
||||
switch (negotiatedProtocol)
|
||||
{
|
||||
case HTTPProtocolFactory.W3C_HTTP1:
|
||||
var http1Consumer = new HTTP1ContentConsumer(this);
|
||||
|
||||
this.requestHandler = http1Consumer;
|
||||
stream.SetTwoWayBinding(http1Consumer);
|
||||
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HostProtocolSupport.HTTP1));
|
||||
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/179
|
||||
// Thoughts:
|
||||
// - Many requests, especially if they are uploading slowly, can occupy all background threads.
|
||||
// Use short-living thread when:
|
||||
// - It's a GET request
|
||||
// - The negotiated protocol is equal to HTTP/1.1
|
||||
// - It's not an upgrade request
|
||||
|
||||
bool isRequestWithoutBody = this.CurrentRequest.MethodType == HTTPMethods.Get ||
|
||||
this.CurrentRequest.MethodType == HTTPMethods.Head ||
|
||||
this.CurrentRequest.MethodType == HTTPMethods.Delete ||
|
||||
this.CurrentRequest.MethodType == HTTPMethods.Options;
|
||||
bool isUpgrade = this.CurrentRequest.HasHeader("upgrade");
|
||||
useShortLivingThread = HTTPManager.PerHostSettings.Get(this.HostKey.Host).HTTP1ConnectionSettings.ForceUseThreadPool ||
|
||||
(isRequestWithoutBody && !isUpgrade);
|
||||
break;
|
||||
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
case HTTPProtocolFactory.W3C_HTTP2:
|
||||
var http2Consumer = new HTTP2ContentConsumer(this);
|
||||
|
||||
this.requestHandler = http2Consumer;
|
||||
stream.SetTwoWayBinding(http2Consumer);
|
||||
|
||||
this.CurrentRequest = null;
|
||||
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HostProtocolSupport.HTTP2));
|
||||
break;
|
||||
#endif
|
||||
|
||||
default:
|
||||
HTTPManager.Logger.Error(nameof(HTTPOverTCPConnection), $"Unknown negotiated protocol: {negotiatedProtocol}", this.Context);
|
||||
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(CurrentRequest, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestHandler.Context.Add("Connection", this.Context.GetStringField("Hash"));
|
||||
this.Context.Add("RequestHandler", this.requestHandler.Context.GetStringField("Hash"));
|
||||
|
||||
LastProcessTime = DateTime.UtcNow;
|
||||
if (IsThreaded)
|
||||
{
|
||||
if (useShortLivingThread)
|
||||
ThreadedRunner.RunShortLiving(ThreadFunc);
|
||||
else
|
||||
ThreadedRunner.RunLongLiving(ThreadFunc);
|
||||
}
|
||||
else
|
||||
ThreadFunc();
|
||||
}
|
||||
|
||||
protected override void ThreadFunc()
|
||||
{
|
||||
this.requestHandler.RunHandler();
|
||||
}
|
||||
|
||||
public override void Shutdown(ShutdownTypes type)
|
||||
{
|
||||
base.Shutdown(type);
|
||||
|
||||
if (this.requestHandler != null)
|
||||
this.requestHandler.Shutdown(type);
|
||||
else
|
||||
{
|
||||
// if the request handler is null, we can't do a gentle shutdown.
|
||||
this._negotiator?.Streamer?.Close();
|
||||
}
|
||||
|
||||
switch (this.ShutdownType)
|
||||
{
|
||||
case ShutdownTypes.Immediate:
|
||||
this._negotiator.Stream?.Dispose();
|
||||
break;
|
||||
|
||||
//case ShutdownTypes.Gentle:
|
||||
// this._streamer?.Close();
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
LastProcessedUri = null;
|
||||
if (this.State != HTTPConnectionStates.WaitForProtocolShutdown)
|
||||
{
|
||||
this._negotiator?.Stream?.Dispose();
|
||||
|
||||
if (this.requestHandler != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.requestHandler.Dispose();
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
this.requestHandler = null;
|
||||
}
|
||||
|
||||
this._negotiator?.Streamer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
void IContentConsumer.SetBinding(PeekableContentProviderStream contentProvider)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.SetBinding)}({contentProvider})", this.Context);
|
||||
}
|
||||
|
||||
void IContentConsumer.UnsetBinding()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.UnsetBinding)}()", this.Context);
|
||||
}
|
||||
|
||||
void IContentConsumer.OnContent()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.OnContent)}()", this.Context);
|
||||
}
|
||||
|
||||
void IContentConsumer.OnConnectionClosed()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.OnConnectionClosed)}()", this.Context);
|
||||
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
void IContentConsumer.OnError(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPOverTCPConnection), $"{nameof(IContentConsumer.OnError)}({ex})", this.Context);
|
||||
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d070df829d416b44a01888044a6c316
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
public enum SupportedProtocols
|
||||
{
|
||||
Unknown,
|
||||
HTTP,
|
||||
|
||||
WebSocket,
|
||||
|
||||
ServerSentEvents
|
||||
}
|
||||
|
||||
public static class HTTPProtocolFactory
|
||||
{
|
||||
public const string W3C_HTTP1 = "http/1.1";
|
||||
#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL
|
||||
public const string W3C_HTTP2 = "h2";
|
||||
#endif
|
||||
|
||||
public static SupportedProtocols GetProtocolFromUri(Uri uri)
|
||||
{
|
||||
if (uri == null || uri.Scheme == null)
|
||||
throw new Exception("Malformed URI in GetProtocolFromUri");
|
||||
|
||||
string scheme = uri.Scheme.ToLowerInvariant();
|
||||
switch (scheme)
|
||||
{
|
||||
case "ws":
|
||||
case "wss":
|
||||
return SupportedProtocols.WebSocket;
|
||||
|
||||
default:
|
||||
return SupportedProtocols.HTTP;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSecureProtocol(Uri uri)
|
||||
{
|
||||
if (uri == null || uri.Scheme == null)
|
||||
throw new Exception("Malformed URI in IsSecureProtocol");
|
||||
|
||||
string scheme = uri.Scheme.ToLowerInvariant();
|
||||
switch (scheme)
|
||||
{
|
||||
// http
|
||||
case "https":
|
||||
|
||||
// WebSocket
|
||||
case "wss":
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bb1fcd8332d626418af763bd17dcde8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using Best.HTTP.Response;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an interface for notifying connections when space becomes available in a buffer for downloading data.
|
||||
/// Connections implementating of this interface are used to signal their internal logic that they can transfer data into the available buffer space.
|
||||
/// </summary>
|
||||
public interface IDownloadContentBufferAvailable
|
||||
{
|
||||
/// <summary>
|
||||
/// Notifies a connection that space has become available in the buffer for downloading data.
|
||||
/// When invoked, this method indicates to a connection that it can transfer additional data into the buffer for further processing.
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="DownloadContentStream"/> instance associated with the buffer.</param>
|
||||
void BufferAvailable(DownloadContentStream stream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78437af7092039f4aa757f40b4551f6e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,43 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Common interface for implementations that will coordinate request processing inside a connection.
|
||||
/// </summary>
|
||||
public interface IHTTPRequestHandler : IDisposable
|
||||
{
|
||||
KeepAliveHeader KeepAlive { get; }
|
||||
|
||||
bool CanProcessMultiple { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of assigned requests to process.
|
||||
/// </summary>
|
||||
int AssignedRequests { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of assignable requests.
|
||||
/// </summary>
|
||||
int MaxAssignedRequests { get; }
|
||||
|
||||
ShutdownTypes ShutdownType { get; }
|
||||
|
||||
LoggingContext Context { get; }
|
||||
|
||||
void Process(HTTPRequest request);
|
||||
|
||||
void RunHandler();
|
||||
|
||||
/// <summary>
|
||||
/// An immediate shutdown request that called only on application closure.
|
||||
/// </summary>
|
||||
void Shutdown(ShutdownTypes type);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8e38e7e11973324eba759c722fd904b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
public enum SignalHandlerTypes
|
||||
{
|
||||
Signal,
|
||||
InlineIfPossible
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for signaling upload threads.
|
||||
/// </summary>
|
||||
public interface IThreadSignaler
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="LoggingContext"/> instance for debugging purposes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To help <see cref="Best.HTTP.Request.Upload.UploadStreamBase"/> implementors log in the IThreadSignaler's context,
|
||||
/// the interface implementors must make their logging context accessible.
|
||||
/// </remarks>
|
||||
public LoggingContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Signals the associated thread to resume or wake up.
|
||||
/// </summary>
|
||||
void SignalThread(SignalHandlerTypes signalType = SignalHandlerTypes.Signal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77193f8334848e0409e39d3b4c42a422
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,654 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Caching;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Settings;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections
|
||||
{
|
||||
public enum RequestEvents
|
||||
{
|
||||
Upgraded,
|
||||
DownloadProgress,
|
||||
UploadProgress,
|
||||
StreamingData,
|
||||
DownloadStarted,
|
||||
StateChange,
|
||||
SetState,
|
||||
QueuedResend,
|
||||
Resend,
|
||||
Headers,
|
||||
Timing,
|
||||
}
|
||||
|
||||
public readonly struct RequestEventInfo
|
||||
{
|
||||
public readonly HTTPRequest SourceRequest;
|
||||
public readonly RequestEvents Event;
|
||||
|
||||
public readonly HTTPRequestStates State;
|
||||
|
||||
public readonly Exception Error;
|
||||
|
||||
public readonly long Progress;
|
||||
public readonly long ProgressLength;
|
||||
|
||||
public readonly byte[] Data;
|
||||
public readonly int DataLength;
|
||||
|
||||
public readonly TimingEventInfo timingEvent;
|
||||
|
||||
// Headers
|
||||
public readonly Dictionary<string, List<string>> Headers;
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, RequestEvents @event)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = @event;
|
||||
|
||||
this.State = HTTPRequestStates.Initial;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, RequestEvents @event, HTTPRequestStates newState)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = @event;
|
||||
|
||||
this.State = newState;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = RequestEvents.StateChange;
|
||||
this.State = newState;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState, Exception error)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = RequestEvents.SetState;
|
||||
this.State = newState;
|
||||
|
||||
this.Error = error;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, RequestEvents @event, long progress, long progressLength)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = @event;
|
||||
this.State = HTTPRequestStates.Initial;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = progress;
|
||||
this.ProgressLength = progressLength;
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, byte[] data, int dataLength)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = RequestEvents.StreamingData;
|
||||
this.State = HTTPRequestStates.Initial;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
this.Data = data;
|
||||
this.DataLength = dataLength;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, Dictionary<string, List<string>> headers)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = RequestEvents.Headers;
|
||||
this.State = HTTPRequestStates.Initial;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = headers;
|
||||
|
||||
this.timingEvent = default;
|
||||
}
|
||||
|
||||
public RequestEventInfo(HTTPRequest request, TimingEventInfo timingEvent)
|
||||
{
|
||||
this.SourceRequest = request;
|
||||
this.Event = RequestEvents.Timing;
|
||||
this.State = HTTPRequestStates.Initial;
|
||||
|
||||
this.Error = null;
|
||||
|
||||
this.Progress = this.ProgressLength = 0;
|
||||
this.Data = null;
|
||||
this.DataLength = 0;
|
||||
|
||||
// Headers
|
||||
this.Headers = null;
|
||||
|
||||
this.timingEvent = timingEvent;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
switch (this.Event)
|
||||
{
|
||||
case RequestEvents.Upgraded:
|
||||
return string.Format("[RequestEventInfo Event: Upgraded, Source: {0}]", this.SourceRequest.Context.Hash);
|
||||
case RequestEvents.DownloadProgress:
|
||||
return string.Format("[RequestEventInfo Event: DownloadProgress, Progress: {1}, ProgressLength: {2}, Source: {0}]", this.SourceRequest.Context.Hash, this.Progress, this.ProgressLength);
|
||||
case RequestEvents.UploadProgress:
|
||||
return string.Format("[RequestEventInfo Event: UploadProgress, Progress: {1}, ProgressLength: {2}, Source: {0}]", this.SourceRequest.Context.Hash, this.Progress, this.ProgressLength);
|
||||
case RequestEvents.StreamingData:
|
||||
return string.Format("[RequestEventInfo Event: StreamingData, DataLength: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.DataLength);
|
||||
case RequestEvents.DownloadStarted:
|
||||
return $"[RequestEventInfo Event: DownloadStarted, Source: {this.SourceRequest.Context.Hash}]";
|
||||
case RequestEvents.StateChange:
|
||||
return string.Format("[RequestEventInfo Event: StateChange, State: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.State);
|
||||
case RequestEvents.SetState:
|
||||
return string.Format("[RequestEventInfo Event: SetState, State: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.State);
|
||||
case RequestEvents.Resend:
|
||||
return string.Format("[RequestEventInfo Event: Resend, Source: {0}]", this.SourceRequest.Context.Hash);
|
||||
case RequestEvents.Headers:
|
||||
return string.Format("[RequestEventInfo Event: Headers, Source: {0}]", this.SourceRequest.Context.Hash);
|
||||
case RequestEvents.QueuedResend:
|
||||
return $"[RequestEventInfo Event: QueuedResend, Source: {this.SourceRequest.Context.Hash}]";
|
||||
case RequestEvents.Timing:
|
||||
return $"[RequestEventInfo Event: Timing, TimingEvent: {this.timingEvent}, Source:{this.SourceRequest.Context.Hash}]";
|
||||
default:
|
||||
throw new NotImplementedException(this.Event.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ProgressFlattener
|
||||
{
|
||||
struct FlattenedProgress
|
||||
{
|
||||
public HTTPRequest request;
|
||||
public OnProgressDelegate onProgress;
|
||||
public long progress;
|
||||
public long length;
|
||||
}
|
||||
|
||||
private FlattenedProgress[] progresses;
|
||||
private bool hasProgress;
|
||||
|
||||
public void InsertOrUpdate(RequestEventInfo info, OnProgressDelegate onProgress)
|
||||
{
|
||||
if (progresses == null)
|
||||
progresses = new FlattenedProgress[1];
|
||||
|
||||
hasProgress = true;
|
||||
|
||||
var newProgress = new FlattenedProgress { request = info.SourceRequest, progress = info.Progress, length = info.ProgressLength, onProgress = onProgress };
|
||||
|
||||
int firstEmptyIdx = -1;
|
||||
for (int i = 0; i < progresses.Length; i++)
|
||||
{
|
||||
var progress = progresses[i];
|
||||
if (object.ReferenceEquals(progress.request, info.SourceRequest))
|
||||
{
|
||||
progresses[i] = newProgress;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstEmptyIdx == -1 && progress.request == null)
|
||||
firstEmptyIdx = i;
|
||||
}
|
||||
|
||||
if (firstEmptyIdx == -1)
|
||||
{
|
||||
Array.Resize(ref progresses, progresses.Length + 1);
|
||||
progresses[progresses.Length - 1] = newProgress;
|
||||
}
|
||||
else
|
||||
progresses[firstEmptyIdx] = newProgress;
|
||||
}
|
||||
|
||||
public void DispatchProgressCallbacks()
|
||||
{
|
||||
if (progresses == null || !hasProgress)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < progresses.Length; ++i)
|
||||
{
|
||||
var @event = progresses[i];
|
||||
var source = @event.request;
|
||||
if (source != null && @event.onProgress != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@event.onProgress(source, @event.progress, @event.length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("ProgressFlattener", "DispatchProgressCallbacks", ex, source.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Array.Clear(progresses, 0, progresses.Length);
|
||||
hasProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RequestEventHelper
|
||||
{
|
||||
private static ConcurrentQueue<RequestEventInfo> requestEventQueue = new ConcurrentQueue<RequestEventInfo>();
|
||||
|
||||
#pragma warning disable 0649
|
||||
public static Action<RequestEventInfo> OnEvent;
|
||||
#pragma warning restore
|
||||
|
||||
// Low frame rate and high download/upload speed can add more download/upload progress events to dispatch in one frame.
|
||||
// This can add higher CPU usage as it might cause updating the UI/do other things unnecessary in the same frame.
|
||||
// To avoid this, instead of calling the events directly, we store the last event's data and call download/upload callbacks only once per frame.
|
||||
|
||||
private static ProgressFlattener downloadProgress;
|
||||
private static ProgressFlattener uploadProgress;
|
||||
|
||||
private static List<HTTPRequest> registeredRequests = new List<HTTPRequest>();
|
||||
|
||||
public static void EnqueueRequestEvent(RequestEventInfo ev)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("RequestEventHelper", "Enqueue " + ev.ToString(), ev.SourceRequest.Context);
|
||||
|
||||
requestEventQueue.Enqueue(ev);
|
||||
}
|
||||
|
||||
internal static void Clear()
|
||||
{
|
||||
requestEventQueue.Clear();
|
||||
}
|
||||
|
||||
internal static void RegisterRequest(HTTPRequest request) => registeredRequests.Add(request);
|
||||
|
||||
internal static void AbortAllRequests()
|
||||
{
|
||||
foreach (var request in registeredRequests)
|
||||
request.Abort();
|
||||
}
|
||||
|
||||
internal static bool UnregisterRequest(HTTPRequest request) => registeredRequests.Remove(request);
|
||||
internal static void RemoveAllRegisteredRequests() => registeredRequests.Clear();
|
||||
|
||||
internal static void ProcessQueue()
|
||||
{
|
||||
RequestEventInfo requestEvent;
|
||||
while (requestEventQueue.TryDequeue(out requestEvent))
|
||||
{
|
||||
HTTPRequest source = requestEvent.SourceRequest;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("RequestEventHelper", "Processing request event: " + requestEvent.ToString(), source.Context);
|
||||
|
||||
if (OnEvent != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var _ = new Unity.Profiling.ProfilerMarker(nameof(OnEvent)).Auto())
|
||||
OnEvent(requestEvent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "ProcessQueue", ex, source.Context);
|
||||
}
|
||||
}
|
||||
|
||||
switch (requestEvent.Event)
|
||||
{
|
||||
case RequestEvents.DownloadProgress:
|
||||
try
|
||||
{
|
||||
if (source.DownloadSettings.OnDownloadProgress != null)
|
||||
{
|
||||
if (downloadProgress == null)
|
||||
downloadProgress = new ProgressFlattener();
|
||||
downloadProgress.InsertOrUpdate(requestEvent, source.DownloadSettings.OnDownloadProgress);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.DownloadProgress", ex, source.Context);
|
||||
}
|
||||
break;
|
||||
|
||||
case RequestEvents.UploadProgress:
|
||||
try
|
||||
{
|
||||
if (source.UploadSettings.OnUploadProgress != null)
|
||||
{
|
||||
if (uploadProgress == null)
|
||||
uploadProgress = new ProgressFlattener();
|
||||
uploadProgress.InsertOrUpdate(requestEvent, source.UploadSettings.OnUploadProgress);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.UploadProgress", ex, source.Context);
|
||||
}
|
||||
break;
|
||||
|
||||
case RequestEvents.QueuedResend:
|
||||
if (source.IsCancellationRequested || HTTPManager.IsQuitting)
|
||||
break;
|
||||
|
||||
HandleQueued(source);
|
||||
goto case RequestEvents.Resend;
|
||||
|
||||
case RequestEvents.Resend:
|
||||
source.State = HTTPRequestStates.Initial;
|
||||
|
||||
var host = HostManager.GetHostVariant(source);
|
||||
|
||||
host.Send(source);
|
||||
break;
|
||||
|
||||
case RequestEvents.Headers:
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = source.Response;
|
||||
if (source.DownloadSettings.OnHeadersReceived != null && response != null)
|
||||
source.DownloadSettings.OnHeadersReceived(source, response, requestEvent.Headers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Headers", ex, source.Context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RequestEvents.DownloadStarted:
|
||||
try
|
||||
{
|
||||
// It's possible that response.DownStream is already null when this event is handled!
|
||||
|
||||
var response = source.Response;
|
||||
var downStream = response?.DownStream;
|
||||
|
||||
if (response != null && downStream != null)
|
||||
source.DownloadSettings.OnDownloadStarted?.Invoke(source, response, response.DownStream);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "DownloadStarted", ex, source.Context);
|
||||
}
|
||||
break;
|
||||
|
||||
case RequestEvents.Timing:
|
||||
requestEvent.timingEvent.SourceRequest.Timing.AddEvent(requestEvent.timingEvent);
|
||||
break;
|
||||
|
||||
case RequestEvents.SetState:
|
||||
// In a case where the request is aborted its state is set to a >= Finished state then,
|
||||
// on another thread the request processing will fail too queuing up a >= Finished state again.
|
||||
if (source.State >= HTTPRequestStates.Finished && requestEvent.State >= HTTPRequestStates.Finished)
|
||||
continue;
|
||||
|
||||
// It's different from the next condition! (this is >= and the next is only >)
|
||||
if (requestEvent.State >= HTTPRequestStates.Finished)
|
||||
source?.Response?.DownStream?.CompleteAdding(requestEvent.Error);
|
||||
|
||||
if (requestEvent.State > HTTPRequestStates.Finished)
|
||||
{
|
||||
HTTPManager.Logger.Information("RequestEventHelper", $"{requestEvent.State}: discarding response!", source.Response?.Context ?? source.Context);
|
||||
|
||||
source.Response?.Dispose();
|
||||
source.Response = null;
|
||||
}
|
||||
|
||||
source.Exception = requestEvent.Error;
|
||||
source.State = requestEvent.State;
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc5861.html#section-1
|
||||
// The stale-if-error HTTP Cache-Control extension allows a cache to
|
||||
// return a stale response when an error -- e.g., a 500 Internal Server
|
||||
// Error, a network segment, or DNS failure -- is encountered, rather
|
||||
// than returning a "hard" error.
|
||||
if (requestEvent.State > HTTPRequestStates.Finished && requestEvent.State != HTTPRequestStates.Aborted)
|
||||
{
|
||||
if (HTTPManager.LocalCache != null && !source.DownloadSettings.DisableCache)
|
||||
{
|
||||
var hash = Caching.HTTPCache.CalculateHash(source.MethodType, source.CurrentUri);
|
||||
if (HTTPManager.LocalCache.CanServeWithoutValidation(hash, ErrorTypeForValidation.ConnectionError, source.Context))
|
||||
{
|
||||
HTTPManager.LocalCache.Redirect(source, hash);
|
||||
goto case RequestEvents.Resend;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goto case RequestEvents.StateChange;
|
||||
|
||||
case RequestEvents.StateChange:
|
||||
try
|
||||
{
|
||||
using (var _ = new Unity.Profiling.ProfilerMarker(nameof(RequestEventHelper.HandleRequestStateChange)).Auto())
|
||||
RequestEventHelper.HandleRequestStateChange(ref requestEvent);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange", ex, source.Context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uploadProgress?.DispatchProgressCallbacks();
|
||||
downloadProgress?.DispatchProgressCallbacks();
|
||||
}
|
||||
|
||||
// TODO: don't start/repeat if can't time out?
|
||||
private static bool AbortRequestWhenTimedOut(DateTime now, object context)
|
||||
{
|
||||
HTTPRequest request = context as HTTPRequest;
|
||||
|
||||
if (request.State >= HTTPRequestStates.Finished || request.IsCancellationRequested)
|
||||
return false; // don't repeat
|
||||
|
||||
var downStream = request.Response?.DownStream;
|
||||
|
||||
if (downStream != null && downStream.DoFullCheck(limit: 2))
|
||||
{
|
||||
var warning = $"Request's download stream is full({downStream.Length:N0}/{downStream.MaxBuffered:N0}) without any Read attempt! You can either increase HTTPRequest's DownloadSettings.ContentStreamMaxBuffered or use streaming. Request's uri: {request.Uri}. See https://bestdocshub.pages.dev/HTTP/getting-started/downloads/ for more details!";
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Warning(nameof(RequestEventHelper), warning, request.Context);
|
||||
else
|
||||
UnityEngine.Debug.Log(warning);
|
||||
|
||||
// increase buffer limit
|
||||
downStream.EmergencyIncreaseMaxBuffered();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Upgradable protocols will shut down themselves
|
||||
if (request?.Response?.IsUpgraded is bool upgraded && upgraded)
|
||||
return false;
|
||||
|
||||
if (request.TimeoutSettings.IsTimedOut(HTTPManager.CurrentFrameDateTime))
|
||||
{
|
||||
HTTPManager.Logger.Information("RequestEventHelper", "AbortRequestWhenTimedOut - Request timed out. CurrentUri: " + request.CurrentUri.ToString(), request.Context);
|
||||
request.Abort();
|
||||
|
||||
return false; // don't repeat
|
||||
}
|
||||
|
||||
return true; // repeat
|
||||
}
|
||||
|
||||
private static void HandleQueued(HTTPRequest source)
|
||||
{
|
||||
source.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
source.TimeoutSettings.QueuedAt = HTTPManager.CurrentFrameDateTime;
|
||||
Timer.Add(new TimerData(TimeSpan.FromSeconds(1), source, AbortRequestWhenTimedOut));
|
||||
}
|
||||
|
||||
static readonly string[] RequestStateNames = new string[] { "Initial", "Queued", "Processing", "Finished", "Error", "Aborted", "ConnectionTimedOut", "TimedOut" };
|
||||
|
||||
private static void HandleRequestStateChange(ref RequestEventInfo @event)
|
||||
{
|
||||
HTTPRequest source = @event.SourceRequest;
|
||||
|
||||
// Because there's a race condition between setting the request's State in its Abort() function running on Unity's main thread
|
||||
// and the HTTP1/HTTP2 handlers running on an another one.
|
||||
// Because of these race conditions cases violating expectations can be:
|
||||
// 1.) State is finished but the response null
|
||||
// 2.) State is (Connection)TimedOut and the response non-null
|
||||
// We have to make sure that no callbacks are called twice and in the request must be in a consistent state!
|
||||
|
||||
// State | Request
|
||||
// --------- +---------
|
||||
// 1 Null
|
||||
// Finished | Skip
|
||||
// Timeout/Abort | Deliver
|
||||
//
|
||||
// 2 Non-Null
|
||||
// Finished | Deliver
|
||||
// Timeout/Abort | Skip
|
||||
|
||||
using var _ = new Unity.Profiling.ProfilerMarker(RequestStateNames[(int)@event.State]).Auto();
|
||||
|
||||
switch (@event.State)
|
||||
{
|
||||
case HTTPRequestStates.Queued:
|
||||
HandleQueued(source);
|
||||
break;
|
||||
|
||||
case HTTPRequestStates.ConnectionTimedOut:
|
||||
case HTTPRequestStates.TimedOut:
|
||||
case HTTPRequestStates.Error:
|
||||
case HTTPRequestStates.Aborted:
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("RequestEventHelper", $"{@event.State}: discarding response!", source.Response?.Context ?? source.Context);
|
||||
|
||||
source.Response?.Dispose();
|
||||
source.Response = null;
|
||||
goto case HTTPRequestStates.Finished;
|
||||
|
||||
case HTTPRequestStates.Finished:
|
||||
// Dispatch any collected download/upload progress, otherwise they would _after_ the callback!
|
||||
uploadProgress?.DispatchProgressCallbacks();
|
||||
downloadProgress?.DispatchProgressCallbacks();
|
||||
|
||||
if (source.Callback != null)
|
||||
{
|
||||
source.Timing.AddEvent(new TimingEventInfo(source, TimingEvents.StartNext, TimingEventNames.Callback));
|
||||
try
|
||||
{
|
||||
if (registeredRequests.Contains(source))
|
||||
{
|
||||
using (var __ = new Unity.Profiling.ProfilerMarker(nameof(source.Callback)).Auto())
|
||||
source.Callback(source, source.Response);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange " + @event.State, ex, source.Context);
|
||||
}
|
||||
}
|
||||
|
||||
source.Timing.AddEvent(new TimingEventInfo(source, TimingEvents.Finish, null));
|
||||
|
||||
if (source.Callback == null)
|
||||
{
|
||||
// This delay required because with coroutines these lines are executed first
|
||||
// before the coroutine has a chance to do something with a finished request.
|
||||
// By adding a delay there's a time window that the coroutine can run its logic too inbetween.
|
||||
Timer.Add(new TimerData(TimeSpan.FromSeconds(1), source, OnDelayedDisposeTimer));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("RequestEventHelper", "disposing response!", source.Context);
|
||||
source.Dispose();
|
||||
}
|
||||
|
||||
HostManager.GetHostVariant(source)
|
||||
.TryToSendQueuedRequests();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool OnDelayedDisposeTimer(DateTime time, object request)
|
||||
{
|
||||
var source = request as HTTPRequest;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("RequestEventHelper", $"{nameof(OnDelayedDisposeTimer)} - disposing response!", source.Context);
|
||||
source.Dispose();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f7c4983d75e7a548aeabb756f98142f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e57931c226f15e4182b4668dde0b945
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,332 @@
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Caching;
|
||||
using Best.HTTP.Hosts.Connections.File;
|
||||
using Best.HTTP.Hosts.Connections.HTTP1;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Authentication;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.WebGL
|
||||
{
|
||||
class PeekableIncomingSegmentContentProviderStream : PeekableContentProviderStream
|
||||
{
|
||||
private int peek_listIdx;
|
||||
private int peek_pos;
|
||||
|
||||
public override void BeginPeek()
|
||||
{
|
||||
peek_listIdx = 0;
|
||||
peek_pos = base.bufferList.Count > 0 ? base.bufferList[0].Offset : 0;
|
||||
}
|
||||
|
||||
public override int PeekByte()
|
||||
{
|
||||
if (base.bufferList.Count == 0)
|
||||
return -1;
|
||||
|
||||
var segment = base.bufferList[this.peek_listIdx];
|
||||
if (peek_pos >= segment.Offset + segment.Count)
|
||||
{
|
||||
if (base.bufferList.Count <= this.peek_listIdx + 1)
|
||||
return -1;
|
||||
|
||||
segment = base.bufferList[++this.peek_listIdx];
|
||||
this.peek_pos = segment.Offset;
|
||||
}
|
||||
|
||||
return segment.Data[this.peek_pos++];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal sealed class WebGLXHRConnection : ConnectionBase
|
||||
{
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
int NativeId;
|
||||
PeekableHTTP1Response _response;
|
||||
|
||||
public WebGLXHRConnection(HostKey hostKey)
|
||||
: base(hostKey, false)
|
||||
{
|
||||
WebGLXHRNativeInterface.XHR_SetLoglevel((byte)HTTPManager.Logger.Level);
|
||||
}
|
||||
|
||||
public override void Shutdown(ShutdownTypes type)
|
||||
{
|
||||
base.Shutdown(type);
|
||||
|
||||
WebGLXHRNativeInterface.XHR_Abort(this.NativeId);
|
||||
}
|
||||
|
||||
protected override void ThreadFunc()
|
||||
{
|
||||
// XmlHttpRequest setup
|
||||
|
||||
CurrentRequest.Prepare();
|
||||
|
||||
Credentials credentials = null;// CurrentRequest.Authenticator?.Credentials;
|
||||
|
||||
this.NativeId = WebGLXHRNativeInterface.XHR_Create(HTTPRequest.MethodNames[(byte)CurrentRequest.MethodType],
|
||||
CurrentRequest.CurrentUri.OriginalString,
|
||||
credentials?.UserName, credentials?.Password, CurrentRequest.WithCredentials ? 1 : 0);
|
||||
WebGLXHRNativeConnectionLayer.Add(NativeId, this);
|
||||
|
||||
CurrentRequest.EnumerateHeaders((header, values) =>
|
||||
{
|
||||
if (!header.Equals("Content-Length"))
|
||||
for (int i = 0; i < values.Count; ++i)
|
||||
WebGLXHRNativeInterface.XHR_SetRequestHeader(NativeId, header, values[i]);
|
||||
}, /*callBeforeSendCallback:*/ true);
|
||||
|
||||
WebGLXHRNativeConnectionLayer.SetupHandlers(NativeId, CurrentRequest);
|
||||
|
||||
WebGLXHRNativeInterface.XHR_SetTimeout(NativeId, (uint)(CurrentRequest.TimeoutSettings.ConnectTimeout.TotalMilliseconds + CurrentRequest.TimeoutSettings.Timeout.TotalMilliseconds));
|
||||
|
||||
Stream upStream = CurrentRequest.UploadSettings.UploadStream;
|
||||
byte[] body = null;
|
||||
int length = 0;
|
||||
bool releaseBodyBuffer = false;
|
||||
|
||||
if (upStream != null)
|
||||
{
|
||||
var internalBuffer = BufferPool.Get(upStream.Length > 0 ? upStream.Length : CurrentRequest.UploadSettings.UploadChunkSize, true);
|
||||
using (BufferPoolMemoryStream ms = new BufferPoolMemoryStream(internalBuffer, 0, internalBuffer.Length, true, true, false, true))
|
||||
{
|
||||
var buffer = BufferPool.Get(CurrentRequest.UploadSettings.UploadChunkSize, true);
|
||||
int readCount = -1;
|
||||
while ((readCount = upStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
ms.Write(buffer, 0, readCount);
|
||||
|
||||
BufferPool.Release(buffer);
|
||||
|
||||
length = (int)ms.Position;
|
||||
body = ms.GetBuffer();
|
||||
|
||||
releaseBodyBuffer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._response == null)
|
||||
this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, false, null);
|
||||
|
||||
this.ContentProvider = new PeekableIncomingSegmentContentProviderStream();
|
||||
|
||||
WebGLXHRNativeInterface.XHR_Send(NativeId, body, length);
|
||||
|
||||
if (releaseBodyBuffer)
|
||||
BufferPool.Release(body);
|
||||
|
||||
this.CurrentRequest.TimeoutSettings.QueuedAt = DateTime.MinValue;
|
||||
this.CurrentRequest.TimeoutSettings.ProcessingStarted = DateTime.UtcNow;
|
||||
this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
||||
}
|
||||
|
||||
#region Callback Implementations
|
||||
|
||||
private void OnCancellationRequested(HTTPRequest req)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnCancellationRequested()", this.Context);
|
||||
|
||||
Interlocked.Exchange(ref this._response, null);
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
WebGLXHRNativeInterface.XHR_Abort(this.NativeId);
|
||||
}
|
||||
|
||||
internal void OnBuffer(BufferSegment buffer)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnBuffer({buffer})", this.Context);
|
||||
|
||||
try
|
||||
{
|
||||
if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
this.ContentProvider?.Write(buffer);
|
||||
this._response?.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
}
|
||||
|
||||
internal void OnError(string error)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnError({error})", this.Context);
|
||||
|
||||
FinishedProcessing(new Exception(error));
|
||||
}
|
||||
|
||||
internal void OnResponse(BufferSegment payload)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnResponse({payload})", this.Context);
|
||||
|
||||
this._response?.DownStream?.EmergencyIncreaseMaxBuffered();
|
||||
OnBuffer(payload);
|
||||
}
|
||||
|
||||
void FinishedProcessing(Exception ex)
|
||||
{
|
||||
// Warning: FinishedProcessing might be called from different threads in parallel:
|
||||
// - send thread triggered by a write failure
|
||||
// - read thread oncontent/OnError/OnConnectionClosed
|
||||
|
||||
var resp = Interlocked.Exchange(ref this._response, null);
|
||||
if (resp == null)
|
||||
return;
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);
|
||||
|
||||
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
||||
this.ContentProvider?.Unbind();
|
||||
this.ContentProvider?.Dispose();
|
||||
this.ContentProvider = null;
|
||||
|
||||
var req = this.CurrentRequest;
|
||||
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
bool resendRequest = false;
|
||||
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
||||
HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
|
||||
Exception error = ex;
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Timeout is a non-retryable error
|
||||
if (ex is TimeoutException)
|
||||
{
|
||||
error = null;
|
||||
requestState = HTTPRequestStates.TimedOut;
|
||||
}
|
||||
else if (ex is HTTPCacheAcquireLockException)
|
||||
{
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries)
|
||||
{
|
||||
req.RetrySettings.Retries++;
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestState = HTTPRequestStates.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
|
||||
connectionState = HTTPConnectionStates.Closed;
|
||||
|
||||
resp.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// After HandleResponse connectionState can have the following values:
|
||||
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
|
||||
// - Closed: server sent an connection: close header.
|
||||
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
|
||||
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
|
||||
|
||||
KeepAliveHeader keepAlive = null;
|
||||
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
|
||||
connectionState = HTTPConnectionStates.Recycle;
|
||||
|
||||
if (error != null)
|
||||
requestState = HTTPRequestStates.Error;
|
||||
else if (!resendRequest && resp.IsUpgraded)
|
||||
requestState = HTTPRequestStates.Processing;
|
||||
}
|
||||
|
||||
req.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
|
||||
|
||||
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
|
||||
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
|
||||
{
|
||||
ConnectionHelper.ResendRequestAndCloseConnection(this, req);
|
||||
}
|
||||
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
||||
{
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise set the request's then the connection's state
|
||||
ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnDownloadProgress(int down, int total) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.CurrentRequest, RequestEvents.DownloadProgress, down, total));
|
||||
internal void OnUploadProgress(int up, int total) => RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.CurrentRequest, RequestEvents.UploadProgress, up, total));
|
||||
|
||||
internal void OnTimeout()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnTimeout", this.Context);
|
||||
|
||||
CurrentRequest.Response = null;
|
||||
CurrentRequest.State = HTTPRequestStates.TimedOut;
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
internal void OnAborted()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"{this.NativeId} - OnAborted", this.Context);
|
||||
|
||||
CurrentRequest.Response = null;
|
||||
CurrentRequest.State = HTTPRequestStates.Aborted;
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
WebGLXHRNativeConnectionLayer.Remove(NativeId);
|
||||
WebGLXHRNativeInterface.XHR_Release(NativeId);
|
||||
|
||||
this.ContentProvider?.Dispose();
|
||||
this.ContentProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 678df3294a2b9c64ba56e9dd095f08fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,153 @@
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.WebGL
|
||||
{
|
||||
internal static class WebGLXHRNativeConnectionLayer
|
||||
{
|
||||
static Dictionary<int, WebGLXHRConnection> Connections = new Dictionary<int, WebGLXHRConnection>(1);
|
||||
|
||||
public static void Add(int nativeId, WebGLXHRConnection connection) => Connections.Add(nativeId, connection);
|
||||
public static void Remove(int nativeId) => Connections.Remove(nativeId);
|
||||
|
||||
public static void SetupHandlers(int nativeId, HTTPRequest request)
|
||||
{
|
||||
WebGLXHRNativeInterface.XHR_SetResponseHandler(nativeId, OnResponse, OnError, OnTimeout, OnAborted, OnBufferCallback, OnAllocArray);
|
||||
|
||||
// Setting OnUploadProgress result in an addEventListener("progress", ...) call making the request non-simple.
|
||||
// https://forum.unity.com/threads/best-http-released.200006/page-49#post-3696220
|
||||
WebGLXHRNativeInterface.XHR_SetProgressHandler(nativeId,
|
||||
request.DownloadSettings.OnDownloadProgress == null ? (OnWebGLXHRProgressDelegate)null : OnDownloadProgress,
|
||||
request.UploadSettings.OnUploadProgress == null ? (OnWebGLXHRProgressDelegate)null : OnUploadProgress);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRAllocArray))]
|
||||
static unsafe IntPtr OnAllocArray(int nativeId, int length)
|
||||
{
|
||||
byte[] buffer = BufferPool.Get(length, true);
|
||||
fixed (byte* ptr = buffer)
|
||||
{
|
||||
var p = (IntPtr)ptr;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(WebGLXHRConnection), $"({p}) <= OnAllocArray({nativeId}, {length})");
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRRequestHandlerDelegate))]
|
||||
static void OnResponse(int nativeId, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] pBuffer, int length)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection", $"OnResponse({nativeId}, {pBuffer}, {length}): No WebGL connection found for nativeId({nativeId})!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose("WebGLConnection", $"OnResponse({nativeId}, {pBuffer}, {length})", conn.Context);
|
||||
|
||||
BufferSegment payload = BufferSegment.Empty;
|
||||
if (length > 0)
|
||||
payload = pBuffer.AsBuffer(length);
|
||||
|
||||
conn.OnResponse(payload);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRBufferDelegate))]
|
||||
public static void OnBufferCallback(int nativeId, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] pBuffer, int length)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection - OnBufferCallback", "No WebGL connection found for nativeId: " + nativeId.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
BufferSegment payload = BufferSegment.Empty;
|
||||
if (length > 0)
|
||||
payload = pBuffer.AsBuffer(length);
|
||||
|
||||
conn.OnBuffer(payload);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRProgressDelegate))]
|
||||
static void OnDownloadProgress(int nativeId, int downloaded, int total)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection - OnDownloadProgress", "No WebGL connection found for nativeId: " + nativeId.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
HTTPManager.Logger.Information(nativeId + " OnDownloadProgress", downloaded.ToString() + " / " + total.ToString(), conn.Context);
|
||||
|
||||
conn.OnDownloadProgress(downloaded, total);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRProgressDelegate))]
|
||||
static void OnUploadProgress(int nativeId, int uploaded, int total)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection - OnUploadProgress", "No WebGL connection found for nativeId: " + nativeId.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
HTTPManager.Logger.Information(nativeId + " OnUploadProgress", uploaded.ToString() + " / " + total.ToString(), conn.Context);
|
||||
|
||||
conn.OnUploadProgress(uploaded, total);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRErrorDelegate))]
|
||||
static void OnError(int nativeId, string error)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection - OnError", "No WebGL connection found for nativeId: " + nativeId.ToString() + " Error: " + error);
|
||||
return;
|
||||
}
|
||||
|
||||
conn.OnError(error);
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRTimeoutDelegate))]
|
||||
static void OnTimeout(int nativeId)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection - OnTimeout", "No WebGL connection found for nativeId: " + nativeId.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
conn.OnTimeout();
|
||||
}
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(OnWebGLXHRAbortedDelegate))]
|
||||
static void OnAborted(int nativeId)
|
||||
{
|
||||
WebGLXHRConnection conn = null;
|
||||
if (!Connections.TryGetValue(nativeId, out conn))
|
||||
{
|
||||
HTTPManager.Logger.Error("WebGLConnection - OnAborted", "No WebGL connection found for nativeId: " + nativeId.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
conn.OnAborted();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42cfa2a1d7f27744da3528da58df7ef7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.WebGL
|
||||
{
|
||||
delegate void OnWebGLXHRRequestHandlerDelegate(int nativeId, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] pBuffer, int length);
|
||||
delegate void OnWebGLXHRBufferDelegate(int nativeId, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] pBuffer, int length);
|
||||
delegate void OnWebGLXHRProgressDelegate(int nativeId, int downloaded, int total);
|
||||
delegate void OnWebGLXHRErrorDelegate(int nativeId, string error);
|
||||
delegate void OnWebGLXHRTimeoutDelegate(int nativeId);
|
||||
delegate void OnWebGLXHRAbortedDelegate(int nativeId);
|
||||
delegate IntPtr OnWebGLXHRAllocArray(int nativeId, int size);
|
||||
|
||||
internal static class WebGLXHRNativeInterface
|
||||
{
|
||||
[DllImport("__Internal")]
|
||||
public static extern int XHR_Create(string method, string url, string userName, string passwd, int withCredentials);
|
||||
|
||||
/// <summary>
|
||||
/// Is an unsigned long representing the number of milliseconds a request can take before automatically being terminated. A value of 0 (which is the default) means there is no timeout.
|
||||
/// </summary>
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_SetTimeout(int nativeId, uint timeout);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_SetRequestHeader(int nativeId, string header, string value);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_SetResponseHandler(int nativeId,
|
||||
OnWebGLXHRRequestHandlerDelegate onresponse,
|
||||
OnWebGLXHRErrorDelegate onerror,
|
||||
OnWebGLXHRTimeoutDelegate ontimeout,
|
||||
OnWebGLXHRAbortedDelegate onabort,
|
||||
OnWebGLXHRBufferDelegate onbuffer,
|
||||
OnWebGLXHRAllocArray onallocArray);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_SetProgressHandler(int nativeId, OnWebGLXHRProgressDelegate onDownloadProgress, OnWebGLXHRProgressDelegate onUploadProgress);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_Send(int nativeId, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] body, int length);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_Abort(int nativeId);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_Release(int nativeId);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
public static extern void XHR_SetLoglevel(int logLevel);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef4c9a06ea58925459016164ae22bedf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b5b158db1b013b48a07aa808b6ff1f5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Request.Settings;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Best.HTTP.HostSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="HostKey"/> struct represents a unique key for identifying hosts based on their <see cref="System.Uri"/> and <see cref="ProxySettings"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="HostKey"/> struct is designed to uniquely identify a host based on its URI (Uniform Resource Identifier) and optional proxy settings.
|
||||
/// It provides a way to create, compare, and hash host keys, enabling efficient host variant management in the <see cref="HostManager"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Key features of the <see cref="HostKey"/> struct include:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Uniqueness</term>
|
||||
/// <description>
|
||||
/// Each <see cref="HostKey"/> is guaranteed to be unique for a specific host, considering both the URI and proxy settings.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Hashing</term>
|
||||
/// <description>
|
||||
/// The struct provides a method to calculate a hash code for a <see cref="HostKey"/>, making it suitable for use as a dictionary key.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Creation</term>
|
||||
/// <description>
|
||||
/// You can create a <see cref="HostKey"/> instance from a <see cref="System.Uri"/> and optional <see cref="ProxySettings"/>.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Usage of the <see cref="HostKey"/> struct is typically handled internally by the BestHTTP library to manage unique hosts and optimize resource usage.
|
||||
/// Developers can use it when dealing with host-specific operations or customization of the library's behavior.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public readonly struct HostKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the URI (Uniform Resource Identifier) associated with the host.
|
||||
/// </summary>
|
||||
public readonly Uri Uri;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the proxy settings associated with the host.
|
||||
/// </summary>
|
||||
public readonly ProxySettings Proxy;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique hash key for the host.
|
||||
/// </summary>
|
||||
public readonly Hash128 Key;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the host name from the URI or "file" if the URI is a file URI.
|
||||
/// </summary>
|
||||
public string Host { get => !this.Uri.IsFile ? this.Uri.Host : "file"; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HostKey"/> struct with the specified URI and proxy settings.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the host.</param>
|
||||
/// <param name="proxy">The proxy settings associated with the host, or <c>null</c> if no proxy is used.</param>
|
||||
public HostKey(Uri uri, ProxySettings proxy)
|
||||
{
|
||||
this.Uri = uri;
|
||||
this.Proxy = proxy;
|
||||
|
||||
this.Key = CalculateHash(uri, proxy);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) => obj switch
|
||||
{
|
||||
HostKey hostKey => hostKey.Equals(this),
|
||||
_ => false
|
||||
};
|
||||
|
||||
public bool Equals(HostKey hostKey) => this.Key.Equals(hostKey.Key);
|
||||
|
||||
public override int GetHashCode() => this.Key.GetHashCode();
|
||||
|
||||
public override string ToString() => $"{{\"Uri\":\"{this.Uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)}\", \"Proxy\": {this.Proxy.ToString()}, \"Key\": {this.Key.ToString()}}}";
|
||||
|
||||
private static Hash128 CalculateHash(Uri uri, ProxySettings proxy)
|
||||
{
|
||||
Hash128 hash = new Hash128();
|
||||
|
||||
Append(uri, ref hash);
|
||||
proxy?.AddToHash(uri, ref hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
internal static void Append(Uri uri, ref Hash128 hash)
|
||||
{
|
||||
if (uri != null)
|
||||
{
|
||||
hash.Append(uri.Scheme);
|
||||
hash.Append(uri.Host);
|
||||
hash.Append(uri.Port);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HostKey"/> instance from an HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request from which to extract the current URI and proxy settings.</param>
|
||||
/// <returns>A <see cref="HostKey"/> representing the host of the HTTP request.</returns>
|
||||
public static HostKey From(HTTPRequest request) => new HostKey(request.CurrentUri, request.ProxySettings);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HostKey"/> instance from a URI and proxy settings.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the host.</param>
|
||||
/// <param name="proxy">The proxy settings associated with the host, or <c>null</c> if no proxy is used.</param>
|
||||
/// <returns>A <see cref="HostKey"/> representing the host with the given URI and proxy settings.</returns>
|
||||
public static HostKey From(Uri uri, ProxySettings proxy) => new HostKey(uri, proxy);
|
||||
}
|
||||
|
||||
internal sealed class HostKeyEqualityComparer : IEqualityComparer<HostKey>
|
||||
{
|
||||
public bool Equals(HostKey x, HostKey y) => x.Equals(y);
|
||||
public int GetHashCode(HostKey obj) => obj.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19e27b6611d69a348b1cd90fe7fb4cfb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
namespace Best.HTTP.HostSetting
|
||||
{
|
||||
/*
|
||||
┌───────────────┐
|
||||
┌──────┤ HostManager ├──────────────────────┐
|
||||
│ └───────────────┘ │
|
||||
│ │
|
||||
┌──────────▼───────┐ ┌──────────▼────────┐
|
||||
│ HostVariant │ │ HostVariant │
|
||||
│(http://host:port)│ │(https://host:port)│
|
||||
┌────┴──────┬──────────┬┘ ┌────┴──────┬───────────┬┘
|
||||
│ │ │ │ │ │
|
||||
┌──────▼──────┐ ┌──▼──┐ ▼ ┌──────▼──────┐ ┌──▼──┐ ▼
|
||||
│ Connections │ │Queue│ ProtocolSupport │ Connections │ │Queue│ ProtocolSupport
|
||||
├─────────────┤ ├─────┤ (http/1.1) ├─────────────┤ ├─────┤ (http/1.1, h2)
|
||||
│ ... │ │ ... │ │ ... │ │ ... │
|
||||
│ ... │ │ ... │ │ ... │ │ ... │
|
||||
│ ... │ │ ... │ │ ... │ │ ... │
|
||||
│ ... │ │ ... │ │ ... │ │ ... │
|
||||
└─────────────┘ └─────┘ └─────────────┘ └─────┘
|
||||
*/
|
||||
/// <summary>
|
||||
/// The <see cref="HostManager"/> class provides centralized management for <see cref="HostVariant"/> objects associated with HTTP requests and connections.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="HostManager"/> class acts as a central registry for managing <see cref="HostVariant"/> objects, each associated with a unique <see cref="HostKey"/>.
|
||||
/// It facilitates the creation, retrieval, and management of <see cref="HostVariant"/> instances based on HTTP requests and connections.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A <see cref="HostVariant"/> represents a specific host and port combination (e.g., "http://example.com:80" or "https://example.com:443") and
|
||||
/// manages the connections and request queues for that host. The class ensures that a single <see cref="HostVariant"/> instance is used for
|
||||
/// each unique host, helping optimize resource usage and connection pooling.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Key features of the <see cref="HostManager"/> class include:
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <term>Creation and Retrieval</term>
|
||||
/// <description>
|
||||
/// The class allows you to create and retrieve <see cref="HostVariant"/> instances based on HTTP requests, connections, or <see cref="HostKey"/>.
|
||||
/// It ensures that a single <see cref="HostVariant"/> is used for each unique host.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Queue Management</term>
|
||||
/// <description>
|
||||
/// The <see cref="HostManager"/> manages the queue of pending requests for each <see cref="HostVariant"/>, ensuring efficient request processing.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Connection Management</term>
|
||||
/// <description>
|
||||
/// The class handles the management of connections associated with <see cref="HostVariant"/> objects, including recycling idle connections,
|
||||
/// removing idle connections, and shutting down connections when needed.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Usage of the <see cref="HostManager"/> class is typically transparent to developers and is handled internally by the Best HTTP library. However,
|
||||
/// it provides a convenient and efficient way to manage connections and requests when needed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class HostManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Dictionary to store <see cref="HostKey"/>-<see cref="HostVariant"/> mappings.
|
||||
/// </summary>
|
||||
private static Dictionary<HostKey, HostVariant> hosts = new Dictionary<HostKey, HostVariant>(new HostKeyEqualityComparer());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="HostVariant"/> associated with an HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <returns>The <see cref="HostVariant"/> for the request's host.</returns>
|
||||
public static HostVariant GetHostVariant(HTTPRequest request) => GetHostVariant(request.CurrentHostKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="HostVariant"/> associated with a connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">The HTTP connection.</param>
|
||||
/// <returns>The <see cref="HostVariant"/> for the connection's host.</returns>
|
||||
public static HostVariant GetHostVariant(ConnectionBase connection) => GetHostVariant(connection.HostKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="HostVariant"/> associated with a HostKey.
|
||||
/// </summary>
|
||||
/// <param name="key">The HostKey for which to get the HostVariant.</param>
|
||||
/// <returns>The <see cref="HostVariant"/> for the specified HostKey.</returns>
|
||||
public static HostVariant GetHostVariant(HostKey key)
|
||||
{
|
||||
if (!hosts.TryGetValue(key, out var variant))
|
||||
{
|
||||
var settings = HTTPManager.PerHostSettings.Get(key).HostVariantSettings;
|
||||
|
||||
variant = settings?.VariantFactory?.Invoke(settings, key) ?? new HostVariant(key);
|
||||
|
||||
hosts.Add(key, variant);
|
||||
|
||||
HTTPManager.Logger.Information("HostManager", $"Variant added with key: {key}");
|
||||
}
|
||||
|
||||
return variant;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all idle connections for all hosts.
|
||||
/// </summary>
|
||||
public static void RemoveAllIdleConnections()
|
||||
{
|
||||
HTTPManager.Logger.Information("HostManager", "RemoveAllIdleConnections");
|
||||
foreach (var host in hosts)
|
||||
host.Value.RemoveAllIdleConnections();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to send queued requests for all hosts.
|
||||
/// </summary>
|
||||
public static void TryToSendQueuedRequests()
|
||||
{
|
||||
foreach (var kvp in hosts)
|
||||
kvp.Value.TryToSendQueuedRequests();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down all connections for all hosts.
|
||||
/// </summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
HTTPManager.Logger.Information("HostManager", "Shutdown()");
|
||||
foreach (var kvp in hosts)
|
||||
kvp.Value.Shutdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all hosts and their associated variants.
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
HTTPManager.Logger.Information("HostManager", "Clearing()");
|
||||
hosts.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8717fe3a1943db149958dd51a8dc3a86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,357 @@
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Hosts.Connections.File;
|
||||
using Best.HTTP.Hosts.Settings;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Best.HTTP.HostSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// An enumeration representing the protocol support for a host.
|
||||
/// </summary>
|
||||
public enum HostProtocolSupport : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Protocol support is unknown or undetermined.
|
||||
/// </summary>
|
||||
Unknown = 0x00,
|
||||
|
||||
/// <summary>
|
||||
/// The host supports HTTP/1.
|
||||
/// </summary>
|
||||
HTTP1 = 0x01,
|
||||
|
||||
/// <summary>
|
||||
/// The host supports HTTP/2.
|
||||
/// </summary>
|
||||
HTTP2 = 0x02,
|
||||
|
||||
/// <summary>
|
||||
/// This is a file-based host.
|
||||
/// </summary>
|
||||
File = 0x03,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>The HostVariant class is a critical component in managing HTTP connections and handling HTTP requests for a specific host. It maintains a queue of requests and a list of active connections associated with the host, ensuring efficient utilization of available resources. Additionally, it supports protocol version detection (HTTP/1 or HTTP/2) for optimized communication with the host.</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>It maintains a queue of requests to ensure efficient and controlled use of available connections.</description></item>
|
||||
/// <item><description>It supports HTTP/1 and HTTP/2 protocol versions, allowing requests to be sent using the appropriate protocol based on the host's protocol support.</description></item>
|
||||
/// <item><description>Provides methods for sending requests, recycling connections, managing connection state, and handling the shutdown of connections and the host variant itself.</description></item>
|
||||
/// <item><description>It includes logging for diagnostic purposes, helping to monitor and debug the behavior of connections and requests.</description></item>
|
||||
/// </list>
|
||||
/// <para>In summary, the HostVariant class plays a central role in managing HTTP connections and requests for a specific host, ensuring efficient and reliable communication with that host while supporting different protocol versions.</para>
|
||||
/// </summary>
|
||||
public class HostVariant
|
||||
{
|
||||
public HostKey Host { get; private set; }
|
||||
|
||||
public HostProtocolSupport ProtocolSupport { get; private set; }
|
||||
|
||||
public DateTime LastProtocolSupportUpdate { get; private set; }
|
||||
|
||||
public LoggingContext Context { get; private set; }
|
||||
|
||||
// All the connections. Free and processing ones too.
|
||||
protected readonly List<ConnectionBase> Connections = new List<ConnectionBase>();
|
||||
|
||||
// Queued requests that aren't passed yet to a connection.
|
||||
protected readonly Queue<HTTPRequest> Queue = new Queue<HTTPRequest>();
|
||||
|
||||
// Host-variant settings
|
||||
protected readonly HostVariantSettings _settings;
|
||||
|
||||
// Cached list
|
||||
protected List<KeyValuePair<int, ConnectionBase>> availableConnections;
|
||||
|
||||
public HostVariant(HostKey host)
|
||||
{
|
||||
this.Host = host;
|
||||
if (this.Host.Uri.IsFile)
|
||||
this.ProtocolSupport = HostProtocolSupport.File;
|
||||
|
||||
this.Context = new LoggingContext(this);
|
||||
this.Context.Add("Host", this.Host.Host);
|
||||
|
||||
this._settings = HTTPManager.PerHostSettings.Get(this).HostVariantSettings;
|
||||
this.availableConnections = new List<KeyValuePair<int, ConnectionBase>>(2);
|
||||
}
|
||||
|
||||
public virtual void AddProtocol(HostProtocolSupport protocolSupport)
|
||||
{
|
||||
this.LastProtocolSupportUpdate = HTTPManager.CurrentFrameDateTime;
|
||||
|
||||
var oldProtocol = this.ProtocolSupport;
|
||||
|
||||
if (oldProtocol != protocolSupport)
|
||||
{
|
||||
this.ProtocolSupport = protocolSupport;
|
||||
|
||||
HTTPManager.Logger.Information(nameof(HostVariant), $"AddProtocol({oldProtocol} => {protocolSupport})", this.Context);
|
||||
}
|
||||
|
||||
TryToSendQueuedRequests();
|
||||
}
|
||||
|
||||
public virtual HostVariant Send(HTTPRequest request)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HostVariant), $"Send({request})", this.Context);
|
||||
|
||||
request.Context.Remove(nameof(HostVariant));
|
||||
request.Context.Add(nameof(HostVariant), this.Context);
|
||||
|
||||
this.Queue.Enqueue(request);
|
||||
return TryToSendQueuedRequests();
|
||||
}
|
||||
|
||||
public virtual HostVariant TryToSendQueuedRequests()
|
||||
{
|
||||
if (this.Queue.Count == 0)
|
||||
return this;
|
||||
|
||||
(int activeConnections, int theoreticalMaximumPerConnection, int assignedRequests) = QueryAnyAvailableOrNew(ref availableConnections);
|
||||
|
||||
if (availableConnections.Count == 0)
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
if (activeConnections > 0 && this.ProtocolSupport == HostProtocolSupport.Unknown)
|
||||
return this;
|
||||
#endif
|
||||
|
||||
if (activeConnections < this._settings.MaxConnectionPerVariant)
|
||||
{
|
||||
int queueSize = this.Queue.Count;
|
||||
int currentMaximum = (activeConnections * theoreticalMaximumPerConnection) - assignedRequests;
|
||||
|
||||
while (activeConnections < this._settings.MaxConnectionPerVariant && currentMaximum < queueSize)
|
||||
{
|
||||
availableConnections.Add(new KeyValuePair<int, ConnectionBase>(0, CreateNew()));
|
||||
currentMaximum += theoreticalMaximumPerConnection;
|
||||
activeConnections++;
|
||||
}
|
||||
}
|
||||
else
|
||||
return this;
|
||||
}
|
||||
|
||||
// Sort connections by theirs key (assigned requests count)
|
||||
availableConnections.Sort((a, b) => a.Key - b.Key);
|
||||
|
||||
while (this.Queue.Count > 0 && availableConnections.Count > 0)
|
||||
{
|
||||
var nextRequest = this.Queue.Peek();
|
||||
|
||||
// If the queue is large, or timeouts are set low, a request might be in a queue while its state is set to > Finished.
|
||||
// So we have to prevent sending it again.
|
||||
|
||||
if (nextRequest.State <= HTTPRequestStates.Queued)
|
||||
{
|
||||
var kvp = availableConnections[0];
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
{
|
||||
nextRequest.Context.Remove(nameof(HostVariant));
|
||||
nextRequest.Context.Add(nameof(HostVariant), this.Context);
|
||||
|
||||
var key = kvp.Value.GetType().Name;
|
||||
nextRequest.Context.Add(key, kvp.Value.Context);
|
||||
|
||||
HTTPManager.Logger.Information(nameof(HostVariant), $"Send({nextRequest.Context.Hash}, {key} => {kvp.Value.Context.Hash})", nextRequest.Context);
|
||||
}
|
||||
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(nextRequest, HTTPRequestStates.Processing, null));
|
||||
|
||||
OnConnectionStartedProcessingRequest(kvp.Value, nextRequest);
|
||||
// then start process the request
|
||||
kvp.Value.Process(nextRequest);
|
||||
|
||||
if (kvp.Key + 1 >= kvp.Value.MaxAssignedRequests)
|
||||
availableConnections.RemoveAt(0);
|
||||
else
|
||||
availableConnections[0] = new KeyValuePair<int, ConnectionBase>(kvp.Key + 1, kvp.Value);
|
||||
|
||||
availableConnections.Sort((a, b) => a.Key - b.Key);
|
||||
}
|
||||
|
||||
this.Queue.Dequeue();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public virtual (int activeConnections, int theoreticalMaximumPerConnection, int assignedRequests) QueryAnyAvailableOrNew(ref List<KeyValuePair<int, ConnectionBase>> connectionCollector)
|
||||
{
|
||||
int activeConnections = 0;
|
||||
int maxAssignedRequest = 1;
|
||||
int assignedRequests = 0;
|
||||
|
||||
connectionCollector.Clear();
|
||||
|
||||
// Check the last created connection first. This way, if a higher level protocol is present that can handle more requests (== HTTP/2) that protocol will be chosen
|
||||
// and others will be closed when their inactivity time is reached.
|
||||
for (int i = Connections.Count - 1; i >= 0; --i)
|
||||
{
|
||||
var conn = Connections[i];
|
||||
|
||||
if (conn.State == HTTPConnectionStates.Initial ||
|
||||
conn.State == HTTPConnectionStates.Free ||
|
||||
(conn.CanProcessMultiple && conn.AssignedRequests < conn.MaxAssignedRequests))
|
||||
connectionCollector.Add(new KeyValuePair<int, ConnectionBase>(conn.AssignedRequests, conn));
|
||||
|
||||
maxAssignedRequest = Math.Max(maxAssignedRequest, conn.MaxAssignedRequests);
|
||||
assignedRequests += conn.AssignedRequests;
|
||||
|
||||
activeConnections++;
|
||||
}
|
||||
|
||||
return (activeConnections, Math.Max(1, (int)(maxAssignedRequest * this._settings.MaxAssignedRequestsFactor)), assignedRequests);
|
||||
}
|
||||
|
||||
public virtual ConnectionBase CreateNew()
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HostVariant), $"CreateNew({this.Host})", this.Context);
|
||||
|
||||
ConnectionBase conn = this._settings.ConnectionFactory?.Invoke(this._settings, this);
|
||||
|
||||
if (conn == null)
|
||||
{
|
||||
if (this.ProtocolSupport == HostProtocolSupport.File)
|
||||
conn = new FileConnection(this.Host);
|
||||
else
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
conn = new Best.HTTP.Hosts.Connections.WebGL.WebGLXHRConnection(this.Host);
|
||||
#else
|
||||
conn = new HTTPOverTCPConnection(this.Host);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Connections.Add(conn);
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
protected virtual void OnConnectionStartedProcessingRequest(ConnectionBase connection, HTTPRequest request)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public virtual HostVariant RecycleConnection(ConnectionBase conn)
|
||||
{
|
||||
conn.State = HTTPConnectionStates.Free;
|
||||
|
||||
Best.HTTP.Shared.Extensions.Timer.Add(new TimerData(TimeSpan.FromSeconds(1), conn, CloseConnectionAfterInactivity));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected virtual bool RemoveConnectionImpl(ConnectionBase conn, HTTPConnectionStates setState)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HostVariant), $"RemoveConnectionImpl({conn}, {setState})", this.Context);
|
||||
|
||||
conn.State = setState;
|
||||
conn.Dispose();
|
||||
|
||||
bool found = this.Connections.Remove(conn);
|
||||
|
||||
if (!found) //
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information(nameof(HostVariant),
|
||||
$"RemoveConnectionImpl - Couldn't find connection! key: {conn.HostKey}", this.Context);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
public virtual HostVariant RemoveConnection(ConnectionBase conn, HTTPConnectionStates setState)
|
||||
{
|
||||
RemoveConnectionImpl(conn, setState);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConnectionBase Find(Predicate<ConnectionBase> match) => this.Connections.Find(match);
|
||||
|
||||
public bool HasConnection(ConnectionBase connection) => this.Connections.Contains(connection);
|
||||
|
||||
protected virtual bool CloseConnectionAfterInactivity(DateTime now, object context)
|
||||
{
|
||||
var conn = context as ConnectionBase;
|
||||
|
||||
bool closeConnection = conn.State == HTTPConnectionStates.Free && now - conn.LastProcessTime >= conn.KeepAliveTime;
|
||||
if (closeConnection)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HostVariant), string.Format("CloseConnectionAfterInactivity - [{0}] Closing! State: {1}, Now: {2}, LastProcessTime: {3}, KeepAliveTime: {4}",
|
||||
conn.ToString(), conn.State, now.ToString(System.Globalization.CultureInfo.InvariantCulture), conn.LastProcessTime.ToString(System.Globalization.CultureInfo.InvariantCulture), conn.KeepAliveTime), this.Context);
|
||||
|
||||
RemoveConnection(conn, HTTPConnectionStates.Closed);
|
||||
return false;
|
||||
}
|
||||
|
||||
// repeat until the connection's state is free
|
||||
return conn.State == HTTPConnectionStates.Free;
|
||||
}
|
||||
|
||||
public virtual void RemoveAllIdleConnections()
|
||||
{
|
||||
for (int i = 0; i < this.Connections.Count; i++)
|
||||
if (this.Connections[i].State == HTTPConnectionStates.Free)
|
||||
{
|
||||
int countBefore = this.Connections.Count;
|
||||
RemoveConnection(this.Connections[i], HTTPConnectionStates.Closed);
|
||||
|
||||
if (countBefore != this.Connections.Count)
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Shutdown()
|
||||
{
|
||||
this.Queue.Clear();
|
||||
|
||||
foreach (var conn in this.Connections)
|
||||
{
|
||||
// Swallow any exceptions, we are quitting anyway.
|
||||
try
|
||||
{
|
||||
conn.Shutdown(ShutdownTypes.Immediate);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
//this.Connections.Clear();
|
||||
}
|
||||
|
||||
public virtual void SaveTo(System.IO.BinaryWriter bw)
|
||||
{
|
||||
bw.Write(this.LastProtocolSupportUpdate.ToBinary());
|
||||
bw.Write((byte)this.ProtocolSupport);
|
||||
}
|
||||
|
||||
public virtual void LoadFrom(int version, System.IO.BinaryReader br)
|
||||
{
|
||||
this.LastProtocolSupportUpdate = DateTime.FromBinary(br.ReadInt64());
|
||||
this.ProtocolSupport = (HostProtocolSupport)br.ReadByte();
|
||||
|
||||
if (DateTime.UtcNow - this.LastProtocolSupportUpdate >= TimeSpan.FromDays(1))
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HostVariant), $"LoadFrom - Too Old! LastProtocolSupportUpdate: {this.LastProtocolSupportUpdate.ToString(CultureInfo.InvariantCulture)}, ProtocolSupport: {this.ProtocolSupport}", this.Context);
|
||||
this.ProtocolSupport = HostProtocolSupport.Unknown;
|
||||
}
|
||||
else if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(HostVariant), $"LoadFrom - LastProtocolSupportUpdate: {this.LastProtocolSupportUpdate.ToString(CultureInfo.InvariantCulture)}, ProtocolSupport: {this.ProtocolSupport}", this.Context);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{this.Host}, {this.Queue.Count}/{this.Connections?.Count}, {this.ProtocolSupport}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc3e7ed2db74f6b46aed6ce220fea2e7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a347efe8fc532a4ab45fd07ee707989
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Best.HTTP.Hosts.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves any added asterisk(*) to the end of the list.
|
||||
/// </summary>
|
||||
[Best.HTTP.Shared.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstruction]
|
||||
internal sealed class AsteriskStringComparer : IComparer<string>
|
||||
{
|
||||
public static readonly AsteriskStringComparer Instance = new AsteriskStringComparer();
|
||||
|
||||
public int Compare(string x, string y)
|
||||
/*{
|
||||
var comparedTo = x.CompareTo(y);
|
||||
|
||||
// Equal?
|
||||
if (comparedTo == 0)
|
||||
return 0;
|
||||
|
||||
return (x, y) switch
|
||||
{
|
||||
("*", _) => 1,
|
||||
(_, "*") => -1,
|
||||
_ => x.CompareTo(y)
|
||||
};
|
||||
}*/
|
||||
=> (x, y) switch
|
||||
{
|
||||
("*", "*") => 0,
|
||||
("*", _) => 1,
|
||||
(_, "*") => -1,
|
||||
_ => x.CompareTo(y)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df72827b7ff4ad04cb70fdad8c347622
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user