From df25602f95dc0fa4762d72e4ea32904bc5f238d2 Mon Sep 17 00:00:00 2001 From: "Haga R." Date: Tue, 7 Jan 2025 11:06:47 +0100 Subject: [PATCH] Add customization of `JA4` fingerprint, `H2` settings, headers for browser impersonation (#341) * Add `SetCiphersAction` * Add more extension testing * Use custom `Ja3FingerPrint` struct * Exclude client_extension 34 * Add more client extension default value * Update fingerprint tests * Use https://check.ja3.zone/ for test endpoint * Add repetition for local test * Add handling of extra encrypted extensions (client) * Fix fake certificate compress (unhandled by bouncycastle) * Fix ja3.zone always return 711 even on TLS13 * Update local tests * Add `delegate_credentials` mock * Regroup settings announcement in on flushed buffer * Add H2 initialwindow size announcement * Add grease in protocol version * More signature adjustment * Remove custom sorting at fluxzy level * Add ImpersonateAction * Put scheme before Path (controlled by cloud flare) * Add option to avoid replacing if header already exists * Add ImpersonageAgent to identify client * Add firefox 133 configuration * Add secp256r1 to default early keys * Fix encrypted client hello should work without grease * Make SignatureAndHash algorithmls customizable through Fingerprint * Add Impersonate Profile `Chrome_Android_131` * Refactor namespaces for Impersonate classes * Use stock Fluxzy.BouncyCastle package * Added edge 131 impersonate profile * Rename ImpersonateAgent to ImpersonateProfile * More renaming * Remove SetTlsCiphersActions * Add sample for Impersonation * Refactor names * Add default impersonate profile generation * Minimize allocations on protocol versions resolving * Refactor TlsFingerPrint add Comments * Change default test values for fingerprint * Add Early key shared group as editable settings * Sanitize unit tests * Rename to built-in profiles --- docs/actions/ImpersonateAction.md | 55 ++ docs/actions/SetJa3FingerPrintAction.md | 43 + .../Chrome_Android_131.json | 95 +++ .../Chrome_Windows_131.json | 95 +++ .../Edge_Windows_131.json | 95 +++ .../Firefox_Windows_133.json | 86 ++ docs/searchable-items.json | 2 +- .../Program.cs | 40 + .../Samples.No016.ImpersonateBrowser.csproj | 19 + fluxzy.core.sln | 7 + src/Fluxzy.Core/Archiving/SslInfo.cs | 194 ++--- .../DotNetBridge/FluxzyHttp2Handler.cs | 2 +- .../Clients/H11/Http11ConnectionPool.cs | 488 +++++------ .../Clients/H11/TunnelOnlyConnectionPool.cs | 346 ++++---- .../Clients/H2/Encoder/Utils/Http11Parser.cs | 5 +- .../Clients/H2/Frames/SettingFrame.cs | 15 +- .../Clients/H2/H2ConnectionPool.cs | 8 +- src/Fluxzy.Core/Clients/H2/H2StreamSetting.cs | 72 +- src/Fluxzy.Core/Clients/H2/HeaderEncoder.cs | 2 +- src/Fluxzy.Core/Clients/H2/SettingHelper.cs | 44 +- src/Fluxzy.Core/Clients/H2/StreamPool.cs | 246 +++--- src/Fluxzy.Core/Clients/H2/StreamWorker.cs | 2 +- src/Fluxzy.Core/Clients/H2Logger.cs | 784 +++++++++--------- .../Clients/Headers/HeaderAlterationAdd.cs | 44 +- .../Headers/HeaderAlterationReplace.cs | 54 +- .../Clients/IRemoteConnectionBuilder.cs | 303 +++---- src/Fluxzy.Core/Clients/PoolBuilder.cs | 6 +- .../Clients/Ssl/AdvancedTlsSettings.cs | 19 + .../BouncyCastleConnectionBuilder.cs | 11 +- .../Clients/Ssl/BouncyCastle/ConstBuffers.cs | 51 ++ .../FingerPrintTlsExtensionsEnforcer.cs | 205 +++++ .../Ssl/BouncyCastle/FluxzyClientProtocol.cs | 20 +- .../Clients/Ssl/BouncyCastle/FluxzyCrypto.cs | 1 + .../Ssl/BouncyCastle/FluxzyTlsClient.cs | 139 +++- .../Ssl/BouncyCastle/ProtocolVersionHelper.cs | 162 ++++ .../Clients/Ssl/CipherSuiteNames.cs | 335 ++++++++ .../Clients/Ssl/ImpersonateConfiguration.cs | 134 +++ .../Ssl/ImpersonateConfigurationManager.cs | 96 +++ .../Clients/Ssl/ImpersonateProfile.cs | 110 +++ .../Ssl/SslConnectionBuilderOptions.cs | 9 +- ...ectionBuilderOptionsCipherConfiguration.cs | 2 + src/Fluxzy.Core/Clients/Ssl/TlsFingerPrint.cs | 224 +++++ src/Fluxzy.Core/Core/ExchangeContext.cs | 12 + src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs | 14 + src/Fluxzy.Core/Fluxzy.Core.csproj | 2 +- .../Rules/ActionDistinctiveAttribute.cs | 2 +- .../Rules/Actions/ImpersonateAction.cs | 130 +++ .../Actions/ImpersonateProfileManager.cs | 226 +++++ .../Rules/Actions/SetJa3FingerPrintAction.cs | 51 ++ .../Actions/UpdateRequestHeaderAction.cs | 16 +- src/Fluxzy.Tools.DocGen/Program.cs | 22 + src/Fluxzy/Commands/StartCommandBuilder.cs | 6 +- src/Fluxzy/Properties/launchSettings.json | 6 + test/Fluxzy.Tests/Fluxzy.Tests.csproj | 3 + .../ImpersonateConfigurationTests.cs | 28 + test/Fluxzy.Tests/ImpersonateTests.cs | 75 ++ test/Fluxzy.Tests/Ja3FingerPrintResponse.cs | 59 ++ test/Fluxzy.Tests/ProxyTests.cs | 2 +- test/Fluxzy.Tests/Startup.cs | 2 + test/Fluxzy.Tests/TlsFingerPrintTests.cs | 150 ++++ .../Rules/RequestHeaderAlterationRules.cs | 522 ++++++------ .../Rules/ResponseHeaderAlerationRules.cs | 2 +- test/Fluxzy.Tests/_Files/Ja3/fingerprints.txt | 5 + .../_Fixtures/AddHocConfigurableProxy.cs | 39 +- 64 files changed, 4442 insertions(+), 1602 deletions(-) create mode 100644 docs/actions/ImpersonateAction.md create mode 100644 docs/actions/SetJa3FingerPrintAction.md create mode 100644 docs/impersonate-profiles/Chrome_Android_131.json create mode 100644 docs/impersonate-profiles/Chrome_Windows_131.json create mode 100644 docs/impersonate-profiles/Edge_Windows_131.json create mode 100644 docs/impersonate-profiles/Firefox_Windows_133.json create mode 100644 examples/Samples.No016.ImpersonateBrowser/Program.cs create mode 100644 examples/Samples.No016.ImpersonateBrowser/Samples.No016.ImpersonateBrowser.csproj create mode 100644 src/Fluxzy.Core/Clients/Ssl/AdvancedTlsSettings.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ConstBuffers.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FingerPrintTlsExtensionsEnforcer.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ProtocolVersionHelper.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/CipherSuiteNames.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/ImpersonateConfiguration.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/ImpersonateConfigurationManager.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/ImpersonateProfile.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptionsCipherConfiguration.cs create mode 100644 src/Fluxzy.Core/Clients/Ssl/TlsFingerPrint.cs create mode 100644 src/Fluxzy.Core/Rules/Actions/ImpersonateAction.cs create mode 100644 src/Fluxzy.Core/Rules/Actions/ImpersonateProfileManager.cs create mode 100644 src/Fluxzy.Core/Rules/Actions/SetJa3FingerPrintAction.cs create mode 100644 test/Fluxzy.Tests/ImpersonateConfigurationTests.cs create mode 100644 test/Fluxzy.Tests/ImpersonateTests.cs create mode 100644 test/Fluxzy.Tests/Ja3FingerPrintResponse.cs create mode 100644 test/Fluxzy.Tests/TlsFingerPrintTests.cs create mode 100644 test/Fluxzy.Tests/_Files/Ja3/fingerprints.txt diff --git a/docs/actions/ImpersonateAction.md b/docs/actions/ImpersonateAction.md new file mode 100644 index 000000000..ae03692be --- /dev/null +++ b/docs/actions/ImpersonateAction.md @@ -0,0 +1,55 @@ +## impersonateAction + +### Description + +Impersonate a browser or client by changing the TLS fingerprint, HTTP/2 settings and headers. + +### Evaluation scope + +Evaluation scope defines the timing where this filter will be applied. + +{.alert .alert-info} +::: +**requestHeaderReceivedFromClient** This scope occurs the moment fluxzy parsed the request header receiveid from client +::: + +### YAML configuration name + +impersonateAction + +### Settings + +The following table describes the customizable properties available for this action: + +{.property-table .property-table-action} +::: +| Property | Type | Description | DefaultValue | +| :------- | :------- | :------- | -------- | +| nameOrConfigFile | string | | | + +::: +### Example of usage + +The following examples apply this action to any exchanges + +Impersonate CHROME 131 on Windows. + +```yaml +rules: +- filter: + typeKind: AnyFilter + actions: + - typeKind: ImpersonateAction + nameOrConfigFile: Chrome_Windows_131 +``` + + + +### .NET reference + +View definition of [ImpersonateAction](https://docs.fluxzy.io/api/Fluxzy.Rules.Actions.ImpersonateAction.html) for .NET integration. + +### See also + +This action has no related action + diff --git a/docs/actions/SetJa3FingerPrintAction.md b/docs/actions/SetJa3FingerPrintAction.md new file mode 100644 index 000000000..337ae2b45 --- /dev/null +++ b/docs/actions/SetJa3FingerPrintAction.md @@ -0,0 +1,43 @@ +## setJa3FingerPrintAction + +### Description + +Set a JA3 fingerprint of ongoing connection. + +### Evaluation scope + +Evaluation scope defines the timing where this filter will be applied. + +{.alert .alert-info} +::: +**requestHeaderReceivedFromClient** This scope occurs the moment fluxzy parsed the request header receiveid from client +::: + +### YAML configuration name + +setJa3FingerPrintAction + +### Settings + +The following table describes the customizable properties available for this action: + +{.property-table .property-table-action} +::: +| Property | Type | Description | DefaultValue | +| :------- | :------- | :------- | -------- | +| value | string | | | + +::: +### Example of usage + +This filter has no specific usage example + + +### .NET reference + +View definition of [SetJa3FingerPrintAction](https://docs.fluxzy.io/api/Fluxzy.Rules.Actions.SetJa3FingerPrintAction.html) for .NET integration. + +### See also + +This action has no related action + diff --git a/docs/impersonate-profiles/Chrome_Android_131.json b/docs/impersonate-profiles/Chrome_Android_131.json new file mode 100644 index 000000000..4bf14343d --- /dev/null +++ b/docs/impersonate-profiles/Chrome_Android_131.json @@ -0,0 +1,95 @@ +{ + "networkSettings": { + "ja3FingerPrint": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,27-13-65281-18-43-0-35-10-5-51-11-16-17513-65037-23-45,4588-29-23-24,0", + "greaseMode": true, + "overrideClientExtensionsValues": null, + "signatureAlgorithms": [ + 1027, + 2052, + 1025, + 1283, + 2053, + 1281, + 2054, + 1537 + ] + }, + "h2Settings": { + "settings": [ + { + "identifier": 1, + "value": 65536 + }, + { + "identifier": 2, + "value": 0 + }, + { + "identifier": 4, + "value": 6291456 + }, + { + "identifier": 6, + "value": 262144 + } + ], + "removeDefaultValues": true + }, + "headers": [ + { + "name": "sec-ch-ua", + "value": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Android\"" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "skipIfExists": true + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br, zstd", + "skipIfExists": true + }, + { + "name": "Accept-language", + "value": "en-US,en;q=0.9", + "skipIfExists": true + }, + { + "name": "Priority", + "value": "u=0, i" + } + ] +} \ No newline at end of file diff --git a/docs/impersonate-profiles/Chrome_Windows_131.json b/docs/impersonate-profiles/Chrome_Windows_131.json new file mode 100644 index 000000000..d62395d07 --- /dev/null +++ b/docs/impersonate-profiles/Chrome_Windows_131.json @@ -0,0 +1,95 @@ +{ + "networkSettings": { + "ja3FingerPrint": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,45-0-65037-17513-35-10-13-65281-16-51-23-27-18-43-11-5,4588-29-23-24,0", + "greaseMode": true, + "overrideClientExtensionsValues": null, + "signatureAlgorithms": [ + 1027, + 2052, + 1025, + 1283, + 2053, + 1281, + 2054, + 1537 + ] + }, + "h2Settings": { + "settings": [ + { + "identifier": 1, + "value": 65536 + }, + { + "identifier": 2, + "value": 0 + }, + { + "identifier": 4, + "value": 6291456 + }, + { + "identifier": 6, + "value": 262144 + } + ], + "removeDefaultValues": true + }, + "headers": [ + { + "name": "sec-ch-ua", + "value": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Windows\"" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "skipIfExists": true + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br, zstd", + "skipIfExists": true + }, + { + "name": "Accept-language", + "value": "en-US,en;q=0.9", + "skipIfExists": true + }, + { + "name": "Priority", + "value": "u=0, i" + } + ] +} \ No newline at end of file diff --git a/docs/impersonate-profiles/Edge_Windows_131.json b/docs/impersonate-profiles/Edge_Windows_131.json new file mode 100644 index 000000000..062951f63 --- /dev/null +++ b/docs/impersonate-profiles/Edge_Windows_131.json @@ -0,0 +1,95 @@ +{ + "networkSettings": { + "ja3FingerPrint": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,5-10-35-51-23-43-18-0-27-17513-11-16-65281-13-45-65037,4588-29-23-24,0", + "greaseMode": true, + "overrideClientExtensionsValues": null, + "signatureAlgorithms": [ + 1027, + 2052, + 1025, + 1283, + 2053, + 1281, + 2054, + 1537 + ] + }, + "h2Settings": { + "settings": [ + { + "identifier": 1, + "value": 65536 + }, + { + "identifier": 2, + "value": 0 + }, + { + "identifier": 4, + "value": 6291456 + }, + { + "identifier": 6, + "value": 262144 + } + ], + "removeDefaultValues": true + }, + "headers": [ + { + "name": "sec-ch-ua", + "value": "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Windows\"" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "skipIfExists": true + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br, zstd", + "skipIfExists": true + }, + { + "name": "Accept-language", + "value": "en-US,en;q=0.9", + "skipIfExists": true + }, + { + "name": "Priority", + "value": "u=0, i" + } + ] +} \ No newline at end of file diff --git a/docs/impersonate-profiles/Firefox_Windows_133.json b/docs/impersonate-profiles/Firefox_Windows_133.json new file mode 100644 index 000000000..bd14c230e --- /dev/null +++ b/docs/impersonate-profiles/Firefox_Windows_133.json @@ -0,0 +1,86 @@ +{ + "networkSettings": { + "ja3FingerPrint": "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-51-43-13-45-28-27-65037,4588-29-23-24-25-256-257,0", + "greaseMode": false, + "overrideClientExtensionsValues": null, + "signatureAlgorithms": [ + 1027, + 1283, + 1539, + 2052, + 2053, + 2054, + 1025, + 1281, + 1537, + 515, + 513 + ] + }, + "h2Settings": { + "settings": [ + { + "identifier": 1, + "value": 65536 + }, + { + "identifier": 2, + "value": 0 + }, + { + "identifier": 4, + "value": 6291456 + }, + { + "identifier": 6, + "value": 262144 + } + ], + "removeDefaultValues": true + }, + "headers": [ + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "skipIfExists": true + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate, br, zstd", + "skipIfExists": true + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "Sec-Fetch-Dest", + "value": "document" + }, + { + "name": "Sec-Fetch-Mode", + "value": "navigate" + }, + { + "name": "Sec-Fetch-Site", + "value": "none" + }, + { + "name": "Sec-Fetch-User", + "value": "?1" + }, + { + "name": "Priority", + "value": "u=0, i" + }, + { + "name": "Accept-language", + "value": "en-US,en;q=0.9", + "skipIfExists": true + } + ] +} \ No newline at end of file diff --git a/docs/searchable-items.json b/docs/searchable-items.json index 569a03858..cf030bd7d 100644 --- a/docs/searchable-items.json +++ b/docs/searchable-items.json @@ -1 +1 @@ -[{"title":"anyFilter","description":"Select all exchanges","fullTypeName":"Fluxzy.Rules.Filters.AnyFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"commentSearchFilter","description":"Select exchanges by searching a string pattern into the comment property.","fullTypeName":"Fluxzy.Rules.Filters.CommentSearchFilter","category":"Filter","scope":"outOfScope"},{"title":"filterCollection","description":"FilterCollection is a combination of multiple filters with a merging operator (OR / AND).","fullTypeName":"Fluxzy.Rules.Filters.FilterCollection","category":"Filter","scope":"onAuthorityReceived"},{"title":"hasCommentFilter","description":"Select exchanges having comment.","fullTypeName":"Fluxzy.Rules.Filters.HasCommentFilter","category":"Filter","scope":"outOfScope"},{"title":"hasTagFilter","description":"Select exchanges having tag.","fullTypeName":"Fluxzy.Rules.Filters.HasTagFilter","category":"Filter","scope":"outOfScope"},{"title":"ipEgressFilter","description":"Select exchanges according to upstream IP address. Full IP notation is used from IPv6.","fullTypeName":"Fluxzy.Rules.Filters.IpEgressFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"ipIngressFilter","description":"Select exchanges according to client ip address. Full IP notation is used from IPv6.","fullTypeName":"Fluxzy.Rules.Filters.IpIngressFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"isWebSocketFilter","description":"Select websocket exchange.","fullTypeName":"Fluxzy.Rules.Filters.IsWebSocketFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"contentTypeXmlFilter","description":"Select exchanges having XML response body.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.ContentTypeXmlFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"cssStyleFilter","description":"Select exchanges having response content type mime matching css.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.CssStyleFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"fontFilter","description":"Select exchanges having response content type matching a font payload.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.FontFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"htmlResponseFilter","description":"Select exchanges having HTML body. The content-type header is checked to determine if the content body is has text/html hint.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.HtmlResponseFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"imageFilter","description":"Select exchanges having response content type mime matching image.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.ImageFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"jsonResponseFilter","description":"Select exchanges having JSON response body. The content-type header is checked to determine if the content body is a JSON.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.JsonResponseFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"networkErrorFilter","description":"Select exchanges that fails due to network error.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.NetworkErrorFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"responseHeaderFilter","description":"Select exchanges according to response header values.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.ResponseHeaderFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeClientErrorFilter","description":"Select exchanges that HTTP status code indicates a client error (4XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeClientErrorFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeFilter","description":"Select exchanges according to HTTP status code.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeRedirectionFilter","description":"Select exchanges that HTTP status code indicates a redirect (3XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeRedirectionFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeServerErrorFilter","description":"Select exchanges that HTTP status code indicates a server/intermediary error (5XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeServerErrorFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeSuccessFilter","description":"Select exchanges that HTTP status code indicates a successful request (2XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeSuccessFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"absoluteUriFilter","description":"Select exchanges according to URI (scheme, FQDN, path and query). Supports common string search option and regular expression.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.AbsoluteUriFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"agentLabelFilter","description":"Select exchanges according to configured source agent (user agent or process) with a regular string search.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.AgentLabelFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"authorityFilter","description":"Select exchange according to hostname and a port","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.AuthorityFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"deleteFilter","description":"Select exchanges with DELETE method","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.DeleteFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"formRequestFilter","description":"Select request sending \u0027multipart/form-data\u0027 or \u0027application/x-www-form-urlencoded\u0027 body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.FormRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"formUrlEncodedRequestFilter","description":"Select request sending \u0027application/x-www-form-urlencoded\u0027 body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.FormUrlEncodedRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"getFilter","description":"Select exchanges with GET method","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.GetFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"h11TrafficOnlyFilter","description":"Select HTTP/1.1 exchanges only.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.H11TrafficOnlyFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"h2TrafficOnlyFilter","description":"Select H2 exchanges only.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.H2TrafficOnlyFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasAnyCookieOnRequestFilter","description":"Select exchanges having any request cookie","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasAnyCookieOnRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasAuthorizationBearerFilter","description":"Select exchanges having bearer token in authorization.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasAuthorizationBearerFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasAuthorizationFilter","description":"Select exchanges having authorization header.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasAuthorizationFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasCookieOnRequestFilter","description":"Exchange having a request cookie with a specific name","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasCookieOnRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasRequestBodyFilter","description":"Select request having body.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasRequestBodyFilter","category":"Filter","scope":"responseBodyReceivedFromRemote"},{"title":"hasSetCookieOnResponseFilter","description":"Search for a cookie value present in a \u0060set-cookie\u0060 header response.If cookie name is not defined or empty, the filter will returns any cookie having the value.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasSetCookieOnResponseFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"hostFilter","description":"Select exchanges according to hostname (excluding port). To select authority (combination of host:port), use \u003Cgoto\u003EAuthorityFilter\u003C/goto\u003E.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HostFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"isSecureFilter","description":"Select secure exchange only (non plain HTTP).","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.IsSecureFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"jsonRequestFilter","description":"Select request sending JSON body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.JsonRequestFilter","category":"Filter","scope":"requestBodyReceivedFromClient"},{"title":"methodFilter","description":"Select exchanges according to request method.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.MethodFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"multipartDataRequestFilter","description":"Select request sending \u0027multipart/form-data\u0027 body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.MultipartDataRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"patchFilter","description":"Select exchanges with PATCH method","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PatchFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"pathFilter","description":"Select exchanges according to url path. Path includes query string if any. Path must start with \u0060/\u0060","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PathFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"postFilter","description":"Select POST (request method) only exchanges.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PostFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"putFilter","description":"Select exchanges according to request method.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PutFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"queryStringFilter","description":"Select exchanges containing a specific query string. If \u0060name\u0060 is not defined or empty, the search will be performed on any query string values.The search will pass if at least one value match.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.QueryStringFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"requestHeaderFilter","description":"Select exchanges according to request header values.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.RequestHeaderFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"abortAction","description":"Abort an exchange at the transport level. This action will close connection between fluxzy and client which may lead to depended exchanges to be aborted too.","fullTypeName":"Fluxzy.Rules.Actions.AbortAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"addRequestHeaderAction","description":"Append a request header.","fullTypeName":"Fluxzy.Rules.Actions.AddRequestHeaderAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"addResponseHeaderAction","description":"Append a response header. H2 pseudo header will be ignored.","fullTypeName":"Fluxzy.Rules.Actions.AddResponseHeaderAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"applyCommentAction","description":"Add comment to exchange. Comment has no effect on the stream behaviour.","fullTypeName":"Fluxzy.Rules.Actions.ApplyCommentAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"applyTagAction","description":"Affect a tag to exchange. Tags are meta-information and do not alter the connection.","fullTypeName":"Fluxzy.Rules.Actions.ApplyTagAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"averageThrottleAction","description":"Throttle and simulate bandwidth condition.","fullTypeName":"Fluxzy.Rules.Actions.AverageThrottleAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"changeRequestMethodAction","description":"Alter the method of an exchange.","fullTypeName":"Fluxzy.Rules.Actions.ChangeRequestMethodAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"changeRequestPathAction","description":"Change request uri path. This action alters only the path of the request. Request path includes query string.","fullTypeName":"Fluxzy.Rules.Actions.ChangeRequestPathAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"delayAction","description":"Add a latency to the exchange.","fullTypeName":"Fluxzy.Rules.Actions.DelayAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"deleteRequestHeaderAction","description":"Remove request headers. This action removes \u003Cb\u003Eevery\u003C/b\u003E occurrence of the header from the request.","fullTypeName":"Fluxzy.Rules.Actions.DeleteRequestHeaderAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"deleteResponseHeaderAction","description":"Remove response headers. This action removes \u003Cb\u003Eevery\u003C/b\u003E occurrence of the header from the response.","fullTypeName":"Fluxzy.Rules.Actions.DeleteResponseHeaderAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"fileAppendAction","description":"Write to a file. Captured variable are interpreted.","fullTypeName":"Fluxzy.Rules.Actions.FileAppendAction","category":"Action","scope":"copySibling"},{"title":"forceHttp11Action","description":"Force the connection between fluxzy and remote to be HTTP/1.1. This value is enforced by ALPN settings set during the SSL/Handshake handshake.","fullTypeName":"Fluxzy.Rules.Actions.ForceHttp11Action","category":"Action","scope":"onAuthorityReceived"},{"title":"forceHttp2Action","description":"Forces the connection between fluxzy and remote to be HTTP/2.0. This value is enforced when setting up ALPN settings during SSL/TLS negotiation. \u003Cbr/\u003EThe exchange will break if the remote does not support HTTP/2.0. \u003Cbr/\u003EThis action will be ignored when the communication is clear (h2c not supported).","fullTypeName":"Fluxzy.Rules.Actions.ForceHttp2Action","category":"Action","scope":"onAuthorityReceived"},{"title":"forceRemotePortAction","description":"Ignores the default port used by the current authority and use the provided port instead.","fullTypeName":"Fluxzy.Rules.Actions.ForceRemotePortAction","category":"Action","scope":"onAuthorityReceived"},{"title":"forceTlsVersionAction","description":"Force the usage of a specific TLS version. Values can be chosen among : Tls, Tls11, Tls12, Tls13, Ssl3, Ssl2. \u003Cbr/\u003EForcing the usage of a specific TLS version can break the exchange if the remote does not support the requested protocol.","fullTypeName":"Fluxzy.Rules.Actions.ForceTlsVersionAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"forwardAction","description":"Forward request to a specific URL. This action makes fluxzy act as a reverse proxy. Unlike [SpoofDnsAction](https://www.fluxzy.io/rule/item/spoofDnsAction), host header is automatically set and protocol switch is supported (http to https, http/1.1 to h2, ...). The URL must be an absolute path.","fullTypeName":"Fluxzy.Rules.Actions.ForwardAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"mountCertificateAuthorityAction","description":"Reply with the default root certificate used by fluxzy","fullTypeName":"Fluxzy.Rules.Actions.MountCertificateAuthorityAction","category":"Action","scope":"dnsSolveDone"},{"title":"noOpAction","description":"An action doing no operation.","fullTypeName":"Fluxzy.Rules.Actions.NoOpAction","category":"Action","scope":"requestBodyReceivedFromClient"},{"title":"removeCacheAction","description":"Remove all cache directive from request and response headers. This will force the clientto ask the latest version of the requested resource.","fullTypeName":"Fluxzy.Rules.Actions.RemoveCacheAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setClientCertificateAction","description":"Add a client certificate to the exchange. The client certificate will be used for establishing the mTLS authentication if the remote request it. The client certificate can be retrieved from the default store (my) or from a PKCS#12 file (.p12, pfx). \u003Cbr/\u003EThe certificate will not be stored in fluxzy settings and, therefore, must be available at runtime. ","fullTypeName":"Fluxzy.Rules.Actions.SetClientCertificateAction","category":"Action","scope":"onAuthorityReceived"},{"title":"setUserAgentAction","description":"Change the User-AgentThis action is used to change the User-Agent header of the request from a list of built-in user-agent values.","fullTypeName":"Fluxzy.Rules.Actions.SetUserAgentAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setVariableAction","description":"Set a variable or update an existing","fullTypeName":"Fluxzy.Rules.Actions.SetVariableAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"skipRemoteCertificateValidationAction","description":"Skip validating remote certificate. Fluxzy will ignore any validation errors on the server certificate.","fullTypeName":"Fluxzy.Rules.Actions.SkipRemoteCertificateValidationAction","category":"Action","scope":"onAuthorityReceived"},{"title":"skipSslTunnelingAction","description":"Instructs fluxzy to not decrypt the current traffic. The associated filter must be on OnAuthorityReceived scope in order to make this action effective. ","fullTypeName":"Fluxzy.Rules.Actions.SkipSslTunnelingAction","category":"Action","scope":"onAuthorityReceived"},{"title":"spoofDnsAction","description":"Fix statically the remote ip or port disregards to the dns or host resolution of the current running system. Use this action to force the resolution of a hostname to a fixed IP address. ","fullTypeName":"Fluxzy.Rules.Actions.SpoofDnsAction","category":"Action","scope":"onAuthorityReceived"},{"title":"stdErrAction","description":"Write text to standard error. Captured variable are interpreted.","fullTypeName":"Fluxzy.Rules.Actions.StdErrAction","category":"Action","scope":"copySibling"},{"title":"stdOutAction","description":"Write text to standard output. Captured variable are interpreted.","fullTypeName":"Fluxzy.Rules.Actions.StdOutAction","category":"Action","scope":"outOfScope"},{"title":"updateRequestHeaderAction","description":"Update and existing request header. If the header does not exists in the original request, the header will be added. \u003Cbr/\u003EUse {{previous}} keyword to refer to the original value of the header. \u003Cbr/\u003E\u003Cstrong\u003ENote\u003C/strong\u003E Headers that alter the connection behaviour will be ignored.","fullTypeName":"Fluxzy.Rules.Actions.UpdateRequestHeaderAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"updateResponseHeaderAction","description":"Update and existing response header. If the header does not exists in the original response, the header will be added.\u003Cbr/\u003EUse {{previous}} keyword to refer to the original value of the header.\u003Cbr/\u003E\u003Cstrong\u003ENote\u003C/strong\u003E Headers that alter the connection behaviour will be ignored.","fullTypeName":"Fluxzy.Rules.Actions.UpdateResponseHeaderAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"upStreamProxyAction","description":"Use an upstream proxy.","fullTypeName":"Fluxzy.Rules.Actions.UpStreamProxyAction","category":"Action","scope":"onAuthorityReceived"},{"title":"useCertificateAction","description":"Use a specific server certificate. Certificate can be retrieved from user store or from a PKCS12 file","fullTypeName":"Fluxzy.Rules.Actions.UseCertificateAction","category":"Action","scope":"onAuthorityReceived"},{"title":"useDnsOverHttpsAction","description":"Use DoH (DNS over HTTPS) to resolve domain names instead of the default DNS provided by the OS","fullTypeName":"Fluxzy.Rules.Actions.UseDnsOverHttpsAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"addBasicAuthenticationAction","description":"Add a basic authentication (RFC 7617) to incoming exchanges with an username and a password","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.AddBasicAuthenticationAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"injectHtmlTagAction","description":"This action stream a response body and inject a text after the first specified html tag.This action can be used to inject a html code snippet after opening \u0060\u003Chead\u003E\u0060 tag in any traversing html page.This action supports chunked transfer stream and the following body encodings: gzip, deflate, brotli and lzw.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.InjectHtmlTagAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"mockedResponseAction","description":"Reply with a pre-made response from a raw text or file","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.MockedResponseAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"mountWelcomePageAction","description":"Reply with fluxzy welcome page","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.MountWelcomePageAction","category":"Action","scope":"dnsSolveDone"},{"title":"removeResponseCookieAction","description":"Remove a response cookie by setting the expiration date to a past date.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.RemoveResponseCookieAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"serveDirectoryAction","description":"Serve a folder as a static web site. This action is made for mocking purpose and not production ready for a web site.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.ServeDirectoryAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setRequestCookieAction","description":"Add a cookie to request. This action is performed by adding/replacing \u0060Cookie\u0060 header in request.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.SetRequestCookieAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setResponseCookieAction","description":"Add a response cookie. This action is performed by adding \u0060Set-Cookie\u0060 header in response.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.SetResponseCookieAction","category":"Action","scope":"responseHeaderReceivedFromRemote"}] \ No newline at end of file +[{"title":"anyFilter","description":"Select all exchanges","fullTypeName":"Fluxzy.Rules.Filters.AnyFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"commentSearchFilter","description":"Select exchanges by searching a string pattern into the comment property.","fullTypeName":"Fluxzy.Rules.Filters.CommentSearchFilter","category":"Filter","scope":"outOfScope"},{"title":"filterCollection","description":"FilterCollection is a combination of multiple filters with a merging operator (OR / AND).","fullTypeName":"Fluxzy.Rules.Filters.FilterCollection","category":"Filter","scope":"onAuthorityReceived"},{"title":"hasCommentFilter","description":"Select exchanges having comment.","fullTypeName":"Fluxzy.Rules.Filters.HasCommentFilter","category":"Filter","scope":"outOfScope"},{"title":"hasTagFilter","description":"Select exchanges having tag.","fullTypeName":"Fluxzy.Rules.Filters.HasTagFilter","category":"Filter","scope":"outOfScope"},{"title":"ipEgressFilter","description":"Select exchanges according to upstream IP address. Full IP notation is used from IPv6.","fullTypeName":"Fluxzy.Rules.Filters.IpEgressFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"ipIngressFilter","description":"Select exchanges according to client ip address. Full IP notation is used from IPv6.","fullTypeName":"Fluxzy.Rules.Filters.IpIngressFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"isWebSocketFilter","description":"Select websocket exchange.","fullTypeName":"Fluxzy.Rules.Filters.IsWebSocketFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"contentTypeXmlFilter","description":"Select exchanges having XML response body.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.ContentTypeXmlFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"cssStyleFilter","description":"Select exchanges having response content type mime matching css.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.CssStyleFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"fontFilter","description":"Select exchanges having response content type matching a font payload.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.FontFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"htmlResponseFilter","description":"Select exchanges having HTML body. The content-type header is checked to determine if the content body is has text/html hint.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.HtmlResponseFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"imageFilter","description":"Select exchanges having response content type mime matching image.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.ImageFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"jsonResponseFilter","description":"Select exchanges having JSON response body. The content-type header is checked to determine if the content body is a JSON.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.JsonResponseFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"networkErrorFilter","description":"Select exchanges that fails due to network error.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.NetworkErrorFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"responseHeaderFilter","description":"Select exchanges according to response header values.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.ResponseHeaderFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeClientErrorFilter","description":"Select exchanges that HTTP status code indicates a client error (4XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeClientErrorFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeFilter","description":"Select exchanges according to HTTP status code.","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeRedirectionFilter","description":"Select exchanges that HTTP status code indicates a redirect (3XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeRedirectionFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeServerErrorFilter","description":"Select exchanges that HTTP status code indicates a server/intermediary error (5XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeServerErrorFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"statusCodeSuccessFilter","description":"Select exchanges that HTTP status code indicates a successful request (2XX).","fullTypeName":"Fluxzy.Rules.Filters.ResponseFilters.StatusCodeSuccessFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"absoluteUriFilter","description":"Select exchanges according to URI (scheme, FQDN, path and query). Supports common string search option and regular expression.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.AbsoluteUriFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"agentLabelFilter","description":"Select exchanges according to configured source agent (user agent or process) with a regular string search.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.AgentLabelFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"authorityFilter","description":"Select exchange according to hostname and a port","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.AuthorityFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"deleteFilter","description":"Select exchanges with DELETE method","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.DeleteFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"formRequestFilter","description":"Select request sending \u0027multipart/form-data\u0027 or \u0027application/x-www-form-urlencoded\u0027 body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.FormRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"formUrlEncodedRequestFilter","description":"Select request sending \u0027application/x-www-form-urlencoded\u0027 body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.FormUrlEncodedRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"getFilter","description":"Select exchanges with GET method","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.GetFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"h11TrafficOnlyFilter","description":"Select HTTP/1.1 exchanges only.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.H11TrafficOnlyFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"h2TrafficOnlyFilter","description":"Select H2 exchanges only.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.H2TrafficOnlyFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasAnyCookieOnRequestFilter","description":"Select exchanges having any request cookie","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasAnyCookieOnRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasAuthorizationBearerFilter","description":"Select exchanges having bearer token in authorization.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasAuthorizationBearerFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasAuthorizationFilter","description":"Select exchanges having authorization header.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasAuthorizationFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasCookieOnRequestFilter","description":"Exchange having a request cookie with a specific name","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasCookieOnRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"hasRequestBodyFilter","description":"Select request having body.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasRequestBodyFilter","category":"Filter","scope":"responseBodyReceivedFromRemote"},{"title":"hasSetCookieOnResponseFilter","description":"Search for a cookie value present in a \u0060set-cookie\u0060 header response.If cookie name is not defined or empty, the filter will returns any cookie having the value.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HasSetCookieOnResponseFilter","category":"Filter","scope":"responseHeaderReceivedFromRemote"},{"title":"hostFilter","description":"Select exchanges according to hostname (excluding port). To select authority (combination of host:port), use \u003Cgoto\u003EAuthorityFilter\u003C/goto\u003E.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.HostFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"isSecureFilter","description":"Select secure exchange only (non plain HTTP).","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.IsSecureFilter","category":"Filter","scope":"onAuthorityReceived"},{"title":"jsonRequestFilter","description":"Select request sending JSON body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.JsonRequestFilter","category":"Filter","scope":"requestBodyReceivedFromClient"},{"title":"methodFilter","description":"Select exchanges according to request method.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.MethodFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"multipartDataRequestFilter","description":"Select request sending \u0027multipart/form-data\u0027 body. Filtering is made by inspecting value of \u0060Content-Type\u0060 header","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.MultipartDataRequestFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"patchFilter","description":"Select exchanges with PATCH method","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PatchFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"pathFilter","description":"Select exchanges according to url path. Path includes query string if any. Path must start with \u0060/\u0060","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PathFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"postFilter","description":"Select POST (request method) only exchanges.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PostFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"putFilter","description":"Select exchanges according to request method.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.PutFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"queryStringFilter","description":"Select exchanges containing a specific query string. If \u0060name\u0060 is not defined or empty, the search will be performed on any query string values.The search will pass if at least one value match.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.QueryStringFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"requestHeaderFilter","description":"Select exchanges according to request header values.","fullTypeName":"Fluxzy.Rules.Filters.RequestFilters.RequestHeaderFilter","category":"Filter","scope":"requestHeaderReceivedFromClient"},{"title":"abortAction","description":"Abort an exchange at the transport level. This action will close connection between fluxzy and client which may lead to depended exchanges to be aborted too.","fullTypeName":"Fluxzy.Rules.Actions.AbortAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"addRequestHeaderAction","description":"Append a request header.","fullTypeName":"Fluxzy.Rules.Actions.AddRequestHeaderAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"addResponseHeaderAction","description":"Append a response header. H2 pseudo header will be ignored.","fullTypeName":"Fluxzy.Rules.Actions.AddResponseHeaderAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"applyCommentAction","description":"Add comment to exchange. Comment has no effect on the stream behaviour.","fullTypeName":"Fluxzy.Rules.Actions.ApplyCommentAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"applyTagAction","description":"Affect a tag to exchange. Tags are meta-information and do not alter the connection.","fullTypeName":"Fluxzy.Rules.Actions.ApplyTagAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"averageThrottleAction","description":"Throttle and simulate bandwidth condition.","fullTypeName":"Fluxzy.Rules.Actions.AverageThrottleAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"changeRequestMethodAction","description":"Alter the method of an exchange.","fullTypeName":"Fluxzy.Rules.Actions.ChangeRequestMethodAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"changeRequestPathAction","description":"Change request uri path. This action alters only the path of the request. Request path includes query string.","fullTypeName":"Fluxzy.Rules.Actions.ChangeRequestPathAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"delayAction","description":"Add a latency to the exchange.","fullTypeName":"Fluxzy.Rules.Actions.DelayAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"deleteRequestHeaderAction","description":"Remove request headers. This action removes \u003Cb\u003Eevery\u003C/b\u003E occurrence of the header from the request.","fullTypeName":"Fluxzy.Rules.Actions.DeleteRequestHeaderAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"deleteResponseHeaderAction","description":"Remove response headers. This action removes \u003Cb\u003Eevery\u003C/b\u003E occurrence of the header from the response.","fullTypeName":"Fluxzy.Rules.Actions.DeleteResponseHeaderAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"fileAppendAction","description":"Write to a file. Captured variable are interpreted.","fullTypeName":"Fluxzy.Rules.Actions.FileAppendAction","category":"Action","scope":"copySibling"},{"title":"forceHttp11Action","description":"Force the connection between fluxzy and remote to be HTTP/1.1. This value is enforced by ALPN settings set during the SSL/Handshake handshake.","fullTypeName":"Fluxzy.Rules.Actions.ForceHttp11Action","category":"Action","scope":"onAuthorityReceived"},{"title":"forceHttp2Action","description":"Forces the connection between fluxzy and remote to be HTTP/2.0. This value is enforced when setting up ALPN settings during SSL/TLS negotiation. \u003Cbr/\u003EThe exchange will break if the remote does not support HTTP/2.0. \u003Cbr/\u003EThis action will be ignored when the communication is clear (h2c not supported).","fullTypeName":"Fluxzy.Rules.Actions.ForceHttp2Action","category":"Action","scope":"onAuthorityReceived"},{"title":"forceRemotePortAction","description":"Ignores the default port used by the current authority and use the provided port instead.","fullTypeName":"Fluxzy.Rules.Actions.ForceRemotePortAction","category":"Action","scope":"onAuthorityReceived"},{"title":"forceTlsVersionAction","description":"Force the usage of a specific TLS version. Values can be chosen among : Tls, Tls11, Tls12, Tls13, Ssl3, Ssl2. \u003Cbr/\u003EForcing the usage of a specific TLS version can break the exchange if the remote does not support the requested protocol.","fullTypeName":"Fluxzy.Rules.Actions.ForceTlsVersionAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"forwardAction","description":"Forward request to a specific URL. This action makes fluxzy act as a reverse proxy. Unlike [SpoofDnsAction](https://www.fluxzy.io/rule/item/spoofDnsAction), host header is automatically set and protocol switch is supported (http to https, http/1.1 to h2, ...). The URL must be an absolute path.","fullTypeName":"Fluxzy.Rules.Actions.ForwardAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"impersonateAction","description":"Impersonate a browser or client by changing the TLS fingerprint, HTTP/2 settings and headers.","fullTypeName":"Fluxzy.Rules.Actions.ImpersonateAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"mountCertificateAuthorityAction","description":"Reply with the default root certificate used by fluxzy","fullTypeName":"Fluxzy.Rules.Actions.MountCertificateAuthorityAction","category":"Action","scope":"dnsSolveDone"},{"title":"noOpAction","description":"An action doing no operation.","fullTypeName":"Fluxzy.Rules.Actions.NoOpAction","category":"Action","scope":"requestBodyReceivedFromClient"},{"title":"removeCacheAction","description":"Remove all cache directive from request and response headers. This will force the clientto ask the latest version of the requested resource.","fullTypeName":"Fluxzy.Rules.Actions.RemoveCacheAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setClientCertificateAction","description":"Add a client certificate to the exchange. The client certificate will be used for establishing the mTLS authentication if the remote request it. The client certificate can be retrieved from the default store (my) or from a PKCS#12 file (.p12, pfx). \u003Cbr/\u003EThe certificate will not be stored in fluxzy settings and, therefore, must be available at runtime. ","fullTypeName":"Fluxzy.Rules.Actions.SetClientCertificateAction","category":"Action","scope":"onAuthorityReceived"},{"title":"setJa3FingerPrintAction","description":"Set a JA3 fingerprint of ongoing connection.","fullTypeName":"Fluxzy.Rules.Actions.SetJa3FingerPrintAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setUserAgentAction","description":"Change the User-AgentThis action is used to change the User-Agent header of the request from a list of built-in user-agent values.","fullTypeName":"Fluxzy.Rules.Actions.SetUserAgentAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setVariableAction","description":"Set a variable or update an existing","fullTypeName":"Fluxzy.Rules.Actions.SetVariableAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"skipRemoteCertificateValidationAction","description":"Skip validating remote certificate. Fluxzy will ignore any validation errors on the server certificate.","fullTypeName":"Fluxzy.Rules.Actions.SkipRemoteCertificateValidationAction","category":"Action","scope":"onAuthorityReceived"},{"title":"skipSslTunnelingAction","description":"Instructs fluxzy to not decrypt the current traffic. The associated filter must be on OnAuthorityReceived scope in order to make this action effective. ","fullTypeName":"Fluxzy.Rules.Actions.SkipSslTunnelingAction","category":"Action","scope":"onAuthorityReceived"},{"title":"spoofDnsAction","description":"Fix statically the remote ip or port disregards to the dns or host resolution of the current running system. Use this action to force the resolution of a hostname to a fixed IP address. ","fullTypeName":"Fluxzy.Rules.Actions.SpoofDnsAction","category":"Action","scope":"onAuthorityReceived"},{"title":"stdErrAction","description":"Write text to standard error. Captured variable are interpreted.","fullTypeName":"Fluxzy.Rules.Actions.StdErrAction","category":"Action","scope":"copySibling"},{"title":"stdOutAction","description":"Write text to standard output. Captured variable are interpreted.","fullTypeName":"Fluxzy.Rules.Actions.StdOutAction","category":"Action","scope":"outOfScope"},{"title":"updateRequestHeaderAction","description":"Update and existing request header. If the header does not exists in the original request, the header will be added. \u003Cbr/\u003EUse {{previous}} keyword to refer to the original value of the header. \u003Cbr/\u003E\u003Cstrong\u003ENote\u003C/strong\u003E Headers that alter the connection behaviour will be ignored.","fullTypeName":"Fluxzy.Rules.Actions.UpdateRequestHeaderAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"updateResponseHeaderAction","description":"Update and existing response header. If the header does not exists in the original response, the header will be added.\u003Cbr/\u003EUse {{previous}} keyword to refer to the original value of the header.\u003Cbr/\u003E\u003Cstrong\u003ENote\u003C/strong\u003E Headers that alter the connection behaviour will be ignored.","fullTypeName":"Fluxzy.Rules.Actions.UpdateResponseHeaderAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"upStreamProxyAction","description":"Use an upstream proxy.","fullTypeName":"Fluxzy.Rules.Actions.UpStreamProxyAction","category":"Action","scope":"onAuthorityReceived"},{"title":"useCertificateAction","description":"Use a specific server certificate. Certificate can be retrieved from user store or from a PKCS12 file","fullTypeName":"Fluxzy.Rules.Actions.UseCertificateAction","category":"Action","scope":"onAuthorityReceived"},{"title":"useDnsOverHttpsAction","description":"Use DoH (DNS over HTTPS) to resolve domain names instead of the default DNS provided by the OS","fullTypeName":"Fluxzy.Rules.Actions.UseDnsOverHttpsAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"addBasicAuthenticationAction","description":"Add a basic authentication (RFC 7617) to incoming exchanges with an username and a password","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.AddBasicAuthenticationAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"injectHtmlTagAction","description":"This action stream a response body and inject a text after the first specified html tag.This action can be used to inject a html code snippet after opening \u0060\u003Chead\u003E\u0060 tag in any traversing html page.This action supports chunked transfer stream and the following body encodings: gzip, deflate, brotli and lzw.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.InjectHtmlTagAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"mockedResponseAction","description":"Reply with a pre-made response from a raw text or file","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.MockedResponseAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"mountWelcomePageAction","description":"Reply with fluxzy welcome page","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.MountWelcomePageAction","category":"Action","scope":"dnsSolveDone"},{"title":"removeResponseCookieAction","description":"Remove a response cookie by setting the expiration date to a past date.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.RemoveResponseCookieAction","category":"Action","scope":"responseHeaderReceivedFromRemote"},{"title":"serveDirectoryAction","description":"Serve a folder as a static web site. This action is made for mocking purpose and not production ready for a web site.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.ServeDirectoryAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setRequestCookieAction","description":"Add a cookie to request. This action is performed by adding/replacing \u0060Cookie\u0060 header in request.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.SetRequestCookieAction","category":"Action","scope":"requestHeaderReceivedFromClient"},{"title":"setResponseCookieAction","description":"Add a response cookie. This action is performed by adding \u0060Set-Cookie\u0060 header in response.","fullTypeName":"Fluxzy.Rules.Actions.HighLevelActions.SetResponseCookieAction","category":"Action","scope":"responseHeaderReceivedFromRemote"}] \ No newline at end of file diff --git a/examples/Samples.No016.ImpersonateBrowser/Program.cs b/examples/Samples.No016.ImpersonateBrowser/Program.cs new file mode 100644 index 000000000..b5b2eb669 --- /dev/null +++ b/examples/Samples.No016.ImpersonateBrowser/Program.cs @@ -0,0 +1,40 @@ +using Fluxzy; +using Fluxzy.Core; +using Fluxzy.Rules.Actions; + +namespace Samples.No016.ImpersonateBrowser +{ + internal class Program + { + /// + /// This example shows how to impersonate Chrome 131's fingerprint + /// + /// + static async Task Main(string[] args) + { + // Create a default run settings + var fluxzyStartupSetting = FluxzySetting.CreateLocalRandomPort(); + + // Mandatory, BouncyCastle must be used to reproduce the fingerprints + fluxzyStartupSetting.UseBouncyCastleSslEngine(); + + // Add an impersonation rule for Chrome 131 + fluxzyStartupSetting.AddAlterationRulesForAny( + new ImpersonateAction(ImpersonateProfileManager.Chrome131Windows)); + + // Create a proxy instance + await using var proxy = new Proxy(fluxzyStartupSetting); + + var endpoints = proxy.Run(); + + await using var proxyRegistration = await SystemProxyRegistrationHelper.Create(endpoints.First()); + + // Fluxzy is now registered as the system proxy, the proxy will revert + // back to the original settings when proxyRegistration is disposed. + + Console.WriteLine("Press any key to halt proxy and unregistered"); + + Console.ReadKey(); + } + } +} diff --git a/examples/Samples.No016.ImpersonateBrowser/Samples.No016.ImpersonateBrowser.csproj b/examples/Samples.No016.ImpersonateBrowser/Samples.No016.ImpersonateBrowser.csproj new file mode 100644 index 000000000..9a2498316 --- /dev/null +++ b/examples/Samples.No016.ImpersonateBrowser/Samples.No016.ImpersonateBrowser.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + PreserveNewest + + + + + + + diff --git a/fluxzy.core.sln b/fluxzy.core.sln index 05b81bb08..660886c4c 100644 --- a/fluxzy.core.sln +++ b/fluxzy.core.sln @@ -95,6 +95,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.No001.RecordAsHarOr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.No015.ReadingArchive", "examples\Samples.No015.ReadingArchive\Samples.No015.ReadingArchive.csproj", "{8DEDB40D-1244-4A8A-A776-69971B219CBC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.No016.ImpersonateBrowser", "examples\Samples.No016.ImpersonateBrowser\Samples.No016.ImpersonateBrowser.csproj", "{644B0F31-E946-4BF9-AFA5-54C80C78AE17}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -197,6 +199,10 @@ Global {8DEDB40D-1244-4A8A-A776-69971B219CBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {8DEDB40D-1244-4A8A-A776-69971B219CBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {8DEDB40D-1244-4A8A-A776-69971B219CBC}.Release|Any CPU.Build.0 = Release|Any CPU + {644B0F31-E946-4BF9-AFA5-54C80C78AE17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {644B0F31-E946-4BF9-AFA5-54C80C78AE17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {644B0F31-E946-4BF9-AFA5-54C80C78AE17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {644B0F31-E946-4BF9-AFA5-54C80C78AE17}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -223,6 +229,7 @@ Global {5B29B355-BA5C-4ED5-AC79-49A58C4ECB07} = {30657256-C907-42C2-B7B2-DA4FD8D0E412} {711E4B08-93E0-4A31-8AE7-8E7C97DA5260} = {30657256-C907-42C2-B7B2-DA4FD8D0E412} {8DEDB40D-1244-4A8A-A776-69971B219CBC} = {30657256-C907-42C2-B7B2-DA4FD8D0E412} + {644B0F31-E946-4BF9-AFA5-54C80C78AE17} = {30657256-C907-42C2-B7B2-DA4FD8D0E412} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {82E271CB-7073-4A6C-8FF6-6DA616720D3A} diff --git a/src/Fluxzy.Core/Archiving/SslInfo.cs b/src/Fluxzy.Core/Archiving/SslInfo.cs index 93e7911bf..d1d636915 100644 --- a/src/Fluxzy.Core/Archiving/SslInfo.cs +++ b/src/Fluxzy.Core/Archiving/SslInfo.cs @@ -1,99 +1,99 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System.Net.Security; -using System.Security.Authentication; -using System.Text.Json.Serialization; -using Fluxzy.Clients.Ssl.BouncyCastle; - -namespace Fluxzy -{ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System.Net.Security; +using System.Security.Authentication; +using System.Text.Json.Serialization; +using Fluxzy.Clients.Ssl.BouncyCastle; + +namespace Fluxzy +{ /// /// Represent a SSL information - /// - public class SslInfo - { - /// - /// Building from OsDefault - /// - /// - public SslInfo(SslStream sslStream) - { - CipherAlgorithm = sslStream.CipherAlgorithm; - HashAlgorithm = sslStream.HashAlgorithm; - KeyExchangeAlgorithm = sslStream.KeyExchangeAlgorithm.ToString(); - NegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol.ToString(); - RemoteCertificateSubject = sslStream.RemoteCertificate?.Subject; - RemoteCertificateIssuer = sslStream.RemoteCertificate?.Issuer; - LocalCertificateIssuer = sslStream.LocalCertificate?.Issuer; - LocalCertificateSubject = sslStream.LocalCertificate?.Subject; - SslProtocol = sslStream.SslProtocol; - } - - /// - /// Building from BouncyCastle - /// - /// - internal SslInfo(FluxzyClientProtocol clientProtocol) - { -//#if NET6_0 -// CipherAlgorithm = ((System.Net.Security.TlsCipherSuite) clientProtocol.SessionParameters.CipherSuite).ToString(); -//#endif - - NegotiatedApplicationProtocol = clientProtocol.GetApplicationProtocol().ToString(); - SslProtocol = clientProtocol.GetSChannelProtocol(); - - if (BcCertificateHelper.ReadInfo(clientProtocol.SessionParameters.LocalCertificate, - out var localSubject, out var localIssuer)) { - LocalCertificateIssuer = localIssuer; - LocalCertificateSubject = localSubject; - } - - if (BcCertificateHelper.ReadInfo(clientProtocol.SessionParameters.PeerCertificate, - out var remoteSubject, out var remoteIssuer)) { - RemoteCertificateIssuer = remoteIssuer; - RemoteCertificateSubject = remoteSubject; - } - - KeyExchangeAlgorithm = string.Empty; - } - - [JsonConstructor] - public SslInfo( - SslProtocols sslProtocol, string? remoteCertificateIssuer, string? remoteCertificateSubject, - string? localCertificateSubject, string? localCertificateIssuer, string negotiatedApplicationProtocol, - string keyExchangeAlgorithm, HashAlgorithmType hashAlgorithm, CipherAlgorithmType cipherAlgorithm) - { - SslProtocol = sslProtocol; - RemoteCertificateIssuer = remoteCertificateIssuer; - RemoteCertificateSubject = remoteCertificateSubject; - LocalCertificateSubject = localCertificateSubject; - LocalCertificateIssuer = localCertificateIssuer; - NegotiatedApplicationProtocol = negotiatedApplicationProtocol; - KeyExchangeAlgorithm = keyExchangeAlgorithm; - HashAlgorithm = hashAlgorithm; - CipherAlgorithm = cipherAlgorithm; - } - - public SslProtocols SslProtocol { get; } - - public string? RemoteCertificateIssuer { get; } - - public string? RemoteCertificateSubject { get; } - - public string? LocalCertificateSubject { get; } - - public string? LocalCertificateIssuer { get; } - - public string NegotiatedApplicationProtocol { get; } - - public string KeyExchangeAlgorithm { get; } - - public HashAlgorithmType HashAlgorithm { get; } - - public CipherAlgorithmType CipherAlgorithm { get; } - - public byte[]? RemoteCertificate { get; set; } - - public byte[]? LocalCertificate { get; set; } - } -} + /// + public class SslInfo + { + /// + /// Building from OsDefault + /// + /// + public SslInfo(SslStream sslStream) + { + CipherAlgorithm = sslStream.CipherAlgorithm; + HashAlgorithm = sslStream.HashAlgorithm; + KeyExchangeAlgorithm = sslStream.KeyExchangeAlgorithm.ToString(); + NegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol.ToString(); + RemoteCertificateSubject = sslStream.RemoteCertificate?.Subject; + RemoteCertificateIssuer = sslStream.RemoteCertificate?.Issuer; + LocalCertificateIssuer = sslStream.LocalCertificate?.Issuer; + LocalCertificateSubject = sslStream.LocalCertificate?.Subject; + SslProtocol = sslStream.SslProtocol; + } + + /// + /// Building from BouncyCastle + /// + /// + internal SslInfo(FluxzyClientProtocol clientProtocol) + { +//#if NET6_0 +// CipherAlgorithm = ((System.Net.Security.TlsCipherSuite) clientProtocol.SessionParameters.CipherSuite).ToString(); +//#endif + + NegotiatedApplicationProtocol = clientProtocol.GetApplicationProtocol().ToString(); + SslProtocol = clientProtocol.GetSChannelProtocol(); + + if (BcCertificateHelper.ReadInfo(clientProtocol.SessionParameters.LocalCertificate, + out var localSubject, out var localIssuer)) { + LocalCertificateIssuer = localIssuer; + LocalCertificateSubject = localSubject; + } + + if (BcCertificateHelper.ReadInfo(clientProtocol.SessionParameters.PeerCertificate, + out var remoteSubject, out var remoteIssuer)) { + RemoteCertificateIssuer = remoteIssuer; + RemoteCertificateSubject = remoteSubject; + } + + KeyExchangeAlgorithm = string.Empty; + } + + [JsonConstructor] + public SslInfo( + SslProtocols sslProtocol, string? remoteCertificateIssuer, string? remoteCertificateSubject, + string? localCertificateSubject, string? localCertificateIssuer, string negotiatedApplicationProtocol, + string keyExchangeAlgorithm, HashAlgorithmType hashAlgorithm, CipherAlgorithmType cipherAlgorithm) + { + SslProtocol = sslProtocol; + RemoteCertificateIssuer = remoteCertificateIssuer; + RemoteCertificateSubject = remoteCertificateSubject; + LocalCertificateSubject = localCertificateSubject; + LocalCertificateIssuer = localCertificateIssuer; + NegotiatedApplicationProtocol = negotiatedApplicationProtocol; + KeyExchangeAlgorithm = keyExchangeAlgorithm; + HashAlgorithm = hashAlgorithm; + CipherAlgorithm = cipherAlgorithm; + } + + public SslProtocols SslProtocol { get; } + + public string? RemoteCertificateIssuer { get; } + + public string? RemoteCertificateSubject { get; } + + public string? LocalCertificateSubject { get; } + + public string? LocalCertificateIssuer { get; } + + public string NegotiatedApplicationProtocol { get; } + + public string KeyExchangeAlgorithm { get; } + + public HashAlgorithmType HashAlgorithm { get; } + + public CipherAlgorithmType CipherAlgorithm { get; } + + public byte[]? RemoteCertificate { get; set; } + + public byte[]? LocalCertificate { get; set; } + } +} diff --git a/src/Fluxzy.Core/Clients/DotNetBridge/FluxzyHttp2Handler.cs b/src/Fluxzy.Core/Clients/DotNetBridge/FluxzyHttp2Handler.cs index 5b098996e..37d7cbf4d 100644 --- a/src/Fluxzy.Core/Clients/DotNetBridge/FluxzyHttp2Handler.cs +++ b/src/Fluxzy.Core/Clients/DotNetBridge/FluxzyHttp2Handler.cs @@ -75,4 +75,4 @@ protected override void Dispose(bool disposing) _semaphore.Dispose(); } } -} +} diff --git a/src/Fluxzy.Core/Clients/H11/Http11ConnectionPool.cs b/src/Fluxzy.Core/Clients/H11/Http11ConnectionPool.cs index 12863b16a..6a0313a42 100644 --- a/src/Fluxzy.Core/Clients/H11/Http11ConnectionPool.cs +++ b/src/Fluxzy.Core/Clients/H11/Http11ConnectionPool.cs @@ -1,247 +1,247 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Security; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Security; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; using Fluxzy.Core; -using Fluxzy.Misc.ResizableBuffers; -using Fluxzy.Writers; - -namespace Fluxzy.Clients.H11 -{ +using Fluxzy.Misc.ResizableBuffers; +using Fluxzy.Writers; + +namespace Fluxzy.Clients.H11 +{ /// /// A pool of proxyRuntimeSetting.ConcurrentConnection HTTP/1.1 connections with the same authority - /// - public class Http11ConnectionPool : IHttpConnectionPool - { - private static readonly List Http11Protocols = new() { SslApplicationProtocol.Http11 }; - private readonly RealtimeArchiveWriter _archiveWriter; - private readonly DnsResolutionResult _resolutionResult; - - private readonly H1Logger _logger; - - private readonly Channel _pendingConnections; - - private readonly ProxyRuntimeSetting _proxyRuntimeSetting; - - private readonly RemoteConnectionBuilder _remoteConnectionBuilder; - private readonly ITimingProvider _timingProvider; - - internal Http11ConnectionPool( - Authority authority, - RemoteConnectionBuilder remoteConnectionBuilder, - ITimingProvider timingProvider, - ProxyRuntimeSetting proxyRuntimeSetting, - RealtimeArchiveWriter archiveWriter, - DnsResolutionResult resolutionResult) - { - _remoteConnectionBuilder = remoteConnectionBuilder; - _timingProvider = timingProvider; - _proxyRuntimeSetting = proxyRuntimeSetting; - _archiveWriter = archiveWriter; - _resolutionResult = resolutionResult; - Authority = authority; - - _pendingConnections = Channel.CreateBounded( - new BoundedChannelOptions(proxyRuntimeSetting.ConcurrentConnection) { - SingleReader = false, - SingleWriter = false - }); - - _logger = new H1Logger(authority); - - ITimingProvider.Default.Instant(); - } - - public Authority Authority { get; } - - public bool Complete => false; - - public void Init() - { - } - - public ValueTask CheckAlive() - { - return new ValueTask(true); - } - - public async ValueTask Send( - Exchange exchange, ILocalLink _, RsBuffer buffer, - CancellationToken cancellationToken) - { - ITimingProvider.Default.Instant(); - - exchange.HttpVersion = "HTTP/1.1"; - - try { - _logger.Trace(exchange, "Begin wait for authority slot"); - - _logger.Trace(exchange.Id, "Acquiring slot"); - - var requestDate = _timingProvider.Instant(); - - while (_pendingConnections.Reader.TryRead(out var state)) { - - if (HasConnectionExpired(requestDate, state)) - { - // The connection pool exceeds timing connection .. - // TODO: Gracefully release connection - - continue; - } - - exchange.Connection = state.Connection; - _logger.Trace(exchange.Id, () => $"Recycling connection : {exchange.Connection.Id}"); - - break; - } - - if (exchange.Connection == null) { - _logger.Trace(exchange.Id, () => "New connection request"); - - var openingResult = - await _remoteConnectionBuilder.OpenConnectionToRemote( - exchange, _resolutionResult , Http11Protocols, - _proxyRuntimeSetting, exchange.Context.ProxyConfiguration, cancellationToken); - - if (exchange.Context.PreMadeResponse != null) { - return; - } - - exchange.Connection = openingResult.Connection; - - openingResult.Connection.HttpVersion = exchange.HttpVersion; - - if (_archiveWriter != null) - _archiveWriter.Update(exchange.Connection, cancellationToken); - - _logger.Trace(exchange.Id, () => $"New connection obtained: {exchange.Connection.Id}"); - } - - var poolProcessing = new Http11PoolProcessing(_logger); - - try { - await poolProcessing.Process(exchange, buffer, cancellationToken) - .ConfigureAwait(false); - - if (exchange.Response.Header != null) - exchange.Connection.TimeoutIdleSeconds = exchange.Response.Header.TimeoutIdleSeconds; - - _logger.Trace(exchange.Id, () => "[Process] return"); - - var lastUsed = _timingProvider.Instant(); - - void OnExchangeCompleteFunction(Task completeTask) - { - var closeConnectionRequest = completeTask.Result; - - if (exchange.Response.Header!.MaxConnection != -1 && - exchange.Response.Header!.MaxConnection <= exchange.Connection.RequestProcessed) { - closeConnectionRequest = true; - } - - if (exchange.Metrics.ResponseBodyEnd == default) - exchange.Metrics.ResponseBodyEnd = ITimingProvider.Default.Instant(); - - if (completeTask.Exception != null && completeTask.Exception.InnerExceptions.Any()) { - _logger.Trace(exchange.Id, () => $"Complete on error {completeTask.Exception.GetType()} : {completeTask.Exception.Message}"); - - foreach (var exception in completeTask.Exception.InnerExceptions) { - exchange.Errors.Add(new Error("Error while reading response", exception)); - } - } - else if (completeTask.IsCompletedSuccessfully && !closeConnectionRequest) { // - - if (_pendingConnections.Writer.TryWrite(new Http11ProcessingState(exchange.Connection, lastUsed))) - { - _logger.Trace(exchange.Id, () => "Complete on success, recycling connection ..."); - return; - } - } - else { - _logger.Trace(exchange.Id, () => "Complete on success, closing connection ..."); - - // should close connection - } - - FreeConnectionStreams(exchange.Connection); - } - - var res = exchange.Complete.ContinueWith(OnExchangeCompleteFunction, cancellationToken); - } - catch (Exception ex) { - - if (ex is ConnectionCloseException) - { - if (exchange.Connection.ReadStream != null) - await exchange.Connection.ReadStream.DisposeAsync(); - - exchange.Connection = null; - } - - _logger.Trace(exchange.Id, () => $"Processing error {ex}"); - - throw; - } - } - finally { - //_semaphoreSlim.Release(); - ITimingProvider.Default.Instant(); - } - } - - private static void FreeConnectionStreams(Connection connection) - { - connection.ReadStream!.Dispose(); - - if (connection.ReadStream != connection.WriteStream) - connection.WriteStream!.Dispose(); - } - - private bool HasConnectionExpired(DateTime instantNow, Http11ProcessingState state) - { - if (state.Connection.TimeoutIdleSeconds != -1) { - - var expireOn = state.LastUsed - .AddSeconds(state.Connection.TimeoutIdleSeconds) - .AddMilliseconds(-200); // 100ms to skip RTT error - - if (expireOn < instantNow) - return true; - } - - var res = - instantNow - state.LastUsed > TimeSpan.FromSeconds(_proxyRuntimeSetting.TimeOutSecondsUnusedConnection); - - - return res; - } - - public ValueTask DisposeAsync() - { - return default; - } - - public void Dispose() - { - } - } - - public class Http11ProcessingState - { - public Http11ProcessingState(Connection connection, DateTime lastUsed) - { - Connection = connection; - LastUsed = lastUsed; - } - - public Connection Connection { get; } - - public DateTime LastUsed { get; set; } - } -} + /// + public class Http11ConnectionPool : IHttpConnectionPool + { + private static readonly List Http11Protocols = new() { SslApplicationProtocol.Http11 }; + private readonly RealtimeArchiveWriter _archiveWriter; + private readonly DnsResolutionResult _resolutionResult; + + private readonly H1Logger _logger; + + private readonly Channel _pendingConnections; + + private readonly ProxyRuntimeSetting _proxyRuntimeSetting; + + private readonly RemoteConnectionBuilder _remoteConnectionBuilder; + private readonly ITimingProvider _timingProvider; + + internal Http11ConnectionPool( + Authority authority, + RemoteConnectionBuilder remoteConnectionBuilder, + ITimingProvider timingProvider, + ProxyRuntimeSetting proxyRuntimeSetting, + RealtimeArchiveWriter archiveWriter, + DnsResolutionResult resolutionResult) + { + _remoteConnectionBuilder = remoteConnectionBuilder; + _timingProvider = timingProvider; + _proxyRuntimeSetting = proxyRuntimeSetting; + _archiveWriter = archiveWriter; + _resolutionResult = resolutionResult; + Authority = authority; + + _pendingConnections = Channel.CreateBounded( + new BoundedChannelOptions(proxyRuntimeSetting.ConcurrentConnection) { + SingleReader = false, + SingleWriter = false + }); + + _logger = new H1Logger(authority); + + ITimingProvider.Default.Instant(); + } + + public Authority Authority { get; } + + public bool Complete => false; + + public void Init() + { + } + + public ValueTask CheckAlive() + { + return new ValueTask(true); + } + + public async ValueTask Send( + Exchange exchange, ILocalLink _, RsBuffer buffer, + CancellationToken cancellationToken) + { + ITimingProvider.Default.Instant(); + + exchange.HttpVersion = "HTTP/1.1"; + + try { + _logger.Trace(exchange, "Begin wait for authority slot"); + + _logger.Trace(exchange.Id, "Acquiring slot"); + + var requestDate = _timingProvider.Instant(); + + while (_pendingConnections.Reader.TryRead(out var state)) { + + if (HasConnectionExpired(requestDate, state)) + { + // The connection pool exceeds timing connection .. + // TODO: Gracefully release connection + + continue; + } + + exchange.Connection = state.Connection; + _logger.Trace(exchange.Id, () => $"Recycling connection : {exchange.Connection.Id}"); + + break; + } + + if (exchange.Connection == null) { + _logger.Trace(exchange.Id, () => "New connection request"); + + var openingResult = + await _remoteConnectionBuilder.OpenConnectionToRemote( + exchange, _resolutionResult , Http11Protocols, + _proxyRuntimeSetting, exchange.Context.ProxyConfiguration, cancellationToken); + + if (exchange.Context.PreMadeResponse != null) { + return; + } + + exchange.Connection = openingResult.Connection; + + openingResult.Connection.HttpVersion = exchange.HttpVersion; + + if (_archiveWriter != null) + _archiveWriter.Update(exchange.Connection, cancellationToken); + + _logger.Trace(exchange.Id, () => $"New connection obtained: {exchange.Connection.Id}"); + } + + var poolProcessing = new Http11PoolProcessing(_logger); + + try { + await poolProcessing.Process(exchange, buffer, cancellationToken) + .ConfigureAwait(false); + + if (exchange.Response.Header != null) + exchange.Connection.TimeoutIdleSeconds = exchange.Response.Header.TimeoutIdleSeconds; + + _logger.Trace(exchange.Id, () => "[Process] return"); + + var lastUsed = _timingProvider.Instant(); + + void OnExchangeCompleteFunction(Task completeTask) + { + var closeConnectionRequest = completeTask.Result; + + if (exchange.Response.Header!.MaxConnection != -1 && + exchange.Response.Header!.MaxConnection <= exchange.Connection.RequestProcessed) { + closeConnectionRequest = true; + } + + if (exchange.Metrics.ResponseBodyEnd == default) + exchange.Metrics.ResponseBodyEnd = ITimingProvider.Default.Instant(); + + if (completeTask.Exception != null && completeTask.Exception.InnerExceptions.Any()) { + _logger.Trace(exchange.Id, () => $"Complete on error {completeTask.Exception.GetType()} : {completeTask.Exception.Message}"); + + foreach (var exception in completeTask.Exception.InnerExceptions) { + exchange.Errors.Add(new Error("Error while reading response", exception)); + } + } + else if (completeTask.IsCompletedSuccessfully && !closeConnectionRequest) { // + + if (_pendingConnections.Writer.TryWrite(new Http11ProcessingState(exchange.Connection, lastUsed))) + { + _logger.Trace(exchange.Id, () => "Complete on success, recycling connection ..."); + return; + } + } + else { + _logger.Trace(exchange.Id, () => "Complete on success, closing connection ..."); + + // should close connection + } + + FreeConnectionStreams(exchange.Connection); + } + + var res = exchange.Complete.ContinueWith(OnExchangeCompleteFunction, cancellationToken); + } + catch (Exception ex) { + + if (ex is ConnectionCloseException) + { + if (exchange.Connection.ReadStream != null) + await exchange.Connection.ReadStream.DisposeAsync(); + + exchange.Connection = null; + } + + _logger.Trace(exchange.Id, () => $"Processing error {ex}"); + + throw; + } + } + finally { + //_semaphoreSlim.Release(); + ITimingProvider.Default.Instant(); + } + } + + private static void FreeConnectionStreams(Connection connection) + { + connection.ReadStream!.Dispose(); + + if (connection.ReadStream != connection.WriteStream) + connection.WriteStream!.Dispose(); + } + + private bool HasConnectionExpired(DateTime instantNow, Http11ProcessingState state) + { + if (state.Connection.TimeoutIdleSeconds != -1) { + + var expireOn = state.LastUsed + .AddSeconds(state.Connection.TimeoutIdleSeconds) + .AddMilliseconds(-200); // 100ms to skip RTT error + + if (expireOn < instantNow) + return true; + } + + var res = + instantNow - state.LastUsed > TimeSpan.FromSeconds(_proxyRuntimeSetting.TimeOutSecondsUnusedConnection); + + + return res; + } + + public ValueTask DisposeAsync() + { + return default; + } + + public void Dispose() + { + } + } + + public class Http11ProcessingState + { + public Http11ProcessingState(Connection connection, DateTime lastUsed) + { + Connection = connection; + LastUsed = lastUsed; + } + + public Connection Connection { get; } + + public DateTime LastUsed { get; set; } + } +} diff --git a/src/Fluxzy.Core/Clients/H11/TunnelOnlyConnectionPool.cs b/src/Fluxzy.Core/Clients/H11/TunnelOnlyConnectionPool.cs index f81408352..a87e48cd9 100644 --- a/src/Fluxzy.Core/Clients/H11/TunnelOnlyConnectionPool.cs +++ b/src/Fluxzy.Core/Clients/H11/TunnelOnlyConnectionPool.cs @@ -1,174 +1,174 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; using Fluxzy.Core; -using Fluxzy.Misc.ResizableBuffers; -using Fluxzy.Misc.Streams; -using Fluxzy.Writers; - -namespace Fluxzy.Clients.H11 -{ - internal class TunnelOnlyConnectionPool : IHttpConnectionPool - { - private readonly RemoteConnectionBuilder _connectionBuilder; - private readonly ProxyRuntimeSetting _proxyRuntimeSetting; - private readonly DnsResolutionResult _resolutionResult; - private readonly SemaphoreSlim _semaphoreSlim; - private readonly ITimingProvider _timingProvider; - - public TunnelOnlyConnectionPool( - Authority authority, - ITimingProvider timingProvider, - RemoteConnectionBuilder connectionBuilder, - ProxyRuntimeSetting proxyRuntimeSetting, DnsResolutionResult resolutionResult) - { - _timingProvider = timingProvider; - _connectionBuilder = connectionBuilder; - _proxyRuntimeSetting = proxyRuntimeSetting; - _resolutionResult = resolutionResult; - Authority = authority; - _semaphoreSlim = new SemaphoreSlim(proxyRuntimeSetting.ConcurrentConnection); - } - - - public Authority Authority { get; } - - public bool Complete { get; private set; } - - public void Init() - { - } - - public ValueTask CheckAlive() - { - return new ValueTask(!Complete); - } - - public async ValueTask Send( - Exchange exchange, ILocalLink localLink, RsBuffer buffer, - CancellationToken cancellationToken = default) - { - try { - await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); - - await using var ex = new TunneledConnectionProcess( - Authority, _timingProvider, - _connectionBuilder, - _proxyRuntimeSetting, null, _resolutionResult); - - await ex.Process(exchange, localLink, buffer.Buffer, CancellationToken.None).ConfigureAwait(false); - } - finally { - _semaphoreSlim.Release(); - Complete = true; - } - } - - public ValueTask DisposeAsync() - { - _semaphoreSlim.Dispose(); - - return new ValueTask(Task.CompletedTask); - } - } - - internal class TunneledConnectionProcess : IDisposable, IAsyncDisposable - { - private readonly RealtimeArchiveWriter? _archiveWriter; - private readonly DnsResolutionResult _dnsResolutionResult; - private readonly Authority _authority; - private readonly ProxyRuntimeSetting _creationSetting; - private readonly RemoteConnectionBuilder _remoteConnectionBuilder; - private readonly ITimingProvider _timingProvider; - - public TunneledConnectionProcess( - Authority authority, - ITimingProvider timingProvider, - RemoteConnectionBuilder remoteConnectionBuilder, - ProxyRuntimeSetting creationSetting, - RealtimeArchiveWriter? archiveWriter, - DnsResolutionResult dnsResolutionResult) - { - _authority = authority; - _timingProvider = timingProvider; - _remoteConnectionBuilder = remoteConnectionBuilder; - _creationSetting = creationSetting; - _archiveWriter = archiveWriter; - _dnsResolutionResult = dnsResolutionResult; - } - - public ValueTask DisposeAsync() - { - return default; - } - - public void Dispose() - { - } - - public async Task Process( - Exchange exchange, ILocalLink localLink, byte[] buffer, - CancellationToken cancellationToken) - { - if (localLink == null) - throw new ArgumentNullException(nameof(localLink)); - - var openingResult = await _remoteConnectionBuilder.OpenConnectionToRemote( - exchange, _dnsResolutionResult, - new List { SslApplicationProtocol.Http11 }, - _creationSetting, exchange.Context.ProxyConfiguration, - cancellationToken).ConfigureAwait(false); - - exchange.Connection = openingResult.Connection; - - _archiveWriter?.Update(exchange.Connection, cancellationToken); - - if (exchange.Request.Header.IsWebSocketRequest) { - var headerLength = exchange.Request.Header.WriteHttp11(false, buffer, false); - await exchange.Connection.WriteStream!.WriteAsync(buffer, 0, headerLength, cancellationToken).ConfigureAwait(false); - } - - try { - await using var remoteStream = exchange.Connection.WriteStream; - using var haltTokenSource = new CancellationTokenSource(); - var copyTokenSource = CancellationTokenSource.CreateLinkedTokenSource( - haltTokenSource.Token, - cancellationToken); - - var tasks = new Task[] { - localLink.ReadStream!.CopyDetailed(remoteStream!, buffer, copied => - exchange.Metrics.TotalSent += copied - , copyTokenSource.Token).AsTask(), - remoteStream!.CopyDetailed(localLink.WriteStream!, 1024 * 16, copied => - exchange.Metrics.TotalReceived += copied - , copyTokenSource.Token).AsTask() - }; - - await Task.WhenAny(tasks).ConfigureAwait(false); - - haltTokenSource.Cancel(); - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - catch (Exception ex) { - if (ex is IOException || ex is SocketException) { - exchange.Errors.Add(new Error("", ex)); - - return; - } - - throw; - } - finally { - exchange.Metrics.RemoteClosed = _timingProvider.Instant(); - } - } - } -} +using Fluxzy.Misc.ResizableBuffers; +using Fluxzy.Misc.Streams; +using Fluxzy.Writers; + +namespace Fluxzy.Clients.H11 +{ + internal class TunnelOnlyConnectionPool : IHttpConnectionPool + { + private readonly RemoteConnectionBuilder _connectionBuilder; + private readonly ProxyRuntimeSetting _proxyRuntimeSetting; + private readonly DnsResolutionResult _resolutionResult; + private readonly SemaphoreSlim _semaphoreSlim; + private readonly ITimingProvider _timingProvider; + + public TunnelOnlyConnectionPool( + Authority authority, + ITimingProvider timingProvider, + RemoteConnectionBuilder connectionBuilder, + ProxyRuntimeSetting proxyRuntimeSetting, DnsResolutionResult resolutionResult) + { + _timingProvider = timingProvider; + _connectionBuilder = connectionBuilder; + _proxyRuntimeSetting = proxyRuntimeSetting; + _resolutionResult = resolutionResult; + Authority = authority; + _semaphoreSlim = new SemaphoreSlim(proxyRuntimeSetting.ConcurrentConnection); + } + + + public Authority Authority { get; } + + public bool Complete { get; private set; } + + public void Init() + { + } + + public ValueTask CheckAlive() + { + return new ValueTask(!Complete); + } + + public async ValueTask Send( + Exchange exchange, ILocalLink localLink, RsBuffer buffer, + CancellationToken cancellationToken = default) + { + try { + await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + + await using var ex = new TunneledConnectionProcess( + Authority, _timingProvider, + _connectionBuilder, + _proxyRuntimeSetting, null, _resolutionResult); + + await ex.Process(exchange, localLink, buffer.Buffer, CancellationToken.None).ConfigureAwait(false); + } + finally { + _semaphoreSlim.Release(); + Complete = true; + } + } + + public ValueTask DisposeAsync() + { + _semaphoreSlim.Dispose(); + + return new ValueTask(Task.CompletedTask); + } + } + + internal class TunneledConnectionProcess : IDisposable, IAsyncDisposable + { + private readonly RealtimeArchiveWriter? _archiveWriter; + private readonly DnsResolutionResult _dnsResolutionResult; + private readonly Authority _authority; + private readonly ProxyRuntimeSetting _creationSetting; + private readonly RemoteConnectionBuilder _remoteConnectionBuilder; + private readonly ITimingProvider _timingProvider; + + public TunneledConnectionProcess( + Authority authority, + ITimingProvider timingProvider, + RemoteConnectionBuilder remoteConnectionBuilder, + ProxyRuntimeSetting creationSetting, + RealtimeArchiveWriter? archiveWriter, + DnsResolutionResult dnsResolutionResult) + { + _authority = authority; + _timingProvider = timingProvider; + _remoteConnectionBuilder = remoteConnectionBuilder; + _creationSetting = creationSetting; + _archiveWriter = archiveWriter; + _dnsResolutionResult = dnsResolutionResult; + } + + public ValueTask DisposeAsync() + { + return default; + } + + public void Dispose() + { + } + + public async Task Process( + Exchange exchange, ILocalLink localLink, byte[] buffer, + CancellationToken cancellationToken) + { + if (localLink == null) + throw new ArgumentNullException(nameof(localLink)); + + var openingResult = await _remoteConnectionBuilder.OpenConnectionToRemote( + exchange, _dnsResolutionResult, + new List { SslApplicationProtocol.Http11 }, + _creationSetting, exchange.Context.ProxyConfiguration, + cancellationToken).ConfigureAwait(false); + + exchange.Connection = openingResult.Connection; + + _archiveWriter?.Update(exchange.Connection, cancellationToken); + + if (exchange.Request.Header.IsWebSocketRequest) { + var headerLength = exchange.Request.Header.WriteHttp11(false, buffer, false); + await exchange.Connection.WriteStream!.WriteAsync(buffer, 0, headerLength, cancellationToken).ConfigureAwait(false); + } + + try { + await using var remoteStream = exchange.Connection.WriteStream; + using var haltTokenSource = new CancellationTokenSource(); + var copyTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + haltTokenSource.Token, + cancellationToken); + + var tasks = new Task[] { + localLink.ReadStream!.CopyDetailed(remoteStream!, buffer, copied => + exchange.Metrics.TotalSent += copied + , copyTokenSource.Token).AsTask(), + remoteStream!.CopyDetailed(localLink.WriteStream!, 1024 * 16, copied => + exchange.Metrics.TotalReceived += copied + , copyTokenSource.Token).AsTask() + }; + + await Task.WhenAny(tasks).ConfigureAwait(false); + + haltTokenSource.Cancel(); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (Exception ex) { + if (ex is IOException || ex is SocketException) { + exchange.Errors.Add(new Error("", ex)); + + return; + } + + throw; + } + finally { + exchange.Metrics.RemoteClosed = _timingProvider.Instant(); + } + } + } +} diff --git a/src/Fluxzy.Core/Clients/H2/Encoder/Utils/Http11Parser.cs b/src/Fluxzy.Core/Clients/H2/Encoder/Utils/Http11Parser.cs index 10785262b..0ee29326f 100644 --- a/src/Fluxzy.Core/Clients/H2/Encoder/Utils/Http11Parser.cs +++ b/src/Fluxzy.Core/Clients/H2/Encoder/Utils/Http11Parser.cs @@ -38,11 +38,12 @@ public static IEnumerable Read( yield return new HeaderField(Http11Constants.MethodVerb, arrayOfValue[0]); + yield return new HeaderField(Http11Constants.SchemeVerb, + isHttps ? Http11Constants.HttpsVerb : Http11Constants.HttpVerb); + yield return new HeaderField(Http11Constants.PathVerb, arrayOfValue[1].RemoveProtocolAndAuthority()); // Remove prefix on path - yield return new HeaderField(Http11Constants.SchemeVerb, - isHttps ? Http11Constants.HttpsVerb : Http11Constants.HttpVerb); if (Http11Constants.SchemeVerb.Span.StartsWith(Http11Constants.HttpsVerb.Span)) isHttps = true; diff --git a/src/Fluxzy.Core/Clients/H2/Frames/SettingFrame.cs b/src/Fluxzy.Core/Clients/H2/Frames/SettingFrame.cs index 896656c8a..ddae64085 100644 --- a/src/Fluxzy.Core/Clients/H2/Frames/SettingFrame.cs +++ b/src/Fluxzy.Core/Clients/H2/Frames/SettingFrame.cs @@ -1,4 +1,4 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak using System; using System.Buffers.Binary; @@ -58,6 +58,19 @@ public int Write(Span buffer) return 9; } + internal static int WriteMultipleHeader(Span buffer, int settingCount) + { + return H2Frame.Write(buffer, settingCount * 6, H2FrameType.Settings, HeaderFlags.None, 0); + } + + internal static int WriteMultipleBody(Span buffer, SettingIdentifier identifier, int value) + { + buffer = buffer.BuWrite_16((ushort)identifier); + buffer = buffer.BuWrite_32(value); + + return 6; + } + public int BodyLength => Ack ? 0 : 6; public void Write(Stream stream) diff --git a/src/Fluxzy.Core/Clients/H2/H2ConnectionPool.cs b/src/Fluxzy.Core/Clients/H2/H2ConnectionPool.cs index 7282ae250..0d61de547 100644 --- a/src/Fluxzy.Core/Clients/H2/H2ConnectionPool.cs +++ b/src/Fluxzy.Core/Clients/H2/H2ConnectionPool.cs @@ -112,8 +112,8 @@ public void Init() _initDone = false; - _baseStream.Write(Preface); - SettingHelper.WriteWelcomeSettings(_baseStream, Setting.Local, _logger); + //_baseStream.Write(Preface); + SettingHelper.WriteWelcomeSettings(Preface, _baseStream, Setting, _logger); _innerReadTask = Task.Run(() => InternalReadLoop(_connectionToken), _connectionToken); _innerWriteRun = Task.Run(() => InternalWriteLoop(_connectionToken), _connectionToken); @@ -445,7 +445,7 @@ private async Task InternalReadLoop(CancellationToken token) try { while (!token.IsCancellationRequested) { _logger.TraceDeep(0, () => "1"); - + var frame = await H2FrameReader.ReadNextFrameAsync(_baseStream, readBuffer, token).ConfigureAwait(false); @@ -678,4 +678,4 @@ await activeStream.ProcessResponse(streamCancellationToken, this) } } } -} +} diff --git a/src/Fluxzy.Core/Clients/H2/H2StreamSetting.cs b/src/Fluxzy.Core/Clients/H2/H2StreamSetting.cs index 424d37a89..a42fdbbb3 100644 --- a/src/Fluxzy.Core/Clients/H2/H2StreamSetting.cs +++ b/src/Fluxzy.Core/Clients/H2/H2StreamSetting.cs @@ -1,18 +1,21 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak using System; +using System.Collections.Generic; +using Fluxzy.Clients.H2.Frames; namespace Fluxzy.Clients.H2 { public class H2StreamSetting { public PeerSetting Local { get; set; } = new() { - WindowSize = 1024 * 1024 * 16 // 512Ko + WindowSize = 6291456, // 512Ko - 15663105 - 15 728 640 + SettingsMaxConcurrentStreams = 256 }; public PeerSetting Remote { get; set; } = new(); - public int SettingsHeaderTableSize { get; set; } = 4096; + public int SettingsHeaderTableSize { get; set; } = 65536; public int OverallWindowSize { get; set; } = 65536; @@ -30,20 +33,77 @@ public class H2StreamSetting /// public int ReadBufferLength { get; set; } = 0x4000; + /// + /// + /// public TimeSpan WaitForSettingDelay { get; set; } = TimeSpan.FromSeconds(30); + + + public HashSet AdvertiseSettings { get; set; } = new HashSet() { + SettingIdentifier.SettingsHeaderTableSize, + SettingIdentifier.SettingsEnablePush, + SettingIdentifier.SettingsInitialWindowSize, + SettingIdentifier.SettingsMaxHeaderListSize, + }; + + public IEnumerable<(SettingIdentifier SettingIdentifier, int Value)> GetAnnouncementSettings() + { + if (AdvertiseSettings.Contains(SettingIdentifier.SettingsHeaderTableSize)) + yield return (SettingIdentifier.SettingsHeaderTableSize, SettingsHeaderTableSize); + + if (AdvertiseSettings.Contains(SettingIdentifier.SettingsEnablePush)) + yield return (SettingIdentifier.SettingsEnablePush, Local.EnablePush ? 1 : 0); + + if (AdvertiseSettings.Contains(SettingIdentifier.SettingsMaxConcurrentStreams)) + yield return (SettingIdentifier.SettingsMaxConcurrentStreams, Local.SettingsMaxConcurrentStreams); + + if (AdvertiseSettings.Contains(SettingIdentifier.SettingsInitialWindowSize)) + yield return (SettingIdentifier.SettingsInitialWindowSize, Local.WindowSize); + + if (AdvertiseSettings.Contains(SettingIdentifier.SettingsMaxFrameSize)) + yield return (SettingIdentifier.SettingsMaxFrameSize, Local.MaxFrameSize); + + if (AdvertiseSettings.Contains(SettingIdentifier.SettingsMaxHeaderListSize)) + yield return (SettingIdentifier.SettingsMaxHeaderListSize, Local.MaxHeaderListSize); + } + + public void SetSetting(SettingIdentifier identifier, int value) + { + switch (identifier) + { + case SettingIdentifier.SettingsHeaderTableSize: + SettingsHeaderTableSize = value; + break; + case SettingIdentifier.SettingsEnablePush: + Local.EnablePush = value == 1; + break; + case SettingIdentifier.SettingsMaxConcurrentStreams: + Local.SettingsMaxConcurrentStreams = value; + break; + case SettingIdentifier.SettingsInitialWindowSize: + Local.WindowSize = value; + break; + case SettingIdentifier.SettingsMaxFrameSize: + Local.MaxFrameSize = value; + break; + case SettingIdentifier.SettingsMaxHeaderListSize: + Local.MaxHeaderListSize = value; + break; + } + } } public class PeerSetting { public static PeerSetting Default { get; } = new(); - public int WindowSize { get; set; } = 0XFFFF; + public int WindowSize { get; set; } = 6291456; public int MaxFrameSize { get; set; } = 0x4000; - public bool EnablePush { get; } = false; + public bool EnablePush { get; set; } = false; - public int MaxHeaderListSize { get; set; } = 0x4000; + public int MaxHeaderListSize { get; set; } = 262144; public int SettingsMaxConcurrentStreams { get; set; } = 100; diff --git a/src/Fluxzy.Core/Clients/H2/HeaderEncoder.cs b/src/Fluxzy.Core/Clients/H2/HeaderEncoder.cs index 52a64e501..80ece9145 100644 --- a/src/Fluxzy.Core/Clients/H2/HeaderEncoder.cs +++ b/src/Fluxzy.Core/Clients/H2/HeaderEncoder.cs @@ -58,4 +58,4 @@ public ReadOnlyMemory Decode(ReadOnlyMemory encodedBuffer, Memory buffer, PeerSetting setting, H2Logger logger) + private static int WriteStartupSetting(Span buffer, H2StreamSetting h2Setting, H2Logger logger) { var written = 0; + var headerCount = 9; - { - //var currentSetting = new SettingFrame(SettingIdentifier.SettingsEnablePush, 0); - //written += currentSetting.Write(buffer); - //logger.OutgoingSetting(ref currentSetting); - } + // 5 bytes header - { - //var currentSetting = new SettingFrame(SettingIdentifier.SettingsInitialWindowSize, 1073741824); - //written += currentSetting.Write(buffer); - //logger.OutgoingSetting(ref currentSetting); - } + var totalSettingCount = 0; + + foreach (var (settingIdentifier, value) in h2Setting.GetAnnouncementSettings()) { - { - var currentSetting = new SettingFrame(SettingIdentifier.SettingsMaxConcurrentStreams, 256); - written += currentSetting.Write(buffer); + var currentSetting = new SettingFrame(settingIdentifier, value); + written += SettingFrame.WriteMultipleBody(buffer.Slice(written + headerCount), settingIdentifier, value); + totalSettingCount++; logger.OutgoingSetting(ref currentSetting); } + // 5 bytes header + + written += SettingFrame.WriteMultipleHeader(buffer, totalSettingCount); return written; } - public static void WriteWelcomeSettings(Stream innerStream, PeerSetting setting, H2Logger logger) + public static void WriteWelcomeSettings(byte [] preface, Stream innerStream, H2StreamSetting h2Setting, H2Logger logger) { - Span settingBuffer = stackalloc byte[128]; + Span settingBuffer = stackalloc byte[512]; + + var written = 0; + + preface.AsSpan().CopyTo(settingBuffer); + written += preface.Length; + written += WriteStartupSetting(settingBuffer.Slice(written), h2Setting, logger); + + var windowSizeAnnounced = h2Setting.Local.WindowSize - 65535; + + if (windowSizeAnnounced != 0) { + var windowFrame = new WindowUpdateFrame(windowSizeAnnounced, 0); + written += windowFrame.Write(settingBuffer.Slice(written)); + } - var written = WriteStartupSetting(settingBuffer, setting, logger); innerStream.Write(settingBuffer[..written]); } diff --git a/src/Fluxzy.Core/Clients/H2/StreamPool.cs b/src/Fluxzy.Core/Clients/H2/StreamPool.cs index 92a0e629e..8d117fab0 100644 --- a/src/Fluxzy.Core/Clients/H2/StreamPool.cs +++ b/src/Fluxzy.Core/Clients/H2/StreamPool.cs @@ -1,125 +1,125 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; using Fluxzy.Core; -namespace Fluxzy.Clients.H2 -{ - internal class StreamPool : IDisposable, IAsyncDisposable - { - private readonly SemaphoreSlim _maxConcurrentStreamBarrier; - private readonly ConcurrentDictionary _runningStreams = new(); - - private int _lastStreamIdentifier = -1; - private bool _onError; - - private int _overallWindowSize; - - public StreamPool(StreamContext context) - { - Context = context; - _maxConcurrentStreamBarrier = new SemaphoreSlim(context.Setting.Remote.SettingsMaxConcurrentStreams); - - _overallWindowSize = context.Setting.Local.WindowSize - context.Setting.Local.MaxFrameSize; - } - - public StreamContext Context { get; } - - // WARNING : to be improved, may be extreme volatile - public int ActiveStreamCount => _runningStreams.Count; - - internal Exception? GoAwayException { get; private set; } - - internal int LastStreamIdentifier => _lastStreamIdentifier; - - public ValueTask DisposeAsync() - { - return default; - } - - public void Dispose() - { - _maxConcurrentStreamBarrier.Dispose(); - } - - public bool TryGetExistingActiveStream(int streamIdentifier, out StreamWorker? result) - { - return _runningStreams.TryGetValue(streamIdentifier, out result); - } - - private StreamWorker CreateActiveStream( - Exchange exchange, - CancellationToken callerCancellationToken, - SemaphoreSlim ongoingStreamInit, CancellationTokenSource resetTokenSource) - { - if (_onError) - throw new InvalidOperationException("This connection is on error"); - - ongoingStreamInit.Wait(callerCancellationToken); - - var streamId = Interlocked.Add(ref _lastStreamIdentifier, 2); - - var activeStream = new StreamWorker(streamId, this, exchange, resetTokenSource); - - _runningStreams[streamId] = activeStream; - - Context.Logger.Trace(exchange, "Affecting streamIdentifier", streamIdentifier: streamId); - - return activeStream; - } - - /// - /// Get or create active stream - /// - /// - public async ValueTask CreateNewStreamProcessing( - Exchange exchange, - CancellationToken callerCancellationToken, SemaphoreSlim ongoingStreamInit, - CancellationTokenSource resetTokenSource) - { - if (_onError) - throw new ConnectionCloseException("This connection is on error"); - - if (!_maxConcurrentStreamBarrier.Wait(TimeSpan.Zero)) - await _maxConcurrentStreamBarrier.WaitAsync(callerCancellationToken).ConfigureAwait(false); - - var res = CreateActiveStream(exchange, callerCancellationToken, ongoingStreamInit, resetTokenSource); - - return res; - } - - public void NotifyDispose(StreamWorker streamWorker) - { - // reset can happens here - - if (_runningStreams.TryRemove(streamWorker.StreamIdentifier, out _)) { - _maxConcurrentStreamBarrier.Release(); - streamWorker.Dispose(); - } - } - - public void OnGoAway(Exception? ex) - { - _onError = ex != null; - GoAwayException = ex; - } - - public int ShouldWindowUpdate(int dataLength) - { - var windowIncrement = 0; - - _overallWindowSize += dataLength; - - if (_overallWindowSize > 0.5 * Context.Setting.Local.WindowSize) { - windowIncrement = _overallWindowSize; - - _overallWindowSize = 0; - } - - return windowIncrement; - } - } -} +namespace Fluxzy.Clients.H2 +{ + internal class StreamPool : IDisposable, IAsyncDisposable + { + private readonly SemaphoreSlim _maxConcurrentStreamBarrier; + private readonly ConcurrentDictionary _runningStreams = new(); + + private int _lastStreamIdentifier = -1; + private bool _onError; + + private int _overallWindowSize; + + public StreamPool(StreamContext context) + { + Context = context; + _maxConcurrentStreamBarrier = new SemaphoreSlim(context.Setting.Remote.SettingsMaxConcurrentStreams); + + _overallWindowSize = context.Setting.Local.WindowSize - context.Setting.Local.MaxFrameSize; + } + + public StreamContext Context { get; } + + // WARNING : to be improved, may be extreme volatile + public int ActiveStreamCount => _runningStreams.Count; + + internal Exception? GoAwayException { get; private set; } + + internal int LastStreamIdentifier => _lastStreamIdentifier; + + public ValueTask DisposeAsync() + { + return default; + } + + public void Dispose() + { + _maxConcurrentStreamBarrier.Dispose(); + } + + public bool TryGetExistingActiveStream(int streamIdentifier, out StreamWorker? result) + { + return _runningStreams.TryGetValue(streamIdentifier, out result); + } + + private StreamWorker CreateActiveStream( + Exchange exchange, + CancellationToken callerCancellationToken, + SemaphoreSlim ongoingStreamInit, CancellationTokenSource resetTokenSource) + { + if (_onError) + throw new InvalidOperationException("This connection is on error"); + + ongoingStreamInit.Wait(callerCancellationToken); + + var streamId = Interlocked.Add(ref _lastStreamIdentifier, 2); + + var activeStream = new StreamWorker(streamId, this, exchange, resetTokenSource); + + _runningStreams[streamId] = activeStream; + + Context.Logger.Trace(exchange, "Affecting streamIdentifier", streamIdentifier: streamId); + + return activeStream; + } + + /// + /// Get or create active stream + /// + /// + public async ValueTask CreateNewStreamProcessing( + Exchange exchange, + CancellationToken callerCancellationToken, SemaphoreSlim ongoingStreamInit, + CancellationTokenSource resetTokenSource) + { + if (_onError) + throw new ConnectionCloseException("This connection is on error"); + + if (!_maxConcurrentStreamBarrier.Wait(TimeSpan.Zero)) + await _maxConcurrentStreamBarrier.WaitAsync(callerCancellationToken).ConfigureAwait(false); + + var res = CreateActiveStream(exchange, callerCancellationToken, ongoingStreamInit, resetTokenSource); + + return res; + } + + public void NotifyDispose(StreamWorker streamWorker) + { + // reset can happens here + + if (_runningStreams.TryRemove(streamWorker.StreamIdentifier, out _)) { + _maxConcurrentStreamBarrier.Release(); + streamWorker.Dispose(); + } + } + + public void OnGoAway(Exception? ex) + { + _onError = ex != null; + GoAwayException = ex; + } + + public int ShouldWindowUpdate(int dataLength) + { + var windowIncrement = 0; + + _overallWindowSize += dataLength; + + if (_overallWindowSize > 0.5 * Context.Setting.Local.WindowSize) { + windowIncrement = _overallWindowSize; + + _overallWindowSize = 0; + } + + return windowIncrement; + } + } +} diff --git a/src/Fluxzy.Core/Clients/H2/StreamWorker.cs b/src/Fluxzy.Core/Clients/H2/StreamWorker.cs index 29443ce97..9cade6504 100644 --- a/src/Fluxzy.Core/Clients/H2/StreamWorker.cs +++ b/src/Fluxzy.Core/Clients/H2/StreamWorker.cs @@ -459,4 +459,4 @@ public override string ToString() return $"Stream Identifier : {StreamIdentifier}"; } } -} +} diff --git a/src/Fluxzy.Core/Clients/H2Logger.cs b/src/Fluxzy.Core/Clients/H2Logger.cs index 613902c49..2291cba07 100644 --- a/src/Fluxzy.Core/Clients/H2Logger.cs +++ b/src/Fluxzy.Core/Clients/H2Logger.cs @@ -1,394 +1,394 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Fluxzy.Clients.H2; -using Fluxzy.Clients.H2.Frames; +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Fluxzy.Clients.H2; +using Fluxzy.Clients.H2.Frames; using Fluxzy.Core; -namespace Fluxzy.Clients -{ - /// - /// Utility for tracing H2 Connection - /// - internal class H2Logger - { - private static readonly string? _directory; - - private static readonly string loggerPath = Environment.ExpandEnvironmentVariables( - Environment.GetEnvironmentVariable("TracingDirectory") - ?? LoggingConstants.DefaultTracingDirectory); - - private readonly bool _active; - - static H2Logger() - { - if (!DebugContext.IsH2TracingEnabled) - return; - - _directory = new DirectoryInfo(Path.Combine(loggerPath, "h2")).FullName; - _directory = Path.Combine(_directory, DebugContext.ReferenceString); - - - var hosts = Environment.GetEnvironmentVariable("EnableH2TracingFilterHosts"); - - if (!string.IsNullOrWhiteSpace(hosts)) { - AuthorizedHosts = - hosts.Split(new[] { ",", ";", " " }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .ToList(); - - return; - } - - Directory.CreateDirectory(_directory); - AuthorizedHosts = null; - } - - public H2Logger(Authority authority, int connectionId, bool? active = null) - { - Authority = authority; - ConnectionId = connectionId; - - active ??= DebugContext.IsH2TracingEnabled; - - _active = active.Value; - - if (_active && AuthorizedHosts != null) - - // Check for domain restriction - { - _active = AuthorizedHosts.Any(c => Authority.HostName.EndsWith( - c, StringComparison.OrdinalIgnoreCase)); - } - } - - public static List? AuthorizedHosts { get; } - - public Authority Authority { get; } - - public int ConnectionId { get; } - - private void WriteLn(int streamIdentifier, string message) - { - if (_directory == null) - return; - - var fullPath = _directory; - var portString = Authority.Port == 443 ? string.Empty : $"-{Authority.Port:00000}"; - - fullPath = Path.Combine(fullPath, - $"{Authority.HostName}{portString}"); - - Directory.CreateDirectory(fullPath); - - fullPath = Path.Combine(fullPath, $"cId={ConnectionId:00000}-sId={streamIdentifier:00000}.txt"); - - lock (string.Intern(fullPath)) { - File.AppendAllText(fullPath, - $"[{ITimingProvider.Default.InstantMillis:000000000}] {message}\r\n"); - } - } - - private void WriteLnHPack(int streamIdentifier, string message) - { - var fullPath = _directory!; - var portString = Authority.Port == 443 ? string.Empty : $"-{Authority.Port:00000}"; - - fullPath = Path.Combine(fullPath, - $"{Authority.HostName}{portString}"); - - Directory.CreateDirectory(fullPath); - - fullPath = Path.Combine(fullPath, $"cId={ConnectionId:00000}-hpack.txt"); - - lock (string.Intern(fullPath)) { - File.AppendAllText(fullPath, - $"[{ITimingProvider.Default.InstantMillis:000000000}] ({streamIdentifier:00000}) - {message}\r\n"); - } - } - - private static string GetFrameExtraMessage(ref H2FrameReadResult frame) - { - switch (frame.BodyType) { - case H2FrameType.Data: { - var innerFrame = frame.GetDataFrame(); - - return $"Length = {innerFrame.BodyLength}, EndStream = {innerFrame.EndStream}"; - } - - case H2FrameType.Headers: { - var innerFrame = frame.GetHeadersFrame(); - - return - $"Length = {innerFrame.BodyLength}, EndHeaders = {innerFrame.EndHeaders}, EndStream = {innerFrame.EndStream}"; - } - - case H2FrameType.Priority: { - var innerFrame = frame.GetPriorityFrame(); - - return - $"Exclusive = {innerFrame.Exclusive}, StreamDependency = {innerFrame.StreamDependency}, Weight = {innerFrame.Weight}"; - } - - case H2FrameType.RstStream: { - var innerFrame = frame.GetRstStreamFrame(); - - return $"ErrorCode = {innerFrame.ErrorCode}"; - } - - case H2FrameType.Settings: { - var builder = new StringBuilder(); - var index = 0; - - while (frame.TryReadNextSetting(out var innerFrame, ref index)) { - builder.Append( - $"Ack = {innerFrame.Ack}, SettingIdentifier = {innerFrame.SettingIdentifier}, Value = {innerFrame.Value}, "); - } - - return builder.ToString(); - } - - case H2FrameType.PushPromise: { - return ""; - } - - case H2FrameType.Ping: { - return ""; - } - - case H2FrameType.Goaway: { - var innerFrame = frame.GetGoAwayFrame(); - - return $"ErrorCode = {innerFrame.ErrorCode}, LastStreamId = {innerFrame.LastStreamId}"; - } - - case H2FrameType.WindowUpdate: { - var innerFrame = frame.GetWindowUpdateFrame(); - - return $"WindowSizeIncrement = {innerFrame.WindowSizeIncrement}"; - } - - case H2FrameType.Continuation: { - var innerFrame = frame.GetContinuationFrame(); - - return $"Length = {innerFrame.BodyLength}, EndHeaders = {innerFrame.EndHeaders}"; - } - - default: - return ""; - } - } - - public void IncomingFrame(ref H2FrameReadResult frame) - { - if (!_active) - return; - - var message = - "RCV <== " + - $"Type = {frame.BodyType}, " + - $"Flags = {frame.Flags}, "; - - message += GetFrameExtraMessage(ref frame); - - WriteLn(frame.StreamIdentifier, message); - } - - public void OutgoingFrame(ref H2FrameReadResult frame) - { - if (!_active) - return; - - var message = - "SNT ==> " + - $"Type = {frame.BodyType}, " + - $"Flags = {frame.Flags}, "; - - message += GetFrameExtraMessage(ref frame); - - WriteLn(frame.StreamIdentifier, message); - } - - public void OutgoingFrame(ReadOnlyMemory buffer) - { - if (!_active) - return; - - var frame = H2FrameReader.ReadFrame(ref buffer); - - OutgoingFrame(ref frame); - } - - public void OutgoingWindowUpdate(int value, int streamIdentifier) - { - if (!_active) - return; - - var message = - "SNT ==> " + - $"Type = {H2FrameType.WindowUpdate}, "; - - message += $"WindowSizeIncrement = {value}"; - ; - - WriteLn(streamIdentifier, message); - } - - public void Trace(int streamId, string message) - { - if (!_active) - return; - - WriteLn(streamId, message); - } - - public void TraceH(int streamId, Func message) - { - if (!_active) - return; - - WriteLnHPack(streamId, message()); - } - - public void Trace(int streamId, Func messageString) - { - if (!_active) - return; - - WriteLn(streamId, messageString()); - } - - public void TraceDeep(int streamId, Func messageString) - { - if (!_active || true) - return; - - WriteLn(streamId, messageString()); - } - - public void TraceDeep(int streamId, string messageString) - { - if (!_active) - return; - - WriteLn(streamId, messageString); - } - - public void Trace(Exchange exchange, string preMessage, Exception? ex = null, int streamIdentifier = 0) - { - if (!_active) - return; - - Trace(exchange, streamIdentifier, preMessage + (ex == null ? string.Empty : ex.ToString())); - } - - public void Trace( - StreamWorker streamWorker, - Exchange exchange, - string preMessage) - { - if (!_active) - return; - - Trace(exchange, streamWorker.StreamIdentifier, preMessage); - } - - public void TraceResponse( - StreamWorker streamWorker, - Exchange exchange) - { - if (!_active) - return; - - var firstLine = exchange.Response.Header?.GetHttp11Header().ToString().Split("\r\n").First(); - - Trace(exchange, streamWorker.StreamIdentifier, "Response : " + firstLine); - } - - public void IncomingSetting(ref SettingFrame settingFrame) - { - if (!_active) - return; - - var message = - "RCV <== "; - - message += - $"Ack = {settingFrame.Ack}, SettingIdentifier = {settingFrame.SettingIdentifier}, Value = {settingFrame.Value}"; - - WriteLn(0, message); - } - - public void OutgoingSetting(ref SettingFrame settingFrame) - { - if (!_active) - return; - - var message = - "SNT ==> "; - - message += - $"Ack = {settingFrame.Ack}, SettingIdentifier = {settingFrame.SettingIdentifier}, Value = {settingFrame.Value}"; - - WriteLn(0, message); - } - - public void Trace( - Exchange exchange, - int streamId, - Func sendMessage) - { - if (!_active) - return; - - Trace(exchange, streamId, sendMessage()); - } - - public void Trace( - Exchange exchange, - int streamId, - string preMessage) - { - if (!_active) - return; - - var method = exchange.Request.Header[":method".AsMemory()].First().Value.ToString(); - var path = exchange.Request.Header[":path".AsMemory()].First().Value.ToString(); - - var maxLength = 30; - - if (path.Length > maxLength) - path = "..." + path.Substring(path.Length - (maxLength - 3), maxLength - 3); - - var message = - $"{method.PadRight(6, ' ')} - " + - $"({path}) - " + - $"Sid = {streamId} " + - $" - {preMessage}"; - - WriteLn(streamId, message); - } - - public void Trace( - WindowSizeHolder holder, - int windowSizeIncrement) - { - if (!_active) - return; - - var message = - "Window Update - " + - $"Before = {holder.WindowSize} - " + - $"Value = {windowSizeIncrement} - " + - $"After = {holder.WindowSize + windowSizeIncrement} - "; - - //$"Sid = {holder.StreamIdentifier} " + - - WriteLn(holder.StreamIdentifier, message); - } - } -} +namespace Fluxzy.Clients +{ + /// + /// Utility for tracing H2 Connection + /// + internal class H2Logger + { + private static readonly string? _directory; + + private static readonly string loggerPath = Environment.ExpandEnvironmentVariables( + Environment.GetEnvironmentVariable("TracingDirectory") + ?? LoggingConstants.DefaultTracingDirectory); + + private readonly bool _active; + + static H2Logger() + { + if (!DebugContext.IsH2TracingEnabled) + return; + + _directory = new DirectoryInfo(Path.Combine(loggerPath, "h2")).FullName; + _directory = Path.Combine(_directory, DebugContext.ReferenceString); + + + var hosts = Environment.GetEnvironmentVariable("EnableH2TracingFilterHosts"); + + if (!string.IsNullOrWhiteSpace(hosts)) { + AuthorizedHosts = + hosts.Split(new[] { ",", ";", " " }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .ToList(); + + return; + } + + Directory.CreateDirectory(_directory); + AuthorizedHosts = null; + } + + public H2Logger(Authority authority, int connectionId, bool? active = null) + { + Authority = authority; + ConnectionId = connectionId; + + active ??= DebugContext.IsH2TracingEnabled; + + _active = active.Value; + + if (_active && AuthorizedHosts != null) + + // Check for domain restriction + { + _active = AuthorizedHosts.Any(c => Authority.HostName.EndsWith( + c, StringComparison.OrdinalIgnoreCase)); + } + } + + public static List? AuthorizedHosts { get; } + + public Authority Authority { get; } + + public int ConnectionId { get; } + + private void WriteLn(int streamIdentifier, string message) + { + if (_directory == null) + return; + + var fullPath = _directory; + var portString = Authority.Port == 443 ? string.Empty : $"-{Authority.Port:00000}"; + + fullPath = Path.Combine(fullPath, + $"{Authority.HostName}{portString}"); + + Directory.CreateDirectory(fullPath); + + fullPath = Path.Combine(fullPath, $"cId={ConnectionId:00000}-sId={streamIdentifier:00000}.txt"); + + lock (string.Intern(fullPath)) { + File.AppendAllText(fullPath, + $"[{ITimingProvider.Default.InstantMillis:000000000}] {message}\r\n"); + } + } + + private void WriteLnHPack(int streamIdentifier, string message) + { + var fullPath = _directory!; + var portString = Authority.Port == 443 ? string.Empty : $"-{Authority.Port:00000}"; + + fullPath = Path.Combine(fullPath, + $"{Authority.HostName}{portString}"); + + Directory.CreateDirectory(fullPath); + + fullPath = Path.Combine(fullPath, $"cId={ConnectionId:00000}-hpack.txt"); + + lock (string.Intern(fullPath)) { + File.AppendAllText(fullPath, + $"[{ITimingProvider.Default.InstantMillis:000000000}] ({streamIdentifier:00000}) - {message}\r\n"); + } + } + + private static string GetFrameExtraMessage(ref H2FrameReadResult frame) + { + switch (frame.BodyType) { + case H2FrameType.Data: { + var innerFrame = frame.GetDataFrame(); + + return $"Length = {innerFrame.BodyLength}, EndStream = {innerFrame.EndStream}"; + } + + case H2FrameType.Headers: { + var innerFrame = frame.GetHeadersFrame(); + + return + $"Length = {innerFrame.BodyLength}, EndHeaders = {innerFrame.EndHeaders}, EndStream = {innerFrame.EndStream}"; + } + + case H2FrameType.Priority: { + var innerFrame = frame.GetPriorityFrame(); + + return + $"Exclusive = {innerFrame.Exclusive}, StreamDependency = {innerFrame.StreamDependency}, Weight = {innerFrame.Weight}"; + } + + case H2FrameType.RstStream: { + var innerFrame = frame.GetRstStreamFrame(); + + return $"ErrorCode = {innerFrame.ErrorCode}"; + } + + case H2FrameType.Settings: { + var builder = new StringBuilder(); + var index = 0; + + while (frame.TryReadNextSetting(out var innerFrame, ref index)) { + builder.Append( + $"Ack = {innerFrame.Ack}, SettingIdentifier = {innerFrame.SettingIdentifier}, Value = {innerFrame.Value}, "); + } + + return builder.ToString(); + } + + case H2FrameType.PushPromise: { + return ""; + } + + case H2FrameType.Ping: { + return ""; + } + + case H2FrameType.Goaway: { + var innerFrame = frame.GetGoAwayFrame(); + + return $"ErrorCode = {innerFrame.ErrorCode}, LastStreamId = {innerFrame.LastStreamId}"; + } + + case H2FrameType.WindowUpdate: { + var innerFrame = frame.GetWindowUpdateFrame(); + + return $"WindowSizeIncrement = {innerFrame.WindowSizeIncrement}"; + } + + case H2FrameType.Continuation: { + var innerFrame = frame.GetContinuationFrame(); + + return $"Length = {innerFrame.BodyLength}, EndHeaders = {innerFrame.EndHeaders}"; + } + + default: + return ""; + } + } + + public void IncomingFrame(ref H2FrameReadResult frame) + { + if (!_active) + return; + + var message = + "RCV <== " + + $"Type = {frame.BodyType}, " + + $"Flags = {frame.Flags}, "; + + message += GetFrameExtraMessage(ref frame); + + WriteLn(frame.StreamIdentifier, message); + } + + public void OutgoingFrame(ref H2FrameReadResult frame) + { + if (!_active) + return; + + var message = + "SNT ==> " + + $"Type = {frame.BodyType}, " + + $"Flags = {frame.Flags}, "; + + message += GetFrameExtraMessage(ref frame); + + WriteLn(frame.StreamIdentifier, message); + } + + public void OutgoingFrame(ReadOnlyMemory buffer) + { + if (!_active) + return; + + var frame = H2FrameReader.ReadFrame(ref buffer); + + OutgoingFrame(ref frame); + } + + public void OutgoingWindowUpdate(int value, int streamIdentifier) + { + if (!_active) + return; + + var message = + "SNT ==> " + + $"Type = {H2FrameType.WindowUpdate}, "; + + message += $"WindowSizeIncrement = {value}"; + ; + + WriteLn(streamIdentifier, message); + } + + public void Trace(int streamId, string message) + { + if (!_active) + return; + + WriteLn(streamId, message); + } + + public void TraceH(int streamId, Func message) + { + if (!_active) + return; + + WriteLnHPack(streamId, message()); + } + + public void Trace(int streamId, Func messageString) + { + if (!_active) + return; + + WriteLn(streamId, messageString()); + } + + public void TraceDeep(int streamId, Func messageString) + { + if (!_active || true) + return; + + WriteLn(streamId, messageString()); + } + + public void TraceDeep(int streamId, string messageString) + { + if (!_active) + return; + + WriteLn(streamId, messageString); + } + + public void Trace(Exchange exchange, string preMessage, Exception? ex = null, int streamIdentifier = 0) + { + if (!_active) + return; + + Trace(exchange, streamIdentifier, preMessage + (ex == null ? string.Empty : ex.ToString())); + } + + public void Trace( + StreamWorker streamWorker, + Exchange exchange, + string preMessage) + { + if (!_active) + return; + + Trace(exchange, streamWorker.StreamIdentifier, preMessage); + } + + public void TraceResponse( + StreamWorker streamWorker, + Exchange exchange) + { + if (!_active) + return; + + var firstLine = exchange.Response.Header?.GetHttp11Header().ToString().Split("\r\n").First(); + + Trace(exchange, streamWorker.StreamIdentifier, "Response : " + firstLine); + } + + public void IncomingSetting(ref SettingFrame settingFrame) + { + if (!_active) + return; + + var message = + "RCV <== "; + + message += + $"Ack = {settingFrame.Ack}, SettingIdentifier = {settingFrame.SettingIdentifier}, Value = {settingFrame.Value}"; + + WriteLn(0, message); + } + + public void OutgoingSetting(ref SettingFrame settingFrame) + { + if (!_active) + return; + + var message = + "SNT ==> "; + + message += + $"Ack = {settingFrame.Ack}, SettingIdentifier = {settingFrame.SettingIdentifier}, Value = {settingFrame.Value}"; + + WriteLn(0, message); + } + + public void Trace( + Exchange exchange, + int streamId, + Func sendMessage) + { + if (!_active) + return; + + Trace(exchange, streamId, sendMessage()); + } + + public void Trace( + Exchange exchange, + int streamId, + string preMessage) + { + if (!_active) + return; + + var method = exchange.Request.Header[":method".AsMemory()].First().Value.ToString(); + var path = exchange.Request.Header[":path".AsMemory()].First().Value.ToString(); + + var maxLength = 30; + + if (path.Length > maxLength) + path = "..." + path.Substring(path.Length - (maxLength - 3), maxLength - 3); + + var message = + $"{method.PadRight(6, ' ')} - " + + $"({path}) - " + + $"Sid = {streamId} " + + $" - {preMessage}"; + + WriteLn(streamId, message); + } + + public void Trace( + WindowSizeHolder holder, + int windowSizeIncrement) + { + if (!_active) + return; + + var message = + "Window Update - " + + $"Before = {holder.WindowSize} - " + + $"Value = {windowSizeIncrement} - " + + $"After = {holder.WindowSize + windowSizeIncrement} - "; + + //$"Sid = {holder.StreamIdentifier} " + + + WriteLn(holder.StreamIdentifier, message); + } + } +} diff --git a/src/Fluxzy.Core/Clients/Headers/HeaderAlterationAdd.cs b/src/Fluxzy.Core/Clients/Headers/HeaderAlterationAdd.cs index fd5e42750..8b171f7c4 100644 --- a/src/Fluxzy.Core/Clients/Headers/HeaderAlterationAdd.cs +++ b/src/Fluxzy.Core/Clients/Headers/HeaderAlterationAdd.cs @@ -1,24 +1,24 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + using Fluxzy.Core; -namespace Fluxzy.Clients.Headers -{ - public class HeaderAlterationAdd : HeaderAlteration - { - public HeaderAlterationAdd(string headerName, string headerValue) - { - HeaderName = headerName; - HeaderValue = headerValue; - } - - public string HeaderName { get; } - - public string HeaderValue { get; } - - public override void Apply(Header header) - { - header.AltAddHeader(HeaderName, HeaderValue); - } - } -} +namespace Fluxzy.Clients.Headers +{ + public class HeaderAlterationAdd : HeaderAlteration + { + public HeaderAlterationAdd(string headerName, string headerValue) + { + HeaderName = headerName; + HeaderValue = headerValue; + } + + public string HeaderName { get; } + + public string HeaderValue { get; } + + public override void Apply(Header header) + { + header.AltAddHeader(HeaderName, HeaderValue); + } + } +} diff --git a/src/Fluxzy.Core/Clients/Headers/HeaderAlterationReplace.cs b/src/Fluxzy.Core/Clients/Headers/HeaderAlterationReplace.cs index 246288b8d..7ef2bf01a 100644 --- a/src/Fluxzy.Core/Clients/Headers/HeaderAlterationReplace.cs +++ b/src/Fluxzy.Core/Clients/Headers/HeaderAlterationReplace.cs @@ -1,29 +1,29 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + using Fluxzy.Core; -namespace Fluxzy.Clients.Headers -{ - public class HeaderAlterationReplace : HeaderAlteration - { - public HeaderAlterationReplace(string headerName, string headerValue, bool addIfMissing) - { - HeaderName = headerName; - HeaderValue = headerValue; - AddIfMissing = addIfMissing; - } - - public string HeaderName { get; } - - public string HeaderValue { get; } - - public bool AddIfMissing { get; } - - public string ? AppendSeparator { get; set; } - - public override void Apply(Header header) - { - header.AltReplaceHeaders(HeaderName, HeaderValue, AddIfMissing, AppendSeparator); - } - } -} +namespace Fluxzy.Clients.Headers +{ + public class HeaderAlterationReplace : HeaderAlteration + { + public HeaderAlterationReplace(string headerName, string headerValue, bool addIfMissing) + { + HeaderName = headerName; + HeaderValue = headerValue; + AddIfMissing = addIfMissing; + } + + public string HeaderName { get; } + + public string HeaderValue { get; } + + public bool AddIfMissing { get; } + + public string ? AppendSeparator { get; set; } + + public override void Apply(Header header) + { + header.AltReplaceHeaders(HeaderName, HeaderValue, AddIfMissing, AppendSeparator); + } + } +} diff --git a/src/Fluxzy.Core/Clients/IRemoteConnectionBuilder.cs b/src/Fluxzy.Core/Clients/IRemoteConnectionBuilder.cs index 7177486df..35f326be6 100644 --- a/src/Fluxzy.Core/Clients/IRemoteConnectionBuilder.cs +++ b/src/Fluxzy.Core/Clients/IRemoteConnectionBuilder.cs @@ -1,152 +1,153 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Threading; -using System.Threading.Tasks; -using Fluxzy.Clients.Headers; -using Fluxzy.Clients.Ssl; +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Threading; +using System.Threading.Tasks; +using Fluxzy.Clients.Headers; +using Fluxzy.Clients.Ssl; using Fluxzy.Core; -using Fluxzy.Misc.Streams; -using Fluxzy.Rules; - -namespace Fluxzy.Clients -{ - internal enum RemoteConnectionResultType : byte - { - Unknown = 0, - Http11, - Http2 - } - - internal readonly struct RemoteConnectionResult - { - public RemoteConnectionResult(RemoteConnectionResultType type, Connection connection) - { - Type = type; - Connection = connection; - } - - public RemoteConnectionResultType Type { get; } - - public Connection Connection { get; } - } - - internal class RemoteConnectionBuilder - { - private readonly ISslConnectionBuilder _sslConnectionBuilder; - private readonly ITimingProvider _timeProvider; - - public RemoteConnectionBuilder( - ITimingProvider timeProvider, ISslConnectionBuilder sslConnectionBuilder) - { - _timeProvider = timeProvider; - _sslConnectionBuilder = sslConnectionBuilder; - } - - public async ValueTask OpenConnectionToRemote( - Exchange exchange, DnsResolutionResult resolutionResult, - List httpProtocols, - ProxyRuntimeSetting setting, - ProxyConfiguration? proxyConfiguration, - CancellationToken token) - { - exchange.Connection = new Connection(exchange.Authority, setting.IdProvider) { - TcpConnectionOpening = _timeProvider.Instant(), - - // tcpClient.LingerState. - DnsSolveStart = resolutionResult.DnsSolveStart, - DnsSolveEnd = resolutionResult.DnsSolveEnd - }; - - exchange.Connection.RemoteAddress = resolutionResult.EndPoint.Address; - - var tcpConnection = setting.TcpConnectionProvider - .Create( - setting.ArchiveWriter != null! - ? setting.ArchiveWriter.GetDumpfilePath(exchange.Connection.Id)! - : string.Empty); - - - var localEndpoint = await tcpConnection.ConnectAsync( - resolutionResult.EndPoint.Address, - resolutionResult.EndPoint.Port).ConfigureAwait(false); - - exchange.Connection.TcpConnectionOpened = _timeProvider.Instant(); - exchange.Connection.LocalPort = localEndpoint.Port; - exchange.Connection.LocalAddress = localEndpoint.Address.ToString(); - - var newlyOpenedStream = tcpConnection.GetStream(); - - if (proxyConfiguration != null) { - exchange.Connection.ProxyConnectStart = _timeProvider.Instant(); - - if (exchange.Authority.Secure) { - // Simulate CONNECT only when the connection is secure - - var connectConfiguration = new ConnectConfiguration(exchange.Authority.HostName, - exchange.Authority.Port, proxyConfiguration.ProxyAuthorizationHeader); - - var proxyOpenResult = - await UpstreamProxyManager.Connect(connectConfiguration, newlyOpenedStream, newlyOpenedStream); - - if (proxyOpenResult != UpstreamProxyConnectResult.Ok) - throw new InvalidOperationException($"Failed to connect to upstream proxy {proxyOpenResult}"); - } - - exchange.Connection.ProxyConnectEnd = _timeProvider.Instant(); - } - - if (!exchange.Authority.Secure || exchange.Context.BlindMode) { - exchange.Connection.ReadStream = exchange.Connection.WriteStream = newlyOpenedStream; - - return new RemoteConnectionResult(RemoteConnectionResultType.Unknown, exchange.Connection); - } - - exchange.Connection.SslNegotiationStart = _timeProvider.Instant(); - - byte[]? remoteCertificate = null; - - var builderOptions = new SslConnectionBuilderOptions( - exchange.Authority.HostName, - exchange.Context.ProxyTlsProtocols, - httpProtocols, - exchange.Context.SkipRemoteCertificateValidation - ? (_, _, _, errors) => true - : null, - exchange.Context.ClientCertificates != null && exchange.Context.ClientCertificates.Any() ? - exchange.Context.ClientCertificates.First() : null); - - var sslConnectionInfo = - await _sslConnectionBuilder.AuthenticateAsClient( - newlyOpenedStream, builderOptions, tcpConnection.OnKeyReceived, token).ConfigureAwait(false); - - exchange.Connection.SslInfo = sslConnectionInfo.SslInfo; - - exchange.Connection.SslNegotiationEnd = _timeProvider.Instant(); - exchange.Connection.SslInfo.RemoteCertificate = remoteCertificate; - - exchange.Context.UnderlyingBcStream = sslConnectionInfo.UnderlyingBcStream; - exchange.Context.EventNotifierStream = sslConnectionInfo.EventNotifierStream; - - var resultStream = sslConnectionInfo.Stream; - - if (DebugContext.EnableNetworkFileDump) { - resultStream = new DebugFileStream($"raw/{exchange.Connection.Id:000000}_remotehost_", - resultStream); - } - - var protoType = sslConnectionInfo.ApplicationProtocol == SslApplicationProtocol.Http2 - ? RemoteConnectionResultType.Http2 - : RemoteConnectionResultType.Http11; - - exchange.Connection.ReadStream = exchange.Connection.WriteStream = resultStream; - - - return new RemoteConnectionResult(protoType, exchange.Connection); - } - } -} +using Fluxzy.Misc.Streams; +using Fluxzy.Rules; + +namespace Fluxzy.Clients +{ + internal enum RemoteConnectionResultType : byte + { + Unknown = 0, + Http11, + Http2 + } + + internal readonly struct RemoteConnectionResult + { + public RemoteConnectionResult(RemoteConnectionResultType type, Connection connection) + { + Type = type; + Connection = connection; + } + + public RemoteConnectionResultType Type { get; } + + public Connection Connection { get; } + } + + internal class RemoteConnectionBuilder + { + private readonly ISslConnectionBuilder _sslConnectionBuilder; + private readonly ITimingProvider _timeProvider; + + public RemoteConnectionBuilder( + ITimingProvider timeProvider, ISslConnectionBuilder sslConnectionBuilder) + { + _timeProvider = timeProvider; + _sslConnectionBuilder = sslConnectionBuilder; + } + + public async ValueTask OpenConnectionToRemote( + Exchange exchange, DnsResolutionResult resolutionResult, + List httpProtocols, + ProxyRuntimeSetting setting, + ProxyConfiguration? proxyConfiguration, + CancellationToken token) + { + exchange.Connection = new Connection(exchange.Authority, setting.IdProvider) { + TcpConnectionOpening = _timeProvider.Instant(), + + // tcpClient.LingerState. + DnsSolveStart = resolutionResult.DnsSolveStart, + DnsSolveEnd = resolutionResult.DnsSolveEnd + }; + + exchange.Connection.RemoteAddress = resolutionResult.EndPoint.Address; + + var tcpConnection = setting.TcpConnectionProvider + .Create( + setting.ArchiveWriter != null! + ? setting.ArchiveWriter.GetDumpfilePath(exchange.Connection.Id)! + : string.Empty); + + + var localEndpoint = await tcpConnection.ConnectAsync( + resolutionResult.EndPoint.Address, + resolutionResult.EndPoint.Port).ConfigureAwait(false); + + exchange.Connection.TcpConnectionOpened = _timeProvider.Instant(); + exchange.Connection.LocalPort = localEndpoint.Port; + exchange.Connection.LocalAddress = localEndpoint.Address.ToString(); + + var newlyOpenedStream = tcpConnection.GetStream(); + + if (proxyConfiguration != null) { + exchange.Connection.ProxyConnectStart = _timeProvider.Instant(); + + if (exchange.Authority.Secure) { + // Simulate CONNECT only when the connection is secure + + var connectConfiguration = new ConnectConfiguration(exchange.Authority.HostName, + exchange.Authority.Port, proxyConfiguration.ProxyAuthorizationHeader); + + var proxyOpenResult = + await UpstreamProxyManager.Connect(connectConfiguration, newlyOpenedStream, newlyOpenedStream); + + if (proxyOpenResult != UpstreamProxyConnectResult.Ok) + throw new InvalidOperationException($"Failed to connect to upstream proxy {proxyOpenResult}"); + } + + exchange.Connection.ProxyConnectEnd = _timeProvider.Instant(); + } + + if (!exchange.Authority.Secure || exchange.Context.BlindMode) { + exchange.Connection.ReadStream = exchange.Connection.WriteStream = newlyOpenedStream; + + return new RemoteConnectionResult(RemoteConnectionResultType.Unknown, exchange.Connection); + } + + exchange.Connection.SslNegotiationStart = _timeProvider.Instant(); + + byte[]? remoteCertificate = null; + + var builderOptions = new SslConnectionBuilderOptions( + exchange.Authority.HostName, + exchange.Context.ProxyTlsProtocols, + httpProtocols, + exchange.Context.SkipRemoteCertificateValidation + ? (_, _, _, errors) => true + : null, + exchange.Context.ClientCertificates != null && exchange.Context.ClientCertificates.Any() ? + exchange.Context.ClientCertificates.First() : null, + exchange.Context.AdvancedTlsSettings); + + var sslConnectionInfo = + await _sslConnectionBuilder.AuthenticateAsClient( + newlyOpenedStream, builderOptions, tcpConnection.OnKeyReceived, token).ConfigureAwait(false); + + exchange.Connection.SslInfo = sslConnectionInfo.SslInfo; + + exchange.Connection.SslNegotiationEnd = _timeProvider.Instant(); + exchange.Connection.SslInfo.RemoteCertificate = remoteCertificate; + + exchange.Context.UnderlyingBcStream = sslConnectionInfo.UnderlyingBcStream; + exchange.Context.EventNotifierStream = sslConnectionInfo.EventNotifierStream; + + var resultStream = sslConnectionInfo.Stream; + + if (DebugContext.EnableNetworkFileDump) { + resultStream = new DebugFileStream($"raw/{exchange.Connection.Id:000000}_remotehost_", + resultStream); + } + + var protoType = sslConnectionInfo.ApplicationProtocol == SslApplicationProtocol.Http2 + ? RemoteConnectionResultType.Http2 + : RemoteConnectionResultType.Http11; + + exchange.Connection.ReadStream = exchange.Connection.WriteStream = resultStream; + + + return new RemoteConnectionResult(protoType, exchange.Connection); + } + } +} diff --git a/src/Fluxzy.Core/Clients/PoolBuilder.cs b/src/Fluxzy.Core/Clients/PoolBuilder.cs index 38ff85872..214028bf5 100644 --- a/src/Fluxzy.Core/Clients/PoolBuilder.cs +++ b/src/Fluxzy.Core/Clients/PoolBuilder.cs @@ -23,8 +23,8 @@ namespace Fluxzy.Clients internal class PoolBuilder : IDisposable { private static readonly List AllProtocols = new() { - SslApplicationProtocol.Http11, - SslApplicationProtocol.Http2 + SslApplicationProtocol.Http2, + SslApplicationProtocol.Http11 }; static PoolBuilder() @@ -252,7 +252,7 @@ public async ValueTask exchange.HttpVersion = exchange.Connection!.HttpVersion = "HTTP/2"; - if (_archiveWriter != null) + if (_archiveWriter != null!) _archiveWriter.Update(openingResult.Connection, cancellationToken); lock (_connectionPools) { diff --git a/src/Fluxzy.Core/Clients/Ssl/AdvancedTlsSettings.cs b/src/Fluxzy.Core/Clients/Ssl/AdvancedTlsSettings.cs new file mode 100644 index 000000000..f229860f4 --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/AdvancedTlsSettings.cs @@ -0,0 +1,19 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using Fluxzy.Clients.H2; + +namespace Fluxzy.Clients.Ssl +{ + public class AdvancedTlsSettings + { + /// + /// TLS fingerprint settings + /// + public TlsFingerPrint? TlsFingerPrint { get; set; } + + /// + /// H2 stream settings + /// + public H2StreamSetting ? H2StreamSetting { get; set; } + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/BouncyCastleConnectionBuilder.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/BouncyCastleConnectionBuilder.cs index 556b80a67..96715c53b 100644 --- a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/BouncyCastleConnectionBuilder.cs +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/BouncyCastleConnectionBuilder.cs @@ -26,10 +26,10 @@ public async Task AuthenticateAsClient( var tlsAuthentication = new FluxzyTlsAuthentication(crypto, builderOptions.GetBouncyCastleClientCertificateInfo()); - var client = new FluxzyTlsClient( - builderOptions.TargetHost!, - builderOptions.EnabledSslProtocols, - builderOptions.ApplicationProtocols!.ToArray(), tlsAuthentication, crypto); + + var fingerPrintEnforcer = new FingerPrintTlsExtensionsEnforcer(); + + var client = new FluxzyTlsClient(builderOptions, tlsAuthentication, crypto, fingerPrintEnforcer); var memoryStream = new MemoryStream(); @@ -40,7 +40,6 @@ public async Task AuthenticateAsClient( if (Environment.GetEnvironmentVariable("SSLKEYLOGFILE") is { } str) { nssWriter.KeyHandler = nss => { onKeyReceived(nss); - lock (SslFileLocker) { try { @@ -53,7 +52,7 @@ public async Task AuthenticateAsClient( }; } - var protocol = new FluxzyClientProtocol(innerStream, nssWriter); + var protocol = new FluxzyClientProtocol(innerStream, fingerPrintEnforcer, nssWriter); try { diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ConstBuffers.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ConstBuffers.cs new file mode 100644 index 000000000..0ed58d51a --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ConstBuffers.cs @@ -0,0 +1,51 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.IO; +using Org.BouncyCastle.Tls; + +namespace Fluxzy.Clients.Ssl.BouncyCastle +{ + internal static class ConstBuffers + { + public static int ExtensionTypeAlps { get; } = 0x4469; + + public static byte[] EmptyOctet { get; } = new byte[1]; + + public static byte[] ClientExtensionsDefaultCompressCertificate { get; } = + TlsExtensionsUtilities.CreateCompressCertificateExtension(new[] { + 2 + }); + + public static byte[] ClientExtensionsPskKeyExchangeModes { get; } = + TlsExtensionsUtilities.CreatePskKeyExchangeModesExtension(new short[] { 1 }); + + public static byte[] ClientExtensionsDefaultMaxSizeRecordLimit { get; } = + BinaryUtilities.GetBytesBigEndian(16884); + + public static byte[] ClientExtensionsDummyPadding { get; } = TlsExtensionsUtilities.CreatePaddingExtension(6); + + public static byte[] Http2ApplicationProtocol { get; } = + new byte[] { 0, 0x3, 0x02, 0x68, 0x32 }; + + public static byte[] EncryptedExtensionAlpsH2 { get; } + + static ConstBuffers() + { + EncryptedExtensionAlpsH2 = CreateEncryptedExtensionAlpsH2Ok(); + } + + private static byte[] CreateEncryptedExtensionAlpsH2Ok() + { + using var memoryStream = new MemoryStream(); + TlsProtocol.WriteExtensions(memoryStream, new Dictionary() + { + [17513] = Array.Empty() + }); + var data = memoryStream.ToArray(); + + return data; + } + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FingerPrintTlsExtensionsEnforcer.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FingerPrintTlsExtensionsEnforcer.cs new file mode 100644 index 000000000..cefa0994f --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FingerPrintTlsExtensionsEnforcer.cs @@ -0,0 +1,205 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Org.BouncyCastle.Tls; + +namespace Fluxzy.Clients.Ssl.BouncyCastle +{ + public class FingerPrintTlsExtensionsEnforcer + { + internal static readonly HashSet UnsupportedClientExtensions = new() { 34 }; + + internal static readonly HashSet GreaseClientExtensionsValues = new() { + 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354, + 35466, 39578, 43690, 47802, 51914, 56026, 60138, 64250 + }; + + internal int greaseCount = 0; + + public IDictionary PrepareExtensions(IDictionary current, + TlsFingerPrint fingerPrint, string targetHost, ProtocolVersion[] protocolVersions) + { + var sorted = current; + + var clientExtensionTypes = fingerPrint.EffectiveClientExtensions; + + var missing = sorted.Where(c => !clientExtensionTypes.Contains(c.Key)) + .Select(s => s.Key).ToList(); + + // Remove + foreach (var type in missing) + { + sorted.Remove(type); + } + + // Add or replace + + foreach (var type in clientExtensionTypes) + { + if (sorted.ContainsKey(type)) + { + continue; // No need to replace + } + + var extensionData = GetDefaultClientValueExtension(type, targetHost, protocolVersions); + + if (extensionData == null) + continue; + + sorted.Add(type, extensionData); + } + + return sorted; + } + + internal byte[]? GetDefaultClientValueExtension( + int type, + string targetHost, ProtocolVersion[] protocolVersions) + { + if (type == 0) + return ServerNameUtilities.CreateFromHost(targetHost); + + if (type == ExtensionType.renegotiation_info) + return ConstBuffers.EmptyOctet; + + if (type == ExtensionType.signed_certificate_timestamp) + return Array.Empty(); + + if (GreaseClientExtensionsValues.Contains(type)) { + if (greaseCount++ >= 1) { + return ConstBuffers.EmptyOctet; + } + return Array.Empty(); + } + + if (type == ExtensionType.extended_master_secret) + return Array.Empty(); + + if (type == ExtensionType.compress_certificate) + return ConstBuffers.ClientExtensionsDefaultCompressCertificate; + + if (type == ExtensionType.session_ticket) + return Array.Empty(); + + if (type == ExtensionType.record_size_limit) + return ConstBuffers.ClientExtensionsDefaultMaxSizeRecordLimit; + + if (type == ExtensionType.padding) + return ConstBuffers.ClientExtensionsDummyPadding; + + if (type == ExtensionType.psk_key_exchange_modes) + return ConstBuffers.ClientExtensionsPskKeyExchangeModes; + + if (type == ExtensionType.supported_versions) + return TlsExtensionsUtilities.CreateSupportedVersionsExtensionClient(protocolVersions); + + if (type == ConstBuffers.ExtensionTypeAlps) // APPLICATION PROTOCOLS 17513 --> https://chromestatus.com/feature/5149147365900288 + return ConstBuffers.Http2ApplicationProtocol; + + if (type == 51) // For TLS 1.2, key_share is not supported but some client may send it with an empty value + return Array.Empty(); + + if (type == 34) // For TLS 1.2, key_share is not supported but some client may send it with an empty value + return new byte[] { 0, 8, 04, 03, 05, 03, 06, 03, 02, 03 }; + + if (type == ExtensionType.encrypted_client_hello) + return GreaseEchExtension.GetGreaseEncryptedClientHello(); + + if (UnsupportedClientExtensions.Contains(type)) + throw new InvalidOperationException($"Unsupported TLS client extension {type}"); + + return null; + } + + public IEnumerable<(short HandshakeType, byte[] Data)> GetAdditionalExtensions( + IDictionary serverExtensions) + { + if (serverExtensions.ContainsKey(ConstBuffers.ExtensionTypeAlps)) + { + yield return (HandshakeType.encrypted_extensions, + ConstBuffers.EncryptedExtensionAlpsH2); + } + } + } + + internal static class BinaryUtilities + { + public static byte[] GetBytesBigEndian(short value) + { + byte [] buffer = new byte[sizeof(short)]; + BinaryPrimitives.WriteInt16BigEndian(buffer, value); + + return buffer; + } + } + + internal static class GreaseEchExtension + { + private static byte[] EncodedData = new byte[32]; + private static byte[] EncodedPayload = new byte[208]; + + static GreaseEchExtension() + { + var random = new Random(9); // Send the same Grease + random.NextBytes(EncodedData); + random.NextBytes(EncodedPayload); + } + + public static byte[] GetGreaseEncryptedClientHello() + { + var clientHello = new EncryptedClientHello { + ClientHelloType = 0, + Encoded = EncodedData, + Payload = EncodedPayload + }; + + var encoded = clientHello.GetBytes(); + + return encoded; + } + } + + internal struct EncryptedClientHello + { + public EncryptedClientHello() + { + Encoded = new byte[] { }; + Payload = new byte[] { }; + } + + public byte ClientHelloType { get; set; } = 0; + + public int CipherSuite { get; set; } = 0x10001; // TLS_AES_128_GCM_SHA256 + + public byte ConfigId { get; set; } = 0x53; + + public byte[] Encoded { get; set; } + + public byte[] Payload { get; set; } + + public byte[] GetBytes() + { + // Marshall to byte array + + var totalSize = 1 + 4 + 1 + 2 + 2 + Encoded.Length + Payload.Length; + var result = new byte[totalSize]; + + using (var memoryStream = new MemoryStream(result, true)) { + + + TlsUtilities.WriteUint8(ClientHelloType, memoryStream); + TlsUtilities.WriteUint32(CipherSuite, memoryStream); + TlsUtilities.WriteUint8(ConfigId, memoryStream); + TlsUtilities.WriteOpaque16(Encoded, memoryStream); + TlsUtilities.WriteOpaque16(Payload, memoryStream); + } + + return result; + } + + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyClientProtocol.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyClientProtocol.cs index 51bd7459e..12187b89f 100644 --- a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyClientProtocol.cs +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyClientProtocol.cs @@ -1,6 +1,8 @@ // Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Security; using System.Security.Authentication; @@ -14,12 +16,14 @@ namespace Fluxzy.Clients.Ssl.BouncyCastle { internal class FluxzyClientProtocol : TlsClientProtocol { + private readonly FingerPrintTlsExtensionsEnforcer _extensionEnforcer; private readonly NssLogWriter _logWriter; private TlsSecret? _localSecret; - public FluxzyClientProtocol(Stream stream, NssLogWriter logWriter) + public FluxzyClientProtocol(Stream stream, FingerPrintTlsExtensionsEnforcer _extensionEnforcer, NssLogWriter logWriter) : base(stream) { + this._extensionEnforcer = _extensionEnforcer; _logWriter = logWriter; } @@ -46,7 +50,7 @@ public SslApplicationProtocol GetApplicationProtocol() return SslApplicationProtocol.Http11; } - + public SslProtocols GetSChannelProtocol() { var version = ProtocolVersion; @@ -85,6 +89,18 @@ protected override void CompleteHandshake() crypto.MasterSecret); } } + + protected override async ValueTask Send13FinishedMessageAsync() + { + var extensions = _extensionEnforcer.GetAdditionalExtensions(m_serverExtensions); + + foreach (var (type, data) in extensions) + { + await HandshakeMessageOutput.SendAsync(this, type, data); + } + + await base.Send13FinishedMessageAsync(); + } protected override void Handle13HandshakeMessage(short type, HandshakeMessageInput buf) { diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyCrypto.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyCrypto.cs index 3fbb15aba..f96381d13 100644 --- a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyCrypto.cs +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyCrypto.cs @@ -17,6 +17,7 @@ public FluxzyCrypto() : base(new SecureRandom()) public byte[]? MasterSecret { get; set; } + public override TlsSecret AdoptSecret(TlsSecret secret) { var resultSecret = base.AdoptSecret(secret); diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyTlsClient.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyTlsClient.cs index 651acc17a..fc3bdd7f8 100644 --- a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyTlsClient.cs +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/FluxzyTlsClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Security; using System.Security.Authentication; +using System.Text; using Org.BouncyCastle.Tls; #pragma warning disable SYSLIB0039 @@ -12,46 +13,121 @@ namespace Fluxzy.Clients.Ssl.BouncyCastle { internal class FluxzyTlsClient : DefaultTlsClient { - private readonly SslApplicationProtocol[] _applicationProtocols; + private static readonly HashSet DefaultEarlyKeyShareNamedGroups = new() { + NamedGroup.X25519MLKEM768, NamedGroup.x25519, + NamedGroup.grease, + NamedGroup.secp256r1, + }; + + + private readonly IReadOnlyCollection_applicationProtocols; private readonly FluxzyCrypto _crypto; + private readonly FingerPrintTlsExtensionsEnforcer _fingerPrintEnforcer; private readonly SslProtocols _sslProtocols; private readonly string _targetHost; private readonly TlsAuthentication _tlsAuthentication; + private readonly TlsFingerPrint? _fingerPrint; + private readonly List _serverNames; + private readonly IList _protocolNames; + - public FluxzyTlsClient( - string targetHost, SslProtocols sslProtocols, - SslApplicationProtocol[] applicationProtocols, TlsAuthentication tlsAuthentication, - FluxzyCrypto crypto) + public FluxzyTlsClient( + SslConnectionBuilderOptions builderOptions, + TlsAuthentication tlsAuthentication, + FluxzyCrypto crypto, + FingerPrintTlsExtensionsEnforcer fingerPrintEnforcer) : base(crypto) { - _targetHost = targetHost; - _sslProtocols = sslProtocols; - _applicationProtocols = applicationProtocols; + _targetHost = builderOptions.TargetHost; + _sslProtocols = builderOptions.EnabledSslProtocols; + _applicationProtocols = builderOptions.ApplicationProtocols; _tlsAuthentication = tlsAuthentication; _crypto = crypto; + _fingerPrintEnforcer = fingerPrintEnforcer; + _fingerPrint = builderOptions.AdvancedTlsSettings?.TlsFingerPrint; + _serverNames = new List() { new ServerName(0, Encoding.UTF8.GetBytes(builderOptions.TargetHost)) + }; + + _protocolNames = InternalGetProtocolNames(); } public override void Init(TlsClientContext context) { base.Init(context); + _crypto.UpdateContext(context); + + if (_fingerPrint != null) + { + m_cipherSuites = _fingerPrint.EffectiveCiphers; + } + + m_protocolVersions = ProtocolVersionHelper.GetProtocolVersions( + _fingerPrint?.ProtocolVersion, + _fingerPrint?.GreaseMode ?? false, _sslProtocols) ?? base.GetProtocolVersions(); + } + + public override IList GetEarlyKeyShareGroups() + { + if (_fingerPrint == null) { + return base.GetEarlyKeyShareGroups(); + } + + if (_fingerPrint.EarlySharedGroups != null) + { + return _fingerPrint.EarlySharedGroups; + } + + return _fingerPrint.EffectiveSupportGroups.Where(r => DefaultEarlyKeyShareNamedGroups.Contains(r)).ToList(); + } + + protected override IList GetSupportedGroups(IList namedGroupRoles) + { + if (_fingerPrint != null) + { + return _fingerPrint.EffectiveSupportGroups; + } + + return base.GetSupportedGroups(namedGroupRoles); + } + + protected override IList GetSniServerNames() + { + return _serverNames; + } + + public override IList GetClientSupplementalData() + { + var entry = new SupplementalDataEntry(0, Encoding.UTF8.GetBytes("Ja3")); + return base.GetClientSupplementalData(); } public override IDictionary GetClientExtensions() { - var extensions = base.GetClientExtensions(); + var baseExtensions = base.GetClientExtensions(); + + if (_fingerPrint != null) + { + var result = _fingerPrintEnforcer.PrepareExtensions(baseExtensions, + _fingerPrint, _targetHost, m_protocolVersions); - extensions.Add(0, ServerNameUtilities.CreateFromHost(_targetHost)); + return result; + } - return extensions; + return baseExtensions; } public override TlsAuthentication GetAuthentication() { return _tlsAuthentication; } - + protected override IList GetProtocolNames() + { + return _protocolNames; + } + + private IList InternalGetProtocolNames() { var result = new List(); @@ -60,11 +136,13 @@ protected override IList GetProtocolNames() } foreach (var applicationProtocol in _applicationProtocols) { - if (applicationProtocol.Protocol.Equals(SslApplicationProtocol.Http11.Protocol)) { + if (applicationProtocol.Protocol.Equals(SslApplicationProtocol.Http11.Protocol)) + { result.Add(ProtocolName.Http_1_1); } - if (applicationProtocol.Protocol.Equals(SslApplicationProtocol.Http2.Protocol)) { + if (applicationProtocol.Protocol.Equals(SslApplicationProtocol.Http2.Protocol)) + { result.Add(ProtocolName.Http_2_Tls); } } @@ -72,35 +150,12 @@ protected override IList GetProtocolNames() return result; } - protected override ProtocolVersion[] GetSupportedVersions() + protected override IList GetSupportedSignatureAlgorithms() { - // map ProtocolVersion with SslProcols - - var listProtocolVersion = new List(); - - if (SslProtocols.None == _sslProtocols) { - return base.GetSupportedVersions(); - } - - if (_sslProtocols.HasFlag(SslProtocols.Tls)) { - listProtocolVersion.Add(ProtocolVersion.TLSv10); - } - - if (_sslProtocols.HasFlag(SslProtocols.Tls11)) { - listProtocolVersion.Add(ProtocolVersion.TLSv11); - } - - if (_sslProtocols.HasFlag(SslProtocols.Tls12)) { - listProtocolVersion.Add(ProtocolVersion.TLSv12); - } - -#if NET6_0_OR_GREATER - if (_sslProtocols.HasFlag(SslProtocols.Tls13)) { - listProtocolVersion.Add(ProtocolVersion.TLSv13); - } -#endif + if (_fingerPrint?.SignatureAndHashAlgorithms == null) + return base.GetSupportedSignatureAlgorithms(); - return listProtocolVersion.ToArray(); + return _fingerPrint.SignatureAndHashAlgorithms; } - } + } } diff --git a/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ProtocolVersionHelper.cs b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ProtocolVersionHelper.cs new file mode 100644 index 000000000..e5fa6b101 --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/BouncyCastle/ProtocolVersionHelper.cs @@ -0,0 +1,162 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; +using Org.BouncyCastle.Tls; + +#pragma warning disable SYSLIB0039 + +namespace Fluxzy.Clients.Ssl.BouncyCastle +{ + internal static class ProtocolVersionHelper + { + private static readonly ProtocolVersion[] SupportedVersions = new ProtocolVersion[] + { + ProtocolVersion.TLSv10, + ProtocolVersion.TLSv11, + ProtocolVersion.TLSv12, + ProtocolVersion.TLSv13 + }; + + private static readonly Dictionary CachedValues = new Dictionary(); + + static ProtocolVersionHelper() + { + int?[] plainVersions = new int?[] { + ProtocolVersion.TLSv10.FullVersion, + ProtocolVersion.TLSv11.FullVersion, + ProtocolVersion.TLSv12.FullVersion, + ProtocolVersion.TLSv13.FullVersion, + null, + }; + + bool[] greaseModes = new[] { false, true }; + var netProtocols = (SslProtocols[]) Enum.GetValues(typeof(SslProtocols)); + + foreach (var netProtocol in netProtocols) + { + foreach (var greaseMode in greaseModes) + { + foreach (var plainVersion in plainVersions) + { + var protocolKey = new ProtocolKey(plainVersion, greaseMode, netProtocol); + CachedValues[protocolKey] = InternalGetProtocolVersions(plainVersion, greaseMode, netProtocol); + } + } + } + } + + public static ProtocolVersion GetFromRawValue(int protocolVersion) + { + var result = SupportedVersions.FirstOrDefault(v => (int)v.FullVersion == protocolVersion); + + if (result == null) + { + throw new ArgumentException($"Invalid protocol version {protocolVersion}"); + } + + return result; + } + + public static ProtocolVersion[]? GetProtocolVersions( + int? plainProtocolVersion, bool greaseMode, SslProtocols protocols) + { + var protocolKey = new ProtocolKey(plainProtocolVersion, greaseMode, protocols); + + if (CachedValues.TryGetValue(protocolKey, out var result)) + { + return result; + } + + return InternalGetProtocolVersions(plainProtocolVersion, greaseMode, protocols); + } + + private static ProtocolVersion[]? InternalGetProtocolVersions(int? plainProtocolVersion, bool greaseMode, SslProtocols protocols) + { + if (plainProtocolVersion != null) + { + var version = GetFromRawValue(plainProtocolVersion.Value); + + if (version.IsEarlierVersionOf(ProtocolVersion.TLSv12)) + return version.Only(); + + if (greaseMode) + { + // those allocation are shity + return new[] { ProtocolVersion.Grease }.Concat(version.DownTo(ProtocolVersion.TLSv12)) + .ToArray(); + } + + return version.DownTo(ProtocolVersion.TLSv12); + } + + if (SslProtocols.None == protocols) + { + return null; + } + + var listProtocolVersion = new List(); + + if (protocols.HasFlag(SslProtocols.Tls)) + { + listProtocolVersion.Add(ProtocolVersion.TLSv10); + } + + if (protocols.HasFlag(SslProtocols.Tls11)) + { + listProtocolVersion.Add(ProtocolVersion.TLSv11); + } + + if (protocols.HasFlag(SslProtocols.Tls12)) + { + listProtocolVersion.Add(ProtocolVersion.TLSv12); + } + +#if NET6_0_OR_GREATER + if (protocols.HasFlag(SslProtocols.Tls13)) + { + listProtocolVersion.Add(ProtocolVersion.TLSv13); + } +#endif + + return listProtocolVersion.ToArray(); + } + + + } + + internal readonly struct ProtocolKey + { + public ProtocolKey(int? plainProtocolVersion, bool greaseMode, SslProtocols sslProtocols) + { + PlainProtocolVersion = plainProtocolVersion; + GreaseMode = greaseMode; + SslProtocols = sslProtocols; + } + + public int ? PlainProtocolVersion { get; } + + public bool GreaseMode { get; } + + public SslProtocols SslProtocols { get; } + + public bool Equals(ProtocolKey other) + { + return PlainProtocolVersion == other.PlainProtocolVersion && GreaseMode == other.GreaseMode && SslProtocols == other.SslProtocols; + } + + public override bool Equals(object? obj) + { + return obj is ProtocolKey other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(PlainProtocolVersion, GreaseMode, (int)SslProtocols); + } + + } + +} diff --git a/src/Fluxzy.Core/Clients/Ssl/CipherSuiteNames.cs b/src/Fluxzy.Core/Clients/Ssl/CipherSuiteNames.cs new file mode 100644 index 000000000..34f63507b --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/CipherSuiteNames.cs @@ -0,0 +1,335 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +// ReSharper disable InconsistentNaming +namespace Fluxzy.Clients.Ssl +{ + public enum CipherSuiteNames + { + TLS_NULL_WITH_NULL_NULL = 0, + TLS_RSA_WITH_NULL_MD5 = 1, + TLS_RSA_WITH_NULL_SHA = 2, + TLS_RSA_EXPORT_WITH_RC4_40_MD5 = 3, + TLS_RSA_WITH_RC4_128_MD5 = 4, + TLS_RSA_WITH_RC4_128_SHA = 5, + TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5 = 6, + TLS_RSA_WITH_IDEA_CBC_SHA = 7, + TLS_RSA_EXPORT_WITH_DES40_CBC_SHA = 8, + TLS_RSA_WITH_DES_CBC_SHA = 9, + TLS_RSA_WITH_3DES_EDE_CBC_SHA = 10, + TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA = 11, + TLS_DH_DSS_WITH_DES_CBC_SHA = 12, + TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA = 13, + TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA = 14, + TLS_DH_RSA_WITH_DES_CBC_SHA = 15, + TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA = 16, + TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA = 17, + TLS_DHE_DSS_WITH_DES_CBC_SHA = 18, + TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA = 19, + TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA = 20, + TLS_DHE_RSA_WITH_DES_CBC_SHA = 21, + TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA = 22, + TLS_DH_anon_EXPORT_WITH_RC4_40_MD5 = 23, + TLS_DH_anon_WITH_RC4_128_MD5 = 24, + TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA = 25, + TLS_DH_anon_WITH_DES_CBC_SHA = 26, + TLS_DH_anon_WITH_3DES_EDE_CBC_SHA = 27, + TLS_RSA_WITH_AES_128_CBC_SHA = 47, + TLS_DH_DSS_WITH_AES_128_CBC_SHA = 48, + TLS_DH_RSA_WITH_AES_128_CBC_SHA = 49, + TLS_DHE_DSS_WITH_AES_128_CBC_SHA = 50, + TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 51, + TLS_DH_anon_WITH_AES_128_CBC_SHA = 52, + TLS_RSA_WITH_AES_256_CBC_SHA = 53, + TLS_DH_DSS_WITH_AES_256_CBC_SHA = 54, + TLS_DH_RSA_WITH_AES_256_CBC_SHA = 55, + TLS_DHE_DSS_WITH_AES_256_CBC_SHA = 56, + TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 57, + TLS_DH_anon_WITH_AES_256_CBC_SHA = 58, + TLS_RSA_WITH_CAMELLIA_128_CBC_SHA = 65, + TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA = 66, + TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA = 67, + TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA = 68, + TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA = 69, + TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA = 70, + TLS_RSA_WITH_CAMELLIA_256_CBC_SHA = 132, + TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA = 133, + TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA = 134, + TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA = 135, + TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA = 136, + TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA = 137, + TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 186, + TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256 = 187, + TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 188, + TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256 = 189, + TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 190, + TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256 = 191, + TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 192, + TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256 = 193, + TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 194, + TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256 = 195, + TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 196, + TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256 = 197, + TLS_RSA_WITH_SEED_CBC_SHA = 150, + TLS_DH_DSS_WITH_SEED_CBC_SHA = 151, + TLS_DH_RSA_WITH_SEED_CBC_SHA = 152, + TLS_DHE_DSS_WITH_SEED_CBC_SHA = 153, + TLS_DHE_RSA_WITH_SEED_CBC_SHA = 154, + TLS_DH_anon_WITH_SEED_CBC_SHA = 155, + TLS_PSK_WITH_RC4_128_SHA = 138, + TLS_PSK_WITH_3DES_EDE_CBC_SHA = 139, + TLS_PSK_WITH_AES_128_CBC_SHA = 140, + TLS_PSK_WITH_AES_256_CBC_SHA = 141, + TLS_DHE_PSK_WITH_RC4_128_SHA = 142, + TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA = 143, + TLS_DHE_PSK_WITH_AES_128_CBC_SHA = 144, + TLS_DHE_PSK_WITH_AES_256_CBC_SHA = 145, + TLS_RSA_PSK_WITH_RC4_128_SHA = 146, + TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA = 147, + TLS_RSA_PSK_WITH_AES_128_CBC_SHA = 148, + TLS_RSA_PSK_WITH_AES_256_CBC_SHA = 149, + TLS_ECDH_ECDSA_WITH_NULL_SHA = 49153, + TLS_ECDH_ECDSA_WITH_RC4_128_SHA = 49154, + TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA = 49155, + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA = 49156, + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA = 49157, + TLS_ECDHE_ECDSA_WITH_NULL_SHA = 49158, + TLS_ECDHE_ECDSA_WITH_RC4_128_SHA = 49159, + TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA = 49160, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 49161, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 49162, + TLS_ECDH_RSA_WITH_NULL_SHA = 49163, + TLS_ECDH_RSA_WITH_RC4_128_SHA = 49164, + TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA = 49165, + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA = 49166, + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA = 49167, + TLS_ECDHE_RSA_WITH_NULL_SHA = 49168, + TLS_ECDHE_RSA_WITH_RC4_128_SHA = 49169, + TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA = 49170, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 49171, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 49172, + TLS_ECDH_anon_WITH_NULL_SHA = 49173, + TLS_ECDH_anon_WITH_RC4_128_SHA = 49174, + TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA = 49175, + TLS_ECDH_anon_WITH_AES_128_CBC_SHA = 49176, + TLS_ECDH_anon_WITH_AES_256_CBC_SHA = 49177, + TLS_PSK_WITH_NULL_SHA = 44, + TLS_DHE_PSK_WITH_NULL_SHA = 45, + TLS_RSA_PSK_WITH_NULL_SHA = 46, + TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA = 49178, + TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA = 49179, + TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA = 49180, + TLS_SRP_SHA_WITH_AES_128_CBC_SHA = 49181, + TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA = 49182, + TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA = 49183, + TLS_SRP_SHA_WITH_AES_256_CBC_SHA = 49184, + TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA = 49185, + TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA = 49186, + TLS_RSA_WITH_NULL_SHA256 = 59, + TLS_RSA_WITH_AES_128_CBC_SHA256 = 60, + TLS_RSA_WITH_AES_256_CBC_SHA256 = 61, + TLS_DH_DSS_WITH_AES_128_CBC_SHA256 = 62, + TLS_DH_RSA_WITH_AES_128_CBC_SHA256 = 63, + TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 = 64, + TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 103, + TLS_DH_DSS_WITH_AES_256_CBC_SHA256 = 104, + TLS_DH_RSA_WITH_AES_256_CBC_SHA256 = 105, + TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 = 106, + TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 107, + TLS_DH_anon_WITH_AES_128_CBC_SHA256 = 108, + TLS_DH_anon_WITH_AES_256_CBC_SHA256 = 109, + TLS_RSA_WITH_AES_128_GCM_SHA256 = 156, + TLS_RSA_WITH_AES_256_GCM_SHA384 = 157, + TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 158, + TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 159, + TLS_DH_RSA_WITH_AES_128_GCM_SHA256 = 160, + TLS_DH_RSA_WITH_AES_256_GCM_SHA384 = 161, + TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 = 162, + TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 = 163, + TLS_DH_DSS_WITH_AES_128_GCM_SHA256 = 164, + TLS_DH_DSS_WITH_AES_256_GCM_SHA384 = 165, + TLS_DH_anon_WITH_AES_128_GCM_SHA256 = 166, + TLS_DH_anon_WITH_AES_256_GCM_SHA384 = 167, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 49187, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 49188, + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 = 49189, + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 = 49190, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 49191, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 49192, + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 = 49193, + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 = 49194, + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 49195, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 49196, + TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 = 49197, + TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 = 49198, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 49199, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 49200, + TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 = 49201, + TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 = 49202, + TLS_PSK_WITH_AES_128_GCM_SHA256 = 168, + TLS_PSK_WITH_AES_256_GCM_SHA384 = 169, + TLS_DHE_PSK_WITH_AES_128_GCM_SHA256 = 170, + TLS_DHE_PSK_WITH_AES_256_GCM_SHA384 = 171, + TLS_RSA_PSK_WITH_AES_128_GCM_SHA256 = 172, + TLS_RSA_PSK_WITH_AES_256_GCM_SHA384 = 173, + TLS_PSK_WITH_AES_128_CBC_SHA256 = 174, + TLS_PSK_WITH_AES_256_CBC_SHA384 = 175, + TLS_PSK_WITH_NULL_SHA256 = 176, + TLS_PSK_WITH_NULL_SHA384 = 177, + TLS_DHE_PSK_WITH_AES_128_CBC_SHA256 = 178, + TLS_DHE_PSK_WITH_AES_256_CBC_SHA384 = 179, + TLS_DHE_PSK_WITH_NULL_SHA256 = 180, + TLS_DHE_PSK_WITH_NULL_SHA384 = 181, + TLS_RSA_PSK_WITH_AES_128_CBC_SHA256 = 182, + TLS_RSA_PSK_WITH_AES_256_CBC_SHA384 = 183, + TLS_RSA_PSK_WITH_NULL_SHA256 = 184, + TLS_RSA_PSK_WITH_NULL_SHA384 = 185, + TLS_ECDHE_PSK_WITH_RC4_128_SHA = 49203, + TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA = 49204, + TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA = 49205, + TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA = 49206, + TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 = 49207, + TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384 = 49208, + TLS_ECDHE_PSK_WITH_NULL_SHA = 49209, + TLS_ECDHE_PSK_WITH_NULL_SHA256 = 49210, + TLS_ECDHE_PSK_WITH_NULL_SHA384 = 49211, + TLS_EMPTY_RENEGOTIATION_INFO_SCSV = 255, + TLS_RSA_WITH_ARIA_128_CBC_SHA256 = 49212, + TLS_RSA_WITH_ARIA_256_CBC_SHA384 = 49213, + TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256 = 49214, + TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384 = 49215, + TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256 = 49216, + TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384 = 49217, + TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256 = 49218, + TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384 = 49219, + TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256 = 49220, + TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384 = 49221, + TLS_DH_anon_WITH_ARIA_128_CBC_SHA256 = 49222, + TLS_DH_anon_WITH_ARIA_256_CBC_SHA384 = 49223, + TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256 = 49224, + TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384 = 49225, + TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256 = 49226, + TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384 = 49227, + TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256 = 49228, + TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384 = 49229, + TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256 = 49230, + TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384 = 49231, + TLS_RSA_WITH_ARIA_128_GCM_SHA256 = 49232, + TLS_RSA_WITH_ARIA_256_GCM_SHA384 = 49233, + TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256 = 49234, + TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384 = 49235, + TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256 = 49236, + TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384 = 49237, + TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256 = 49238, + TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384 = 49239, + TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256 = 49240, + TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384 = 49241, + TLS_DH_anon_WITH_ARIA_128_GCM_SHA256 = 49242, + TLS_DH_anon_WITH_ARIA_256_GCM_SHA384 = 49243, + TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256 = 49244, + TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384 = 49245, + TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256 = 49246, + TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384 = 49247, + TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256 = 49248, + TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384 = 49249, + TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256 = 49250, + TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384 = 49251, + TLS_PSK_WITH_ARIA_128_CBC_SHA256 = 49252, + TLS_PSK_WITH_ARIA_256_CBC_SHA384 = 49253, + TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256 = 49254, + TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384 = 49255, + TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256 = 49256, + TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384 = 49257, + TLS_PSK_WITH_ARIA_128_GCM_SHA256 = 49258, + TLS_PSK_WITH_ARIA_256_GCM_SHA384 = 49259, + TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256 = 49260, + TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384 = 49261, + TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256 = 49262, + TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384 = 49263, + TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256 = 49264, + TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384 = 49265, + TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = 49266, + TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = 49267, + TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = 49268, + TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = 49269, + TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 49270, + TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 = 49271, + TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 49272, + TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 = 49273, + TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 49274, + TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 49275, + TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 49276, + TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 49277, + TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 49278, + TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 49279, + TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256 = 49280, + TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384 = 49281, + TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256 = 49282, + TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384 = 49283, + TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256 = 49284, + TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384 = 49285, + TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = 49286, + TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = 49287, + TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = 49288, + TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = 49289, + TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 49290, + TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 49291, + TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 49292, + TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 49293, + TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256 = 49294, + TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384 = 49295, + TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256 = 49296, + TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384 = 49297, + TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256 = 49298, + TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384 = 49299, + TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256 = 49300, + TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384 = 49301, + TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 = 49302, + TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 = 49303, + TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256 = 49304, + TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384 = 49305, + TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 = 49306, + TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 = 49307, + TLS_RSA_WITH_AES_128_CCM = 49308, + TLS_RSA_WITH_AES_256_CCM = 49309, + TLS_DHE_RSA_WITH_AES_128_CCM = 49310, + TLS_DHE_RSA_WITH_AES_256_CCM = 49311, + TLS_RSA_WITH_AES_128_CCM_8 = 49312, + TLS_RSA_WITH_AES_256_CCM_8 = 49313, + TLS_DHE_RSA_WITH_AES_128_CCM_8 = 49314, + TLS_DHE_RSA_WITH_AES_256_CCM_8 = 49315, + TLS_PSK_WITH_AES_128_CCM = 49316, + TLS_PSK_WITH_AES_256_CCM = 49317, + TLS_DHE_PSK_WITH_AES_128_CCM = 49318, + TLS_DHE_PSK_WITH_AES_256_CCM = 49319, + TLS_PSK_WITH_AES_128_CCM_8 = 49320, + TLS_PSK_WITH_AES_256_CCM_8 = 49321, + TLS_PSK_DHE_WITH_AES_128_CCM_8 = 49322, + TLS_PSK_DHE_WITH_AES_256_CCM_8 = 49323, + TLS_ECDHE_ECDSA_WITH_AES_128_CCM = 49324, + TLS_ECDHE_ECDSA_WITH_AES_256_CCM = 49325, + TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 = 49326, + TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 = 49327, + TLS_FALLBACK_SCSV = 22016, + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 52392, + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 52393, + TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 52394, + TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 = 52395, + TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256 = 52396, + TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256 = 52397, + TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256 = 52398, + TLS_ECDHE_PSK_WITH_AES_128_GCM_SHA256 = 53249, + TLS_ECDHE_PSK_WITH_AES_256_GCM_SHA384 = 53250, + TLS_ECDHE_PSK_WITH_AES_128_CCM_8_SHA256 = 53251, + TLS_ECDHE_PSK_WITH_AES_128_CCM_SHA256 = 53253, + TLS_AES_128_GCM_SHA256 = 4865, + TLS_AES_256_GCM_SHA384 = 4866, + TLS_CHACHA20_POLY1305_SHA256 = 4867, + TLS_AES_128_CCM_SHA256 = 4868, + TLS_AES_128_CCM_8_SHA256 = 4869, + TLS_SM4_GCM_SM3 = 198, + TLS_SM4_CCM_SM3 = 199, + TLS_GOSTR341112_256_WITH_KUZNYECHIK_CTR_OMAC = 49408, + TLS_GOSTR341112_256_WITH_MAGMA_CTR_OMAC = 49409, + TLS_GOSTR341112_256_WITH_28147_CNT_IMIT = 49410, + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/ImpersonateConfiguration.cs b/src/Fluxzy.Core/Clients/Ssl/ImpersonateConfiguration.cs new file mode 100644 index 000000000..3ff2b3461 --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/ImpersonateConfiguration.cs @@ -0,0 +1,134 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Fluxzy.Clients.H2.Frames; + +namespace Fluxzy.Clients.Ssl +{ + /// + /// Configuration holder for an impersonation profile. + /// + public class ImpersonateConfiguration + { + public ImpersonateConfiguration( + ImpersonateNetworkSettings networkSettings, + ImpersonateH2Setting h2Settings, List headers) + { + Headers = headers; + NetworkSettings = networkSettings; + H2Settings = h2Settings; + } + + /// + /// Network settings. + /// + public ImpersonateNetworkSettings NetworkSettings { get; } + + /// + /// HTTP/2 settings. + /// + public ImpersonateH2Setting H2Settings { get; } + + /// + /// Header settings. + /// + public List Headers { get; } + } + + public class ImpersonateNetworkSettings + { + public ImpersonateNetworkSettings(string ja3FingerPrint, bool? greaseMode, + Dictionary? overrideClientExtensionsValues, int[]? signatureAlgorithms, int[]? earlySharedGroups) + { + Ja3FingerPrint = ja3FingerPrint; + GreaseMode = greaseMode; + OverrideClientExtensionsValues = overrideClientExtensionsValues; + SignatureAlgorithms = signatureAlgorithms; + EarlySharedGroups = earlySharedGroups; + } + + /// + /// JA3 fingerprint. + /// + public string Ja3FingerPrint { get; } + + /// + /// When null, Grease mode will be inferred from the client extensions. + /// + public bool? GreaseMode { get; } + + /// + /// Override client extensions values. + /// + public Dictionary? OverrideClientExtensionsValues { get; } + + /// + /// Signature algorithms. Order matters for JA4. + /// + public int[]? SignatureAlgorithms { get; } + + /// + /// When using TLS v1.3, the named group on this list will be used for early shared key (key_share extensions). + /// + public int[]? EarlySharedGroups { get; } + } + + /// + /// http2 announce settings. + /// + public class ImpersonateH2SettingItem + { + public ImpersonateH2SettingItem(SettingIdentifier identifier, int value) + { + Identifier = identifier; + Value = value; + } + + /// + /// Setting identifier. + /// + public SettingIdentifier Identifier { get; } + + /// + /// Setting value. + /// + public int Value { get; } + } + + /// + /// + /// + public class ImpersonateH2Setting + { + public ImpersonateH2Setting(List settings, bool removeDefaultValues) + { + Settings = settings; + RemoveDefaultValues = removeDefaultValues; + } + + public List Settings { get; } + + /// + /// Remove default values. + /// + public bool RemoveDefaultValues { get; } + } + + public class ImpersonateHeader + { + public ImpersonateHeader(string name, string value, bool skipIfExists = false) + { + Name = name; + Value = value; + SkipIfExists = skipIfExists; + } + + public string Name { get; } + + public string Value { get; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool SkipIfExists { get; } + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/ImpersonateConfigurationManager.cs b/src/Fluxzy.Core/Clients/Ssl/ImpersonateConfigurationManager.cs new file mode 100644 index 000000000..023ca1d1d --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/ImpersonateConfigurationManager.cs @@ -0,0 +1,96 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using Fluxzy.Rules.Actions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace Fluxzy.Clients.Ssl +{ + public class ImpersonateConfigurationManager + { + public static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + public static ImpersonateConfigurationManager Instance { get; } = new ImpersonateConfigurationManager(); + + static ImpersonateConfigurationManager() + { + foreach (var (name, configuration) in ImpersonateProfileManager.GetBuiltInProfiles()) + { + Instance.AddOrUpdateDefaultConfiguration(name, configuration); + } + } + + private readonly Dictionary _configurations + = new Dictionary(); + + private ImpersonateConfigurationManager() + { + + } + + public ImpersonateConfiguration? LoadConfiguration(string nameOrConfigFile) + { + if (!ImpersonateProfile.TryParse(nameOrConfigFile, out var agent)) + { + return null; + } + + if (agent.Absolute) + { + if (_configurations.TryGetValue(agent, out var configuration)) + { + return configuration; + } + } + + if (agent.Latest) + { + var latest = _configurations.Keys + .Where(a => string.Equals(a.Name, agent.Name, + StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(r => r.VersionAsVersion) + .FirstOrDefault(); + + if (latest != null) + { + return _configurations[latest]; + } + } + + if (File.Exists(nameOrConfigFile)) + { + var json = File.ReadAllText(nameOrConfigFile); + return JsonSerializer.Deserialize(json, JsonSerializerOptions); + } + + + return null; + } + + public void AddOrUpdateDefaultConfiguration(ImpersonateProfile profile, ImpersonateConfiguration configuration) + { + if (!profile.Absolute) + { + throw new ArgumentException("Agent must be a specific version", nameof(profile)); + } + + _configurations[profile] = configuration; + } + + public IEnumerable<(ImpersonateProfile ConfigurationName, ImpersonateConfiguration)> GetConfigurations() + { + foreach (var (name, configuration) in _configurations) + { + yield return (name, configuration); + } + } + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/ImpersonateProfile.cs b/src/Fluxzy.Core/Clients/Ssl/ImpersonateProfile.cs new file mode 100644 index 000000000..afe14e0c9 --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/ImpersonateProfile.cs @@ -0,0 +1,110 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; + +namespace Fluxzy.Clients.Ssl +{ + public class ImpersonateProfile + { + public ImpersonateProfile(string name, string platform, string version) + { + Name = name; + Version = version; + Platform = platform; + } + + public string Name { get; } + + public string Version { get; } + + public Version? VersionAsVersion + { + get + { + return System.Version.TryParse(Version, out var version) ? version : null; + } + } + + public string Platform { get; } + + public bool Latest => string.Equals(Version, "latest", StringComparison.OrdinalIgnoreCase); + + public bool Absolute => !Latest; + + + protected bool Equals(ImpersonateProfile other) + { + return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) + && string.Equals(Version, other.Version, StringComparison.OrdinalIgnoreCase) + && string.Equals(Platform, other.Platform, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((ImpersonateProfile)obj); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + hashCode.Add(Name, StringComparer.OrdinalIgnoreCase); + hashCode.Add(Version, StringComparer.OrdinalIgnoreCase); + hashCode.Add(Platform, StringComparer.OrdinalIgnoreCase); + + return hashCode.ToHashCode(); + } + + public string ToFlatName() + { + return $"{Name}_{Platform}_{Version}"; + } + + public override string ToString() + { + return ToFlatName(); + } + + public static bool TryParse(string rawString, out ImpersonateProfile result) + { + result = null!; + + if (string.IsNullOrWhiteSpace(rawString)) + return false; + + var parts = rawString.Split('_'); + + if (parts.Length != 3) + return false; + + result = new ImpersonateProfile(parts[0], parts[1], parts[2]); + + return true; + } + + public static ImpersonateProfile Parse(string rawString) + { + if (!TryParse(rawString, out var result)) + { + throw new ArgumentException("Invalid format", nameof(rawString)); + } + + return result; + } + } +} diff --git a/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptions.cs b/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptions.cs index 164f4d115..52d273ac6 100644 --- a/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptions.cs +++ b/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptions.cs @@ -6,6 +6,7 @@ using System.Security.Cryptography.X509Certificates; using Fluxzy.Certificates; using Fluxzy.Clients.Ssl.BouncyCastle; +using Certificate = Fluxzy.Certificates.Certificate; namespace Fluxzy.Clients.Ssl { @@ -15,13 +16,15 @@ public class SslConnectionBuilderOptions public SslConnectionBuilderOptions(string targetHost, SslProtocols enabledSslProtocols, List applicationProtocols, - RemoteCertificateValidationCallback? remoteCertificateValidationCallback, Certificate? clientCertificate) + RemoteCertificateValidationCallback? remoteCertificateValidationCallback, + Certificate? clientCertificate, AdvancedTlsSettings? advancedTlsSettings) { TargetHost = targetHost; EnabledSslProtocols = enabledSslProtocols; ApplicationProtocols = applicationProtocols; RemoteCertificateValidationCallback = remoteCertificateValidationCallback; ClientCertificate = clientCertificate; + AdvancedTlsSettings = advancedTlsSettings; } public string TargetHost { get; } @@ -30,10 +33,12 @@ public SslConnectionBuilderOptions(string targetHost, public List ApplicationProtocols { get; } - public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; } = null; + public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; } public Certificate ? ClientCertificate { get; set; } + public AdvancedTlsSettings? AdvancedTlsSettings { get; } + public SslClientAuthenticationOptions GetSslClientAuthenticationOptions() { if (_authenticationOptions != null) diff --git a/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptionsCipherConfiguration.cs b/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptionsCipherConfiguration.cs new file mode 100644 index 000000000..35ad329e7 --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/SslConnectionBuilderOptionsCipherConfiguration.cs @@ -0,0 +1,2 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + diff --git a/src/Fluxzy.Core/Clients/Ssl/TlsFingerPrint.cs b/src/Fluxzy.Core/Clients/Ssl/TlsFingerPrint.cs new file mode 100644 index 000000000..30aed6051 --- /dev/null +++ b/src/Fluxzy.Core/Clients/Ssl/TlsFingerPrint.cs @@ -0,0 +1,224 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.Linq; +using Org.BouncyCastle.Tls; + +namespace Fluxzy.Clients.Ssl +{ + public class TlsFingerPrint + { + internal static readonly int GreaseLeadValue = 60138; + internal static readonly int GreaseTrailValue = 64250; + + public TlsFingerPrint( + int protocolVersion, + int[] ciphers, + int[] clientExtensions, + int[] supportGroups, + int[] ellipticCurvesFormat, bool ? greaseMode, + Dictionary? overrideClientExtensionsValues, + List? signatureAndHashAlgorithms, + int[]? earlySharedGroups) + { + ProtocolVersion = protocolVersion; + Ciphers = ciphers; + SupportGroups = supportGroups; + EllipticCurvesFormat = ellipticCurvesFormat; + OverrideClientExtensionsValues = overrideClientExtensionsValues; + SignatureAndHashAlgorithms = signatureAndHashAlgorithms; + EarlySharedGroups = earlySharedGroups; + ClientExtensions = clientExtensions; + Ja3Flat = ToString(); + + GreaseMode = ClientExtensions.Contains(0xFE0D); + + if (greaseMode != null) { + GreaseMode = greaseMode.Value; + } + + if (GreaseMode) + { + // Grease enable + EffectiveSupportGroups = new[] { 0x6A6A }.Concat(SupportGroups).ToArray(); + EffectiveClientExtensions = new[] { GreaseLeadValue }.Concat(clientExtensions).Concat(new[] { GreaseTrailValue }).ToArray(); + EffectiveCiphers = new[] { 0x8A8A }.Concat(Ciphers).ToArray(); + } + else { + EffectiveSupportGroups = SupportGroups; + EffectiveClientExtensions = ClientExtensions; + EffectiveCiphers = Ciphers; + } + } + + + /// + /// As in wire format + /// + public int ProtocolVersion { get; } + + /// + /// Ciphers to be used + /// + public int[] Ciphers { get; } + + /// + /// Client extensions to be used + /// + public int[] ClientExtensions { get; } + + /// + /// Supported groups + /// + public int[] SupportGroups { get; } + + /// + /// Elliptic curves format + /// + public int[] EllipticCurvesFormat { get; } + + /// + /// Flat JA3 fingerprint + /// + public string Ja3Flat { get; } + + /// + /// Grease mode, true = ON + /// + public bool GreaseMode { get; } + + /// + /// When using TLS v1.3, the named group on this list will be used for early shared key + /// + public int[]? EarlySharedGroups { get; } + + /// + /// Effective ciphers + /// + internal int[] EffectiveCiphers { get; set; } + + internal int[] EffectiveSupportGroups { get; } + + internal int[] EffectiveClientExtensions { get; } + + /// + /// Override client extensions values (set manually) + /// + public Dictionary? OverrideClientExtensionsValues { get; } + + /// + /// Signature and hash algorithms + /// + public List? SignatureAndHashAlgorithms { get; } + + public sealed override string ToString() + { + return $"{ProtocolVersion}," + + $"{string.Join("-", Ciphers)}," + + $"{string.Join("-", ClientExtensions)}," + + $"{string.Join("-", SupportGroups)}," + + $"{string.Join("-", EllipticCurvesFormat)}"; + } + + public string ToString(bool ordered) + { + if (!ordered) + { + return ToString(); + } + + return $"{ProtocolVersion}," + + $"{string.Join("-", Ciphers.OrderBy(c => c))}," + + $"{string.Join("-", ClientExtensions.OrderBy(c => c))}," + + $"{string.Join("-", SupportGroups)}," + + $"{string.Join("-", EllipticCurvesFormat.OrderBy(c => c))}"; + } + + protected bool Equals(TlsFingerPrint other) + { + return Ja3Flat == other.Ja3Flat; + } + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((TlsFingerPrint)obj); + } + + public override int GetHashCode() + { + return Ja3Flat.GetHashCode(); + } + + public static bool operator ==(TlsFingerPrint? left, TlsFingerPrint? right) + { + return Equals(left, right); + } + + public static bool operator !=(TlsFingerPrint? left, TlsFingerPrint? right) + { + return !Equals(left, right); + } + + /// + /// + /// + /// + /// When null, greaseMode will be determined according to tls value. + /// Instead of using the default built-in values for extension.. + /// + /// + /// + /// + public static TlsFingerPrint ParseFromJa3(string ja3, bool ? greaseMode = null, + Dictionary? overrideClientExtensionsValues = null, + List? signatureAndHashAlgorithms = null, + int[]? earlyShardGroups = null) + { + var parts = ja3.Split(new[] {","}, StringSplitOptions.None); + + if (parts.Length != 5) + { + throw new ArgumentException("Invalid JA3 fingerprint format"); + } + + if (!int.TryParse(parts[0], out var protocolVersion)) + { + throw new ArgumentException($"Invalid JA3 fingerprint format. TLS version non valid `{parts[0]}`"); + } + + var ciphers = parts[1] + .Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse).ToArray(); + + var clientExtensions = parts[2].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse).ToArray(); + + var ellipticCurves = parts[3].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse).ToArray(); + + var ellipticCurvesFormat = parts[4].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse).ToArray(); + + return new TlsFingerPrint( + protocolVersion, ciphers, clientExtensions, ellipticCurves, + ellipticCurvesFormat, greaseMode, overrideClientExtensionsValues, + signatureAndHashAlgorithms, earlyShardGroups); + } + } +} diff --git a/src/Fluxzy.Core/Core/ExchangeContext.cs b/src/Fluxzy.Core/Core/ExchangeContext.cs index a269fbab3..3e7a9e77a 100644 --- a/src/Fluxzy.Core/Core/ExchangeContext.cs +++ b/src/Fluxzy.Core/Core/ExchangeContext.cs @@ -13,6 +13,7 @@ using Fluxzy.Clients; using Fluxzy.Clients.Headers; using Fluxzy.Clients.Mock; +using Fluxzy.Clients.Ssl; using Fluxzy.Core.Breakpoints; using Fluxzy.Extensions; using Fluxzy.Misc.Streams; @@ -102,6 +103,11 @@ public ExchangeContext( /// public SslProtocols ProxyTlsProtocols { get; set; } = SslProtocols.None; + /// + /// Gets or sets advanced TLS settings + /// + public AdvancedTlsSettings AdvancedTlsSettings { get; set; } = new AdvancedTlsSettings(); + /// /// Don't validate the remote certificate /// @@ -193,8 +199,14 @@ public ExchangeContext( /// public bool HasRequestBodySubstitution => _requestBodyStreamSubstitutions != null; + /// + /// Define if the exchange has a request body + /// public bool HasRequestBody { get; set; } + /// + /// + /// public bool DnsOverHttpsCapture { get; set; } /// diff --git a/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs b/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs index e2b2af8b7..fa57c0c29 100644 --- a/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs +++ b/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Net.Security; +using System.Text; using System.Threading.Tasks; using Fluxzy.Clients; using Fluxzy.Rules; @@ -142,4 +143,17 @@ await rule.Enforce(context, exchange, connection, filterScope, return context; } } + + internal class HostInfo + { + public HostInfo(string host) + { + Host = host; + EncodedHost = Encoding.UTF8.GetBytes(host); + } + + public string Host { get; } + + public byte[] EncodedHost { get; set; } + } } diff --git a/src/Fluxzy.Core/Fluxzy.Core.csproj b/src/Fluxzy.Core/Fluxzy.Core.csproj index bace084b8..a03670498 100644 --- a/src/Fluxzy.Core/Fluxzy.Core.csproj +++ b/src/Fluxzy.Core/Fluxzy.Core.csproj @@ -70,7 +70,7 @@ - + diff --git a/src/Fluxzy.Core/Rules/ActionDistinctiveAttribute.cs b/src/Fluxzy.Core/Rules/ActionDistinctiveAttribute.cs index 4c47ae10a..1868de426 100644 --- a/src/Fluxzy.Core/Rules/ActionDistinctiveAttribute.cs +++ b/src/Fluxzy.Core/Rules/ActionDistinctiveAttribute.cs @@ -5,4 +5,4 @@ namespace Fluxzy.Rules public class ActionDistinctiveAttribute : PropertyDistinctiveAttribute, IVariableHolder { } -} +} diff --git a/src/Fluxzy.Core/Rules/Actions/ImpersonateAction.cs b/src/Fluxzy.Core/Rules/Actions/ImpersonateAction.cs new file mode 100644 index 000000000..cb87f2330 --- /dev/null +++ b/src/Fluxzy.Core/Rules/Actions/ImpersonateAction.cs @@ -0,0 +1,130 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Fluxzy.Clients.H2; +using Fluxzy.Clients.H2.Encoder.Utils; +using Fluxzy.Clients.Headers; +using Fluxzy.Clients.Ssl; +using Fluxzy.Core; +using Fluxzy.Core.Breakpoints; +using Org.BouncyCastle.Tls; +using YamlDotNet.Serialization; + +namespace Fluxzy.Rules.Actions +{ + /// + /// Impersonate a browser or client by changing the TLS fingerprint, HTTP/2 settings and headers. + /// + [ActionMetadata("Impersonate a browser or client by changing the TLS fingerprint, HTTP/2 settings and headers.")] + public class ImpersonateAction : Action + { + [JsonIgnore] + [YamlIgnore] + private ImpersonateConfiguration? _configuration; + + [JsonIgnore] + [YamlIgnore] + private TlsFingerPrint? _fingerPrint; + + /// + /// + /// + /// + public ImpersonateAction(string nameOrConfigFile) + { + NameOrConfigFile = nameOrConfigFile; + } + + /// + /// Name or config file + /// + [ActionDistinctive] + public string NameOrConfigFile { get; set; } + + public override FilterScope ActionScope => FilterScope.RequestHeaderReceivedFromClient; + + public override string DefaultDescription { get; } = "Impersonate"; + + public override void Init(StartupContext startupContext) + { + base.Init(startupContext); + + _configuration = ImpersonateConfigurationManager.Instance.LoadConfiguration(NameOrConfigFile); + + if (_configuration == null) + { + throw new FluxzyException($"Impersonate configuration '{NameOrConfigFile}' not found."); + } + + _fingerPrint = TlsFingerPrint.ParseFromJa3( + _configuration.NetworkSettings.Ja3FingerPrint, + _configuration.NetworkSettings.GreaseMode, + signatureAndHashAlgorithms: + _configuration.NetworkSettings + .SignatureAlgorithms?.Select(s => + SignatureAndHashAlgorithm.GetInstance(SignatureScheme.GetHashAlgorithm(s), + SignatureScheme.GetSignatureAlgorithm(s)) + ).ToList(), + earlyShardGroups: _configuration.NetworkSettings.EarlySharedGroups + ); + } + + public override ValueTask InternalAlter( + ExchangeContext context, Exchange? exchange, Connection? connection, FilterScope scope, + BreakPointManager breakPointManager) + { + if (_configuration != null) + { + context.AdvancedTlsSettings.TlsFingerPrint = _fingerPrint; + + var streamSetting = new H2StreamSetting(); + + if (_configuration.H2Settings.RemoveDefaultValues) { + streamSetting.AdvertiseSettings.Clear(); + } + + foreach (var setting in _configuration.H2Settings.Settings) + { + streamSetting.AdvertiseSettings.Add(setting.Identifier); + streamSetting.SetSetting(setting.Identifier, setting.Value); + } + + context.AdvancedTlsSettings.H2StreamSetting = streamSetting; + + var existingHeaders = exchange?.GetRequestHeaders().Select(s => s.Name) + .ToHashSet(SpanCharactersIgnoreCaseComparer.Default); + + if (existingHeaders != null) { + + foreach (var header in _configuration.Headers) + { + if (header.SkipIfExists) { + if (!existingHeaders.Contains(header.Name.AsMemory())) { + context.RequestHeaderAlterations.Add(new HeaderAlterationAdd( + header.Name, header.Value)); + } + } + else { + context.RequestHeaderAlterations.Add(new HeaderAlterationReplace( + header.Name, header.Value, true)); + } + } + } + + } + + return default; + } + + public override IEnumerable GetExamples() + { + yield return new ActionExample("Impersonate CHROME 131 on Windows", + new ImpersonateAction("Chrome_Windows_131") + ); + } + } +} diff --git a/src/Fluxzy.Core/Rules/Actions/ImpersonateProfileManager.cs b/src/Fluxzy.Core/Rules/Actions/ImpersonateProfileManager.cs new file mode 100644 index 000000000..af9b82ad3 --- /dev/null +++ b/src/Fluxzy.Core/Rules/Actions/ImpersonateProfileManager.cs @@ -0,0 +1,226 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System.Collections.Generic; +using Fluxzy.Clients.H2.Frames; +using Fluxzy.Clients.Ssl; +using Org.BouncyCastle.Tls; + +namespace Fluxzy.Rules.Actions +{ + public static class ImpersonateProfileManager + { + public static readonly string Chrome131Windows = "Chrome_Windows_131"; + public static readonly string Chrome131Android = "Chrome_Android_131"; + public static readonly string Edge131Windows = "Edge_Windows_131"; + public static readonly string Firefox133Windows = "Firefox_Windows_133"; + + public static IEnumerable<(ImpersonateProfile Agent, ImpersonateConfiguration Configuration)> GetBuiltInProfiles() + { + yield return (ImpersonateProfile.Parse(Chrome131Windows), Create_Chrome131_Windows()); + yield return (ImpersonateProfile.Parse(Chrome131Android), Create_Chrome131_Android()); + + yield return (ImpersonateProfile.Parse(Edge131Windows), Create_Edge131_Windows()); + + yield return (ImpersonateProfile.Parse(Firefox133Windows), Create_Firefox_133_Windows()); + } + + internal static ImpersonateConfiguration Create_Edge131_Windows() + { + var networkSettings = new ImpersonateNetworkSettings( + "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,5-10-35-51-23-43-18-0-27-17513-11-16-65281-13-45-65037,4588-29-23-24,0", + true, + null, + new int[] { + SignatureScheme.ecdsa_secp256r1_sha256, + SignatureScheme.rsa_pss_rsae_sha256, + SignatureScheme.rsa_pkcs1_sha256, + SignatureScheme.ecdsa_secp384r1_sha384, + SignatureScheme.rsa_pss_rsae_sha384, + SignatureScheme.rsa_pkcs1_sha384, + SignatureScheme.rsa_pss_rsae_sha512, + SignatureScheme.rsa_pkcs1_sha512, + }, + null + ); + + var h2Settings = new ImpersonateH2Setting(new List() { + new ImpersonateH2SettingItem(SettingIdentifier.SettingsHeaderTableSize, 65536), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsEnablePush, 0), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsInitialWindowSize, 6291456), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsMaxHeaderListSize, 262144), + }, true); + + var headers = new List + { + new ImpersonateHeader("sec-ch-ua", "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""), + new ImpersonateHeader("sec-ch-ua-mobile", "?0"), + new ImpersonateHeader("sec-ch-ua-platform", "\"Windows\""), + new ImpersonateHeader("Upgrade-Insecure-Requests", "1"), + new ImpersonateHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"), + new ImpersonateHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", true), + new ImpersonateHeader("Sec-Fetch-Site", "none"), + new ImpersonateHeader("Sec-Fetch-Mode", "navigate"), + new ImpersonateHeader("Sec-Fetch-User", "?1"), + new ImpersonateHeader("Sec-Fetch-Dest", "document"), + new ImpersonateHeader("Accept-Encoding", "gzip, deflate, br, zstd", true), + new ImpersonateHeader("Accept-language", "en-US,en;q=0.9", true), + new ImpersonateHeader("Priority", "u=0, i"), + }; + + var configuration = new ImpersonateConfiguration(networkSettings, + h2Settings, headers); + + return configuration; + + } + + internal static ImpersonateConfiguration Create_Chrome131_Windows() + { + var networkSettings = new ImpersonateNetworkSettings( + "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,45-0-65037-17513-35-10-13-65281-16-51-23-27-18-43-11-5,4588-29-23-24,0", + true, + null, + new int[] { + SignatureScheme.ecdsa_secp256r1_sha256, + SignatureScheme.rsa_pss_rsae_sha256, + SignatureScheme.rsa_pkcs1_sha256, + SignatureScheme.ecdsa_secp384r1_sha384, + SignatureScheme.rsa_pss_rsae_sha384, + SignatureScheme.rsa_pkcs1_sha384, + SignatureScheme.rsa_pss_rsae_sha512, + SignatureScheme.rsa_pkcs1_sha512, + }, + null + ); + + var h2Settings = new ImpersonateH2Setting(new List() { + new ImpersonateH2SettingItem(SettingIdentifier.SettingsHeaderTableSize, 65536), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsEnablePush, 0), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsInitialWindowSize, 6291456), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsMaxHeaderListSize, 262144), + }, true); + + var headers = new List + { + new ImpersonateHeader("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""), + new ImpersonateHeader("sec-ch-ua-mobile", "?0"), + new ImpersonateHeader("sec-ch-ua-platform", "\"Windows\""), + new ImpersonateHeader("Upgrade-Insecure-Requests", "1"), + new ImpersonateHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"), + new ImpersonateHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", true), + new ImpersonateHeader("Sec-Fetch-Site", "none"), + new ImpersonateHeader("Sec-Fetch-Mode", "navigate"), + new ImpersonateHeader("Sec-Fetch-User", "?1"), + new ImpersonateHeader("Sec-Fetch-Dest", "document"), + new ImpersonateHeader("Accept-Encoding", "gzip, deflate, br, zstd", true), + new ImpersonateHeader("Accept-language", "en-US,en;q=0.9", true), + new ImpersonateHeader("Priority", "u=0, i"), + }; + + var configuration = new ImpersonateConfiguration(networkSettings, + h2Settings, headers); + + return configuration; + + } + + internal static ImpersonateConfiguration Create_Chrome131_Android() + { + var networkSettings = new ImpersonateNetworkSettings( + "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,27-13-65281-18-43-0-35-10-5-51-11-16-17513-65037-23-45,4588-29-23-24,0", + true, + null, + new int[] { + SignatureScheme.ecdsa_secp256r1_sha256, + SignatureScheme.rsa_pss_rsae_sha256, + SignatureScheme.rsa_pkcs1_sha256, + SignatureScheme.ecdsa_secp384r1_sha384, + SignatureScheme.rsa_pss_rsae_sha384, + SignatureScheme.rsa_pkcs1_sha384, + SignatureScheme.rsa_pss_rsae_sha512, + SignatureScheme.rsa_pkcs1_sha512, + }, + null + ); + + var h2Settings = new ImpersonateH2Setting(new List() { + new ImpersonateH2SettingItem(SettingIdentifier.SettingsHeaderTableSize, 65536), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsEnablePush, 0), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsInitialWindowSize, 6291456), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsMaxHeaderListSize, 262144), + }, true); + + var headers = new List + { + new ImpersonateHeader("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\""), + new ImpersonateHeader("sec-ch-ua-mobile", "?0"), + new ImpersonateHeader("sec-ch-ua-platform", "\"Android\""), + new ImpersonateHeader("Upgrade-Insecure-Requests", "1"), + new ImpersonateHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36"), + new ImpersonateHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", true), + new ImpersonateHeader("Sec-Fetch-Site", "none"), + new ImpersonateHeader("Sec-Fetch-Mode", "navigate"), + new ImpersonateHeader("Sec-Fetch-User", "?1"), + new ImpersonateHeader("Sec-Fetch-Dest", "document"), + new ImpersonateHeader("Accept-Encoding", "gzip, deflate, br, zstd", true), + new ImpersonateHeader("Accept-language", "en-US,en;q=0.9", true), + new ImpersonateHeader("Priority", "u=0, i"), + }; + + var configuration = new ImpersonateConfiguration(networkSettings, + h2Settings, headers); + + return configuration; + + } + + internal static ImpersonateConfiguration Create_Firefox_133_Windows() + { + var networkSettings = new ImpersonateNetworkSettings( + "772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-34-51-43-13-45-28-27-65037,4588-29-23-24-25-256-257,0", + false, + null, + new int[] { + SignatureScheme.ecdsa_secp256r1_sha256, + SignatureScheme.ecdsa_secp384r1_sha384, + SignatureScheme.ecdsa_secp521r1_sha512, + SignatureScheme.rsa_pss_rsae_sha256, + SignatureScheme.rsa_pss_rsae_sha384, + SignatureScheme.rsa_pss_rsae_sha512, + SignatureScheme.rsa_pkcs1_sha256, + SignatureScheme.rsa_pkcs1_sha384, + SignatureScheme.rsa_pkcs1_sha512, + SignatureScheme.ecdsa_sha1, + SignatureScheme.rsa_pkcs1_sha1, + }, + null); + + var h2Settings = new ImpersonateH2Setting(new List() { + new ImpersonateH2SettingItem(SettingIdentifier.SettingsHeaderTableSize, 65536), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsEnablePush, 0), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsInitialWindowSize, 6291456), + new ImpersonateH2SettingItem(SettingIdentifier.SettingsMaxHeaderListSize, 262144), + }, true); + + var headers = new List + { + new ImpersonateHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0"), + new ImpersonateHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", true), + new ImpersonateHeader("Accept-Encoding", "gzip, deflate, br, zstd", true), + new ImpersonateHeader("Upgrade-Insecure-Requests", "1"), + new ImpersonateHeader("Sec-Fetch-Dest", "document"), + new ImpersonateHeader("Sec-Fetch-Mode", "navigate"), + new ImpersonateHeader("Sec-Fetch-Site", "none"), + new ImpersonateHeader("Sec-Fetch-User", "?1"), + new ImpersonateHeader("Priority", "u=0, i"), + new ImpersonateHeader("Accept-language", "en-US,en;q=0.9", true), + }; + + var configuration = new ImpersonateConfiguration(networkSettings, + h2Settings, headers); + + return configuration; + + } + } +} diff --git a/src/Fluxzy.Core/Rules/Actions/SetJa3FingerPrintAction.cs b/src/Fluxzy.Core/Rules/Actions/SetJa3FingerPrintAction.cs new file mode 100644 index 000000000..777d2904e --- /dev/null +++ b/src/Fluxzy.Core/Rules/Actions/SetJa3FingerPrintAction.cs @@ -0,0 +1,51 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Fluxzy.Clients.Ssl; +using Fluxzy.Core; +using Fluxzy.Core.Breakpoints; +using YamlDotNet.Serialization; + +namespace Fluxzy.Rules.Actions +{ + /// + /// Set a JA3 fingerprint of ongoing connection. + /// + [ActionMetadata("Set a JA3 fingerprint of ongoing connection.")] + public class SetJa3FingerPrintAction : Action + { + [JsonIgnore] + [YamlIgnore] + private TlsFingerPrint? _fingerPrint; + + public SetJa3FingerPrintAction(string value) + { + Value = value; + } + + public override FilterScope ActionScope => FilterScope.RequestHeaderReceivedFromClient; + + [ActionDistinctive] + public string Value { get; set; } + + public override string DefaultDescription => "Set JA3 fingerprint"; + + public override void Init(StartupContext startupContext) + { + _fingerPrint = TlsFingerPrint.ParseFromJa3(Value); + base.Init(startupContext); + } + + public override ValueTask InternalAlter( + ExchangeContext context, Exchange? exchange, Connection? connection, FilterScope scope, + BreakPointManager breakPointManager) + { + if (!string.IsNullOrWhiteSpace(Value)) { + context.AdvancedTlsSettings.TlsFingerPrint = _fingerPrint; + } + + return default; + } + } +} diff --git a/src/Fluxzy.Core/Rules/Actions/UpdateRequestHeaderAction.cs b/src/Fluxzy.Core/Rules/Actions/UpdateRequestHeaderAction.cs index f3aa72d9a..7f994404a 100644 --- a/src/Fluxzy.Core/Rules/Actions/UpdateRequestHeaderAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/UpdateRequestHeaderAction.cs @@ -7,13 +7,13 @@ using Fluxzy.Core.Breakpoints; namespace Fluxzy.Rules.Actions -{ - /// - /// Update and existing request header. If the header does not exists in the original request, the header will be - /// added. - /// Use {{previous}} keyword to refer to the original value of the header. - /// Note Headers that alter the connection behaviour will be ignored. - /// +{ + /// + /// Update and existing request header. If the header does not exists in the original request, the header will be + /// added. + /// Use {{previous}} keyword to refer to the original value of the header. + /// Note Headers that alter the connection behaviour will be ignored. + /// [ActionMetadata( "Update and existing request header. If the header does not exists in the original request, the header will be added.
" + "Use {{previous}} keyword to refer to the original value of the header.
" + @@ -72,4 +72,4 @@ public override IEnumerable GetExamples() new UpdateRequestHeaderAction("User-Agent", "Fluxzy")); } } -} +} diff --git a/src/Fluxzy.Tools.DocGen/Program.cs b/src/Fluxzy.Tools.DocGen/Program.cs index e5766a40a..a49953b98 100644 --- a/src/Fluxzy.Tools.DocGen/Program.cs +++ b/src/Fluxzy.Tools.DocGen/Program.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text.Json; +using Fluxzy.Clients.Ssl; using Fluxzy.Rules; using Fluxzy.Rules.Filters; using Action = Fluxzy.Rules.Action; @@ -26,6 +27,7 @@ public static void Main(string[] args) BuildFilterDocs(docsBaseDirectory, docBuilder, items); BuildActionDocs(docsBaseDirectory, docBuilder, items); + BuildImpersonateProfileDocs(docsBaseDirectory); File.WriteAllText(Path.Combine(docsBaseDirectory.FullName, "searchable-items.json"), JsonSerializer.Serialize(items, new JsonSerializerOptions(JsonSerializerDefaults.Web) {})); @@ -106,5 +108,25 @@ private static void BuildActionDocs(DirectoryInfo docsBaseDirectory, DocBuilder docBuilder.BuildAction(outDirectory, target, items); } } + + private static void BuildImpersonateProfileDocs(DirectoryInfo docsBaseDirectory) + { + var actionDirectory = new DirectoryInfo(Path.Combine(docsBaseDirectory.FullName, "impersonate-profiles")); + + if (actionDirectory.Exists) + actionDirectory.Delete(true); + + actionDirectory.Create(); + + foreach (var (name, configuration) in ImpersonateConfigurationManager.Instance.GetConfigurations()) { + var fullPath = Path.Combine(actionDirectory.FullName, $"{name}.json"); + + using var stream = File.Create(fullPath); + + + JsonSerializer.Serialize(stream, configuration, ImpersonateConfigurationManager.JsonSerializerOptions); + + } + } } } diff --git a/src/Fluxzy/Commands/StartCommandBuilder.cs b/src/Fluxzy/Commands/StartCommandBuilder.cs index fdf0f6763..dc91b1252 100644 --- a/src/Fluxzy/Commands/StartCommandBuilder.cs +++ b/src/Fluxzy/Commands/StartCommandBuilder.cs @@ -280,10 +280,10 @@ public async Task Run(InvocationContext invocationContext, CancellationToken pro await using var scope = new ProxyScope(() => new FluxzyNetOutOfProcessHost(), a => new OutOfProcessCaptureContext(a)); - await using (var tcpConnectionProvider = - proxyStartUpSetting.CaptureRawPacket + await using (var tcpConnectionProvider = proxyStartUpSetting.CaptureRawPacket ? await CapturedTcpConnectionProvider.Create(scope, proxyStartUpSetting.OutOfProcCapture) - : ITcpConnectionProvider.Default) { + : ITcpConnectionProvider.Default) + { await using (var proxy = new Proxy(proxyStartUpSetting, certificateProvider, new DefaultCertificateAuthorityManager(), tcpConnectionProvider, uaParserProvider, externalCancellationSource: linkedTokenSource)) { diff --git a/src/Fluxzy/Properties/launchSettings.json b/src/Fluxzy/Properties/launchSettings.json index 8a1068a63..aa069eb90 100644 --- a/src/Fluxzy/Properties/launchSettings.json +++ b/src/Fluxzy/Properties/launchSettings.json @@ -17,6 +17,12 @@ "environmentVariables": { }, "commandLineArgs": "start --no-cert-cache --trace" + }, + "launch (debug rule)": { + "commandName": "Project", + "environmentVariables": { + }, + "commandLineArgs": "start -b -r testrule.yml -sp -l 0.0.0.0:8877" } } } diff --git a/test/Fluxzy.Tests/Fluxzy.Tests.csproj b/test/Fluxzy.Tests/Fluxzy.Tests.csproj index c8fd24088..68946467a 100644 --- a/test/Fluxzy.Tests/Fluxzy.Tests.csproj +++ b/test/Fluxzy.Tests/Fluxzy.Tests.csproj @@ -21,6 +21,9 @@ + + Always + PreserveNewest diff --git a/test/Fluxzy.Tests/ImpersonateConfigurationTests.cs b/test/Fluxzy.Tests/ImpersonateConfigurationTests.cs new file mode 100644 index 000000000..7c99c9104 --- /dev/null +++ b/test/Fluxzy.Tests/ImpersonateConfigurationTests.cs @@ -0,0 +1,28 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System.IO; +using System.Text.Json; +using Fluxzy.Clients.Ssl; +using Xunit; + +namespace Fluxzy.Tests +{ + public class ImpersonateConfigurationTests + { + [Fact] + public void OutputDefault() + { + var allConfigurations = ImpersonateConfigurationManager.Instance.GetConfigurations(); + + Directory.CreateDirectory("Impersonate"); + + foreach (var (name, configuration) in allConfigurations) + { + var json = JsonSerializer.Serialize(configuration, + ImpersonateConfigurationManager.JsonSerializerOptions); + + File.WriteAllText($"Impersonate/{name}.json", json); + } + } + } +} diff --git a/test/Fluxzy.Tests/ImpersonateTests.cs b/test/Fluxzy.Tests/ImpersonateTests.cs new file mode 100644 index 000000000..90a25158a --- /dev/null +++ b/test/Fluxzy.Tests/ImpersonateTests.cs @@ -0,0 +1,75 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Fluxzy.Clients.Ssl; +using Fluxzy.Rules.Actions; +using Fluxzy.Tests._Fixtures; +using Xunit; + +namespace Fluxzy.Tests +{ + public class ImpersonateTests + { + [Theory] + [InlineData("Chrome_Windows_131")] + [InlineData("Firefox_Windows_133")] + [InlineData("Edge_Windows_131")] + public async Task CheckSignature(string nameOrConfigfile) + { + var testUrl = "https://check.ja3.zone/"; + + await using var proxy = new AddHocConfigurableProxy(1, 10, + configureSetting: setting => { + setting.UseBouncyCastleSslEngine(); + setting.AddAlterationRulesForAny(new ImpersonateAction(nameOrConfigfile)); + }); + + var impersonateLoader = ImpersonateConfigurationManager.Instance.LoadConfiguration(nameOrConfigfile)!; + + var rawFingerPrint = TlsFingerPrint.ParseFromJa3(impersonateLoader.NetworkSettings.Ja3FingerPrint); + var expectedJa3 = rawFingerPrint.ToString(true); + + using var httpClient = proxy.RunAndGetClient(); + using var response = await httpClient.GetAsync(testUrl); + + response.EnsureSuccessStatusCode(); + + var responseString = await response.Content.ReadAsStringAsync(); + + var ja3Response = JsonSerializer.Deserialize(responseString); + + Assert.NotNull(ja3Response); + Assert.Equal(expectedJa3, ja3Response.NormalizedFingerPrint); + } + + [Theory] + [InlineData("Chrome_Windows_131")] + [InlineData("Chrome_Windows_latest")] + [InlineData("Chrome_Android_131")] + [InlineData("Firefox_Windows_133")] + [InlineData("Firefox_Windows_133")] + [InlineData("Edge_Windows_131")] + public async Task CheckBlock(string nameOrConfigfile) + { + string url = Encoding.UTF8.GetString(Convert.FromBase64String("aHR0cHM6Ly93d3cuZ2FsZXJpZXNsYWZheWV0dGUuY29tL25vdGhpbmc=")); + int notExpectedStatusCode = 403; + + await using var proxy = new AddHocConfigurableProxy(1, 10, + configureSetting: setting => { + setting.UseBouncyCastleSslEngine(); + setting.AddAlterationRulesForAny(new ImpersonateAction(nameOrConfigfile)); + }); + + using var httpClient = proxy.RunAndGetClient(); + using var response = await httpClient.GetAsync(url); + + _ = await response.Content.ReadAsStringAsync(); + + Assert.NotEqual(notExpectedStatusCode, (int) response.StatusCode); + Assert.NotEqual(528, (int)response.StatusCode); + } + } +} diff --git a/test/Fluxzy.Tests/Ja3FingerPrintResponse.cs b/test/Fluxzy.Tests/Ja3FingerPrintResponse.cs new file mode 100644 index 000000000..edb181ec9 --- /dev/null +++ b/test/Fluxzy.Tests/Ja3FingerPrintResponse.cs @@ -0,0 +1,59 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using Newtonsoft.Json; +using System.Text.Json.Serialization; +using Fluxzy.Clients.Ssl; + +namespace Fluxzy.Tests +{ + public class Ja3FingerPrintResponse + { + public Ja3FingerPrintResponse(string hash, + string fingerprint, string ciphers, string curves, string protocol, string userAgent) + { + Hash = hash; + Fingerprint = fingerprint; + Ciphers = ciphers; + Curves = curves; + Protocol = protocol; + UserAgent = userAgent; + + if (Protocol == "TLSv1.3") { + // uh ja3.zone always return 771,772 for TLSv1.3 + Fingerprint = Fingerprint.Replace("771,", "772,"); + } + } + + [JsonProperty("hash")] + [JsonPropertyName("hash")] + public string Hash { get; set; } + + [JsonProperty("fingerprint")] + [JsonPropertyName("fingerprint")] + public string Fingerprint { get; set; } + + [JsonProperty("ciphers")] + [JsonPropertyName("ciphers")] + public string Ciphers { get; set; } + + [JsonProperty("curves")] + [JsonPropertyName("curves")] + public string Curves { get; set; } + + [JsonProperty("protocol")] + [JsonPropertyName("protocol")] + public string Protocol { get; set; } + + [JsonProperty("user_agent")] + [JsonPropertyName("user_agent")] + public string UserAgent { get; set; } + + + public string NormalizedFingerPrint { + get + { + return TlsFingerPrint.ParseFromJa3(Fingerprint).ToString(true); + } + } + } +} diff --git a/test/Fluxzy.Tests/ProxyTests.cs b/test/Fluxzy.Tests/ProxyTests.cs index 9d663abee..05532765b 100644 --- a/test/Fluxzy.Tests/ProxyTests.cs +++ b/test/Fluxzy.Tests/ProxyTests.cs @@ -541,4 +541,4 @@ await response.Content.ReadAsStringAsync( } } -} +} diff --git a/test/Fluxzy.Tests/Startup.cs b/test/Fluxzy.Tests/Startup.cs index 3c0085c29..aa2051d54 100644 --- a/test/Fluxzy.Tests/Startup.cs +++ b/test/Fluxzy.Tests/Startup.cs @@ -31,6 +31,8 @@ public class Startup : XunitTestFramework public Startup(IMessageSink messageSink) : base(messageSink) { + Environment.SetEnvironmentVariable("SSLKEYLOGFILE", @"e:\poubelle\keylog-net.txt"); + foreach (var fileSystemInfo in new DirectoryInfo(".").EnumerateFileSystemInfos().ToList()) { if (fileSystemInfo is DirectoryInfo directory && Guid.TryParse(directory.Name, out _)) diff --git a/test/Fluxzy.Tests/TlsFingerPrintTests.cs b/test/Fluxzy.Tests/TlsFingerPrintTests.cs new file mode 100644 index 000000000..a7cd9466a --- /dev/null +++ b/test/Fluxzy.Tests/TlsFingerPrintTests.cs @@ -0,0 +1,150 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Fluxzy.Tests._Fixtures; +using System.Text.Json; +using System.Threading.Tasks; +using Fluxzy.Clients.Ssl; +using Fluxzy.Rules.Actions; +using Xunit; +using Fluxzy.Rules.Filters; +using Fluxzy.Rules.Filters.RequestFilters; + +namespace Fluxzy.Tests +{ + public class TlsFingerPrintTests + { + [Theory] + [InlineData("769,49195-49199-49196-49200-52393-52392-52244-52243-49161-49171-49162-49172-156-157-47-53-10,65281-0-23-35-13-5-18-16-11-10-21,29-23-24,0")] + [InlineData("772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,65281-65037-51-45-35-17513-10-11-43-27-5-13-23-18-0-16,4588-29-23-24,0")] + [InlineData("772,,,4588-29-23-24,")] + public void Format_Parse_Unparse(string originalFingerPrint) + { + var fingerPrint = TlsFingerPrint.ParseFromJa3(originalFingerPrint); + var value = fingerPrint.ToString(); + + Assert.Equal(originalFingerPrint, value); + } + + [Theory] + [MemberData(nameof(Ja3FingerPrintTestLoader.LoadTestDataWithoutHosts), MemberType = typeof(Ja3FingerPrintTestLoader))] + public async Task Validate(string clientName, string expectedJa3) + { + var testUrl = "https://check.ja3.zone/"; + + await using var proxy = new AddHocConfigurableProxy(1, 10, + configureSetting : setting => { + setting.UseBouncyCastleSslEngine(); + setting.AddAlterationRulesForAny(new SetJa3FingerPrintAction(expectedJa3)); + }); + + using var httpClient = proxy.RunAndGetClient(); + using var response = await httpClient.GetAsync(testUrl); + + response.EnsureSuccessStatusCode(); + + var responseString = await response.Content.ReadAsStringAsync(); + + var ja3Response = JsonSerializer.Deserialize(responseString); + + Assert.NotNull(ja3Response); + Assert.Equal(expectedJa3, ja3Response.NormalizedFingerPrint); + } + + [Theory()] + [MemberData(nameof(Ja3FingerPrintTestLoader.LoadTestDataWithHosts), MemberType = typeof(Ja3FingerPrintTestLoader))] + public async Task ConnectOnly(string host, string clientName, string expectedJa3) + { + var testUrl = host; + await using var proxy = new AddHocConfigurableProxy(1, 10, + configureSetting: setting => { + setting.UseBouncyCastleSslEngine(); + setting.AddAlterationRulesForAny(new SetJa3FingerPrintAction(expectedJa3)); + + if (string.Equals(Environment.GetEnvironmentVariable("DevSettings"), "true", + StringComparison.OrdinalIgnoreCase)) + { + // for local testing + + setting.AddAlterationRules(new SpoofDnsAction() + { + RemoteHostIp = "142.250.178.132" + }, new HostFilter("google.com", StringSelectorOperation.EndsWith)); + setting.AddAlterationRules(new SpoofDnsAction() + { + RemoteHostIp = "104.16.123.96" + }, new HostFilter("cloudflare.com", StringSelectorOperation.EndsWith)); + } + }); + + using var httpClient = proxy.RunAndGetClient(); + using var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, testUrl )); + + Assert.NotEqual(528, (int)response.StatusCode); + } + } + + public static class Ja3FingerPrintTestLoader + { + public static IEnumerable<(string FriendlyName, TlsFingerPrint FingerPrint)> LoadTestData() + { + var testFile = "_Files/Ja3/fingerprints.txt"; + + var lines = File.ReadAllLines(testFile); + + foreach (var line in lines) { + + if (line.TrimStart(' ').StartsWith("//")) + continue; + + var lineTab = line.Split(";", 2, StringSplitOptions.RemoveEmptyEntries); + + if (lineTab.Length != 2) + { + continue; + } + + var clientName = lineTab[0].Trim(' ', '\t'); + var ja3 = lineTab[1].Trim(' ', '\t'); ; + + var fingerPrint = TlsFingerPrint.ParseFromJa3(ja3); + var normalizedFingerPrint = TlsFingerPrint.ParseFromJa3(fingerPrint.ToString(true)); + + yield return (clientName, normalizedFingerPrint); + } + } + + public static IEnumerable LoadTestDataWithoutHosts() + { + var testDatas = LoadTestData(); + foreach (var testData in testDatas) + { + yield return new object[] { testData.FriendlyName, testData.FingerPrint.ToString() }; + } + } + + public static IEnumerable LoadTestDataWithHosts() + { + var testDatas = LoadTestData(); + var testedHosts = new List { + "https://check.ja3.zone/", + "https://docs.fluxzy.io/nothing", // YARP + "https://www.google.com/nothing", // GOOGLE + "https://extranet.2befficient.fr/nothing", // IIS + "https://www.cloudflare.com/nothing", // BING + "https://www.galerieslafayette.com/nothing", // BING + }; + + foreach (var testData in testDatas) + { + foreach (var host in testedHosts) + { + yield return new object[] { host, testData.FriendlyName, testData.FingerPrint.ToString() }; + } + } + } + } +} \ No newline at end of file diff --git a/test/Fluxzy.Tests/UnitTests/Rules/RequestHeaderAlterationRules.cs b/test/Fluxzy.Tests/UnitTests/Rules/RequestHeaderAlterationRules.cs index 0364f4846..8b616d471 100644 --- a/test/Fluxzy.Tests/UnitTests/Rules/RequestHeaderAlterationRules.cs +++ b/test/Fluxzy.Tests/UnitTests/Rules/RequestHeaderAlterationRules.cs @@ -1,270 +1,270 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Fluxzy.Rules; -using Fluxzy.Rules.Actions; -using Fluxzy.Rules.Filters.RequestFilters; -using Fluxzy.Tests._Fixtures; -using Xunit; - -namespace Fluxzy.Tests.UnitTests.Rules -{ - public class RequestHeaderAlterationRules - { - [Theory] - [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] - public async Task AddNewRequestHeaderWithFilterHostOnly(string host) - { - var headerValue = "anyrandomtexTyoo!!"; - var headerName = "X-Haga-Unit-Test"; - - await using var proxy = new AddHocConfigurableProxy(1, 10); - - proxy.StartupSetting.AddAlterationRules( - new Rule( - new AddRequestHeaderAction( - headerName, headerValue), - new HostFilter("sandbox.smartizy.com"))); - - var endPoint = proxy.Run().First(); - +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Fluxzy.Rules; +using Fluxzy.Rules.Actions; +using Fluxzy.Rules.Filters.RequestFilters; +using Fluxzy.Tests._Fixtures; +using Xunit; + +namespace Fluxzy.Tests.UnitTests.Rules +{ + public class RequestHeaderAlterationRules + { + [Theory] + [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] + public async Task AddNewRequestHeaderWithFilterHostOnly(string host) + { + var headerValue = "anyrandomtexTyoo!!"; + var headerName = "X-Haga-Unit-Test"; + + await using var proxy = new AddHocConfigurableProxy(1, 10); + + proxy.StartupSetting.AddAlterationRules( + new Rule( + new AddRequestHeaderAction( + headerName, headerValue), + new HostFilter("sandbox.smartizy.com"))); + + var endPoint = proxy.Run().First(); + using var clientHandler = new HttpClientHandler - { - Proxy = new WebProxy($"http://{endPoint}") - }; - - using var httpClient = new HttpClient(clientHandler); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, - $"{host}/global-health-check"); - - using var response = await httpClient.SendAsync(requestMessage); - - var checkResult = await response.GetCheckResult(); - - var matchingHeaders = - checkResult.Headers?.Where(h => - h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) - && h.Value == headerValue) - .ToList(); - - Assert.NotNull(matchingHeaders); - Assert.Single(matchingHeaders); - - await proxy.WaitUntilDone(); - } - - [Theory] - [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] - public async Task UpdateRequestHeaderWithFilterHostOnly(string host) - { - var headerName = "X-Haga-Unit-Test"; - var headerValue = "X-Haga-Unit-Test-value!!"; - var headerNewValue = "updated to ABCDef"; - - await using var proxy = new AddHocConfigurableProxy(1, 10); - - proxy.StartupSetting.AddAlterationRules( - new Rule( - new UpdateRequestHeaderAction( - headerName, headerNewValue), - new HostFilter("sandbox.smartizy.com"))); - - var endPoint = proxy.Run().First(); - + { + Proxy = new WebProxy($"http://{endPoint}") + }; + + using var httpClient = new HttpClient(clientHandler); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, + $"{host}/global-health-check"); + + using var response = await httpClient.SendAsync(requestMessage); + + var checkResult = await response.GetCheckResult(); + + var matchingHeaders = + checkResult.Headers?.Where(h => + h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) + && h.Value == headerValue) + .ToList(); + + Assert.NotNull(matchingHeaders); + Assert.Single(matchingHeaders); + + await proxy.WaitUntilDone(); + } + + [Theory] + [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] + public async Task UpdateRequestHeaderWithFilterHostOnly(string host) + { + var headerName = "X-Haga-Unit-Test"; + var headerValue = "X-Haga-Unit-Test-value!!"; + var headerNewValue = "updated to ABCDef"; + + await using var proxy = new AddHocConfigurableProxy(1, 10); + + proxy.StartupSetting.AddAlterationRules( + new Rule( + new UpdateRequestHeaderAction( + headerName, headerNewValue), + new HostFilter("sandbox.smartizy.com"))); + + var endPoint = proxy.Run().First(); + using var clientHandler = new HttpClientHandler - { - Proxy = new WebProxy($"http://{endPoint}") - }; - - using var httpClient = new HttpClient(clientHandler); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, - $"{host}/global-health-check"); - - requestMessage.Headers.Add(headerName, headerValue); - - using var response = await httpClient.SendAsync(requestMessage); - - var checkResult = await response.GetCheckResult(); - - var matchingHeaders = checkResult.Headers?.Where(h => - h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) - && h.Value == headerNewValue) - .ToList(); - - Assert.NotNull(matchingHeaders); - Assert.Single(matchingHeaders); - - await proxy.WaitUntilDone(); - } - - [Theory] - [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] - public async Task UpdateRequestHeaderWithFilterHostOnlyIfMissing(string host) - { - var headerName = "X-Haga-Unit-Test"; - var headerNewValue = "updated to ABCDef"; - - await using var proxy = new AddHocConfigurableProxy(1, 10); - - proxy.StartupSetting.AddAlterationRules( - new Rule( - new UpdateRequestHeaderAction( + { + Proxy = new WebProxy($"http://{endPoint}") + }; + + using var httpClient = new HttpClient(clientHandler); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, + $"{host}/global-health-check"); + + requestMessage.Headers.Add(headerName, headerValue); + + using var response = await httpClient.SendAsync(requestMessage); + + var checkResult = await response.GetCheckResult(); + + var matchingHeaders = checkResult.Headers?.Where(h => + h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) + && h.Value == headerNewValue) + .ToList(); + + Assert.NotNull(matchingHeaders); + Assert.Single(matchingHeaders); + + await proxy.WaitUntilDone(); + } + + [Theory] + [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] + public async Task UpdateRequestHeaderWithFilterHostOnlyIfMissing(string host) + { + var headerName = "X-Haga-Unit-Test"; + var headerNewValue = "updated to ABCDef"; + + await using var proxy = new AddHocConfigurableProxy(1, 10); + + proxy.StartupSetting.AddAlterationRules( + new Rule( + new UpdateRequestHeaderAction( headerName, headerNewValue) - { - AddIfMissing = true - }, - new HostFilter("sandbox.smartizy.com"))); - - var endPoint = proxy.Run().First(); - + { + AddIfMissing = true + }, + new HostFilter("sandbox.smartizy.com"))); + + var endPoint = proxy.Run().First(); + using var clientHandler = new HttpClientHandler - { - Proxy = new WebProxy($"http://{endPoint}") - }; - - using var httpClient = new HttpClient(clientHandler); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, + { + Proxy = new WebProxy($"http://{endPoint}") + }; + + using var httpClient = new HttpClient(clientHandler); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{host}/global-health-check"); - using var response = await httpClient.SendAsync(requestMessage); - - var checkResult = await response.GetCheckResult(); - - var matchingHeaders = checkResult.Headers?.Where(h => - h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) - && h.Value == headerNewValue) - .ToList(); - - Assert.NotNull(matchingHeaders); - Assert.Single(matchingHeaders); - - await proxy.WaitUntilDone(); - } - - [Theory] - [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] - public async Task UpdateRequestHeaderReuseExistingValueWithFilterHostOnly(string host) - { - var headerName = "x-h"; - var headerValue = "Cd"; - var headerNewValue = "{{previous}} Ab"; - var headerValueAltered = "Cd Ab"; - - await using var proxy = new AddHocConfigurableProxy(1, 10); - - proxy.StartupSetting.AddAlterationRules( - new Rule( - new UpdateRequestHeaderAction( - headerName, headerNewValue), - new HostFilter("sandbox.smartizy.com"))); - - var endPoint = proxy.Run().First(); - + using var response = await httpClient.SendAsync(requestMessage); + + var checkResult = await response.GetCheckResult(); + + var matchingHeaders = checkResult.Headers?.Where(h => + h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) + && h.Value == headerNewValue) + .ToList(); + + Assert.NotNull(matchingHeaders); + Assert.Single(matchingHeaders); + + await proxy.WaitUntilDone(); + } + + [Theory] + [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] + public async Task UpdateRequestHeaderReuseExistingValueWithFilterHostOnly(string host) + { + var headerName = "x-h"; + var headerValue = "Cd"; + var headerNewValue = "{{previous}} Ab"; + var headerValueAltered = "Cd Ab"; + + await using var proxy = new AddHocConfigurableProxy(1, 10); + + proxy.StartupSetting.AddAlterationRules( + new Rule( + new UpdateRequestHeaderAction( + headerName, headerNewValue), + new HostFilter("sandbox.smartizy.com"))); + + var endPoint = proxy.Run().First(); + using var clientHandler = new HttpClientHandler - { - Proxy = new WebProxy($"http://{endPoint}") - }; - - using var httpClient = new HttpClient(clientHandler); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, - $"{host}/global-health-check"); - - requestMessage.Headers.Add(headerName, headerValue); - - using var response = await httpClient.SendAsync(requestMessage); - - var checkResult = await response.GetCheckResult(); - - var matchingHeaders = checkResult.Headers?.Where(h => - h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) - && h.Value == headerValueAltered) - .ToList(); - - Assert.NotNull(matchingHeaders); - Assert.Single(matchingHeaders); - - await proxy.WaitUntilDone(); - } - - [Theory] - [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] - public async Task DeleteRequestHeaderWithFilterHostOnly(string host) - { - var headerName = "X-Haga-Unit-Test"; - - await using var proxy = new AddHocConfigurableProxy(1, 10); - - proxy.StartupSetting.AddAlterationRules( - new Rule( - new DeleteRequestHeaderAction(headerName), - new HostFilter("sandbox.smartizy.com"))); - - var endPoint = proxy.Run().First(); - + { + Proxy = new WebProxy($"http://{endPoint}") + }; + + using var httpClient = new HttpClient(clientHandler); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, + $"{host}/global-health-check"); + + requestMessage.Headers.Add(headerName, headerValue); + + using var response = await httpClient.SendAsync(requestMessage); + + var checkResult = await response.GetCheckResult(); + + var matchingHeaders = checkResult.Headers?.Where(h => + h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase) + && h.Value == headerValueAltered) + .ToList(); + + Assert.NotNull(matchingHeaders); + Assert.Single(matchingHeaders); + + await proxy.WaitUntilDone(); + } + + [Theory] + [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] + public async Task DeleteRequestHeaderWithFilterHostOnly(string host) + { + var headerName = "X-Haga-Unit-Test"; + + await using var proxy = new AddHocConfigurableProxy(1, 10); + + proxy.StartupSetting.AddAlterationRules( + new Rule( + new DeleteRequestHeaderAction(headerName), + new HostFilter("sandbox.smartizy.com"))); + + var endPoint = proxy.Run().First(); + using var clientHandler = new HttpClientHandler - { - Proxy = new WebProxy($"http://{endPoint}") - }; - - using var httpClient = new HttpClient(clientHandler); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, - $"{host}/global-health-check"); - - using var response = await httpClient.SendAsync(requestMessage); - - var checkResult = await response.GetCheckResult(); - - var matchingHeaders = checkResult.Headers? - .Where(h => - h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - Assert.NotNull(matchingHeaders); - Assert.Empty(matchingHeaders); - - await proxy.WaitUntilDone(); - } - - [Theory] - [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] - public async Task ChangeMethodFilterHostOnly(string host) - { - await using var proxy = new AddHocConfigurableProxy(1, 10); - - - proxy.StartupSetting.AddAlterationRules( - new Rule( - new ChangeRequestMethodAction("PATCH"), - new HostFilter("sandbox.smartizy.com"))); - - var endPoint = proxy.Run().First(); - + { + Proxy = new WebProxy($"http://{endPoint}") + }; + + using var httpClient = new HttpClient(clientHandler); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, + $"{host}/global-health-check"); + + using var response = await httpClient.SendAsync(requestMessage); + + var checkResult = await response.GetCheckResult(); + + var matchingHeaders = checkResult.Headers? + .Where(h => + h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + Assert.NotNull(matchingHeaders); + Assert.Empty(matchingHeaders); + + await proxy.WaitUntilDone(); + } + + [Theory] + [MemberData(nameof(TestConstants.GetHosts), MemberType = typeof(TestConstants))] + public async Task ChangeMethodFilterHostOnly(string host) + { + await using var proxy = new AddHocConfigurableProxy(1, 10); + + + proxy.StartupSetting.AddAlterationRules( + new Rule( + new ChangeRequestMethodAction("PATCH"), + new HostFilter("sandbox.smartizy.com"))); + + var endPoint = proxy.Run().First(); + using var clientHandler = new HttpClientHandler - { - Proxy = new WebProxy($"http://{endPoint}") - }; - - using var httpClient = new HttpClient(clientHandler); - - var requestMessage = new HttpRequestMessage(HttpMethod.Get, - $"{host}/global-health-check"); - - using var response = await httpClient.SendAsync(requestMessage); - - var checkResult = await response.GetCheckResult(); - - Assert.Equal("PATCH", checkResult.Method, StringComparer.OrdinalIgnoreCase); - - await proxy.WaitUntilDone(); - } - } -} + { + Proxy = new WebProxy($"http://{endPoint}") + }; + + using var httpClient = new HttpClient(clientHandler); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, + $"{host}/global-health-check"); + + using var response = await httpClient.SendAsync(requestMessage); + + var checkResult = await response.GetCheckResult(); + + Assert.Equal("PATCH", checkResult.Method, StringComparer.OrdinalIgnoreCase); + + await proxy.WaitUntilDone(); + } + } +} diff --git a/test/Fluxzy.Tests/UnitTests/Rules/ResponseHeaderAlerationRules.cs b/test/Fluxzy.Tests/UnitTests/Rules/ResponseHeaderAlerationRules.cs index 92216db95..f78f22682 100644 --- a/test/Fluxzy.Tests/UnitTests/Rules/ResponseHeaderAlerationRules.cs +++ b/test/Fluxzy.Tests/UnitTests/Rules/ResponseHeaderAlerationRules.cs @@ -174,4 +174,4 @@ public async Task DeleteResponseHeaderWithFilterHostOnly(string host) await proxy.WaitUntilDone(); } } -} +} diff --git a/test/Fluxzy.Tests/_Files/Ja3/fingerprints.txt b/test/Fluxzy.Tests/_Files/Ja3/fingerprints.txt new file mode 100644 index 000000000..0cc6e97c8 --- /dev/null +++ b/test/Fluxzy.Tests/_Files/Ja3/fingerprints.txt @@ -0,0 +1,5 @@ +CHROME 131 NON ECH ; 772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-43-45-51-17513-65281,4588-29-23-24,0 +CHROME 131 WITH ECH ; 772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,45-0-65037-17513-35-10-13-65281-16-51-23-27-18-43-11-5,4588-29-23-24,0 +FIREFOX 132 ; 772,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,0-23-65281-10-11-16-5-34-51-43-13-28-27-65037,4588-29-23-24-25-256-257,0 +Generic ; 771,4865-4866-4867-49196-49195-52393-49200-49199-52392-49162-49161-49172-49171-157-156-53-47-49160-49170-10,0-23-65281-10-11-16-5-13-18-51-45-43-27-21,29-23-24-25,0 +Firefox 63.0 ; 771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0 \ No newline at end of file diff --git a/test/Fluxzy.Tests/_Fixtures/AddHocConfigurableProxy.cs b/test/Fluxzy.Tests/_Fixtures/AddHocConfigurableProxy.cs index fc638de49..1ace32d53 100644 --- a/test/Fluxzy.Tests/_Fixtures/AddHocConfigurableProxy.cs +++ b/test/Fluxzy.Tests/_Fixtures/AddHocConfigurableProxy.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Fluxzy.Certificates; @@ -22,7 +24,10 @@ public class AddHocConfigurableProxy : IAsyncDisposable private int _requestCount; - public AddHocConfigurableProxy(int expectedRequestCount = 1, int timeoutSeconds = 5) + public AddHocConfigurableProxy( + int expectedRequestCount = 1, int timeoutSeconds = 5, + Action ? configureSetting = null, + ITcpConnectionProvider ? connectionProvider = null) { _expectedRequestCount = expectedRequestCount; @@ -30,11 +35,14 @@ public AddHocConfigurableProxy(int expectedRequestCount = 1, int timeoutSeconds StartupSetting = FluxzySetting .CreateDefault() - .SetBoundAddress(BindHost, BindPort); + .SetBoundAddress(BindHost, 0); + + configureSetting?.Invoke(StartupSetting); InternalProxy = new Proxy(StartupSetting, new CertificateProvider(StartupSetting.CaCertificate, new InMemoryCertificateCache()), - new DefaultCertificateAuthorityManager(), userAgentProvider: new UaParserUserAgentInfoProvider()); + new DefaultCertificateAuthorityManager(), userAgentProvider: new UaParserUserAgentInfoProvider(), + tcpConnectionProvider: connectionProvider); InternalProxy.Writer.ExchangeUpdated += ProxyOnBeforeResponse; @@ -50,12 +58,17 @@ public AddHocConfigurableProxy(int expectedRequestCount = 1, int timeoutSeconds } public Proxy InternalProxy { get; } - - public int BindPort { get; } - + public string BindHost { get; } - public ImmutableList CapturedExchanges => _capturedExchanges.ToImmutableList(); + public ImmutableList CapturedExchanges { + get + { + lock (_capturedExchanges) { + return _capturedExchanges.ToImmutableList(); + } + } + } public FluxzySetting StartupSetting { get; } @@ -70,6 +83,16 @@ public IReadOnlyCollection Run() return InternalProxy.Run(); } + public HttpClient RunAndGetClient() + { + var endPoint = Run().First(); + var clientHandler = new HttpClientHandler(); + clientHandler.Proxy = new WebProxy($"http://{endPoint}"); + + return new HttpClient(clientHandler); + } + + private void ProxyOnBeforeResponse(object? sender, ExchangeUpdateEventArgs exchangeUpdateEventArgs) { if (exchangeUpdateEventArgs.UpdateType != ArchiveUpdateType.AfterResponseHeader) @@ -88,4 +111,4 @@ public Task WaitUntilDone() return _completionSource.Task; } } -} +}