2024-03-17 00:06:13 +01:00
// ----------------------------------------------------------------------------------------------
2019-02-16 17:34:17 +01:00
// _ _ _ ____ _ _____
2017-11-18 17:27:06 +01:00
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2024-03-17 00:06:13 +01:00
// ----------------------------------------------------------------------------------------------
2024-03-17 02:35:40 +01:00
// |
2024-01-08 11:33:28 +01:00
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
2018-07-27 04:52:14 +02:00
// Contact: JustArchi@JustArchi.net
2024-03-17 02:35:40 +01:00
// |
2018-07-27 04:52:14 +02:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2024-03-17 02:35:40 +01:00
// |
2018-07-27 04:52:14 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2024-03-17 02:35:40 +01:00
// |
2018-07-27 04:52:14 +02:00
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2015-11-29 00:13:03 +01:00
2015-11-25 16:31:39 +01:00
using System ;
2021-06-21 23:20:32 +02:00
using System.Buffers ;
2015-11-25 16:31:39 +01:00
using System.Collections.Generic ;
2024-04-12 01:29:32 +02:00
using System.ComponentModel ;
2024-03-09 18:24:15 +01:00
using System.Diagnostics.CodeAnalysis ;
2020-11-14 22:37:00 +01:00
using System.Globalization ;
2017-12-16 11:34:04 +01:00
using System.IO ;
2020-02-24 21:11:54 +01:00
using System.Linq ;
2015-11-25 16:31:39 +01:00
using System.Net ;
using System.Net.Http ;
2021-07-19 14:37:58 +02:00
using System.Net.Http.Headers ;
2024-02-21 03:09:36 +01:00
using System.Net.Http.Json ;
2023-11-14 20:58:02 +01:00
using System.Threading ;
2015-11-25 16:31:39 +01:00
using System.Threading.Tasks ;
2021-05-08 01:37:22 +02:00
using ArchiSteamFarm.Core ;
2024-02-21 03:09:36 +01:00
using ArchiSteamFarm.Helpers.Json ;
2017-01-06 15:32:12 +01:00
using ArchiSteamFarm.Localization ;
2018-09-08 01:03:55 +02:00
using ArchiSteamFarm.NLog ;
2021-12-20 18:27:54 +01:00
using ArchiSteamFarm.Storage ;
2021-05-08 01:03:08 +02:00
using ArchiSteamFarm.Web.Responses ;
2019-01-10 22:33:07 +01:00
using JetBrains.Annotations ;
2015-11-25 16:31:39 +01:00
2021-11-10 21:23:24 +01:00
namespace ArchiSteamFarm.Web ;
2019-07-05 11:39:19 +02:00
2021-11-10 21:23:24 +01:00
public sealed class WebBrowser : IDisposable {
[PublicAPI]
public const byte MaxTries = 5 ; // Defines maximum number of recommended tries for a single request
2016-01-14 02:48:56 +01:00
2021-11-10 21:23:24 +01:00
internal const byte MaxConnections = 5 ; // Defines maximum number of connections per ServicePoint. Be careful, as it also defines maximum number of sockets in CLOSE_WAIT state
2016-03-15 05:15:22 +01:00
2022-12-23 19:33:38 +01:00
private const ushort ExtendedTimeout = 600 ; // Defines timeout for WebBrowsers dealing with huge data (ASF update)
2021-11-10 21:23:24 +01:00
private const byte MaxIdleTime = 15 ; // Defines in seconds, how long socket is allowed to stay in CLOSE_WAIT state after there are no connections to it
2016-03-06 23:28:56 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public CookieContainer CookieContainer { get ; } = new ( ) ;
2017-09-09 20:24:57 +02:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public TimeSpan Timeout = > HttpClient . Timeout ;
2015-12-01 01:34:05 +01:00
2021-11-10 21:23:24 +01:00
private readonly ArchiLogger ArchiLogger ;
private readonly HttpClient HttpClient ;
private readonly HttpClientHandler HttpClientHandler ;
2016-11-24 07:32:16 +01:00
2021-11-10 21:23:24 +01:00
internal WebBrowser ( ArchiLogger archiLogger , IWebProxy ? webProxy = null , bool extendedTimeout = false ) {
2023-11-14 19:12:33 +01:00
ArgumentNullException . ThrowIfNull ( archiLogger ) ;
ArchiLogger = archiLogger ;
2021-11-10 21:23:24 +01:00
HttpClientHandler = new HttpClientHandler {
AllowAutoRedirect = false , // This must be false if we want to handle custom redirection schemes such as "steammobile://"
AutomaticDecompression = DecompressionMethods . All ,
2023-11-14 20:01:29 +01:00
CookieContainer = CookieContainer ,
MaxConnectionsPerServer = MaxConnections
2021-11-10 21:23:24 +01:00
} ;
2016-11-24 07:46:37 +01:00
2021-11-10 21:23:24 +01:00
if ( webProxy ! = null ) {
HttpClientHandler . Proxy = webProxy ;
HttpClientHandler . UseProxy = true ;
2023-02-03 02:32:17 +01:00
if ( webProxy . Credentials ! = null ) {
// We can be pretty sure that user knows what he's doing and that proxy indeed requires authentication, save roundtrip
HttpClientHandler . PreAuthenticate = true ;
}
2021-11-10 21:23:24 +01:00
}
2019-01-02 18:09:07 +01:00
2021-11-10 21:23:24 +01:00
HttpClient = GenerateDisposableHttpClient ( extendedTimeout ) ;
}
2016-11-24 07:32:16 +01:00
2021-11-10 21:23:24 +01:00
public void Dispose ( ) {
HttpClient . Dispose ( ) ;
HttpClientHandler . Dispose ( ) ;
}
2016-11-24 07:32:16 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public HttpClient GenerateDisposableHttpClient ( bool extendedTimeout = false ) {
2021-12-20 18:27:54 +01:00
byte connectionTimeout = ASF . GlobalConfig ? . ConnectionTimeout ? ? GlobalConfig . DefaultConnectionTimeout ;
2020-08-22 21:41:01 +02:00
2021-11-10 21:23:24 +01:00
HttpClient result = new ( HttpClientHandler , false ) {
DefaultRequestVersion = HttpVersion . Version30 ,
2022-12-23 19:33:38 +01:00
Timeout = TimeSpan . FromSeconds ( extendedTimeout ? ExtendedTimeout : connectionTimeout )
2021-11-10 21:23:24 +01:00
} ;
2020-09-14 10:03:05 +02:00
2021-11-10 21:23:24 +01:00
// Most web services expect that UserAgent is set, so we declare it globally
// If you by any chance came here with a very "clever" idea of hiding your ass by changing default ASF user-agent then here is a very good advice from me: don't, for your own safety - you've been warned
result . DefaultRequestHeaders . UserAgent . Add ( new ProductInfoHeaderValue ( SharedInfo . PublicIdentifier , SharedInfo . Version . ToString ( ) ) ) ;
result . DefaultRequestHeaders . UserAgent . Add ( new ProductInfoHeaderValue ( $"({SharedInfo.BuildInfo.Variant}; {OS.Version.Replace(" ( ", " ", StringComparison.Ordinal).Replace(" ) ", " ", StringComparison.Ordinal)}; +{SharedInfo.ProjectURL})" ) ) ;
2020-09-14 10:03:05 +02:00
2022-11-23 12:07:23 +01:00
// Inform websites that we visit about our preference in language, if possible
result . DefaultRequestHeaders . AcceptLanguage . Add ( new StringWithQualityHeaderValue ( "en-US" , 0.9 ) ) ;
result . DefaultRequestHeaders . AcceptLanguage . Add ( new StringWithQualityHeaderValue ( "en" , 0.8 ) ) ;
2021-11-10 21:23:24 +01:00
return result ;
}
2020-09-14 10:03:05 +02:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < BinaryResponse ? > UrlGetToBinary ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , IProgress < byte > ? progressReporter = null , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 , rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2020-11-14 22:37:00 +01:00
2022-06-19 21:42:22 +02:00
if ( response = = null ) {
2021-11-10 21:23:24 +01:00
// Request timed out, try again
continue ;
2016-05-30 01:57:06 +02:00
}
2021-11-10 21:23:24 +01:00
await using ( response . ConfigureAwait ( false ) ) {
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2020-09-18 00:47:06 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2021-03-11 22:32:59 +01:00
}
2020-04-07 23:12:01 +03:00
}
2020-11-14 22:37:00 +01:00
2022-06-19 21:42:22 +02:00
if ( response . Content = = null ) {
throw new InvalidOperationException ( nameof ( response . Content ) ) ;
}
2022-04-02 16:00:07 +02:00
if ( response . Length > Array . MaxLength ) {
throw new InvalidOperationException ( nameof ( response . Length ) ) ;
}
2021-11-10 21:23:24 +01:00
progressReporter ? . Report ( 0 ) ;
2016-05-30 01:57:06 +02:00
2021-11-10 21:23:24 +01:00
MemoryStream ms = new ( ( int ) response . Length ) ;
2018-11-28 02:30:50 +01:00
2021-11-10 21:23:24 +01:00
await using ( ms . ConfigureAwait ( false ) ) {
byte batch = 0 ;
long readThisBatch = 0 ;
long batchIncreaseSize = response . Length / 100 ;
2019-04-05 16:24:02 +02:00
2021-11-10 21:23:24 +01:00
ArrayPool < byte > bytePool = ArrayPool < byte > . Shared ;
2016-05-30 01:57:06 +02:00
2021-11-10 21:23:24 +01:00
// This is HttpClient's buffer, using more doesn't make sense
byte [ ] buffer = bytePool . Rent ( 8192 ) ;
Rewrite entire ASF HTTP stack
Previous implementation was quite naive and assumed a lot of non-guaranteed premises. Checking if our session is valid before doing each request is very stupid, but it was needed for multi-user sessions. Next, even if we checked that our session is valid (or revalidated it if needed), we then assumed it's valid for at least X seconds, which is once again entirely false. Moreover, on top of those two issues, refreshing our session could do absolutely nothing as there is no guarantee that once-refreshes session stays like that even for our very next request.
Get rid of all of this shit and rewrite it with proper mechanism. We're making a request, if that request results in redirection to session refresh, refresh session, then repeat the same request again. Add failsafes for infinite loops, make it enough thread-safe and optimized for concurrent usage.
This commit won't only improve previously half-valid implementation, but will also greatly optimize number of requests being sent, as we won't need to check our session before each request. The entire thing should work like that since beginning.
2018-02-16 15:49:18 +01:00
2021-03-11 22:32:59 +01:00
try {
2021-11-10 21:23:24 +01:00
while ( response . Content . CanRead ) {
2023-11-14 20:58:02 +01:00
int read = await response . Content . ReadAsync ( buffer . AsMemory ( 0 , buffer . Length ) , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
2024-03-16 23:56:57 +01:00
if ( read < = 0 ) {
2021-11-10 21:23:24 +01:00
break ;
}
2020-11-14 22:37:00 +01:00
2024-03-16 23:56:57 +01:00
// Report progress in-between downloading only if file is big enough to justify it
// Current logic below will report progress if file is bigger than ~800 KB
if ( batchIncreaseSize > = buffer . Length ) {
readThisBatch + = read ;
2020-08-22 21:41:01 +02:00
2024-03-16 23:56:57 +01:00
for ( ; ( readThisBatch > = batchIncreaseSize ) & & ( batch < 99 ) ; readThisBatch - = batchIncreaseSize ) {
// We need a copy of variable being passed when in for loops, as loop will proceed before our event is launched
byte progress = + + batch ;
2021-03-11 22:32:59 +01:00
2024-03-16 23:56:57 +01:00
progressReporter ? . Report ( progress ) ;
}
2021-11-10 21:23:24 +01:00
}
2024-03-16 23:56:57 +01:00
await ms . WriteAsync ( buffer . AsMemory ( 0 , read ) , cancellationToken ) . ConfigureAwait ( false ) ;
2021-03-11 22:32:59 +01:00
}
2023-12-14 21:07:48 +01:00
} catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested ) {
2023-11-14 20:58:02 +01:00
throw ;
2021-03-11 22:32:59 +01:00
} catch ( Exception e ) {
ArchiLogger . LogGenericWarningException ( e ) ;
2021-06-27 23:18:24 +02:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2020-08-22 21:41:01 +02:00
2021-11-10 21:23:24 +01:00
return null ;
} finally {
bytePool . Return ( buffer ) ;
2020-08-22 21:41:01 +02:00
}
2017-04-05 14:16:47 +02:00
2021-11-10 21:23:24 +01:00
progressReporter ? . Report ( 100 ) ;
Rewrite entire ASF HTTP stack
Previous implementation was quite naive and assumed a lot of non-guaranteed premises. Checking if our session is valid before doing each request is very stupid, but it was needed for multi-user sessions. Next, even if we checked that our session is valid (or revalidated it if needed), we then assumed it's valid for at least X seconds, which is once again entirely false. Moreover, on top of those two issues, refreshing our session could do absolutely nothing as there is no guarantee that once-refreshes session stays like that even for our very next request.
Get rid of all of this shit and rewrite it with proper mechanism. We're making a request, if that request results in redirection to session refresh, refresh session, then repeat the same request again. Add failsafes for infinite loops, make it enough thread-safe and optimized for concurrent usage.
This commit won't only improve previously half-valid implementation, but will also greatly optimize number of requests being sent, as we won't need to check our session before each request. The entire thing should work like that since beginning.
2018-02-16 15:49:18 +01:00
2021-11-10 21:23:24 +01:00
return new BinaryResponse ( response , ms . ToArray ( ) ) ;
}
2018-11-28 02:30:50 +01:00
}
2021-11-10 21:23:24 +01:00
}
2018-11-28 02:30:50 +01:00
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2016-04-12 07:40:02 +02:00
2021-11-10 21:23:24 +01:00
return null ;
}
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < HtmlDocumentResponse ? > UrlGetToHtmlDocument ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 , rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
2022-06-19 21:42:22 +02:00
if ( response = = null ) {
2021-11-10 21:23:24 +01:00
// Request timed out, try again
continue ;
}
2020-09-14 10:03:05 +02:00
2021-11-10 21:23:24 +01:00
await using ( response . ConfigureAwait ( false ) ) {
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2020-09-14 10:03:05 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2020-09-18 00:47:06 +02:00
}
2020-09-14 10:03:05 +02:00
}
2022-06-19 21:42:22 +02:00
if ( response . Content = = null ) {
throw new InvalidOperationException ( nameof ( response . Content ) ) ;
}
2021-11-10 21:23:24 +01:00
try {
2023-11-14 20:58:02 +01:00
return await HtmlDocumentResponse . Create ( response , cancellationToken ) . ConfigureAwait ( false ) ;
2023-12-14 21:07:48 +01:00
} catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested ) {
2023-11-14 20:58:02 +01:00
throw ;
2021-11-10 21:23:24 +01:00
} catch ( Exception e ) {
2022-06-01 21:13:40 +02:00
if ( ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnSuccess ) & & response . StatusCode . IsSuccessCode ( ) ) | | ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnErrors ) & & ! response . StatusCode . IsSuccessCode ( ) ) ) {
return new HtmlDocumentResponse ( response ) ;
}
2021-11-10 21:23:24 +01:00
ArchiLogger . LogGenericWarningException ( e ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
}
2020-09-14 10:03:05 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-09-14 10:03:05 +02:00
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2020-09-14 10:03:05 +02:00
2021-11-10 21:23:24 +01:00
return null ;
}
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < ObjectResponse < T > ? > UrlGetToJsonObject < T > ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 , rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
2022-06-19 21:42:22 +02:00
if ( response = = null ) {
2021-11-10 21:23:24 +01:00
// Request timed out, try again
continue ;
}
2020-09-14 10:03:05 +02:00
2021-11-10 21:23:24 +01:00
await using ( response . ConfigureAwait ( false ) ) {
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2020-09-14 10:03:05 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2020-09-18 00:47:06 +02:00
}
2020-09-14 10:03:05 +02:00
}
2022-06-19 21:42:22 +02:00
if ( response . Content = = null ) {
throw new InvalidOperationException ( nameof ( response . Content ) ) ;
}
2021-11-10 21:23:24 +01:00
T ? obj ;
2020-09-14 10:03:05 +02:00
2021-11-10 21:23:24 +01:00
try {
2024-02-21 03:09:36 +01:00
obj = await response . Content . ToJsonObject < T > ( cancellationToken ) . ConfigureAwait ( false ) ;
2023-12-14 21:07:48 +01:00
} catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested ) {
2023-11-14 20:58:02 +01:00
throw ;
2021-11-10 21:23:24 +01:00
} catch ( Exception e ) {
2022-06-01 21:13:40 +02:00
if ( ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnSuccess ) & & response . StatusCode . IsSuccessCode ( ) ) | | ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnErrors ) & & ! response . StatusCode . IsSuccessCode ( ) ) ) {
return new ObjectResponse < T > ( response ) ;
}
2021-11-10 21:23:24 +01:00
ArchiLogger . LogGenericWarningException ( e ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2019-04-05 16:24:02 +02:00
2020-09-18 00:47:06 +02:00
continue ;
2019-04-05 16:24:02 +02:00
}
2016-11-24 07:32:16 +01:00
2022-05-28 20:41:52 +02:00
if ( obj is null ) {
2022-06-01 21:13:40 +02:00
if ( ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnSuccess ) & & response . StatusCode . IsSuccessCode ( ) ) | | ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnErrors ) & & ! response . StatusCode . IsSuccessCode ( ) ) ) {
return new ObjectResponse < T > ( response ) ;
}
2022-05-28 20:41:52 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorIsEmpty , nameof ( obj ) ) ) ;
continue ;
}
2021-11-10 21:23:24 +01:00
return new ObjectResponse < T > ( response , obj ) ;
}
}
2016-11-24 07:32:16 +01:00
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2018-11-28 02:30:50 +01:00
2021-11-10 21:23:24 +01:00
return null ;
}
2021-05-07 21:32:09 +02:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < StreamResponse ? > UrlGetToStream ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
HttpResponseMessage ? response = await InternalGet ( request , headers , referer , requestOptions , HttpCompletionOption . ResponseHeadersRead , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2018-11-28 02:30:50 +01:00
}
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2021-11-10 21:23:24 +01:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2021-11-10 21:23:24 +01:00
}
2016-11-24 07:32:16 +01:00
}
2023-11-14 20:58:02 +01:00
return new StreamResponse ( response , await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ) ;
2016-11-24 07:32:16 +01:00
}
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
return null ;
}
2016-11-24 07:32:16 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < BasicResponse ? > UrlHead ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
using HttpResponseMessage ? response = await InternalHead ( request , headers , referer , requestOptions , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2020-04-07 23:12:01 +03:00
2021-11-10 21:23:24 +01:00
if ( response = = null ) {
continue ;
}
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
}
2021-11-10 21:23:24 +01:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2020-04-07 23:12:01 +03:00
}
2021-11-10 21:23:24 +01:00
}
2020-04-07 23:12:01 +03:00
2021-11-10 21:23:24 +01:00
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2020-04-07 23:12:01 +03:00
}
}
2018-12-15 00:27:15 +01:00
2021-11-10 21:23:24 +01:00
return new BasicResponse ( response ) ;
2016-11-17 21:30:17 +01:00
}
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2021-11-10 21:23:24 +01:00
2022-06-06 23:28:35 +02:00
return null ;
2021-11-10 21:23:24 +01:00
}
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < BasicResponse ? > UrlPost < T > ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , T ? data = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) where T : class {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
using HttpResponseMessage ? response = await InternalPost ( request , headers , data , referer , requestOptions , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
if ( response = = null ) {
continue ;
2016-07-16 21:03:39 +02:00
}
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
}
2021-11-10 21:23:24 +01:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2021-11-10 21:23:24 +01:00
}
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2019-04-05 16:24:02 +02:00
}
2021-11-10 21:23:24 +01:00
}
2018-11-28 02:30:50 +01:00
2021-11-10 21:23:24 +01:00
return new BasicResponse ( response ) ;
}
2021-03-11 22:32:59 +01:00
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2018-11-28 02:30:50 +01:00
2022-06-06 23:28:35 +02:00
return null ;
2021-11-10 21:23:24 +01:00
}
2021-01-10 21:27:52 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < HtmlDocumentResponse ? > UrlPostToHtmlDocument < T > ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , T ? data = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) where T : class {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
StreamResponse ? response = await UrlPostToStream ( request , headers , data , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 , rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2020-08-22 21:41:01 +02:00
2022-06-19 21:42:22 +02:00
if ( response = = null ) {
2021-11-10 21:23:24 +01:00
// Request timed out, try again
continue ;
}
await using ( response . ConfigureAwait ( false ) ) {
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2021-11-10 21:23:24 +01:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2020-08-22 21:41:01 +02:00
}
2017-04-05 14:16:47 +02:00
}
Rewrite entire ASF HTTP stack
Previous implementation was quite naive and assumed a lot of non-guaranteed premises. Checking if our session is valid before doing each request is very stupid, but it was needed for multi-user sessions. Next, even if we checked that our session is valid (or revalidated it if needed), we then assumed it's valid for at least X seconds, which is once again entirely false. Moreover, on top of those two issues, refreshing our session could do absolutely nothing as there is no guarantee that once-refreshes session stays like that even for our very next request.
Get rid of all of this shit and rewrite it with proper mechanism. We're making a request, if that request results in redirection to session refresh, refresh session, then repeat the same request again. Add failsafes for infinite loops, make it enough thread-safe and optimized for concurrent usage.
This commit won't only improve previously half-valid implementation, but will also greatly optimize number of requests being sent, as we won't need to check our session before each request. The entire thing should work like that since beginning.
2018-02-16 15:49:18 +01:00
2022-06-19 21:42:22 +02:00
if ( response . Content = = null ) {
throw new InvalidOperationException ( nameof ( response . Content ) ) ;
}
2021-11-10 21:23:24 +01:00
try {
2023-11-14 20:58:02 +01:00
return await HtmlDocumentResponse . Create ( response , cancellationToken ) . ConfigureAwait ( false ) ;
2023-12-14 21:07:48 +01:00
} catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested ) {
2023-11-14 20:58:02 +01:00
throw ;
2021-11-10 21:23:24 +01:00
} catch ( Exception e ) {
2022-06-01 21:13:40 +02:00
if ( ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnSuccess ) & & response . StatusCode . IsSuccessCode ( ) ) | | ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnErrors ) & & ! response . StatusCode . IsSuccessCode ( ) ) ) {
return new HtmlDocumentResponse ( response ) ;
2020-09-18 00:47:06 +02:00
}
2020-05-27 15:42:12 +03:00
2022-05-28 20:41:52 +02:00
ArchiLogger . LogGenericWarningException ( e ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
}
}
}
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2022-05-28 20:41:52 +02:00
return null ;
}
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < ObjectResponse < TResult > ? > UrlPostToJsonObject < TResult , TData > ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , TData ? data = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) where TData : class {
2022-05-28 20:41:52 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2022-05-28 20:41:52 +02:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2022-05-28 20:41:52 +02:00
}
2021-11-10 21:23:24 +01:00
2023-11-14 20:58:02 +01:00
StreamResponse ? response = await UrlPostToStream ( request , headers , data , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 , rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
2022-06-19 21:42:22 +02:00
if ( response = = null ) {
2022-05-28 20:41:52 +02:00
// Request timed out, try again
continue ;
}
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-28 20:41:52 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2022-05-28 20:41:52 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2021-11-10 21:23:24 +01:00
}
2022-05-28 20:41:52 +02:00
}
2022-06-19 21:42:22 +02:00
if ( response . Content = = null ) {
throw new InvalidOperationException ( nameof ( response . Content ) ) ;
}
2022-05-28 20:41:52 +02:00
TResult ? obj ;
try {
2024-02-21 03:09:36 +01:00
obj = await response . Content . ToJsonObject < TResult > ( cancellationToken ) . ConfigureAwait ( false ) ;
2023-12-14 21:07:48 +01:00
} catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested ) {
2023-11-14 20:58:02 +01:00
throw ;
2021-11-10 21:23:24 +01:00
} catch ( Exception e ) {
2022-05-28 20:41:52 +02:00
if ( ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnSuccess ) & & response . StatusCode . IsSuccessCode ( ) ) | | ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnErrors ) & & ! response . StatusCode . IsSuccessCode ( ) ) ) {
2022-06-01 21:13:40 +02:00
return new ObjectResponse < TResult > ( response ) ;
2022-05-28 20:41:52 +02:00
}
2021-11-10 21:23:24 +01:00
ArchiLogger . LogGenericWarningException ( e ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
continue ;
}
2022-05-28 20:41:52 +02:00
if ( obj is null ) {
if ( ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnSuccess ) & & response . StatusCode . IsSuccessCode ( ) ) | | ( requestOptions . HasFlag ( ERequestOptions . AllowInvalidBodyOnErrors ) & & ! response . StatusCode . IsSuccessCode ( ) ) ) {
2022-06-01 21:13:40 +02:00
return new ObjectResponse < TResult > ( response ) ;
2022-05-28 20:41:52 +02:00
}
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorIsEmpty , nameof ( obj ) ) ) ;
continue ;
}
2022-06-01 21:13:40 +02:00
return new ObjectResponse < TResult > ( response , obj ) ;
2019-01-10 22:33:07 +01:00
}
2021-11-10 21:23:24 +01:00
}
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2021-11-10 21:23:24 +01:00
return null ;
}
[PublicAPI]
2023-11-14 20:58:02 +01:00
public async Task < StreamResponse ? > UrlPostToStream < T > ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , T ? data = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , int rateLimitingDelay = 0 , CancellationToken cancellationToken = default ) where T : class {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2023-11-14 19:12:33 +01:00
ArgumentOutOfRangeException . ThrowIfZero ( maxTries ) ;
ArgumentOutOfRangeException . ThrowIfNegative ( rateLimitingDelay ) ;
2021-12-09 18:24:00 +01:00
2021-11-10 21:23:24 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-12-09 18:24:00 +01:00
if ( ( i > 0 ) & & ( rateLimitingDelay > 0 ) ) {
2023-11-14 20:58:02 +01:00
await Task . Delay ( rateLimitingDelay , cancellationToken ) . ConfigureAwait ( false ) ;
2021-12-09 18:24:00 +01:00
}
2023-11-14 20:58:02 +01:00
HttpResponseMessage ? response = await InternalPost ( request , headers , data , referer , requestOptions , HttpCompletionOption . ResponseHeadersRead , cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2019-01-10 22:33:07 +01:00
}
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
break ;
2022-05-24 12:13:54 +02:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
break ;
2021-11-10 21:23:24 +01:00
}
2022-06-06 23:28:35 +02:00
}
if ( response . StatusCode . IsServerErrorCode ( ) ) {
2022-08-25 20:23:37 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
continue ;
2021-11-10 21:23:24 +01:00
}
}
2023-11-14 20:58:02 +01:00
return new StreamResponse ( response , await response . Content . ReadAsStreamAsync ( cancellationToken ) . ConfigureAwait ( false ) ) ;
2021-11-10 21:23:24 +01:00
}
2022-06-06 23:28:35 +02:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2019-01-10 22:33:07 +01:00
2021-11-10 21:23:24 +01:00
return null ;
}
internal static void Init ( ) {
// Set max connection limit from default of 2 to desired value
ServicePointManager . DefaultConnectionLimit = MaxConnections ;
2020-08-23 19:24:10 +02:00
2021-11-10 21:23:24 +01:00
// Set max idle time from default of 100 seconds (100 * 1000) to desired value
ServicePointManager . MaxServicePointIdleTime = MaxIdleTime * 1000 ;
2020-05-27 15:42:12 +03:00
2021-11-10 21:23:24 +01:00
// Don't use Expect100Continue, we're sure about our POSTs, save some TCP packets
ServicePointManager . Expect100Continue = false ;
2019-01-10 22:33:07 +01:00
2021-11-10 21:23:24 +01:00
// Reuse ports if possible
ServicePointManager . ReusePort = true ;
}
2023-11-14 20:58:02 +01:00
private async Task < HttpResponseMessage ? > InternalGet ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , HttpCompletionOption httpCompletionOption = HttpCompletionOption . ResponseContentRead , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2019-01-10 22:33:07 +01:00
2024-04-12 01:29:32 +02:00
if ( ! Enum . IsDefined ( httpCompletionOption ) ) {
throw new InvalidEnumArgumentException ( nameof ( httpCompletionOption ) , ( int ) httpCompletionOption , typeof ( HttpCompletionOption ) ) ;
}
2023-11-14 20:58:02 +01:00
return await InternalRequest < object > ( request , HttpMethod . Get , headers , null , referer , requestOptions , httpCompletionOption , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
}
2016-03-15 04:20:28 +01:00
2023-11-14 20:58:02 +01:00
private async Task < HttpResponseMessage ? > InternalHead ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , HttpCompletionOption httpCompletionOption = HttpCompletionOption . ResponseContentRead , CancellationToken cancellationToken = default ) {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2016-03-15 04:20:28 +01:00
2024-04-12 01:29:32 +02:00
if ( ! Enum . IsDefined ( httpCompletionOption ) ) {
throw new InvalidEnumArgumentException ( nameof ( httpCompletionOption ) , ( int ) httpCompletionOption , typeof ( HttpCompletionOption ) ) ;
}
2023-11-14 20:58:02 +01:00
return await InternalRequest < object > ( request , HttpMethod . Head , headers , null , referer , requestOptions , httpCompletionOption , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
}
2016-05-30 01:57:06 +02:00
2023-11-14 20:58:02 +01:00
private async Task < HttpResponseMessage ? > InternalPost < T > ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , T ? data = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , HttpCompletionOption httpCompletionOption = HttpCompletionOption . ResponseContentRead , CancellationToken cancellationToken = default ) where T : class {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
2016-05-30 01:57:06 +02:00
2024-04-12 01:29:32 +02:00
if ( ! Enum . IsDefined ( httpCompletionOption ) ) {
throw new InvalidEnumArgumentException ( nameof ( httpCompletionOption ) , ( int ) httpCompletionOption , typeof ( HttpCompletionOption ) ) ;
}
2023-11-14 20:58:02 +01:00
return await InternalRequest ( request , HttpMethod . Post , headers , data , referer , requestOptions , httpCompletionOption , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
2021-11-10 21:23:24 +01:00
}
2016-04-14 22:23:37 +02:00
2024-03-09 18:24:15 +01:00
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
2023-11-14 20:58:02 +01:00
private async Task < HttpResponseMessage ? > InternalRequest < T > ( Uri request , HttpMethod httpMethod , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , T ? data = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , HttpCompletionOption httpCompletionOption = HttpCompletionOption . ResponseContentRead , byte maxRedirections = MaxTries , CancellationToken cancellationToken = default ) where T : class {
2022-05-26 13:29:12 +02:00
ArgumentNullException . ThrowIfNull ( request ) ;
ArgumentNullException . ThrowIfNull ( httpMethod ) ;
2015-11-25 16:31:39 +01:00
2024-04-12 01:29:32 +02:00
if ( ! Enum . IsDefined ( httpCompletionOption ) ) {
throw new InvalidEnumArgumentException ( nameof ( httpCompletionOption ) , ( int ) httpCompletionOption , typeof ( HttpCompletionOption ) ) ;
}
2021-11-10 21:23:24 +01:00
HttpResponseMessage response ;
Rewrite entire ASF HTTP stack
Previous implementation was quite naive and assumed a lot of non-guaranteed premises. Checking if our session is valid before doing each request is very stupid, but it was needed for multi-user sessions. Next, even if we checked that our session is valid (or revalidated it if needed), we then assumed it's valid for at least X seconds, which is once again entirely false. Moreover, on top of those two issues, refreshing our session could do absolutely nothing as there is no guarantee that once-refreshes session stays like that even for our very next request.
Get rid of all of this shit and rewrite it with proper mechanism. We're making a request, if that request results in redirection to session refresh, refresh session, then repeat the same request again. Add failsafes for infinite loops, make it enough thread-safe and optimized for concurrent usage.
This commit won't only improve previously half-valid implementation, but will also greatly optimize number of requests being sent, as we won't need to check our session before each request. The entire thing should work like that since beginning.
2018-02-16 15:49:18 +01:00
2021-11-10 21:23:24 +01:00
while ( true ) {
using ( HttpRequestMessage requestMessage = new ( httpMethod , request ) ) {
requestMessage . Version = HttpClient . DefaultRequestVersion ;
2020-02-21 17:07:05 +01:00
2021-11-10 21:23:24 +01:00
if ( headers ! = null ) {
foreach ( ( string header , string value ) in headers ) {
requestMessage . Headers . Add ( header , value ) ;
2020-09-13 21:43:25 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-09-13 21:43:25 +02:00
2021-11-10 21:23:24 +01:00
if ( data ! = null ) {
switch ( data ) {
case HttpContent content :
requestMessage . Content = content ;
2020-06-06 16:34:20 +02:00
2021-11-10 21:23:24 +01:00
break ;
2021-12-07 21:34:46 +01:00
case IReadOnlyCollection < KeyValuePair < string , string > > nameValueCollection :
2021-11-10 21:23:24 +01:00
try {
requestMessage . Content = new FormUrlEncodedContent ( nameValueCollection ) ;
} catch ( UriFormatException ) {
2023-11-29 00:08:16 +01:00
requestMessage . Content = new StringContent ( string . Join ( '&' , nameValueCollection . Select ( static kv = > $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}" ) ) , null , "application/x-www-form-urlencoded" ) ;
2021-11-10 21:23:24 +01:00
}
2020-06-06 16:34:20 +02:00
2021-11-10 21:23:24 +01:00
break ;
case string text :
requestMessage . Content = new StringContent ( text ) ;
2020-06-06 16:34:20 +02:00
2021-11-10 21:23:24 +01:00
break ;
default :
2024-02-21 03:09:36 +01:00
requestMessage . Content = JsonContent . Create ( data , options : JsonUtilities . DefaultJsonSerialierOptions ) ;
2021-10-15 00:04:16 +02:00
2021-11-10 21:23:24 +01:00
break ;
2016-02-22 18:34:45 +01:00
}
2023-01-15 21:26:03 +01:00
2023-10-31 17:21:34 +00:00
// Compress the request if caller specified it, so they know that the server supports it, and the content is not compressed yet
2023-01-15 21:26:03 +01:00
if ( requestOptions . HasFlag ( ERequestOptions . CompressRequest ) & & ( requestMessage . Content . Headers . ContentEncoding . Count = = 0 ) ) {
HttpContent originalContent = requestMessage . Content ;
2023-01-15 21:46:58 +01:00
requestMessage . Content = await WebBrowserUtilities . CreateCompressedHttpContent ( originalContent ) . ConfigureAwait ( false ) ;
2023-01-15 21:26:03 +01:00
if ( data is not HttpContent ) {
// We don't need to keep old HttpContent around anymore, help GC
originalContent . Dispose ( ) ;
}
}
2021-11-10 21:23:24 +01:00
}
2015-11-25 16:31:39 +01:00
2021-11-10 21:23:24 +01:00
if ( referer ! = null ) {
requestMessage . Headers . Referrer = referer ;
2016-02-22 18:34:45 +01:00
}
2015-11-25 16:31:39 +01:00
2018-04-11 18:32:45 +02:00
if ( Debugging . IsUserDebugging ) {
2021-11-10 21:23:24 +01:00
ArchiLogger . LogGenericDebug ( $"{httpMethod} {request}" ) ;
2018-04-11 18:32:45 +02:00
}
2021-11-10 21:23:24 +01:00
try {
2023-11-14 20:58:02 +01:00
response = await HttpClient . SendAsync ( requestMessage , httpCompletionOption , cancellationToken ) . ConfigureAwait ( false ) ;
2023-12-14 21:07:48 +01:00
} catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested ) {
2023-11-14 20:58:02 +01:00
throw ;
2021-11-10 21:23:24 +01:00
} catch ( Exception e ) {
ArchiLogger . LogGenericDebuggingException ( e ) ;
2015-11-25 16:31:39 +01:00
2021-11-10 21:23:24 +01:00
return null ;
} finally {
if ( data is HttpContent ) {
// We reset the request content to null, as our http content will get disposed otherwise, and we still need it for subsequent calls, such as redirections or retries
requestMessage . Content = null ;
}
}
}
2018-04-11 22:23:31 +02:00
2021-11-10 21:23:24 +01:00
if ( Debugging . IsUserDebugging ) {
ArchiLogger . LogGenericDebug ( $"{response.StatusCode} <- {httpMethod} {request}" ) ;
}
2016-05-13 06:32:42 +02:00
2021-11-10 21:23:24 +01:00
if ( response . IsSuccessStatusCode ) {
return response ;
}
2020-11-10 23:22:57 +01:00
2021-11-10 21:23:24 +01:00
// WARNING: We still have not disposed response by now, make sure to dispose it ASAP if we're not returning it!
2022-05-24 12:13:54 +02:00
if ( response . StatusCode . IsRedirectionCode ( ) & & ( maxRedirections > 0 ) ) {
if ( requestOptions . HasFlag ( ERequestOptions . ReturnRedirections ) ) {
// User wants to handle it manually, that's alright
return response ;
}
2021-11-10 21:23:24 +01:00
Uri ? redirectUri = response . Headers . Location ;
2021-10-15 00:04:16 +02:00
2021-11-10 21:23:24 +01:00
if ( redirectUri = = null ) {
2022-04-13 23:16:36 +02:00
ArchiLogger . LogNullError ( redirectUri ) ;
2020-11-10 23:22:57 +01:00
2021-11-10 21:23:24 +01:00
return null ;
}
2018-02-17 03:57:09 +01:00
2021-11-10 21:23:24 +01:00
if ( redirectUri . IsAbsoluteUri ) {
switch ( redirectUri . Scheme ) {
2022-06-05 15:59:34 +02:00
case "http" or "https" :
2021-11-10 21:23:24 +01:00
break ;
case "steammobile" :
// Those redirections are invalid, but we're aware of that and we have extra logic for them
return response ;
default :
// We have no clue about those, but maybe HttpClient can handle them for us
ArchiLogger . LogGenericError ( string . Format ( CultureInfo . CurrentCulture , Strings . WarningUnknownValuePleaseReport , nameof ( redirectUri . Scheme ) , redirectUri . Scheme ) ) ;
2018-12-15 00:27:15 +01:00
2018-02-17 03:57:09 +01:00
break ;
2017-06-26 04:02:11 +02:00
}
2021-11-10 21:23:24 +01:00
} else {
redirectUri = new Uri ( request , redirectUri ) ;
}
2020-07-23 21:36:05 +02:00
2021-11-10 21:23:24 +01:00
switch ( response . StatusCode ) {
case HttpStatusCode . MovedPermanently : // Per https://tools.ietf.org/html/rfc7231#section-6.4.2, a 301 redirect may be performed using a GET request
case HttpStatusCode . Redirect : // Per https://tools.ietf.org/html/rfc7231#section-6.4.3, a 302 redirect may be performed using a GET request
case HttpStatusCode . SeeOther : // Per https://tools.ietf.org/html/rfc7231#section-6.4.4, a 303 redirect should be performed using a GET request
if ( httpMethod ! = HttpMethod . Head ) {
httpMethod = HttpMethod . Get ;
}
2020-07-23 21:36:05 +02:00
2021-11-10 21:23:24 +01:00
// Data doesn't make any sense for a fetch request, clear it in case it's being used
data = null ;
break ;
}
2020-07-23 21:36:05 +02:00
2021-11-10 21:23:24 +01:00
response . Dispose ( ) ;
2019-06-19 16:42:04 +02:00
2021-11-10 21:23:24 +01:00
// Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a fragment should inherit the fragment from the original URI
if ( ! string . IsNullOrEmpty ( request . Fragment ) & & string . IsNullOrEmpty ( redirectUri . Fragment ) ) {
redirectUri = new UriBuilder ( redirectUri ) { Fragment = request . Fragment } . Uri ;
2018-02-17 03:57:09 +01:00
}
2017-06-26 04:02:11 +02:00
2021-11-10 21:23:24 +01:00
request = redirectUri ;
maxRedirections - - ;
2018-02-17 03:57:09 +01:00
2021-11-10 21:23:24 +01:00
continue ;
2020-07-03 18:54:13 +02:00
}
2021-11-10 21:23:24 +01:00
break ;
}
2019-06-19 14:14:35 +02:00
2021-11-10 21:23:24 +01:00
if ( ! Debugging . IsUserDebugging ) {
ArchiLogger . LogGenericDebug ( $"{response.StatusCode} <- {httpMethod} {request}" ) ;
}
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( Debugging . IsUserDebugging ) {
2023-11-14 20:58:02 +01:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . Content , await response . Content . ReadAsStringAsync ( cancellationToken ) . ConfigureAwait ( false ) ) ) ;
2019-04-05 16:24:02 +02:00
}
2021-11-10 21:23:24 +01:00
// Do not retry on client errors
return response ;
}
2021-01-04 15:06:34 +01:00
2021-11-10 21:23:24 +01:00
if ( requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) & & response . StatusCode . IsServerErrorCode ( ) ) {
if ( Debugging . IsUserDebugging ) {
2023-11-14 20:58:02 +01:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . Content , await response . Content . ReadAsStringAsync ( cancellationToken ) . ConfigureAwait ( false ) ) ) ;
2020-08-23 19:24:10 +02:00
}
2021-11-10 21:23:24 +01:00
// Do not retry on server errors in this case
return response ;
}
2018-04-13 09:17:27 +02:00
2021-11-10 21:23:24 +01:00
using ( response ) {
if ( Debugging . IsUserDebugging ) {
2023-11-14 20:58:02 +01:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . Content , await response . Content . ReadAsStringAsync ( cancellationToken ) . ConfigureAwait ( false ) ) ) ;
2018-04-13 09:17:27 +02:00
}
Rewrite entire ASF HTTP stack
Previous implementation was quite naive and assumed a lot of non-guaranteed premises. Checking if our session is valid before doing each request is very stupid, but it was needed for multi-user sessions. Next, even if we checked that our session is valid (or revalidated it if needed), we then assumed it's valid for at least X seconds, which is once again entirely false. Moreover, on top of those two issues, refreshing our session could do absolutely nothing as there is no guarantee that once-refreshes session stays like that even for our very next request.
Get rid of all of this shit and rewrite it with proper mechanism. We're making a request, if that request results in redirection to session refresh, refresh session, then repeat the same request again. Add failsafes for infinite loops, make it enough thread-safe and optimized for concurrent usage.
This commit won't only improve previously half-valid implementation, but will also greatly optimize number of requests being sent, as we won't need to check our session before each request. The entire thing should work like that since beginning.
2018-02-16 15:49:18 +01:00
2021-11-10 21:23:24 +01:00
return null ;
2020-09-14 10:03:05 +02:00
}
2015-11-25 16:31:39 +01:00
}
2021-11-10 21:23:24 +01:00
[Flags]
public enum ERequestOptions : byte {
None = 0 ,
ReturnClientErrors = 1 ,
2022-05-24 12:13:54 +02:00
ReturnServerErrors = 2 ,
2022-05-28 20:41:52 +02:00
ReturnRedirections = 4 ,
AllowInvalidBodyOnSuccess = 8 ,
2023-01-15 21:26:03 +01:00
AllowInvalidBodyOnErrors = 16 ,
CompressRequest = 32
2021-11-10 21:23:24 +01:00
}
2018-07-28 05:17:52 +02:00
}