2019-02-16 17:34:17 +01:00
// _ _ _ ____ _ _____
2017-11-18 17:27:06 +01:00
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2019-01-14 19:11:17 +01:00
// |
2021-01-03 22:24:22 +01:00
// Copyright 2015-2021 Łukasz "JustArchi" Domeradzki
2018-07-27 04:52:14 +02:00
// Contact: JustArchi@JustArchi.net
2019-01-14 19:11:17 +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
2019-01-14 19:11:17 +01:00
// |
2018-07-27 04:52:14 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2019-01-14 19:11:17 +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 ;
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 ;
2020-06-06 16:34:20 +02:00
using System.Text ;
2015-11-25 16:31:39 +01:00
using System.Threading.Tasks ;
2016-03-15 04:20:28 +01:00
using System.Xml ;
2021-05-08 01:43:08 +02:00
using ArchiSteamFarm.Compatibility ;
2021-05-08 01:37:22 +02:00
using ArchiSteamFarm.Core ;
2017-01-06 15:32:12 +01:00
using ArchiSteamFarm.Localization ;
2018-09-08 01:03:55 +02:00
using ArchiSteamFarm.NLog ;
2021-05-08 01:03:08 +02:00
using ArchiSteamFarm.Web.Responses ;
2019-01-10 22:33:07 +01:00
using JetBrains.Annotations ;
2016-11-24 07:32:16 +01:00
using Newtonsoft.Json ;
2015-11-25 16:31:39 +01:00
2021-05-06 20:16:06 +02:00
namespace ArchiSteamFarm.Web {
2019-01-10 22:33:07 +01:00
public sealed class WebBrowser : IDisposable {
2019-07-05 11:39:19 +02:00
[PublicAPI]
public const byte MaxTries = 5 ; // Defines maximum number of recommended tries for a single request
2019-05-19 23:03:40 +02: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-01-14 02:48:56 +01:00
2017-07-09 09:09:46 +02:00
private const byte ExtendedTimeoutMultiplier = 10 ; // Defines multiplier of timeout for WebBrowsers dealing with huge data (ASF update)
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-15 05:15:22 +01:00
2019-07-05 11:39:19 +02:00
[PublicAPI]
2020-11-14 22:37:00 +01:00
public CookieContainer CookieContainer { get ; } = new ( ) ;
2016-03-06 23:28:56 +01:00
2020-09-13 21:43:25 +02:00
[PublicAPI]
public TimeSpan Timeout = > HttpClient . Timeout ;
2017-09-09 20:24:57 +02:00
2016-11-06 12:06:02 +01:00
private readonly ArchiLogger ArchiLogger ;
2016-04-12 16:58:45 +02:00
private readonly HttpClient HttpClient ;
2019-01-02 18:09:07 +01:00
private readonly HttpClientHandler HttpClientHandler ;
2015-12-01 01:34:05 +01:00
2020-08-22 21:41:01 +02:00
internal WebBrowser ( ArchiLogger archiLogger , IWebProxy ? webProxy = null , bool extendedTimeout = false ) {
2017-03-14 08:20:29 -03:00
ArchiLogger = archiLogger ? ? throw new ArgumentNullException ( nameof ( archiLogger ) ) ;
2016-11-24 07:32:16 +01:00
2019-01-02 18:09:07 +01:00
HttpClientHandler = new HttpClientHandler {
2018-09-08 00:46:40 +02:00
AllowAutoRedirect = false , // This must be false if we want to handle custom redirection schemes such as "steammobile://"
2020-04-17 23:44:36 +02:00
2020-08-22 21:41:01 +02:00
#if NETFRAMEWORK
2016-11-24 07:46:37 +01:00
AutomaticDecompression = DecompressionMethods . Deflate | DecompressionMethods . GZip ,
2020-08-22 21:41:01 +02:00
#else
AutomaticDecompression = DecompressionMethods . All ,
2020-04-17 23:44:36 +02:00
#endif
2019-01-02 18:09:07 +01:00
CookieContainer = CookieContainer
2016-11-24 07:46:37 +01:00
} ;
2019-01-02 18:09:07 +01:00
if ( webProxy ! = null ) {
HttpClientHandler . Proxy = webProxy ;
HttpClientHandler . UseProxy = true ;
}
2021-05-06 20:16:06 +02:00
if ( ! StaticHelpers . IsRunningOnMono ) {
2019-01-02 18:09:07 +01:00
HttpClientHandler . MaxConnectionsPerServer = MaxConnections ;
2018-06-03 07:51:20 +02:00
}
2019-01-02 18:09:07 +01:00
HttpClient = GenerateDisposableHttpClient ( extendedTimeout ) ;
}
2016-11-24 07:32:16 +01:00
2019-01-02 18:09:07 +01:00
public void Dispose ( ) {
HttpClient . Dispose ( ) ;
HttpClientHandler . Dispose ( ) ;
2016-11-24 07:32:16 +01:00
}
2019-01-17 16:37:16 +01:00
[PublicAPI]
public HttpClient GenerateDisposableHttpClient ( bool extendedTimeout = false ) {
2020-08-22 21:41:01 +02:00
if ( ASF . GlobalConfig = = null ) {
2020-11-14 22:37:00 +01:00
throw new InvalidOperationException ( nameof ( ASF . GlobalConfig ) ) ;
2020-08-22 21:41:01 +02:00
}
2020-11-14 22:37:00 +01:00
HttpClient result = new ( HttpClientHandler , false ) {
2019-09-27 20:43:11 +02:00
#if ! NETFRAMEWORK
2020-02-21 18:33:21 +03:00
DefaultRequestVersion = HttpVersion . Version20 ,
2019-09-27 20:43:11 +02:00
#endif
2019-01-17 16:37:16 +01:00
Timeout = TimeSpan . FromSeconds ( extendedTimeout ? ExtendedTimeoutMultiplier * ASF . GlobalConfig . ConnectionTimeout : ASF . GlobalConfig . ConnectionTimeout )
} ;
// 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
2021-07-19 14:37:58 +02:00
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 + ")" ) ) ;
2019-01-17 16:37:16 +01:00
return result ;
}
2020-09-14 10:03:05 +02:00
[PublicAPI]
2021-05-06 22:50:59 +02:00
public async Task < BinaryResponse ? > UrlGetToBinary ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries , IProgress < byte > ? progressReporter = null ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2020-09-14 10:03:05 +02:00
}
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-03-11 22:32:59 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 ) . ConfigureAwait ( false ) ;
2020-09-14 10:03:05 +02:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2020-09-14 10:03:05 +02:00
}
2021-03-11 22:32:59 +01:00
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
// We're not handling this error, do not try again
break ;
}
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-18 00:47:06 +02:00
}
2020-09-14 10:03:05 +02:00
2021-03-11 22:32:59 +01:00
progressReporter ? . Report ( 0 ) ;
2020-09-14 10:03:05 +02:00
2021-05-07 23:56:45 +02:00
#pragma warning disable CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
MemoryStream ms = new ( ( int ) response . Length ) ;
#pragma warning restore CA2000 // False positive, we're actually wrapping it in the using clause below exactly for that purpose
2021-03-14 15:16:38 +01:00
2021-05-07 23:56:45 +02:00
await using ( ms . ConfigureAwait ( false ) ) {
2021-06-21 23:20:32 +02:00
byte batch = 0 ;
long readThisBatch = 0 ;
long batchIncreaseSize = response . Length / 100 ;
ArrayPool < byte > bytePool = ArrayPool < byte > . Shared ;
2020-09-14 10:03:05 +02:00
2021-06-21 23:20:32 +02:00
// This is HttpClient's buffer, using more doesn't make sense
byte [ ] buffer = bytePool . Rent ( 8192 ) ;
2020-09-14 10:03:05 +02:00
2021-06-21 23:20:32 +02:00
try {
2021-03-14 15:16:38 +01:00
while ( response . Content . CanRead ) {
int read = await response . Content . ReadAsync ( buffer . AsMemory ( 0 , buffer . Length ) ) . ConfigureAwait ( false ) ;
2020-09-14 10:03:05 +02:00
2021-03-14 15:16:38 +01:00
if ( read = = 0 ) {
break ;
}
2020-09-14 10:03:05 +02:00
2021-03-14 15:16:38 +01:00
await ms . WriteAsync ( buffer . AsMemory ( 0 , read ) ) . ConfigureAwait ( false ) ;
2020-09-14 10:03:05 +02:00
2021-06-21 23:20:32 +02:00
if ( ( progressReporter = = null ) | | ( batchIncreaseSize = = 0 ) | | ( batch > = 99 ) ) {
2021-03-14 15:16:38 +01:00
continue ;
}
2020-09-14 10:03:05 +02:00
2021-03-14 15:16:38 +01:00
readThisBatch + = read ;
2020-09-14 10:03:05 +02:00
2021-06-21 23:20:32 +02:00
while ( ( readThisBatch > = batchIncreaseSize ) & & ( batch < 99 ) ) {
readThisBatch - = batchIncreaseSize ;
progressReporter . Report ( + + batch ) ;
2021-03-14 15:16:38 +01:00
}
2021-03-11 22:32:59 +01:00
}
2021-03-14 15:16:38 +01:00
} catch ( Exception e ) {
2021-06-27 23:18:24 +02:00
ArchiLogger . LogGenericWarningException ( e ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2021-03-11 22:32:59 +01:00
2021-03-14 15:16:38 +01:00
return null ;
2021-06-21 23:20:32 +02:00
} finally {
bytePool . Return ( buffer ) ;
2020-09-14 10:03:05 +02:00
}
2021-03-14 15:16:38 +01:00
progressReporter ? . Report ( 100 ) ;
2020-09-14 10:03:05 +02:00
2021-03-14 15:16:38 +01:00
return new BinaryResponse ( response , ms . ToArray ( ) ) ;
}
2021-03-11 22:32:59 +01:00
}
2020-09-14 10:03:05 +02:00
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01: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
}
2020-11-28 20:45:47 +01:00
return null ;
2020-09-14 10:03:05 +02:00
}
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02:00
public async Task < HtmlDocumentResponse ? > UrlGetToHtmlDocument ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-05-30 01:57:06 +02:00
}
2020-05-27 14:45:27 +02:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-03-11 22:32:59 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 ) . ConfigureAwait ( false ) ;
2020-04-07 23:12:01 +03:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2020-04-07 23:12:01 +03:00
}
2021-03-11 22:32:59 +01:00
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
// We're not handling this error, do not try again
break ;
}
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-18 00:47:06 +02:00
}
2020-04-07 23:12:01 +03:00
2021-03-11 22:32:59 +01:00
try {
return await HtmlDocumentResponse . Create ( response ) . ConfigureAwait ( false ) ;
} catch ( Exception e ) {
ArchiLogger . LogGenericWarningException ( e ) ;
2021-06-27 23:18:24 +02:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2021-03-11 22:32:59 +01:00
}
2020-04-07 23:12:01 +03:00
}
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2020-04-07 23:12:01 +03:00
}
2020-11-28 20:45:47 +01:00
return null ;
2016-05-30 01:57:06 +02:00
}
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02: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 ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-05-30 01:57:06 +02:00
}
2018-11-28 02:30:50 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-03-11 22:32:59 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 ) . ConfigureAwait ( false ) ;
2018-11-28 02:30:50 +01:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2019-04-05 16:24:02 +02:00
}
2021-03-11 22:32:59 +01:00
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
// We're not handling this error, do not try again
break ;
}
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-18 00:47:06 +02:00
}
2016-05-30 01:57:06 +02:00
2021-03-11 22:32:59 +01:00
T ? obj ;
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 {
using StreamReader streamReader = new ( response . Content ) ;
using JsonTextReader jsonReader = new ( streamReader ) ;
2020-11-14 22:37:00 +01:00
2021-03-11 22:32:59 +01:00
JsonSerializer serializer = new ( ) ;
2020-04-02 18:01:55 +03:00
2021-03-11 22:32:59 +01:00
obj = serializer . Deserialize < T > ( jsonReader ) ;
2020-08-22 21:41:01 +02:00
2021-03-11 22:32:59 +01:00
if ( obj is null ) {
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorIsEmpty , nameof ( obj ) ) ) ;
continue ;
}
} 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
continue ;
}
2017-04-05 14:16:47 +02:00
2021-03-11 22:32:59 +01:00
return new ObjectResponse < T > ( response , obj ) ;
2017-04-05 14:16:47 +02:00
}
2016-04-12 07:40:02 +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
2018-11-28 02:30:50 +01:00
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01: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
}
2020-11-28 20:45:47 +01:00
return null ;
2016-04-12 07:40:02 +02:00
}
2020-09-14 10:03:05 +02:00
[PublicAPI]
2021-05-06 22:50:59 +02:00
public async Task < StreamResponse ? > UrlGetToStream ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2020-09-14 10:03:05 +02:00
}
for ( byte i = 0 ; i < maxTries ; i + + ) {
HttpResponseMessage ? response = await InternalGet ( request , headers , referer , requestOptions , HttpCompletionOption . ResponseHeadersRead ) . ConfigureAwait ( false ) ;
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2020-09-14 10:03:05 +02:00
}
2020-09-18 00:47:06 +02:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
2020-09-18 00:51:29 +02:00
// We're not handling this error, do not try again
break ;
2020-09-14 10:03:05 +02:00
}
2021-01-04 15:40:35 +01:00
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
2020-09-18 00:47:06 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-14 10:03:05 +02:00
}
return new StreamResponse ( response , await response . Content . ReadAsStreamAsync ( ) . ConfigureAwait ( false ) ) ;
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01: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
}
2020-11-28 20:45:47 +01:00
return null ;
2020-09-14 10:03:05 +02:00
}
2021-08-02 22:15:37 +02:00
[Obsolete("ASF no longer uses this function, re-implement it yourself using " + nameof(UrlGetToStream) + " if needed.")]
2020-09-14 10:03:05 +02:00
[PublicAPI]
2021-05-06 22:50:59 +02:00
public async Task < StringResponse ? > UrlGetToString ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2020-09-14 10:03:05 +02:00
}
for ( byte i = 0 ; i < maxTries ; i + + ) {
2020-11-28 19:51:09 +01:00
using HttpResponseMessage ? response = await InternalGet ( request , headers , referer , requestOptions ) . ConfigureAwait ( false ) ;
2020-09-14 10:03:05 +02:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2020-09-14 10:03:05 +02:00
}
2020-09-18 00:47:06 +02:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
2020-09-18 00:51:29 +02:00
// We're not handling this error, do not try again
break ;
2020-09-14 10:03:05 +02:00
}
2021-01-04 15:40:35 +01:00
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
2020-09-18 00:47:06 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-14 10:03:05 +02:00
}
return new StringResponse ( response , await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ) ;
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01: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
}
2020-11-28 20:45:47 +01:00
return null ;
2020-09-14 10:03:05 +02:00
}
2021-08-02 22:15:37 +02:00
[Obsolete("ASF no longer uses any XML-related functions, re-implement it yourself using " + nameof(UrlGetToStream) + " if needed.")]
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02:00
public async Task < XmlDocumentResponse ? > UrlGetToXmlDocument ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-05-30 01:57:06 +02:00
}
2018-11-28 02:30:50 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-03-11 22:32:59 +01:00
StreamResponse ? response = await UrlGetToStream ( request , headers , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 ) . ConfigureAwait ( false ) ;
2019-04-05 16:24:02 +02:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2019-04-05 16:24:02 +02:00
}
2016-11-24 07:32:16 +01:00
2021-03-11 22:32:59 +01:00
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
// We're not handling this error, do not try again
break ;
}
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-18 00:47:06 +02:00
}
2016-11-24 07:32:16 +01:00
2021-03-11 22:32:59 +01:00
XmlDocument xmlDocument = new ( ) ;
2018-11-28 02:30:50 +01:00
2021-03-11 22:32:59 +01:00
try {
2021-05-07 21:32:09 +02:00
using XmlReader xmlReader = XmlReader . Create ( response . Content , new XmlReaderSettings { XmlResolver = null } ) ;
xmlDocument . Load ( xmlReader ) ;
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 ) ) ;
2018-12-15 00:27:15 +01:00
2021-03-11 22:32:59 +01:00
continue ;
}
2018-11-28 02:30:50 +01:00
2021-03-11 22:32:59 +01:00
return new XmlDocumentResponse ( response , xmlDocument ) ;
}
2018-11-28 02:30:50 +01:00
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2016-11-24 07:32:16 +01:00
}
2020-11-28 20:45:47 +01:00
return null ;
2016-11-24 07:32:16 +01:00
}
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02:00
public async Task < BasicResponse ? > UrlHead ( Uri request , IReadOnlyCollection < KeyValuePair < string , string > > ? headers = null , Uri ? referer = null , ERequestOptions requestOptions = ERequestOptions . None , byte maxTries = MaxTries ) {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-11-24 07:32:16 +01:00
}
2020-08-22 21:41:01 +02:00
BasicResponse ? result = null ;
2019-04-05 16:24:02 +02:00
2018-03-09 23:48:47 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2020-11-28 19:51:09 +01:00
using HttpResponseMessage ? response = await InternalHead ( request , headers , referer , requestOptions ) . ConfigureAwait ( false ) ;
2016-11-24 07:32:16 +01:00
2019-09-27 20:43:11 +02:00
if ( response = = null ) {
continue ;
}
2019-04-05 16:24:02 +02:00
2019-09-27 20:43:11 +02:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
result = new BasicResponse ( response ) ;
2019-04-05 16:24:02 +02:00
}
2019-09-27 20:43:11 +02:00
break ;
2018-03-09 23:48:47 +01:00
}
2019-09-27 20:43:11 +02:00
2020-08-23 19:24:10 +02:00
if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
result = new BasicResponse ( response ) ;
}
2020-09-18 00:51:29 +02:00
continue ;
2020-08-23 19:24:10 +02:00
}
2019-09-27 20:43:11 +02:00
return new BasicResponse ( response ) ;
2016-12-04 01:07:37 +01:00
}
2018-03-10 02:33:50 +01:00
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2018-03-10 02:33:50 +01:00
}
2019-04-05 16:24:02 +02:00
return result ;
2016-12-04 01:07:37 +01:00
}
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02: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 ) where T : class {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-05-30 01:57:06 +02:00
}
2020-08-22 21:41:01 +02:00
BasicResponse ? result = null ;
2019-04-05 16:24:02 +02:00
2018-03-09 23:48:47 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2020-11-28 19:51:09 +01:00
using HttpResponseMessage ? response = await InternalPost ( request , headers , data , referer , requestOptions ) . ConfigureAwait ( false ) ;
2016-05-30 01:57:06 +02:00
2019-09-27 20:43:11 +02:00
if ( response = = null ) {
continue ;
}
2019-04-05 16:24:02 +02:00
2019-09-27 20:43:11 +02:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
result = new BasicResponse ( response ) ;
2019-04-05 16:24:02 +02:00
}
2019-09-27 20:43:11 +02:00
break ;
2018-03-09 23:48:47 +01:00
}
2019-09-27 20:43:11 +02:00
2020-08-23 19:24:10 +02:00
if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
result = new BasicResponse ( response ) ;
}
2020-09-18 00:51:29 +02:00
continue ;
2020-08-23 19:24:10 +02:00
}
2019-09-27 20:43:11 +02:00
return new BasicResponse ( response ) ;
2016-05-30 01:57:06 +02:00
}
2018-03-10 02:33:50 +01:00
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2018-03-10 02:33:50 +01:00
}
2019-04-05 16:24:02 +02:00
return result ;
2016-05-30 01:57:06 +02:00
}
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02: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 ) where T : class {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-11-17 21:30:17 +01:00
}
2020-05-27 14:45:27 +02:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-03-11 22:32:59 +01:00
StreamResponse ? response = await UrlPostToStream ( request , headers , data , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 ) . ConfigureAwait ( false ) ;
2020-04-07 23:12:01 +03:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2020-04-07 23:12:01 +03:00
}
2021-03-11 22:32:59 +01:00
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
// We're not handling this error, do not try again
break ;
}
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-18 00:47:06 +02:00
}
2020-04-07 23:12:01 +03:00
2021-03-11 22:32:59 +01:00
try {
return await HtmlDocumentResponse . Create ( response ) . ConfigureAwait ( false ) ;
} catch ( Exception e ) {
ArchiLogger . LogGenericWarningException ( e ) ;
2021-06-27 23:18:24 +02:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2021-03-11 22:32:59 +01:00
}
2020-04-07 23:12:01 +03:00
}
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorRequestFailedTooManyTimes , maxTries ) ) ;
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorFailingRequest , request ) ) ;
2020-04-07 23:12:01 +03:00
}
2018-12-15 00:27:15 +01:00
2020-11-28 20:45:47 +01:00
return null ;
2016-11-17 21:30:17 +01:00
}
2019-01-10 22:33:07 +01:00
[PublicAPI]
2021-05-06 22:50:59 +02: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 ) where TData : class {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2016-07-16 21:03:39 +02:00
}
2020-11-28 20:45:47 +01:00
for ( byte i = 0 ; i < maxTries ; i + + ) {
2021-03-11 22:32:59 +01:00
StreamResponse ? response = await UrlPostToStream ( request , headers , data , referer , requestOptions | ERequestOptions . ReturnClientErrors , 1 ) . ConfigureAwait ( false ) ;
2019-04-05 16:24:02 +02:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2019-04-05 16:24:02 +02:00
}
2021-03-11 22:32:59 +01:00
await using ( response . ConfigureAwait ( false ) ) {
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
// We're not handling this error, do not try again
break ;
}
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-09-18 00:47:06 +02:00
}
2018-11-28 02:30:50 +01:00
2021-03-11 22:32:59 +01:00
TResult ? obj ;
try {
using StreamReader steamReader = new ( response . Content ) ;
using JsonReader jsonReader = new JsonTextReader ( steamReader ) ;
2018-11-28 02:30:50 +01:00
2021-03-11 22:32:59 +01:00
JsonSerializer serializer = new ( ) ;
2021-01-10 21:27:52 +01:00
2021-03-11 22:32:59 +01:00
obj = serializer . Deserialize < TResult > ( jsonReader ) ;
2020-04-02 18:01:55 +03:00
2021-03-11 22:32:59 +01:00
if ( obj is null ) {
ArchiLogger . LogGenericWarning ( string . Format ( CultureInfo . CurrentCulture , Strings . ErrorIsEmpty , nameof ( obj ) ) ) ;
2020-08-22 21:41:01 +02:00
2021-03-11 22:32:59 +01:00
continue ;
}
} 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
continue ;
}
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
return new ObjectResponse < TResult > ( response , obj ) ;
2017-04-05 14:16:47 +02:00
}
2016-07-16 21:03:39 +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
2018-11-28 02:30:50 +01:00
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01: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
}
2020-11-28 20:45:47 +01:00
return null ;
2016-07-16 21:03:39 +02:00
}
2020-09-14 10:03:05 +02:00
[PublicAPI]
2021-05-06 22:50:59 +02: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 ) where T : class {
if ( request = = null ) {
2020-11-14 22:37:00 +01:00
throw new ArgumentNullException ( nameof ( request ) ) ;
}
if ( maxTries = = 0 ) {
throw new ArgumentOutOfRangeException ( nameof ( maxTries ) ) ;
2019-01-10 22:33:07 +01:00
}
for ( byte i = 0 ; i < maxTries ; i + + ) {
2020-09-14 10:03:05 +02:00
HttpResponseMessage ? response = await InternalPost ( request , headers , data , referer , requestOptions , HttpCompletionOption . ResponseHeadersRead ) . ConfigureAwait ( false ) ;
2019-01-10 22:33:07 +01:00
2020-09-18 00:47:06 +02:00
if ( response = = null ) {
// Request timed out, try again
continue ;
2019-09-27 20:43:11 +02:00
}
2019-01-10 22:33:07 +01:00
2020-09-18 00:47:06 +02:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnClientErrors ) ) {
2020-09-18 00:51:29 +02:00
// We're not handling this error, do not try again
break ;
2020-08-23 19:24:10 +02:00
}
2021-01-04 15:40:35 +01:00
} else if ( response . StatusCode . IsServerErrorCode ( ) ) {
2020-09-18 00:47:06 +02:00
if ( ! requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) ) {
// We're not handling this error, try again
continue ;
}
2020-05-27 15:42:12 +03:00
}
2020-09-14 10:03:05 +02:00
return new StreamResponse ( response , await response . Content . ReadAsStreamAsync ( ) . ConfigureAwait ( false ) ) ;
2019-01-10 22:33:07 +01:00
}
if ( maxTries > 1 ) {
2020-11-14 22:37:00 +01: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
}
2020-11-28 20:45:47 +01:00
return null ;
2019-01-10 22:33:07 +01:00
}
2020-09-14 10:03:05 +02:00
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
2020-09-14 10:03:05 +02: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
2020-09-14 10:03:05 +02: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
2020-09-14 10:03:05 +02:00
// Reuse ports if possible
2021-05-06 20:16:06 +02:00
if ( ! StaticHelpers . IsRunningOnMono ) {
2020-09-14 10:03:05 +02:00
ServicePointManager . ReusePort = true ;
2019-01-10 22:33:07 +01:00
}
}
2021-05-06 22:50:59 +02: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 ) {
if ( request = = null ) {
2020-08-22 21:41:01 +02:00
throw new ArgumentNullException ( nameof ( request ) ) ;
2016-03-15 04:20:28 +01:00
}
2021-05-06 22:50:59 +02:00
return await InternalRequest < object > ( request , HttpMethod . Get , headers , null , referer , requestOptions , httpCompletionOption ) . ConfigureAwait ( false ) ;
2016-03-15 04:20:28 +01:00
}
2021-05-06 22:50:59 +02: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 ) {
if ( request = = null ) {
2020-08-22 21:41:01 +02:00
throw new ArgumentNullException ( nameof ( request ) ) ;
2016-05-30 01:57:06 +02:00
}
2021-05-06 22:50:59 +02:00
return await InternalRequest < object > ( request , HttpMethod . Head , headers , null , referer , requestOptions , httpCompletionOption ) . ConfigureAwait ( false ) ;
2016-05-30 01:57:06 +02:00
}
2021-05-06 22:50:59 +02: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 ) where T : class {
if ( request = = null ) {
2020-08-22 21:41:01 +02:00
throw new ArgumentNullException ( nameof ( request ) ) ;
2016-04-14 22:23:37 +02:00
}
2021-05-06 22:50:59 +02:00
return await InternalRequest ( request , HttpMethod . Post , headers , data , referer , requestOptions , httpCompletionOption ) . ConfigureAwait ( false ) ;
2016-04-14 22:23:37 +02:00
}
2021-05-06 22:50:59 +02: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 ) where T : class {
if ( request = = null ) {
throw new ArgumentNullException ( nameof ( request ) ) ;
2020-11-14 22:37:00 +01:00
}
if ( httpMethod = = null ) {
throw new ArgumentNullException ( nameof ( httpMethod ) ) ;
2015-11-25 16:31:39 +01: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
HttpResponseMessage response ;
2021-05-06 22:50:59 +02:00
using ( HttpRequestMessage requestMessage = new ( httpMethod , request ) ) {
2020-02-21 17:07:05 +01:00
#if ! NETFRAMEWORK
2021-05-06 22:50:59 +02:00
requestMessage . Version = HttpClient . DefaultRequestVersion ;
2020-02-21 17:07:05 +01:00
#endif
2020-09-13 21:43:25 +02:00
if ( headers ! = null ) {
foreach ( ( string header , string value ) in headers ) {
2021-05-06 22:50:59 +02:00
requestMessage . Headers . Add ( header , value ) ;
2020-09-13 21:43:25 +02:00
}
}
2016-07-16 21:03:39 +02:00
if ( data ! = null ) {
2020-06-06 16:34:20 +02:00
switch ( data ) {
2020-06-07 18:09:01 +03:00
case HttpContent content :
2021-05-06 22:50:59 +02:00
requestMessage . Content = content ;
2020-06-07 18:09:01 +03:00
break ;
2021-05-30 17:12:39 +02:00
case IReadOnlyCollection < KeyValuePair < string? , string? > > nameValueCollection :
2020-06-06 16:34:20 +02:00
try {
2021-05-30 17:12:39 +02:00
requestMessage . Content = new FormUrlEncodedContent ( nameValueCollection ) ;
2020-06-06 16:34:20 +02:00
} catch ( UriFormatException ) {
2021-05-30 17:12:39 +02:00
requestMessage . Content = new StringContent ( string . Join ( "&" , nameValueCollection . Select ( kv = > WebUtility . UrlEncode ( kv . Key ) + "=" + WebUtility . UrlEncode ( kv . Value ) ) ) , null , "application/x-www-form-urlencoded" ) ;
2020-06-06 16:34:20 +02:00
}
break ;
case string text :
2021-05-06 22:50:59 +02:00
requestMessage . Content = new StringContent ( text ) ;
2020-06-06 16:34:20 +02:00
break ;
default :
2021-05-06 22:50:59 +02:00
requestMessage . Content = new StringContent ( JsonConvert . SerializeObject ( data ) , Encoding . UTF8 , "application/json" ) ;
2020-06-06 16:34:20 +02:00
break ;
2016-02-22 18:34:45 +01:00
}
}
2015-11-25 16:31:39 +01:00
2021-05-06 22:50:59 +02: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-05-06 22:50:59 +02:00
ArchiLogger . LogGenericDebug ( httpMethod + " " + request ) ;
2018-04-11 18:32:45 +02:00
}
2016-02-22 18:34:45 +01:00
try {
2021-05-06 22:50:59 +02:00
response = await HttpClient . SendAsync ( requestMessage , httpCompletionOption ) . ConfigureAwait ( false ) ;
2016-06-11 03:39:25 +02:00
} catch ( Exception e ) {
2017-11-26 19:08:48 +01:00
ArchiLogger . LogGenericDebuggingException ( e ) ;
2018-12-15 00:27:15 +01:00
2016-02-22 18:34:45 +01:00
return null ;
2020-09-12 13:17:43 +02:00
} 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
2021-05-06 22:50:59 +02:00
requestMessage . Content = null ;
2020-09-12 13:17:43 +02:00
}
2016-02-22 18:34:45 +01:00
}
2015-11-25 16:31:39 +01:00
}
2018-04-11 22:23:31 +02:00
if ( Debugging . IsUserDebugging ) {
2021-05-06 22:50:59 +02:00
ArchiLogger . LogGenericDebug ( response . StatusCode + " <- " + httpMethod + " " + request ) ;
2018-04-11 22:23:31 +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
if ( response . IsSuccessStatusCode ) {
return response ;
2016-05-13 06:32:42 +02:00
}
2018-09-08 00:46:40 +02:00
// WARNING: We still have not disposed response by now, make sure to dispose it ASAP if we're not returning it!
2021-04-11 00:33:32 +02:00
if ( response . StatusCode is > = HttpStatusCode . Ambiguous and < HttpStatusCode . BadRequest & & ( maxRedirections > 0 ) ) {
2020-11-10 23:22:57 +01:00
Uri ? redirectUri = response . Headers . Location ;
if ( redirectUri = = null ) {
ArchiLogger . LogNullError ( nameof ( redirectUri ) ) ;
return null ;
}
2018-02-17 03:57:09 +01:00
if ( redirectUri . IsAbsoluteUri ) {
switch ( redirectUri . Scheme ) {
case "http" :
case "https" :
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
2020-11-14 22:37:00 +01:00
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
}
} else {
2021-05-06 22:50:59 +02:00
redirectUri = new Uri ( request , redirectUri ) ;
2019-01-07 19:01:09 +01:00
}
2020-07-23 21:36:05 +02:00
switch ( response . StatusCode ) {
2020-09-12 13:27:39 +02:00
case HttpStatusCode . MovedPermanently : // Per https://tools.ietf.org/html/rfc7231#section-6.4.2, a 301 redirect may be performed using a GET request
2020-09-12 13:17:43 +02:00
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
2020-09-12 13:17:43 +02:00
// Data doesn't make any sense for a fetch request, clear it in case it's being used
2020-07-23 21:36:05 +02:00
data = null ;
break ;
}
2019-06-19 16:42:04 +02:00
response . Dispose ( ) ;
2019-01-07 19:01:09 +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
2021-05-06 22:50:59 +02:00
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
2020-09-13 21:43:25 +02:00
return await InternalRequest ( redirectUri , httpMethod , headers , data , referer , requestOptions , httpCompletionOption , - - maxRedirections ) . ConfigureAwait ( false ) ;
2018-02-17 03:57:09 +01:00
}
2020-07-03 18:54:13 +02:00
if ( ! Debugging . IsUserDebugging ) {
2021-05-06 22:50:59 +02:00
ArchiLogger . LogGenericDebug ( response . StatusCode + " <- " + httpMethod + " " + request ) ;
2020-07-03 18:54:13 +02:00
}
2019-04-05 16:44:31 +02:00
if ( response . StatusCode . IsClientErrorCode ( ) ) {
2019-06-19 14:14:35 +02:00
if ( Debugging . IsUserDebugging ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . Content , await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ) ) ;
2019-06-19 14:14:35 +02:00
}
2019-04-05 16:24:02 +02:00
// Do not retry on client errors
return response ;
}
2020-08-23 19:24:10 +02:00
if ( requestOptions . HasFlag ( ERequestOptions . ReturnServerErrors ) & & response . StatusCode . IsServerErrorCode ( ) ) {
2021-01-04 15:06:34 +01:00
if ( Debugging . IsUserDebugging ) {
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . Content , await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ) ) ;
}
2020-08-23 19:24:10 +02:00
// Do not retry on server errors in this case
return response ;
}
2018-04-13 09:17:27 +02:00
using ( response ) {
if ( Debugging . IsUserDebugging ) {
2020-11-14 22:37:00 +01:00
ArchiLogger . LogGenericDebug ( string . Format ( CultureInfo . CurrentCulture , Strings . Content , await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ) ) ;
2018-04-13 09:17:27 +02:00
}
return null ;
}
2015-11-25 16:31:39 +01: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
2020-09-14 10:03:05 +02:00
[Flags]
public enum ERequestOptions : byte {
None = 0 ,
ReturnClientErrors = 1 ,
ReturnServerErrors = 2
}
2015-11-25 16:31:39 +01:00
}
2018-07-28 05:17:52 +02:00
}