1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2024 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
16 // http://www.apache.org/licenses/LICENSE-2.0
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
26 using System
.Collections
.Generic
;
27 using System
.ComponentModel
;
28 using System
.Diagnostics
.CodeAnalysis
;
29 using System
.Globalization
;
33 using System
.Net
.Http
;
34 using System
.Net
.Http
.Headers
;
35 using System
.Net
.Http
.Json
;
36 using System
.Threading
;
37 using System
.Threading
.Tasks
;
38 using ArchiSteamFarm
.Core
;
39 using ArchiSteamFarm
.Helpers
.Json
;
40 using ArchiSteamFarm
.Localization
;
41 using ArchiSteamFarm
.NLog
;
42 using ArchiSteamFarm
.Storage
;
43 using ArchiSteamFarm
.Web
.Responses
;
44 using JetBrains
.Annotations
;
46 namespace ArchiSteamFarm
.Web
;
48 public sealed class WebBrowser
: IDisposable
{
50 public const byte MaxTries
= 5; // Defines maximum number of recommended tries for a single request
52 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
54 private const ushort ExtendedTimeout
= 600; // Defines timeout for WebBrowsers dealing with huge data (ASF update)
55 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
58 public CookieContainer CookieContainer { get; }
= new();
61 public TimeSpan Timeout
=> HttpClient
.Timeout
;
63 private readonly ArchiLogger ArchiLogger
;
64 private readonly HttpClient HttpClient
;
65 private readonly HttpClientHandler HttpClientHandler
;
67 internal WebBrowser(ArchiLogger archiLogger
, IWebProxy
? webProxy
= null, bool extendedTimeout
= false) {
68 ArgumentNullException
.ThrowIfNull(archiLogger
);
70 ArchiLogger
= archiLogger
;
72 HttpClientHandler
= new HttpClientHandler
{
73 AllowAutoRedirect
= false, // This must be false if we want to handle custom redirection schemes such as "steammobile://"
74 AutomaticDecompression
= DecompressionMethods
.All
,
75 CookieContainer
= CookieContainer
,
76 MaxConnectionsPerServer
= MaxConnections
79 if (webProxy
!= null) {
80 HttpClientHandler
.Proxy
= webProxy
;
81 HttpClientHandler
.UseProxy
= true;
83 if (webProxy
.Credentials
!= null) {
84 // We can be pretty sure that user knows what he's doing and that proxy indeed requires authentication, save roundtrip
85 HttpClientHandler
.PreAuthenticate
= true;
89 HttpClient
= GenerateDisposableHttpClient(extendedTimeout
);
92 public void Dispose() {
94 HttpClientHandler
.Dispose();
98 public HttpClient
GenerateDisposableHttpClient(bool extendedTimeout
= false) {
99 byte connectionTimeout
= ASF
.GlobalConfig
?.ConnectionTimeout
?? GlobalConfig
.DefaultConnectionTimeout
;
101 HttpClient result
= new(HttpClientHandler
, false) {
102 DefaultRequestVersion
= HttpVersion
.Version30
,
103 Timeout
= TimeSpan
.FromSeconds(extendedTimeout
? ExtendedTimeout
: connectionTimeout
)
106 // Most web services expect that UserAgent is set, so we declare it globally
107 // 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
108 result
.DefaultRequestHeaders
.UserAgent
.Add(new ProductInfoHeaderValue(SharedInfo
.PublicIdentifier
, SharedInfo
.Version
.ToString()));
109 result
.DefaultRequestHeaders
.UserAgent
.Add(new ProductInfoHeaderValue($"({SharedInfo.BuildInfo.Variant}; {OS.Version.Replace("(", "", StringComparison.Ordinal).Replace(")", "", StringComparison.Ordinal)}; +{SharedInfo.ProjectURL})"));
111 // Inform websites that we visit about our preference in language, if possible
112 result
.DefaultRequestHeaders
.AcceptLanguage
.Add(new StringWithQualityHeaderValue("en-US", 0.9));
113 result
.DefaultRequestHeaders
.AcceptLanguage
.Add(new StringWithQualityHeaderValue("en", 0.8));
119 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) {
120 ArgumentNullException
.ThrowIfNull(request
);
121 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
122 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
124 for (byte i
= 0; i
< maxTries
; i
++) {
125 if ((i
> 0) && (rateLimitingDelay
> 0)) {
126 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
129 StreamResponse
? response
= await UrlGetToStream(request
, headers
, referer
, requestOptions
| ERequestOptions
.ReturnClientErrors
, 1, rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
131 if (response
== null) {
132 // Request timed out, try again
136 await using (response
.ConfigureAwait(false)) {
137 if (response
.StatusCode
.IsRedirectionCode()) {
138 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
143 if (response
.StatusCode
.IsClientErrorCode()) {
144 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
149 if (response
.StatusCode
.IsServerErrorCode()) {
150 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
155 if (response
.Content
== null) {
156 throw new InvalidOperationException(nameof(response
.Content
));
159 if (response
.Length
> Array
.MaxLength
) {
160 throw new InvalidOperationException(nameof(response
.Length
));
163 progressReporter
?.Report(0);
165 MemoryStream ms
= new((int) response
.Length
);
167 await using (ms
.ConfigureAwait(false)) {
169 long readThisBatch
= 0;
170 long batchIncreaseSize
= response
.Length
/ 100;
172 ArrayPool
<byte> bytePool
= ArrayPool
<byte>.Shared
;
174 // This is HttpClient's buffer, using more doesn't make sense
175 byte[] buffer
= bytePool
.Rent(8192);
178 while (response
.Content
.CanRead
) {
179 int read
= await response
.Content
.ReadAsync(buffer
.AsMemory(0, buffer
.Length
), cancellationToken
).ConfigureAwait(false);
185 // Report progress in-between downloading only if file is big enough to justify it
186 // Current logic below will report progress if file is bigger than ~800 KB
187 if (batchIncreaseSize
>= buffer
.Length
) {
188 readThisBatch
+= read
;
190 for (; (readThisBatch
>= batchIncreaseSize
) && (batch
< 99); readThisBatch
-= batchIncreaseSize
) {
191 // We need a copy of variable being passed when in for loops, as loop will proceed before our event is launched
192 byte progress
= ++batch
;
194 progressReporter
?.Report(progress
);
198 await ms
.WriteAsync(buffer
.AsMemory(0, read
), cancellationToken
).ConfigureAwait(false);
200 } catch (OperationCanceledException
) when (cancellationToken
.IsCancellationRequested
) {
202 } catch (Exception e
) {
203 ArchiLogger
.LogGenericWarningException(e
);
204 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
208 bytePool
.Return(buffer
);
211 progressReporter
?.Report(100);
213 return new BinaryResponse(response
, ms
.ToArray());
218 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
219 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
225 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) {
226 ArgumentNullException
.ThrowIfNull(request
);
227 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
228 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
230 for (byte i
= 0; i
< maxTries
; i
++) {
231 if ((i
> 0) && (rateLimitingDelay
> 0)) {
232 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
235 StreamResponse
? response
= await UrlGetToStream(request
, headers
, referer
, requestOptions
| ERequestOptions
.ReturnClientErrors
, 1, rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
237 if (response
== null) {
238 // Request timed out, try again
242 await using (response
.ConfigureAwait(false)) {
243 if (response
.StatusCode
.IsRedirectionCode()) {
244 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
249 if (response
.StatusCode
.IsClientErrorCode()) {
250 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
255 if (response
.StatusCode
.IsServerErrorCode()) {
256 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
261 if (response
.Content
== null) {
262 throw new InvalidOperationException(nameof(response
.Content
));
266 return await HtmlDocumentResponse
.Create(response
, cancellationToken
).ConfigureAwait(false);
267 } catch (OperationCanceledException
) when (cancellationToken
.IsCancellationRequested
) {
269 } catch (Exception e
) {
270 if ((requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnSuccess
) && response
.StatusCode
.IsSuccessCode()) || (requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnErrors
) && !response
.StatusCode
.IsSuccessCode())) {
271 return new HtmlDocumentResponse(response
);
274 ArchiLogger
.LogGenericWarningException(e
);
275 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
280 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
281 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
287 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) {
288 ArgumentNullException
.ThrowIfNull(request
);
289 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
290 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
292 for (byte i
= 0; i
< maxTries
; i
++) {
293 if ((i
> 0) && (rateLimitingDelay
> 0)) {
294 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
297 StreamResponse
? response
= await UrlGetToStream(request
, headers
, referer
, requestOptions
| ERequestOptions
.ReturnClientErrors
, 1, rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
299 if (response
== null) {
300 // Request timed out, try again
304 await using (response
.ConfigureAwait(false)) {
305 if (response
.StatusCode
.IsRedirectionCode()) {
306 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
311 if (response
.StatusCode
.IsClientErrorCode()) {
312 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
317 if (response
.StatusCode
.IsServerErrorCode()) {
318 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
323 if (response
.Content
== null) {
324 throw new InvalidOperationException(nameof(response
.Content
));
330 obj
= await response
.Content
.ToJsonObject
<T
>(cancellationToken
).ConfigureAwait(false);
331 } catch (OperationCanceledException
) when (cancellationToken
.IsCancellationRequested
) {
333 } catch (Exception e
) {
334 if ((requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnSuccess
) && response
.StatusCode
.IsSuccessCode()) || (requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnErrors
) && !response
.StatusCode
.IsSuccessCode())) {
335 return new ObjectResponse
<T
>(response
);
338 ArchiLogger
.LogGenericWarningException(e
);
339 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
345 if ((requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnSuccess
) && response
.StatusCode
.IsSuccessCode()) || (requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnErrors
) && !response
.StatusCode
.IsSuccessCode())) {
346 return new ObjectResponse
<T
>(response
);
349 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorIsEmpty
, nameof(obj
)));
354 return new ObjectResponse
<T
>(response
, obj
);
358 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
359 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
365 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) {
366 ArgumentNullException
.ThrowIfNull(request
);
367 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
368 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
370 for (byte i
= 0; i
< maxTries
; i
++) {
371 if ((i
> 0) && (rateLimitingDelay
> 0)) {
372 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
375 HttpResponseMessage
? response
= await InternalGet(request
, headers
, referer
, requestOptions
, HttpCompletionOption
.ResponseHeadersRead
, cancellationToken
).ConfigureAwait(false);
377 if (response
== null) {
378 // Request timed out, try again
382 if (response
.StatusCode
.IsRedirectionCode()) {
383 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
388 if (response
.StatusCode
.IsClientErrorCode()) {
389 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
394 if (response
.StatusCode
.IsServerErrorCode()) {
395 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
400 return new StreamResponse(response
, await response
.Content
.ReadAsStreamAsync(cancellationToken
).ConfigureAwait(false));
403 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
404 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
410 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) {
411 ArgumentNullException
.ThrowIfNull(request
);
412 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
413 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
415 for (byte i
= 0; i
< maxTries
; i
++) {
416 if ((i
> 0) && (rateLimitingDelay
> 0)) {
417 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
420 using HttpResponseMessage
? response
= await InternalHead(request
, headers
, referer
, requestOptions
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
422 if (response
== null) {
426 if (response
.StatusCode
.IsRedirectionCode()) {
427 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
432 if (response
.StatusCode
.IsClientErrorCode()) {
433 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
438 if (response
.StatusCode
.IsServerErrorCode()) {
439 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
444 return new BasicResponse(response
);
447 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
448 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
454 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 {
455 ArgumentNullException
.ThrowIfNull(request
);
456 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
457 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
459 for (byte i
= 0; i
< maxTries
; i
++) {
460 if ((i
> 0) && (rateLimitingDelay
> 0)) {
461 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
464 using HttpResponseMessage
? response
= await InternalPost(request
, headers
, data
, referer
, requestOptions
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
466 if (response
== null) {
470 if (response
.StatusCode
.IsRedirectionCode()) {
471 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
476 if (response
.StatusCode
.IsClientErrorCode()) {
477 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
482 if (response
.StatusCode
.IsServerErrorCode()) {
483 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
488 return new BasicResponse(response
);
491 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
492 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
498 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 {
499 ArgumentNullException
.ThrowIfNull(request
);
500 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
501 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
503 for (byte i
= 0; i
< maxTries
; i
++) {
504 if ((i
> 0) && (rateLimitingDelay
> 0)) {
505 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
508 StreamResponse
? response
= await UrlPostToStream(request
, headers
, data
, referer
, requestOptions
| ERequestOptions
.ReturnClientErrors
, 1, rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
510 if (response
== null) {
511 // Request timed out, try again
515 await using (response
.ConfigureAwait(false)) {
516 if (response
.StatusCode
.IsRedirectionCode()) {
517 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
522 if (response
.StatusCode
.IsClientErrorCode()) {
523 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
528 if (response
.StatusCode
.IsServerErrorCode()) {
529 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
534 if (response
.Content
== null) {
535 throw new InvalidOperationException(nameof(response
.Content
));
539 return await HtmlDocumentResponse
.Create(response
, cancellationToken
).ConfigureAwait(false);
540 } catch (OperationCanceledException
) when (cancellationToken
.IsCancellationRequested
) {
542 } catch (Exception e
) {
543 if ((requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnSuccess
) && response
.StatusCode
.IsSuccessCode()) || (requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnErrors
) && !response
.StatusCode
.IsSuccessCode())) {
544 return new HtmlDocumentResponse(response
);
547 ArchiLogger
.LogGenericWarningException(e
);
548 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
553 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
554 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
560 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 {
561 ArgumentNullException
.ThrowIfNull(request
);
562 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
563 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
565 for (byte i
= 0; i
< maxTries
; i
++) {
566 if ((i
> 0) && (rateLimitingDelay
> 0)) {
567 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
570 StreamResponse
? response
= await UrlPostToStream(request
, headers
, data
, referer
, requestOptions
| ERequestOptions
.ReturnClientErrors
, 1, rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
572 if (response
== null) {
573 // Request timed out, try again
577 await using (response
.ConfigureAwait(false)) {
578 if (response
.StatusCode
.IsRedirectionCode()) {
579 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
584 if (response
.StatusCode
.IsClientErrorCode()) {
585 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
590 if (response
.StatusCode
.IsServerErrorCode()) {
591 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
596 if (response
.Content
== null) {
597 throw new InvalidOperationException(nameof(response
.Content
));
603 obj
= await response
.Content
.ToJsonObject
<TResult
>(cancellationToken
).ConfigureAwait(false);
604 } catch (OperationCanceledException
) when (cancellationToken
.IsCancellationRequested
) {
606 } catch (Exception e
) {
607 if ((requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnSuccess
) && response
.StatusCode
.IsSuccessCode()) || (requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnErrors
) && !response
.StatusCode
.IsSuccessCode())) {
608 return new ObjectResponse
<TResult
>(response
);
611 ArchiLogger
.LogGenericWarningException(e
);
612 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
618 if ((requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnSuccess
) && response
.StatusCode
.IsSuccessCode()) || (requestOptions
.HasFlag(ERequestOptions
.AllowInvalidBodyOnErrors
) && !response
.StatusCode
.IsSuccessCode())) {
619 return new ObjectResponse
<TResult
>(response
);
622 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorIsEmpty
, nameof(obj
)));
627 return new ObjectResponse
<TResult
>(response
, obj
);
631 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
632 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
638 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 {
639 ArgumentNullException
.ThrowIfNull(request
);
640 ArgumentOutOfRangeException
.ThrowIfZero(maxTries
);
641 ArgumentOutOfRangeException
.ThrowIfNegative(rateLimitingDelay
);
643 for (byte i
= 0; i
< maxTries
; i
++) {
644 if ((i
> 0) && (rateLimitingDelay
> 0)) {
645 await Task
.Delay(rateLimitingDelay
, cancellationToken
).ConfigureAwait(false);
648 HttpResponseMessage
? response
= await InternalPost(request
, headers
, data
, referer
, requestOptions
, HttpCompletionOption
.ResponseHeadersRead
, cancellationToken
).ConfigureAwait(false);
650 if (response
== null) {
651 // Request timed out, try again
655 if (response
.StatusCode
.IsRedirectionCode()) {
656 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
661 if (response
.StatusCode
.IsClientErrorCode()) {
662 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnClientErrors
)) {
667 if (response
.StatusCode
.IsServerErrorCode()) {
668 if (!requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
)) {
673 return new StreamResponse(response
, await response
.Content
.ReadAsStreamAsync(cancellationToken
).ConfigureAwait(false));
676 ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorRequestFailedTooManyTimes
, maxTries
));
677 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.ErrorFailingRequest
, request
));
682 internal static void Init() {
683 // Set max connection limit from default of 2 to desired value
684 ServicePointManager
.DefaultConnectionLimit
= MaxConnections
;
686 // Set max idle time from default of 100 seconds (100 * 1000) to desired value
687 ServicePointManager
.MaxServicePointIdleTime
= MaxIdleTime
* 1000;
689 // Don't use Expect100Continue, we're sure about our POSTs, save some TCP packets
690 ServicePointManager
.Expect100Continue
= false;
692 // Reuse ports if possible
693 ServicePointManager
.ReusePort
= true;
696 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) {
697 ArgumentNullException
.ThrowIfNull(request
);
699 if (!Enum
.IsDefined(httpCompletionOption
)) {
700 throw new InvalidEnumArgumentException(nameof(httpCompletionOption
), (int) httpCompletionOption
, typeof(HttpCompletionOption
));
703 return await InternalRequest
<object>(request
, HttpMethod
.Get
, headers
, null, referer
, requestOptions
, httpCompletionOption
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
706 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) {
707 ArgumentNullException
.ThrowIfNull(request
);
709 if (!Enum
.IsDefined(httpCompletionOption
)) {
710 throw new InvalidEnumArgumentException(nameof(httpCompletionOption
), (int) httpCompletionOption
, typeof(HttpCompletionOption
));
713 return await InternalRequest
<object>(request
, HttpMethod
.Head
, headers
, null, referer
, requestOptions
, httpCompletionOption
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
716 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 {
717 ArgumentNullException
.ThrowIfNull(request
);
719 if (!Enum
.IsDefined(httpCompletionOption
)) {
720 throw new InvalidEnumArgumentException(nameof(httpCompletionOption
), (int) httpCompletionOption
, typeof(HttpCompletionOption
));
723 return await InternalRequest(request
, HttpMethod
.Post
, headers
, data
, referer
, requestOptions
, httpCompletionOption
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
726 [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification
= "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
727 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 {
728 ArgumentNullException
.ThrowIfNull(request
);
729 ArgumentNullException
.ThrowIfNull(httpMethod
);
731 if (!Enum
.IsDefined(httpCompletionOption
)) {
732 throw new InvalidEnumArgumentException(nameof(httpCompletionOption
), (int) httpCompletionOption
, typeof(HttpCompletionOption
));
735 HttpResponseMessage response
;
738 using (HttpRequestMessage requestMessage
= new(httpMethod
, request
)) {
739 requestMessage
.Version
= HttpClient
.DefaultRequestVersion
;
741 if (headers
!= null) {
742 foreach ((string header
, string value) in headers
) {
743 requestMessage
.Headers
.Add(header
, value);
749 case HttpContent content
:
750 requestMessage
.Content
= content
;
753 case IReadOnlyCollection
<KeyValuePair
<string, string>> nameValueCollection
:
755 requestMessage
.Content
= new FormUrlEncodedContent(nameValueCollection
);
756 } catch (UriFormatException
) {
757 requestMessage
.Content
= new StringContent(string.Join('&', nameValueCollection
.Select(static kv
=> $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")), null, "application/x-www-form-urlencoded");
762 requestMessage
.Content
= new StringContent(text
);
766 requestMessage
.Content
= JsonContent
.Create(data
, options
: JsonUtilities
.DefaultJsonSerialierOptions
);
771 // Compress the request if caller specified it, so they know that the server supports it, and the content is not compressed yet
772 if (requestOptions
.HasFlag(ERequestOptions
.CompressRequest
) && (requestMessage
.Content
.Headers
.ContentEncoding
.Count
== 0)) {
773 HttpContent originalContent
= requestMessage
.Content
;
775 requestMessage
.Content
= await WebBrowserUtilities
.CreateCompressedHttpContent(originalContent
).ConfigureAwait(false);
777 if (data
is not HttpContent
) {
778 // We don't need to keep old HttpContent around anymore, help GC
779 originalContent
.Dispose();
784 if (referer
!= null) {
785 requestMessage
.Headers
.Referrer
= referer
;
788 if (Debugging
.IsUserDebugging
) {
789 ArchiLogger
.LogGenericDebug($"{httpMethod} {request}");
793 response
= await HttpClient
.SendAsync(requestMessage
, httpCompletionOption
, cancellationToken
).ConfigureAwait(false);
794 } catch (OperationCanceledException
) when (cancellationToken
.IsCancellationRequested
) {
796 } catch (Exception e
) {
797 ArchiLogger
.LogGenericDebuggingException(e
);
801 if (data
is HttpContent
) {
802 // 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
803 requestMessage
.Content
= null;
808 if (Debugging
.IsUserDebugging
) {
809 ArchiLogger
.LogGenericDebug($"{response.StatusCode} <- {httpMethod} {request}");
812 if (response
.IsSuccessStatusCode
) {
816 // WARNING: We still have not disposed response by now, make sure to dispose it ASAP if we're not returning it!
817 if (response
.StatusCode
.IsRedirectionCode() && (maxRedirections
> 0)) {
818 if (requestOptions
.HasFlag(ERequestOptions
.ReturnRedirections
)) {
819 // User wants to handle it manually, that's alright
823 Uri
? redirectUri
= response
.Headers
.Location
;
825 if (redirectUri
== null) {
826 ArchiLogger
.LogNullError(redirectUri
);
831 if (redirectUri
.IsAbsoluteUri
) {
832 switch (redirectUri
.Scheme
) {
833 case "http" or
"https":
836 // Those redirections are invalid, but we're aware of that and we have extra logic for them
839 // We have no clue about those, but maybe HttpClient can handle them for us
840 ArchiLogger
.LogGenericError(string.Format(CultureInfo
.CurrentCulture
, Strings
.WarningUnknownValuePleaseReport
, nameof(redirectUri
.Scheme
), redirectUri
.Scheme
));
845 redirectUri
= new Uri(request
, redirectUri
);
848 switch (response
.StatusCode
) {
849 case HttpStatusCode
.MovedPermanently
: // Per https://tools.ietf.org/html/rfc7231#section-6.4.2, a 301 redirect may be performed using a GET request
850 case HttpStatusCode
.Redirect
: // Per https://tools.ietf.org/html/rfc7231#section-6.4.3, a 302 redirect may be performed using a GET request
851 case HttpStatusCode
.SeeOther
: // Per https://tools.ietf.org/html/rfc7231#section-6.4.4, a 303 redirect should be performed using a GET request
852 if (httpMethod
!= HttpMethod
.Head
) {
853 httpMethod
= HttpMethod
.Get
;
856 // Data doesn't make any sense for a fetch request, clear it in case it's being used
864 // 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
865 if (!string.IsNullOrEmpty(request
.Fragment
) && string.IsNullOrEmpty(redirectUri
.Fragment
)) {
866 redirectUri
= new UriBuilder(redirectUri
) { Fragment = request.Fragment }
.Uri
;
869 request
= redirectUri
;
878 if (!Debugging
.IsUserDebugging
) {
879 ArchiLogger
.LogGenericDebug($"{response.StatusCode} <- {httpMethod} {request}");
882 if (response
.StatusCode
.IsClientErrorCode()) {
883 if (Debugging
.IsUserDebugging
) {
884 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.Content
, await response
.Content
.ReadAsStringAsync(cancellationToken
).ConfigureAwait(false)));
887 // Do not retry on client errors
891 if (requestOptions
.HasFlag(ERequestOptions
.ReturnServerErrors
) && response
.StatusCode
.IsServerErrorCode()) {
892 if (Debugging
.IsUserDebugging
) {
893 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.Content
, await response
.Content
.ReadAsStringAsync(cancellationToken
).ConfigureAwait(false)));
896 // Do not retry on server errors in this case
901 if (Debugging
.IsUserDebugging
) {
902 ArchiLogger
.LogGenericDebug(string.Format(CultureInfo
.CurrentCulture
, Strings
.Content
, await response
.Content
.ReadAsStringAsync(cancellationToken
).ConfigureAwait(false)));
910 public enum ERequestOptions
: byte {
912 ReturnClientErrors
= 1,
913 ReturnServerErrors
= 2,
914 ReturnRedirections
= 4,
915 AllowInvalidBodyOnSuccess
= 8,
916 AllowInvalidBodyOnErrors
= 16,