├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE ├── README.md ├── SecureHttpClient.OkHttp ├── SecureHttpClient.OkHttp.csproj ├── Transforms │ ├── EnumFields.xml │ ├── EnumMethods.xml │ └── Metadata.xml └── java │ ├── DecompressInterceptor.java │ ├── HeadersOrderInterceptor.java │ └── buildjar.bat ├── SecureHttpClient.Test ├── CertificatePinnerTest.cs ├── Helpers │ ├── AssertExtensions.cs │ ├── HttpResponseMessageExtensions.cs │ ├── JsonExtensions.cs │ └── ResourceHelper.cs ├── HttpTest.cs ├── SecureHttpClient.Test.csproj ├── SpkiFingerprintTest.cs ├── SslTest.cs ├── TestBase.cs ├── TestFixture.cs └── res │ ├── badssl.com-client.p12 │ ├── dsa_certificate.pem │ ├── ecdsa_certificate.pem │ ├── rsa_certificate.pem │ └── untrusted_root_badssl_com_certificate.pem ├── SecureHttpClient.TestRunner.Maui ├── MauiProgram.cs ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ ├── MainApplication.cs │ │ └── Resources │ │ │ └── xml │ │ │ └── network_security_config.xml │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Package.appxmanifest │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs ├── Properties │ └── launchSettings.json └── SecureHttpClient.TestRunner.Maui.csproj ├── SecureHttpClient.TestRunner.Net ├── Program.cs └── SecureHttpClient.TestRunner.Net.csproj ├── SecureHttpClient.sln ├── SecureHttpClient ├── Abstractions │ ├── IClientCertificateProvider.cs │ └── ISecureHttpClientHandler.cs ├── CertificatePinning │ ├── CertificatePinner.cs │ └── SpkiFingerprint.cs ├── Extensions │ └── RequestPropertiesExtensions.cs ├── Platforms │ ├── Android │ │ ├── ClientCertificateProvider.cs │ │ ├── SecureHttpClientHandler.cs │ │ ├── SpkiProvider.cs │ │ └── TlsSslSocketFactory.cs │ ├── Net │ │ ├── ClientCertificateProvider.cs │ │ ├── SecureHttpClientHandler.cs │ │ └── SpkiProvider.cs │ └── iOS │ │ ├── AsyncLock.cs │ │ ├── ByteArrayListStream.cs │ │ ├── CancellableStreamContent.cs │ │ ├── ClientCertificateProvider.cs │ │ ├── DataTaskDelegate.cs │ │ ├── EmptyDisposable.cs │ │ ├── InflightOperation.cs │ │ ├── ProgressStreamContent.cs │ │ ├── SecureHttpClientHandler.cs │ │ ├── SetCookieHeaderSplitter.cs │ │ └── SpkiProvider.cs └── SecureHttpClient.csproj ├── build └── SecureHttpClient.nuspec ├── global.json ├── icon.png ├── scripts ├── clean.bat └── nuget_pack.bat └── version.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | SecureHttpClient.OkHttp/Jars/ 5 | SecureHttpClient.OkHttp/import/ 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | **/[Rr]esource.[Dd]esigner.cs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # NCrunch 118 | _NCrunch_* 119 | .*crunch*.local.xml 120 | nCrunchTemp_* 121 | 122 | # MightyMoose 123 | *.mm.* 124 | AutoTest.Net/ 125 | 126 | # Web workbench (sass) 127 | .sass-cache/ 128 | 129 | # Installshield output folder 130 | [Ee]xpress/ 131 | 132 | # DocProject is a documentation generator add-in 133 | DocProject/buildhelp/ 134 | DocProject/Help/*.HxT 135 | DocProject/Help/*.HxC 136 | DocProject/Help/*.hhc 137 | DocProject/Help/*.hhk 138 | DocProject/Help/*.hhp 139 | DocProject/Help/Html2 140 | DocProject/Help/html 141 | 142 | # Click-Once directory 143 | publish/ 144 | 145 | # Publish Web Output 146 | *.[Pp]ublish.xml 147 | *.azurePubxml 148 | # TODO: Comment the next line if you want to checkin your web deploy settings 149 | # but database connection strings (with potential passwords) will be unencrypted 150 | *.pubxml 151 | *.publishproj 152 | 153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 154 | # checkin your Azure Web App publish settings, but sensitive information contained 155 | # in these scripts will be unencrypted 156 | PublishScripts/ 157 | 158 | # NuGet Packages 159 | *.nupkg 160 | *.snupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directories and files 180 | AppPackages/ 181 | BundleArtifacts/ 182 | Package.StoreAssociation.xml 183 | _pkginfo.txt 184 | 185 | # Visual Studio cache files 186 | # files ending in .cache can be ignored 187 | *.[Cc]ache 188 | # but keep track of directories ending in .cache 189 | !*.[Cc]ache/ 190 | 191 | # Others 192 | ClientBin/ 193 | ~$* 194 | *~ 195 | *.dbmdl 196 | *.dbproj.schemaview 197 | *.pfx 198 | *.publishsettings 199 | node_modules/ 200 | orleans.codegen.cs 201 | 202 | # Since there are multiple workflows, uncomment next line to ignore bower_components 203 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 204 | #bower_components/ 205 | 206 | # RIA/Silverlight projects 207 | Generated_Code/ 208 | 209 | # Backup & report files from converting an old project file 210 | # to a newer Visual Studio version. Backup files are not needed, 211 | # because we have git ;-) 212 | _UpgradeReport_Files/ 213 | Backup*/ 214 | UpgradeLog*.XML 215 | UpgradeLog*.htm 216 | 217 | # SQL Server files 218 | *.mdf 219 | *.ldf 220 | 221 | # Business Intelligence projects 222 | *.rdl.data 223 | *.bim.layout 224 | *.bim_*.settings 225 | 226 | # Microsoft Fakes 227 | FakesAssemblies/ 228 | 229 | # GhostDoc plugin setting file 230 | *.GhostDoc.xml 231 | 232 | # Node.js Tools for Visual Studio 233 | .ntvs_analysis.dat 234 | 235 | # Visual Studio 6 build log 236 | *.plg 237 | 238 | # Visual Studio 6 workspace options file 239 | *.opt 240 | 241 | # Visual Studio LightSwitch build output 242 | **/*.HTMLClient/GeneratedArtifacts 243 | **/*.DesktopClient/GeneratedArtifacts 244 | **/*.DesktopClient/ModelManifest.xml 245 | **/*.Server/GeneratedArtifacts 246 | **/*.Server/ModelManifest.xml 247 | _Pvt_Extensions 248 | 249 | # Paket dependency manager 250 | .paket/paket.exe 251 | paket-files/ 252 | 253 | # FAKE - F# Make 254 | .fake/ 255 | 256 | # JetBrains Rider 257 | .idea/ 258 | *.sln.iml 259 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.3.1 2 | - set isAotCompatible=true 3 | - test : Microsoft.Maui.* 9.0.10 4 | 5 | ## 2.3.0 6 | - vs : 17.12.0 7 | - dotnet sdk 9.0.100 8 | - android 35.0.7 9 | - ios 18.0.9617 10 | - C# 13.0 11 | - Microsoft.Extensions.Logging.Abstractions 9.0.0 12 | - test : Microsoft.Extensions.DependencyInjection 9.0.0 13 | - test : Microsoft.Extensions.Logging 9.0.0 14 | - test : Microsoft.Maui.* 9.0.0 15 | - test : System.Text.Json 9.0.0 16 | 17 | ## 2.2.7 18 | - vs : 17.11.5 (dotnet sdk 8.0.403 ; android 34.0.113 ; ios 18.0.8303) 19 | - Microsoft.Extensions.Logging.Abstractions 8.0.2 20 | - Square.OkHttp3 4.12.0.7 21 | - Square.OkHttp3.UrlConnection 4.12.0.7 22 | - Square.OkIO 3.9.1.1 23 | - kotlin-stdlib 2.0.21 (jar) 24 | - test : Microsoft.Extensions.DependencyInjection 8.0.1 25 | - test : Microsoft.Extensions.Logging 8.0.1 26 | - test : Microsoft.Maui.* 8.0.93 27 | - test : Serilog 4.1.0 28 | - test : System.Text.Json 8.0.5 29 | - test : xunit & xunit.runner.utility 2.9.2 30 | 31 | ## 2.2.6 32 | - vs : 17.10.0 (dotnet sdk 8.0.300) 33 | - android workload 34.0.95 ; ios workload 17.2.8053 34 | - BouncyCastle.Cryptography 2.4.0 35 | - Square.OkHttp3 4.12.0.4 36 | - Square.OkHttp3.UrlConnection 4.12.0.4 37 | - Square.OkIO 3.9.0 38 | - kotlin-stdlib 2.0.0 (jar) 39 | - test : Microsoft.Maui.* 8.0.60 40 | - test : Serilog 4.0.0 41 | - test : Serilog.Sinks.Console 6.0.0 42 | - test : Serilog.Sinks.Debug 3.0.0 43 | - test : xunit 2.8.1 44 | - test : xunit.runner.utility 2.8.1 45 | 46 | ## 2.2.5 47 | - vs : 17.9.3 (dotnet sdk 8.0.202 ; maui 8.0.7 ; android 34.0.52 ; ios 17.2.8004) 48 | - Microsoft.Extensions.Logging.Abstractions 8.0.1 49 | - test : Microsoft.Maui.* 8.0.10 50 | - test : System.Text.Json 8.0.3 51 | 52 | ## 2.2.4 53 | - ios 17.2 54 | - add code of conduct 55 | - add nuget package icon 56 | - enable deterministic build 57 | - generate symbols package 58 | - update readme file 59 | - test : fix pin 60 | 61 | ## 2.2.3 62 | - vs : 17.9.0 (dotnet sdk 8.0.200 ; maui 8.0.6 ; android 34.0.52 ; ios 17.2.8004) 63 | - xcode 15.2 (ios 17.2) 64 | - BouncyCastle.Cryptography 2.3.0 (instead of Portable.BouncyCastle) 65 | - test : fix badssl client certificate 66 | - test : Microsoft.Maui.* 8.0.7 67 | - test : Serilog.Sinks.Console 5.0.1 68 | - test : System.Text.Json 8.0.2 69 | - test : xunit 2.7.0 70 | - test : xunit.runner.utility 2.7.0 71 | 72 | ## 2.2.2 73 | - add brotli 74 | - set response version on android 75 | - fix resource not closed in android decompressinterceptor 76 | - fix deflate in android decompressinterceptor (rfc 1950 vs 1951) 77 | - build : add readme to nuspec 78 | - Square.OkHttp3 4.12.0.1 79 | - Square.OkHttp3.UrlConnection 4.12.0.1 80 | - Square.OkIO 3.6.0.1 81 | - test : fix pin 82 | 83 | ## 2.2.1 84 | - Square.OkHttp3 4.12.0 85 | - Square.OkHttp3.UrlConnection 4.12.0 86 | - Square.OkIO 3.6.0 87 | - test : xunit 2.6.2 88 | - test : xunit.runner.utility 2.6.2 89 | 90 | ## 2.2.0 91 | - vs : 17.8.0 (dotnet sdk 8.0.100 ; maui 8.0.3 ; android 34.0.43 ; ios 17.0.8478) 92 | - dotnet sdk 8.0.100 93 | - target net8 94 | - C# 12.0 95 | - android 14 (api 34) 96 | - xcode 15.0 (ios 17.0) 97 | - fix ios test 98 | - Microsoft.Extensions.Logging.Abstractions 8.0.0 99 | - Square.OkHttp3 4.11.0.3 100 | - Square.OkHttp3.UrlConnection 4.11.0.3 101 | - Square.OkIO 3.5.0.1 102 | - test : fix pin 103 | - test : Microsoft.Extensions.* 8.0.0 104 | - test : Serilog 3.1.1 105 | - test : Serilog.Extensions.Logging 8.0.0 106 | - test : Serilog.Sinks.Console 5.0.0 107 | - test : System.Text.Json 8.0.0 108 | - test : xunit 2.6.1 109 | - test : xunit.runner.utility 2.6.1 110 | 111 | ## 2.1.3 112 | - vs : 17.5.4 (maui 7.0.86 ; android 33.0.46 ; ios 16.4.7054) 113 | - dotnet sdk 7.0.302 114 | - Square.OkHttp3 4.11.0.1 115 | - Square.OkHttp3.UrlConnection 4.11.0.1 116 | - Square.OkIO 3.3.0.1 117 | - test : use httpbingo instead of httpbin 118 | - test : Serilog.Extensions.Logging 7.0.0 119 | - test : System.Text.Json 7.0.2 120 | 121 | ## 2.1.2 122 | - vs : 17.5.4 (maui 7.0.81 ; android 33.0.46 ; ios 16.2.2035) 123 | - dotnet sdk 7.0.203 124 | - add headers order (android only) 125 | 126 | ## 2.1.1 127 | - vs : 17.5.0 (maui 7.0.59 ; android 33.0.26 ; ios 16.2.1024) 128 | - dotnet sdk 7.0.200 129 | - xcode 14.2 (ios 16.2) 130 | - test : fix pin 131 | - test trimmode full 132 | 133 | ## 2.1.0 134 | - vs : 17.4.3 (xamarin.vs 17.4.0.312 ; xamarin.android 13.1.0.1 ; xamarin.ios 16.1.1.27 ; maui 7.0.52/7.0.100) 135 | - donet sdk 7.0.101 136 | - target net7.0 137 | - C# 11.0 138 | - xcode 14.1 (ios 16.1) 139 | - fix wrong architecture for ios dll in nuget 140 | - fix tests 141 | - Microsoft.Extensions.Logging.Abstractions 7.0.0 142 | - test : Microsoft.Extensions.* 7.0.0 143 | - test : System.Text.Json 7.0.1 (remove Newtonsoft.Json) 144 | 145 | ## 2.0.1 146 | - add net6.0 target and remove net6.0-windows target in libs 147 | - get rid of singleproject/usemaui in csproj when possible 148 | - fix tests 149 | - test : add again testrunner.net 150 | 151 | ## 2.0.0 152 | - vs : 17.3.6 (xamarin.vs 17.3.0.308 ; xamarin.android 13.0.0.0 ; xamarin.ios 16.0.0.75) 153 | - dotnet sdk 6.0.400 154 | - xcode 14.0.1 (ios 16.0) 155 | - migrate projects to net6.0-android31.0, net6.0-ios15.4, net6.0-windows10.0.19041.0 156 | - kotlin-stdlib 1.6.21 (jar) 157 | - Microsoft.Extensions.Logging.Abstractions 6.0.2 158 | - Square.OkHttp3 4.9.3.2 159 | - Square.OkHttp3.UrlConnection 4.9.3.2 160 | - Square.OkIO 2.10.0.5 161 | - build : use nuspec file instead of NuGetizer 162 | - test : migrate testrunner to maui single project 163 | - test : fix pins 164 | - test : add spkifingerprint tests 165 | - test : Shiny.Xunit.Runners.Maui 1.0.0 166 | - test : Serilog.Sinks.Xamarin 1.0.0 167 | - test : xunit 2.4.2 168 | 169 | ## 1.18.8 170 | - vs : 17.3.6 (xamarin.vs 17.3.0.308 ; xamarin.android 13.0.0.0 ; xamarin.ios 16.0.0.75) 171 | - xcode 14.0.1 (ios 16.0) 172 | - dotnet sdk 6.0.400 173 | - Microsoft.Extensions.* 6.0.2 174 | - kotlin-stdlib 1.6.21 (jar) 175 | - Square.OkHttp3 4.9.3.2 176 | - Square.OkHttp3.UrlConnection 4.9.3.2 177 | - Square.OkIO 2.10.0.5 178 | - fix proguard.cfg 179 | - build : NuGetizer 0.9.0 180 | - test : fix pin 181 | - test : Serilog 2.12.0 182 | - test : Serilog.Sinks.Console 4.1.0 183 | - test : Serilog.Sinks.Xamarin 1.0.0 184 | - test : Xamarin.Forms 5.0.0.2515 185 | - test : xunit 2.4.1 186 | - test : xunit.runner.utility 2.4.2 187 | 188 | ## 1.18.7 189 | - vs : 17.2.4 (xamarin.vs 17.2.0.177 ; xamarin.android 12.3.3.3 ; xamarin.ios 15.10.0.5) 190 | - xcode 13.4.1 (ios 15.5) 191 | - android 12L (api 32) 192 | - dotnet sdk 6.0.300 193 | - MSBuild.Sdk.Extras 3.0.44 194 | - fix certificate pins in tests 195 | - Square.OkHttp3 4.9.3.1 196 | - Square.OkHttp3.UrlConnection 4.9.3.1 197 | - Square.OkIO 2.10.0.4 198 | - build : NuGetizer 0.8.0 199 | - test : Serilog 2.11.0 200 | - test : Xamarin.Essentials 1.7.3 201 | - test : Xamarin.Forms 5.0.0.2478 202 | 203 | ## 1.18.6 204 | - vs : 17.1.1 (xamarin.vs 17.1.0.309 ; xamarin.android 12.2.0.4 ; xamarin.ios 15.6.0.3) 205 | - dotnet sdk 6.0.200 206 | - xcode 13.2 (ios 15.2) 207 | - Microsoft.Extensions.* 6.0.1 208 | - fix nuget/project references in release/debug configurations (get rid of bait and switch) 209 | - fix tests 210 | - SquareUp.OkHttp3 4.9.3 211 | - SquareUp.OkHttp3.UrlConnection 4.9.3 212 | - test : Serilog.Sinks.Console 4.0.1 213 | - test : Xamarin.Essentials 1.7.1 214 | - test : Xamarin.Forms 5.0.0.2337 215 | 216 | ## 1.18.5 217 | - vs : 17.0.0 (xamarin.vs 17.0.0.336 ; xamarin.android 12.1.0.5 ; xamarin.ios 15.0.0.18) 218 | - dotnet sdk 6.0.100 219 | - build : centralize securehttpclient nuget version in directory.build.targets 220 | - C# 10.0 221 | - Microsoft.Extensions.* 6.0.0 222 | - Portable.BouncyCastle 1.9.0 223 | - SquareUp.OkHttp3 4.9.2 224 | - SquareUp.OkHttp3.UrlConnection 4.9.2 225 | - test : Serilog.Extensions.Logging 3.1.0 226 | - test : Xamarin.Forms 5.0.0.2196 227 | - test : .net6 228 | 229 | ## 1.18.4 230 | - vs : 17.0.0-rc (xamarin.vs 17.0.0.315 ; xamarin.android 12.1.0.4 ; xamarin.ios 15.0.0.8) 231 | - xcode 13.0 (ios 15.0) 232 | - android 12 (api 31) 233 | - dotnet sdk 5.0.400 234 | - fix java unhandled exceptions on android by porting DecompressInterceptor c# code to java 235 | - build : NuGetizer 0.7.5 236 | - test : Microsoft.Extensions.DependencyInjection 5.0.2 237 | - test : Serilog.Sinks.Console 4.0.0 238 | - test : Xamarin.Essentials 1.7.0 239 | - test : Xamarin.Forms 5.0.0.2125 240 | 241 | ## 1.18.3 242 | - use official square bindings instead of forked ones 243 | - remove android's sslsocketfactory code that is not used anymore 244 | 245 | ## 1.18.2 246 | - vs : 16.10.0 (xamarin.vs 16.10.000.228 ; xamarin.android 11.3.0.1 ; xamarin.ios 14.20.0.1) 247 | - dotnet sdk 5.0.300 248 | - xcode 12.5 (ios 14.5) 249 | - test : fix pin 250 | - test : Newtonsoft.Json 13.0.1 251 | - test : Xamarin.Kotlin.StdLib 1.5.0.1 252 | - build : use nugetizer 0.7.0 and get rid of manually updated .nuspec file 253 | 254 | ## 1.18.1 255 | - vs : 16.8.6 (xamarin.vs 16.8.000.262 ; xamarin.android 11.1.0.26 ; xamarin.ios 14.10.0.4) 256 | - iOS: import full certificate chain instead of only the last one 257 | - Portable.BouncyCastle 1.8.10 258 | 259 | ## 1.18.0 260 | - fix support for AllowAutoRedirect=false (was missing on android) 261 | - fix support for UseCookies=false (was missing on android and ios) 262 | - fix support for UseProxy=false (was missing on android and ios) 263 | - fix error on android and ios when get request has an empty body 264 | - fix parsing of set-cookie header with folding on ios 265 | - ios now supports both system's proxy and httpclienthandler's proxy 266 | - test : Xamarin.Forms 5.0.0.2012 267 | 268 | ## 1.17.5 269 | - vs : 16.8.5 (xamarin.vs 16.8.000.262 ; xamarin.android 11.1.0.26 ; xamarin.ios 14.10.0.4) 270 | - xcode 12.4 (ios 14.4) 271 | - Microsoft.Extensions.* 5.0.x 272 | 273 | ## 1.17.4 274 | - fix nuspec 275 | 276 | ## 1.17.3 277 | - remove duplicate code 278 | - Xamarin.SquareUp.OkHttp3 4.9.1 279 | - Xamarin.SquareUp.OkHttp3.UrlConnection 4.9.1 280 | - test : fix pin 281 | 282 | ## 1.17.2 283 | - add buildTransitive for nuget targets 284 | - test : Xamarin.Essentials 1.6.1 285 | - test : Xamarin.Forms 5.0.0.1931 286 | 287 | ## 1.17.1 288 | - improve handling of timeout and unknownhost exceptions on android 289 | - Xamarin.SquareUp.Okio 2.10.0 290 | 291 | ## 1.17.0 292 | - vs : 16.8.4 (xamarin.vs 16.8.000.261 ; xamarin.android 11.1.0.26 ; xamarin.ios 14.8.0.3) 293 | - Portable.BouncyCastle 1.8.9 294 | - netstandard2.1 295 | - compute spki without bouncycastle thanks to netstandard2.1 on .net 296 | - C# 9.0 297 | - dotnet sdk 5.0.101 298 | - xcode 12.3 (ios 14.3) 299 | - Microsoft.Extensions.* 3.1.11 300 | - MSBuild.Sdk.Extras 3.0.23 301 | - test : remove UWP project 302 | - test : netcoreapp5.0 303 | - test : Xamarin.Kotlin.StdLib 1.4.20 304 | - test : Xamarin.Essentials 1.6.0 305 | - test : Xamarin.Forms 5.0.0.1874 306 | 307 | ## 1.16.1 308 | - vs : 16.8.3 (xamarin.vs 16.8.000.260 ; xamarin.android 11.1.0.17 ; xamarin.ios 14.6.0.15) 309 | - xcode 12.2 (ios 14.2) 310 | - MSBuild.Sdk.Extras 3.0.22 311 | - test : Microsoft.NETCore.UniversalWindowsPlatform 6.2.11 312 | 313 | ## 1.16.0 314 | - android now supports both system's proxy and httpclienthandler's proxy 315 | - fix decompression (deflate and gzip) on android 316 | - get rid of specific code for android version < 21 317 | - test : add android network security config in order to trust user ca and simplify http traffic inspection 318 | 319 | ## 1.15.0 320 | - vs : 16.8.0 (xamarin.vs 16.8.000.255 ; xamarin.android 11.1.0.17 ; xamarin.ios 14.4.1.3) 321 | - fix msbuild warnings (ignore VSX1000 warning ; use license instead of licenseUrl in nuspec) 322 | - Microsoft.Extensions.Logging.Abstractions 3.1.10 323 | - Xamarin.SquareUp.OkHttp3 4.9.0 324 | - Xamarin.SquareUp.OkHttp3.UrlConnection 4.9.0 325 | - Xamarin.SquareUp.Okio 2.9.0 326 | - android 11 327 | - xcode 12.1 & ios 14.1 328 | - test : Xamarin.Kotlin.StdLib 1.3.61 329 | - test : remove Flurl 330 | - test : netcoreapp3.1 331 | - test : Microsoft.Extensions.DependencyInjection 3.1.10 332 | - test : Microsoft.Extensions.Logging 3.1.10 333 | - test : Xamarin.Forms 4.8.0.1687 334 | 335 | ## 1.14.1 336 | - vs : 16.7.6 (xamarin.vs 16.7.000.456 ; xamarin.android 11.0.2.0 ; xamarin.ios 14.00.0.0) 337 | - .net core sdk 3.1.400 338 | - ios : xcode 12.0 (ios 14.0) 339 | - fix tests 340 | - MSBuild.Sdk.Extras 2.1.2 341 | - Microsoft.Extensions.Logging.Abstractions 3.1.9 342 | - Portable.BouncyCastle 1.8.8 343 | - test : Xamarin.Essentials 1.5.3.2 344 | - test : Xamarin.Forms 4.8.0.1534 345 | - test : Xunit.SkippableFact 1.4.13 346 | - test : Microsoft.Extensions.DependencyInjection 3.1.9 347 | - test : Microsoft.Extensions.Logging 3.1.9 348 | - test : Serilog 2.10.0 349 | - test : reference nuget only in release 350 | 351 | ## 1.14.0 352 | - vs : 16.6.0 preview 4.0 (xamarin.vs 16.6.000.1052 ; xamarin.android 10.3.0.74 ; xamarin.ios 13.18.1.31) 353 | - make logger mandatory (required for DI scenario) 354 | - test : add UWP test runner 355 | - test : add test fixture 356 | - test : Xamarin.Essentials 1.5.3.1 357 | - test : Xamarin.Forms 4.6.0.726 358 | - test : Xunit.SkippableFact 1.4.8 359 | - test : Serilog.Sinks.Xamarin 0.2.0.64 360 | - test : Microsoft.Extensions.DependencyInjection 3.1.3 361 | 362 | ## 1.13.7 363 | - vs : 16.6.0 preview 2.1 (xamarin.vs 16.6.000.984 ; xamarin.android 10.3.0.33 ; xamarin.ios 13.18.0.22) 364 | - .net core sdk 3.0.200 365 | - add tests for ecc certificates 366 | - Microsoft.Extensions.Logging.Abstractions 3.1.3 367 | - Portable.BouncyCastle 1.8.6.7 368 | - test : Xamarin.Essentials 1.5.2 369 | - test : Xamarin.Forms 4.5.0.617 370 | 371 | ## 1.13.6 372 | - vs : 16.5.0 preview 4.0 (xamarin.vs 16.5.000.468 ; xamarin.android 10.2.0.99 ; xamarin.ios 13.14.1.38) 373 | - Microsoft.Extensions.Logging.Abstractions 3.1.2 374 | - Portable.BouncyCastle 1.8.6 375 | - test : Xamarin.Essentials 1.5.0 376 | - test : Xamarin.Forms 4.5.0.356 377 | 378 | ## 1.13.5 379 | - fix nuspec 380 | 381 | ## 1.13.4 382 | - vs : 16.5.0 preview 2.0 (xamarin.vs 16.5.000.400 ; xamarin.android 10.2.0.84 ; xamarin.ios 13.14.1.17) 383 | - C# 8.0 384 | - Microsoft.Extensions.Logging.Abstractions 3.1.1 385 | - test : Xamarin.Forms 4.3.0.991640 386 | 387 | ## 1.13.3 388 | - vs : 16.5.0 preview 1.0 (xamarin.vs 16.5.000.307 ; xamarin.android 10.2.0.16 ; xamarin.ios 13.14.0.6) 389 | - test : Xamarin.Forms 4.4.0.991265 390 | - add proguard.cfg to nuget package 391 | 392 | ## 1.13.2 393 | - vs : 16.3.10 (xamarin.vs 16.3.0.281 ; xamarin.android 10.0.6.2 ; xamarin.ios 13.6.0.12) 394 | - Microsoft.Extensions.Logging.Abstractions 3.1.0 395 | - Portable.BouncyCastle 1.8.5.2 396 | - test : Newtonsoft.Json 12.0.3 397 | - test : Xamarin.Forms 4.3.0.991211 398 | - test : move certs to resources 399 | - test : add http2 check 400 | 401 | ## 1.13.1 402 | - fix exception mess 403 | - test project cleanup 404 | - test : Xunit.SkippableFact 1.3.12 405 | - test : Xamarin.Essentials 1.3.1 406 | - test : Xamarin.Forms 4.3.0.908675 407 | - test : Flurl.Http 2.4.2 408 | 409 | ## 1.13.0 410 | - vs : 16.3.6 (xamarin.vs 16.3.0.277 ; xamarin.android 10.0.3.0 ; xamarin.ios 13.4.0.2) 411 | - .net core sdk 3.0.100 412 | - MSBuild.Sdk.Extras 2.0.54 413 | - add build files to solution 414 | - portable pdb 415 | - test on android 10 and ios 13 416 | - netcoreapp3.0 417 | - fix pin in tests 418 | - fix expected tls version in tests 419 | - Serilog 2.9.0 420 | - Serilog.Sinks.Xamarin 0.1.37 421 | - Serilog.Extensions.Logging 3.0.1 422 | - Microsoft.Extensions.Logging.Abstractions 3.0.0 423 | - Square.OkHttp3 3.14.4 424 | - Square.Okio 1.17.4 425 | - Square.OkHttp3.UrlConnection 3.12.3 426 | - removed Karamunting.Square.* 427 | 428 | ## 1.12.4 429 | - finally fixing properly the redirect uri bug on android 430 | 431 | ## 1.12.3 432 | - fix missing data in reponse's last request for android 433 | 434 | ## 1.12.2 435 | - vs : 16.2.0 preview 3.0 (xamarin.vs 16.2.0.81 ; xamarin.android 9.4.0.34 ; xamarin.ios 12.14.0.93) 436 | - android: make sure the request url in the response corresponds to the last redirect url 437 | - MSBuild.Sdk.Extras 2.0.29 438 | 439 | ## 1.12.1 440 | - vs : 16.2.0 preview 2.0 (xamarin.vs 16.2.0.61 ; xamarin.android 9.4.0.17 ; xamarin.ios 12.14.0.83) 441 | - .net core sdk 2.1.700 442 | - Karamunting.Android.Square.OkHttp 3.14.2 443 | - Karamunting.Android.Square.Okio 1.17.4 444 | - Karamunting.Square.OkHttp3.UrlConnection 3.14.2 445 | - Newtonsoft.Json 12.0.2 446 | - Serilog.Extensions.Logging 2.0.4 447 | 448 | ## 1.12.0 449 | - vs 2019 : 16.0.0 preview 4.3 (xamarin.vs 16.0.0.513 ; xamarin.android 9.2.0.5 ; xamarin.ios 12.6.0.23 ; vs for mac 8.0 build 2931) 450 | - global.json dotnet sdk 2.1.602 451 | - MSBuild.Sdk.Extras 1.6.68 452 | - Portable.BouncyCastle 1.8.5 453 | - Serilog 2.8.0 454 | - Newtonsoft.Json 12.0.1 455 | - Microsoft.Extensions.Logging.Abstractions 2.2.0 456 | - xunit 2.4.1 457 | - xunit.runner.utility 2.4.1 458 | - xunit.runner.devices 2.5.25 459 | - fix certificate pin in tests 460 | - Karamunting.Android.Square.OkHttp 3.14.0 461 | - Karamunting.Android.Square.Okio 1.17.3 462 | - Karamunting.Square.OkHttp3.UrlConnection 3.14.0 463 | - min Android version 21 (for okhttp 3.13.0) 464 | - clean android test csproj ; use d8 465 | 466 | ## 1.11.0 467 | - xamarin : 15.8.5 (xamarin.vs 4.11.0.776 ; xamarin.android 9.0.0.19 ; xamarin.ios 12.0.0.15 ; vs for mac 7.6.8.38) 468 | - MSBuild.Sdk.Extras 1.6.55 469 | - Portable.BouncyCastle 1.8.3 470 | - xunit 2.4.0 471 | - add global.json for .net sdk version 2.1.402 472 | - support Android 9.0 and iOS 12 473 | - use JavaNetCookieJar from OkHttp3.UrlConnection 474 | 475 | ## 1.10.0 476 | - fix android Set-Cookie header issue (#7) 477 | - ios : xcode 9.4.1 (ios 11.4) 478 | - abstract client certificates into certificate providers (#6) 479 | 480 | ## 1.9.0 481 | - xamarin : 15.7.3 (xamarin.vs 4.10.10.1 ; xamarin.android 8.3.3.2 ; xamarin.ios 11.12.0.4 ; mono 5.10.1.57 ; vs for mac 7.5.2.40) 482 | - add support for setting custom Root CAs 483 | - Portable.BouncyCastle 1.8.2 484 | 485 | ## 1.8.0 486 | - xamarin : 15.7.2 (xamarin.vs 4.10.0.448 ; xamarin.android 8.3.0.19 ; xamarin.ios 11.10.1.178 ; mono 5.10.1.47 ; vs for mac 7.5.1.22) 487 | - fix certificatepinner test 488 | - better logging with ILogger (and Serilog in test runners) 489 | - use multi-targeting for source project 490 | - use default debugtype (portable) as it's now supported by xamarin 491 | - simplify nuget pack 492 | - add xmldoc 493 | 494 | ## 1.7.0 495 | - add support for client certificates (by gtbX) 496 | - xamarin : 15.6.5 servicing release (xamarin.vs 4.9.0.753 ; xamarin.android 8.2.0.16 ; xamarin.ios 11.9.1.24 ; mono 5.8.1.0 ; vs for mac 7.4.2.12) 497 | - Portable.BouncyCastle 1.8.1.4 498 | - Newtonsoft.Json 11.0.2 499 | - support Android 8.1 500 | - android : build-tools 27.0.3, platform-tools 27.0.1 501 | - ios : xcode 9.3 502 | - netstandard 2.0 ; clean csproj ; fix warnings 503 | - fix certificatepinner test 504 | - xUnit 2.3.1 505 | - Square.OkHttp3 3.8.1 ; Square.OkIO 1.13.0 506 | 507 | ## 1.6.0 508 | - xamarin : 15.4 stable (xamarin.vs 4.7.10.22 ; xamarin.android 8.0.0.33 ; xamarin.ios 11.2.0.8 ; mono 5.4.0.201) 509 | - android : build-tools 26.0.2, platform-tools 26.0.1 510 | - support Android 8.0 511 | - support iOS 11 512 | - fix pin change in certificate test 513 | 514 | ## 1.5.1 515 | - security fix for iOS 516 | - fix android certificate pinner when adding several hostnames 517 | - change architecture for iOS 518 | - more certificatePinner tests 519 | 520 | ## 1.5.0 521 | - several bug fixes 522 | - simplify version management 523 | - migration for VS2017 & VS For Mac 524 | - xamarin : xamarin 4.5.0.486 ; xamarin.android 7.3.1.2 ; xamarin.ios 10.10.0.37 ; mono 5.0.1 525 | - android : build-tools 26, platform-tools 26 526 | - Portable.BouncyCastle 1.8.1.2 527 | - Newtonsoft.Json 10.0.3 528 | - xunit 2.2.0 529 | - Square.OkHttp3 3.5.0 ; Square.OkIO 1.11.0 530 | - support Android 7.1 531 | 532 | ## 1.4.2 533 | - netstandard 1.6.1 534 | - netcoreapp 1.1.0 535 | - system.net.requests 4.3.0 536 | - fix iOS projects order (xamarin bug #44887) 537 | 538 | ## 1.4.1 539 | - test : fix delete cookie test 540 | - android : better implementation of cookiejar 541 | 542 | ## 1.4.0 543 | - xamarin : upgrade to cycle 8 sr1 stable (xamarin.vs 4.2.1.60 ; xamarin.android 7.0.2.37 ; xamarin.ios 10.2.1.5 ; mono 4.6.2.7) 544 | - test : add delete cookie test 545 | - test : fix pin in ssl test following certificate change 546 | - android : fix delete cookie 547 | 548 | ## 1.3.0 549 | - xamarin : upgrade to cycle 8 sr0 stable update (xamarin.vs 4.2.0.703 ; xamarin.android 7.0.1.3 ; xamarin.ios 10.0.1.10 ; mono 4.6.1.5) 550 | - rename project : NativeHttpClient -> SecureHttpClientHandler 551 | - iOS / portable : certificate pinner is not static anymore 552 | - test : fix certificate pinner tests 553 | - test : autostart 554 | - get rid of proxy 555 | - Square.OkIO 1.10.0 556 | - Square.OkHttp3 3.4.1.1 557 | - Microsoft.NETCore.App 1.0.1 558 | 559 | ## 1.2.3 560 | - build : modify test csproj to delete nuget.props file after build 561 | - test : add http tests 562 | 563 | ## 1.2.2 564 | - add certificatePinning project (for iOS and netstandard) 565 | - iOS : add certificate pinning 566 | 567 | ## 1.2.1 568 | - xamarin : upgrade to cycle 8 sr0 beta (xamarin.vs 4.2.0.688 ; xamarin.android 7.0.1.0 ; xamarin.ios 10.0.1.5 ; mono 4.6.0.251) 569 | - xamarin : upgrade to cycle 8 sr0 stable (xamarin.vs 4.2.0.695 ; xamarin.android 7.0.1.2 ; xamarin.ios 10.0.1.8 ; mono 4.6.1.3) 570 | - test : now accepts improvable tls1.2 (required for ios) 571 | 572 | ## 1.2.0 573 | - xamarin : upgrade to cycle 8 stable (xamarin.vs 4.2.0.680 ; xamarin.android 7.0.0.18 ; xamarin.ios 10.0.0.6 ; mono 4.6.0.245) 574 | - add iOS projects and update nuget script 575 | - iOs implementation using NSUrlSessionHandler, but no certificate pinning for the moment 576 | 577 | ## 1.1.5 578 | - generate versions automatically from version file 579 | 580 | ## 1.1.4 581 | - xamarin : upgrade to cycle 8 beta pre3 (xamarin.vs 4.2.0.628 ; xamarin.android 7.0.0.3 ; mono 4.6.0.182) 582 | - android : support android N (7.0) 583 | 584 | ## 1.1.3 585 | - android : remove one useless dependency 586 | - merge assemblies before packing nuget 587 | 588 | ## 1.1.2 589 | - clean android resources 590 | 591 | ## 1.1.1 592 | - remove pdb and mdb from nuget 593 | 594 | ## 1.1.0 595 | - remove useless bait dll and use directly the portable versionning 596 | - do not expose some public classes (thanks to internalsvisibleto) 597 | 598 | ## 1.0.5 599 | - bait and switch : add abstractions dll 600 | - clean nuget pack 601 | 602 | ## 1.0.4 603 | - clean some references, force Square.OkIO 1.9.0 604 | - implement bait and switch trick more properly with abstract dll 605 | 606 | ## 1.0.3 607 | - add proxy (on supported platforms) 608 | 609 | ## 1.0.2 610 | - webexception in case of trust failure 611 | 612 | ## 1.0.1 613 | - re-add debug logs 614 | - change nativemessagehandler constructors 615 | 616 | ## 1.0.0 617 | - new project name : NativeHttpClient 618 | - android : get rid of useless options (progressstreamcontent ; throwoncaptive ; disablecaching) 619 | - android : use okhttp's certificatepinner and get rid of dirty legacy code 620 | - portable : add certificate pinner 621 | 622 | ## 0.3.4.1 623 | - use OkHttp 3.4.1 624 | - fix concurrency issue with cookie manager 625 | - increase timeout to 100s 626 | 627 | ## 0.3.1.2 628 | - merge tests in common dll 629 | - get rid of servicepointmanager 630 | - cleaning 631 | 632 | ## 0.2.7.5 633 | - support Tls1.2 on android <5.0 634 | - validate certificates properly using subjectAltNames (for zesto) 635 | - use OkHttp 2.7.5 636 | - cleaning 637 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | contact@zitch.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13.0 4 | 9.0.10 5 | 6 | 7 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ZitchCode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SecureHttpClient 2 | 3 | SecureHttpClient is a dotnet cross-platform HttpClientHandler library, with additional security features. 4 | 5 | ## Features 6 | 7 | | Feature | Android | iOS | Windows | 8 | | ---: | :---: | :---: | :---: | 9 | | Certificate pinning | :white_check_mark: | :white_check_mark: | :white_check_mark: | 10 | | TLS 1.2+ | :white_check_mark: | :white_check_mark: | :white_check_mark: | 11 | | HTTP/2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | 12 | | Compression (gzip / deflate / br) | :white_check_mark: | :white_check_mark: | :white_check_mark: | 13 | | Client certificates | :white_check_mark: | :white_check_mark: | :white_check_mark: | 14 | | Headers ordering | :white_check_mark: | :x: | :x: | 15 | | Cookies | :white_check_mark: | :white_check_mark: | :white_check_mark: | 16 | 17 | ## Installation 18 | 19 | [![NuGet](https://img.shields.io/nuget/v/SecureHttpClient)](https://www.nuget.org/packages/SecureHttpClient/) 20 | 21 | The most recent version is available (and is tested) on the following platforms: 22 | - Android 5-15 (API 21-35) 23 | - iOS 18.0 24 | - .net 9.0 25 | 26 | Older versions support older frameworks (but they are not maintained anymore): 27 | - v2.2: net8.0 (android / ios / windows) 28 | - v2.1: net7.0 (android / ios / windows) 29 | - v2.0: net6.0 (android / ios / windows) 30 | - v1.x: MonoAndroid ; Xamarin.iOS ; NetStandard 31 | 32 | ## Basic usage 33 | 34 | Basic usage is similar to using `System.Net.Http.HttpClientHandler`. 35 | ```csharp 36 | // create the SecureHttpClientHandler 37 | var secureHttpClientHandler = new SecureHttpClientHandler(null); 38 | 39 | // create the HttpClient 40 | var httpClient = new HttpClient(secureHttpClientHandler); 41 | 42 | // example of a simple GET request 43 | var response = await httpClient.GetAsync("https://www.github.com"); 44 | var html = await response.Content.ReadAsStringAsync(); 45 | ``` 46 | 47 | ## Certificate pining 48 | 49 | After creating a `SecureHttpClientHandler` object, call `AddCertificatePinner` to add one or more certificate pinner. 50 | 51 | The request will fail if the certificate pin is not correct. 52 | 53 | ```csharp 54 | // create the SecureHttpClientHandler 55 | var secureHttpClientHandler = new SecureHttpClientHandler(null); 56 | 57 | // add certificate pinner 58 | secureHttpClientHandler.AddCertificatePinner("www.github.com", ["sha256/YH8+l6PDvIo1Q5o6varvw2edPgfyJFY5fHuSlsVdvdc="]); 59 | 60 | // create the HttpClient 61 | var httpClient = new HttpClient(secureHttpClientHandler); 62 | 63 | // example of a simple GET request 64 | var response = await httpClient.GetAsync("https://www.github.com"); 65 | var html = await response.Content.ReadAsStringAsync(); 66 | ``` 67 | 68 | In order to compute the pin (SPKI fingerprint of the server's SSL certificate), you can execute the following command (here for `www.github.com` host): 69 | ```shell 70 | openssl s_client -connect www.github.com:443 -servername www.github.com | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -noout -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 71 | ``` 72 | 73 | ## Cookies and Redirect 74 | 75 | SecureHttpClient handles cookies and redirects, but the behavior can differ a bit from one platform to another, because of different implementations in the native libraries used internally. 76 | 77 | For strictly identical behavior between platforms, it's recommended to use [Flurl](https://github.com/tmenier/Flurl) on top of SecureHttpClient, and let it handle cookies and redirects. 78 | 79 | ```csharp 80 | // create the SecureHttpClientHandler 81 | var secureHttpClientHandler = new SecureHttpClientHandler(null); 82 | 83 | // disable redirect and cookies management in this handler 84 | secureHttpClientHandler.AllowAutoRedirect = false; 85 | secureHttpClientHandler.UseCookies = false; 86 | 87 | // create the FlurlClient and CookieSession, they will manage redirect and cookies 88 | var httpClient = new HttpClient(secureHttpClientHandler); 89 | var flurlClient = new FlurlClient(httpClient); 90 | var flurlSession = new CookieSession(flurlClient); 91 | 92 | // example of a simple GET request using Flurl 93 | var html = await flurlSession 94 | .Request("https://www.github.com") 95 | .GetStringAsync(); 96 | ``` 97 | 98 | ## Advanced usage 99 | 100 | For more advanced usage (logging, client certificates, cookies ordering...), have a look into the SecureHttpClient.Test folder for more code examples. 101 | -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/SecureHttpClient.OkHttp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0-android35.0 5 | true 6 | 21.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/Transforms/EnumFields.xml: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/Transforms/EnumMethods.xml: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/Transforms/Metadata.xml: -------------------------------------------------------------------------------- 1 |  2 | SecureHttpClient.OkHttp 3 | 4 | -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/java/DecompressInterceptor.java: -------------------------------------------------------------------------------- 1 | package securehttpclient.okhttp; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.BufferedInputStream; 7 | import java.util.zip.Inflater; 8 | import java.util.zip.InflaterInputStream; 9 | import java.util.zip.GZIPInputStream; 10 | import org.brotli.dec.BrotliInputStream; 11 | 12 | import okhttp3.Headers; 13 | import okhttp3.Interceptor; 14 | import okhttp3.Response; 15 | import okhttp3.ResponseBody; 16 | import okio.BufferedSource; 17 | import okio.Okio; 18 | 19 | public class DecompressInterceptor implements Interceptor { 20 | @Override 21 | public Response intercept(Chain chain) throws IOException { 22 | Response response = chain.proceed(chain.request()); 23 | 24 | if (isCompressed(response)) { 25 | return decompress(response); 26 | } else { 27 | return response; 28 | } 29 | } 30 | 31 | private Response decompress(final Response response) throws IOException { 32 | 33 | if (response.body() == null) { 34 | return response; 35 | } 36 | 37 | BufferedSource source = response.body().source(); 38 | InputStream inputStream = null; 39 | 40 | switch(response.header("Content-Encoding").toLowerCase()) { 41 | case "gzip": { 42 | inputStream = new GZIPInputStream(source.inputStream()); 43 | break; 44 | } 45 | case "deflate": { 46 | boolean hasZlibHeader = false; 47 | BufferedInputStream bufferedInputStream = new BufferedInputStream(source.inputStream()); 48 | try { 49 | source.require(2); // throws EOFException if size < 2 50 | byte[] headerBytes = new byte[2]; 51 | bufferedInputStream.mark(0); 52 | bufferedInputStream.read(headerBytes); 53 | bufferedInputStream.reset(); 54 | hasZlibHeader = (headerBytes[0] & 0xFF) == 0x78 && (headerBytes[1] & 0xFF) <= 0xDA; 55 | } catch (EOFException e) { 56 | } 57 | if (hasZlibHeader) { 58 | // zlib decompression (rfc 1951) 59 | inputStream = new InflaterInputStream(bufferedInputStream, new Inflater()); 60 | } else { 61 | // raw deflate decompression (rfc 1950) 62 | inputStream = new InflaterInputStream(bufferedInputStream, new Inflater(true)); 63 | } 64 | break; 65 | } 66 | case "br": { 67 | inputStream = new BrotliInputStream(source.inputStream()); 68 | break; 69 | } 70 | } 71 | 72 | byte[] bodyBytes = Okio.buffer(Okio.source(inputStream)).readByteArray(); 73 | ResponseBody responseBody = ResponseBody.create(bodyBytes, response.body().contentType()); 74 | inputStream.close(); 75 | 76 | Headers strippedHeaders = response.headers().newBuilder() 77 | .removeAll("Content-Encoding") 78 | .removeAll("Content-Length") 79 | .build(); 80 | return response.newBuilder() 81 | .headers(strippedHeaders) 82 | .body(responseBody) 83 | .message(response.message()) 84 | .build(); 85 | } 86 | 87 | private Boolean isCompressed(Response response) { 88 | String contentEncoding = response.header("Content-Encoding"); 89 | return contentEncoding != null 90 | && (contentEncoding.equalsIgnoreCase("gzip") || contentEncoding.equalsIgnoreCase("deflate") || contentEncoding.equalsIgnoreCase("br")); 91 | } 92 | } -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/java/HeadersOrderInterceptor.java: -------------------------------------------------------------------------------- 1 | package securehttpclient.okhttp; 2 | 3 | import java.io.IOException; 4 | 5 | import okhttp3.Headers; 6 | import okhttp3.Interceptor; 7 | import okhttp3.Request; 8 | import okhttp3.Response; 9 | 10 | public class HeadersOrderInterceptor implements Interceptor { 11 | 12 | private static final String HEADERS_ORDER_HEADER = "securehttpclient-headers-order"; 13 | 14 | @Override 15 | public Response intercept(Chain chain) throws IOException { 16 | Request original = chain.request(); 17 | 18 | if (original.header(HEADERS_ORDER_HEADER) == null) { 19 | return chain.proceed(original); 20 | } 21 | 22 | Request.Builder requestBuilder = original.newBuilder(); 23 | 24 | String headersOrderHeader = original.header(HEADERS_ORDER_HEADER); 25 | String[] headersOrderArray = headersOrderHeader.split(";"); 26 | 27 | for (String name : headersOrderArray) { 28 | if (original.header(name) != null) { 29 | String header = original.header(name); 30 | requestBuilder 31 | .removeHeader(name) 32 | .addHeader(name, header); 33 | } 34 | } 35 | 36 | Request request = requestBuilder 37 | .removeHeader(HEADERS_ORDER_HEADER) 38 | .build(); 39 | 40 | return chain.proceed(request); 41 | } 42 | } -------------------------------------------------------------------------------- /SecureHttpClient.OkHttp/java/buildjar.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SETLOCAL 4 | set Version_Okhttp=4.12.0 5 | set Version_Okio=3.9.1 6 | set Version_KotlinStdlib=2.0.21 7 | set Version_Brotli=0.1.2 8 | 9 | echo -- CLEAN --------------------------------------------------------------------------------------------------------------------------------------------------- 10 | for /d /r . %%d in (jars,src) do @if exist "%%d" rd /s/q "%%d" 11 | 12 | echo -- DOWNLOAD JARS ------------------------------------------------------------------------------------------------------------------------------------------- 13 | mkdir jars 14 | bitsadmin.exe /transfer "Download okhttp %Version_Okhttp%" https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/%Version_Okhttp%/okhttp-%Version_Okhttp%.jar "%~dp0\jars\okhttp-%Version_Okhttp%.jar" 15 | bitsadmin.exe /transfer "Download okio %Version_Okio%" https://repo1.maven.org/maven2/com/squareup/okio/okio/%Version_Okio%/okio-%Version_Okio%.jar "%~dp0\jars\okio-%Version_Okio%.jar" 16 | bitsadmin.exe /transfer "Download okio-jvm %Version_Okio%" https://repo1.maven.org/maven2/com/squareup/okio/okio-jvm/%Version_Okio%/okio-jvm-%Version_Okio%.jar "%~dp0\jars\okio-jvm-%Version_Okio%.jar" 17 | bitsadmin.exe /transfer "Download kotlin-stdlib %Version_KotlinStdlib%" https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/%Version_KotlinStdlib%/kotlin-stdlib-%Version_KotlinStdlib%.jar "%~dp0\jars\kotlin-stdlib-%Version_KotlinStdlib%.jar" 18 | bitsadmin.exe /transfer "Download org.brotli.dec %Version_Brotli%" https://repo1.maven.org/maven2/org/brotli/dec/%Version_Brotli%/dec-%Version_Brotli%.jar "%~dp0\jars\org.brotli.dec-%Version_Brotli%.jar" 19 | 20 | echo -- COPY IMPORTS -------------------------------------------------------------------------------------------------------------------------------------------- 21 | copy "%~dp0\jars\org.brotli.dec-%Version_Brotli%.jar" ..\import\org.brotli.dec-%Version_Brotli%.jar 22 | 23 | echo -- BUILD JAVA ---------------------------------------------------------------------------------------------------------------------------------------------- 24 | javac -Xlint:all -classpath jars/* *.java 25 | 26 | echo -- BUILD JAR ----------------------------------------------------------------------------------------------------------------------------------------------- 27 | mkdir src 28 | mkdir src\securehttpclient-okhttp 29 | move *.class src\securehttpclient-okhttp\ 30 | jar cvf securehttpclient-okhttp.jar -C src . 31 | move securehttpclient-okhttp.jar ..\Jars\ 32 | 33 | echo -- CLEAN --------------------------------------------------------------------------------------------------------------------------------------------------- 34 | for /d /r . %%d in (jars,src) do @if exist "%%d" rd /s/q "%%d" 35 | 36 | echo -- DONE !! ------------------------------------------------------------------------------------------------------------------------------------------------- 37 | ENDLOCAL 38 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/CertificatePinnerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using SecureHttpClient.Test.Helpers; 3 | using Xunit; 4 | 5 | namespace SecureHttpClient.Test 6 | { 7 | public class CertificatePinnerTest : TestBase, IClassFixture 8 | { 9 | private const string Hostname = @"www.howsmyssl.com"; 10 | private const string Page = @"https://www.howsmyssl.com/a/check"; 11 | private static readonly string[] PinsOk = { @"sha256/KKplOzUVa+5zlQKc746S0JKhIFdoTM2l1a+rWyKCS8M=" }; 12 | private static readonly string[] PinsKo = { @"sha256/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=" }; 13 | 14 | private const string Hostname2 = @"github.com"; 15 | private const string Page2 = @"https://github.com"; 16 | private static readonly string[] Pins2Ok = { @"sha256/Gs+dT9kUC17nDYZXH52mKzGnlUU/Q5mS0UruTQW3H0U=" }; 17 | private static readonly string[] Pins2Ko = { @"sha256/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy=" }; 18 | 19 | private const string Hostname3 = @"ecc256.badssl.com"; 20 | private const string Page3 = @"https://ecc256.badssl.com/"; 21 | private static readonly string[] Pins3Ok = { @"sha256/TFE7uYfWbntDS4QhyoioxRZ8AvbfJBmr8/FaF5iBy5o=" }; 22 | private static readonly string[] Pins3Ko = { @"sha256/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=" }; 23 | 24 | public CertificatePinnerTest(TestFixture testFixture) : base(testFixture) 25 | { 26 | } 27 | 28 | [Fact] 29 | public async Task CertificatePinnerTest_OneHost_Success() 30 | { 31 | AddCertificatePinner(Hostname, PinsOk); 32 | await GetAsync(Page); 33 | } 34 | 35 | [Fact] 36 | public async Task CertificatePinnerTest_OneHost_Failure() 37 | { 38 | AddCertificatePinner(Hostname, PinsKo); 39 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(Page)); 40 | } 41 | 42 | [Fact] 43 | public async Task CertificatePinnerTest_TwoHosts_Success() 44 | { 45 | AddCertificatePinner(Hostname, PinsOk); 46 | AddCertificatePinner(Hostname2, Pins2Ok); 47 | 48 | await GetAsync(Page); 49 | await GetAsync(Page2); 50 | } 51 | 52 | [Fact] 53 | public async Task CertificatePinnerTest_TwoHosts_FirstHostFails() 54 | { 55 | AddCertificatePinner(Hostname, PinsKo); 56 | AddCertificatePinner(Hostname2, Pins2Ok); 57 | 58 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(Page)); 59 | await GetAsync(Page2); 60 | } 61 | 62 | [Fact] 63 | public async Task CertificatePinnerTest_TwoHosts_SecondHostFails() 64 | { 65 | AddCertificatePinner(Hostname, PinsOk); 66 | AddCertificatePinner(Hostname2, Pins2Ko); 67 | 68 | await GetAsync(Page); 69 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(Page2)); 70 | } 71 | 72 | [Fact] 73 | public async Task CertificatePinnerTest_EccCertificate_Success() 74 | { 75 | AddCertificatePinner(Hostname3, Pins3Ok); 76 | await GetAsync(Page3); 77 | } 78 | 79 | [Fact] 80 | public async Task CertificatePinnerTest_EccCertificate_Failure() 81 | { 82 | AddCertificatePinner(Hostname3, Pins3Ko); 83 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(Page3)); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/Helpers/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Security.Authentication; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace SecureHttpClient.Test.Helpers 8 | { 9 | public static class AssertExtensions 10 | { 11 | public static async Task ThrowsTrustFailureAsync(Func testCode) 12 | { 13 | var exception = await Assert.ThrowsAsync(testCode).ConfigureAwait(false); 14 | Assert.IsType(exception.InnerException); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/Helpers/HttpResponseMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace SecureHttpClient.Test.Helpers 5 | { 6 | internal static class HttpResponseMessageExtensions 7 | { 8 | public static async Task ReceiveString(this Task response) 9 | { 10 | using var resp = await response.ConfigureAwait(false); 11 | if (resp == null) return null; 12 | return await resp.Content.ReadAsStringAsync().ConfigureAwait(false); 13 | } 14 | 15 | public static async Task ReceiveBytes(this Task response) 16 | { 17 | using var resp = await response.ConfigureAwait(false); 18 | if (resp == null) return null; 19 | return await resp.Content.ReadAsByteArrayAsync().ConfigureAwait(false); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/Helpers/JsonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | 4 | namespace SecureHttpClient.Test.Helpers 5 | { 6 | public static class JsonExtensions 7 | { 8 | public static Dictionary GetDictionary(this JsonElement jsonElement) 9 | { 10 | var d = new Dictionary(); 11 | foreach (var p in jsonElement.EnumerateObject()) 12 | { 13 | d[p.Name] = p.Value.GetString(); 14 | } 15 | return d; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/Helpers/ResourceHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | 6 | namespace SecureHttpClient.Test.Helpers 7 | { 8 | public static class ResourceHelper 9 | { 10 | public static Task GetStringAsync(string resource) 11 | { 12 | using var resourceStream = GetResourceStream(resource); 13 | using var reader = new StreamReader(resourceStream); 14 | return reader.ReadToEndAsync(); 15 | } 16 | 17 | public static async Task GetBytesAsync(string resource) 18 | { 19 | using var resourceStream = GetResourceStream(resource); 20 | using var memoryStream = new MemoryStream(); 21 | await resourceStream.CopyToAsync(memoryStream); 22 | return memoryStream.ToArray(); 23 | } 24 | 25 | private static Stream GetResourceStream(string resource) 26 | { 27 | var assembly = Assembly.GetExecutingAssembly(); 28 | var resourceName = assembly.GetManifestResourceNames().Single(str => str.EndsWith($"res.{resource}")); 29 | var resourceStream = assembly.GetManifestResourceStream(resourceName); 30 | return resourceStream; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/HttpTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | using Microsoft.Maui.Devices; 9 | using SecureHttpClient.Extensions; 10 | using SecureHttpClient.Test.Helpers; 11 | using Xunit; 12 | 13 | namespace SecureHttpClient.Test 14 | { 15 | public class HttpTest : TestBase, IClassFixture 16 | { 17 | public HttpTest(TestFixture testFixture) : base(testFixture) 18 | { 19 | } 20 | 21 | [Fact] 22 | public async Task HttpTest_Get() 23 | { 24 | const string page = @"https://httpbingo.org/get"; 25 | var result = await GetAsync(page).ReceiveString(); 26 | var json = JsonDocument.Parse(result); 27 | var url = json.RootElement.GetProperty("url").GetString(); 28 | Assert.Equal(page, url); 29 | } 30 | 31 | [Fact] 32 | public async Task HttpTest_Compression_Gzip_WithoutRequestHeader() 33 | { 34 | const string page = @"https://httpbingo.org/gzip"; 35 | var result = await GetAsync(page).ReceiveString(); 36 | var json = JsonDocument.Parse(result); 37 | var url = json.RootElement.GetProperty("gzipped").GetBoolean(); 38 | Assert.True(url); 39 | var requestEncoding = json.RootElement.GetProperty("headers").GetProperty("Accept-Encoding")[0].GetString(); 40 | Assert.Contains("gzip", requestEncoding); 41 | } 42 | 43 | [Fact] 44 | public async Task HttpTest_Compression_Gzip_WithRequestHeader() 45 | { 46 | const string page = @"https://httpbingo.org/gzip"; 47 | var req = new HttpRequestMessage(HttpMethod.Get, page); 48 | req.Headers.Add("Accept-Encoding", "gzip"); 49 | var result = await SendAsync(req).ReceiveString(); 50 | var json = JsonDocument.Parse(result); 51 | var url = json.RootElement.GetProperty("gzipped").GetBoolean(); 52 | Assert.True(url); 53 | var requestEncoding = json.RootElement.GetProperty("headers").GetProperty("Accept-Encoding")[0].GetString(); 54 | Assert.Contains("gzip", requestEncoding); 55 | } 56 | 57 | [Fact] 58 | public async Task HttpTest_Compression_Deflate_WithoutRequestHeader() 59 | { 60 | const string page = @"https://httpbingo.org/deflate"; // has zlib header (RFC 1951) 61 | var result = await GetAsync(page).ReceiveString(); 62 | var json = JsonDocument.Parse(result); 63 | var url = json.RootElement.GetProperty("deflated").GetBoolean(); 64 | Assert.True(url); 65 | var requestEncoding = json.RootElement.GetProperty("headers").GetProperty("Accept-Encoding")[0].GetString(); 66 | Assert.Contains("deflate", requestEncoding); 67 | } 68 | 69 | [Fact] 70 | public async Task HttpTest_Compression_Deflate_WithRequestHeader() 71 | { 72 | const string page = @"https://httpbingo.org/deflate"; 73 | var req = new HttpRequestMessage(HttpMethod.Get, page); 74 | req.Headers.Add("Accept-Encoding", "deflate"); 75 | var result = await SendAsync(req).ReceiveString(); 76 | var json = JsonDocument.Parse(result); 77 | var url = json.RootElement.GetProperty("deflated").GetBoolean(); 78 | Assert.True(url); 79 | var requestEncoding = json.RootElement.GetProperty("headers").GetProperty("Accept-Encoding")[0].GetString(); 80 | Assert.Contains("deflate", requestEncoding); 81 | } 82 | 83 | [Fact] 84 | public async Task HttpTest_Compression_Brotli_WithoutRequestHeader() 85 | { 86 | const string page = @"https://httpbin.org/brotli"; 87 | var result = await GetAsync(page).ReceiveString(); 88 | var json = JsonDocument.Parse(result); 89 | var url = json.RootElement.GetProperty("brotli").GetBoolean(); 90 | Assert.True(url); 91 | var requestEncoding = json.RootElement.GetProperty("headers").GetProperty("Accept-Encoding").GetString(); 92 | Assert.Contains("br", requestEncoding); 93 | } 94 | 95 | [Fact] 96 | public async Task HttpTest_Compression_Brotli_WithRequestHeader() 97 | { 98 | const string page = @"https://httpbin.org/brotli"; 99 | var req = new HttpRequestMessage(HttpMethod.Get, page); 100 | req.Headers.Add("Accept-Encoding", "br"); 101 | var result = await SendAsync(req).ReceiveString(); 102 | var json = JsonDocument.Parse(result); 103 | var url = json.RootElement.GetProperty("brotli").GetBoolean(); 104 | Assert.True(url); 105 | var requestEncoding = json.RootElement.GetProperty("headers").GetProperty("Accept-Encoding").GetString(); 106 | Assert.Contains("br", requestEncoding); 107 | } 108 | 109 | [Fact] 110 | public async Task HttpTest_Headers() 111 | { 112 | const string page = @"https://postman-echo.com/get"; 113 | var req = new HttpRequestMessage(HttpMethod.Get, page); 114 | req.Headers.Add("header1", "value1"); 115 | req.Headers.Add("header2", "value2"); 116 | req.Headers.Add("header3", "value3"); 117 | var result = await SendAsync(req).ReceiveString(); 118 | var json = JsonDocument.Parse(result); 119 | var headers = json.RootElement.GetProperty("headers").GetDictionary().Select(kv => kv.Key).ToList(); 120 | Assert.Contains("header1", headers); 121 | Assert.Contains("header2", headers); 122 | Assert.Contains("header3", headers); 123 | } 124 | 125 | [SkippableFact] 126 | public async Task HttpTest_HeadersOrder() 127 | { 128 | Skip.If(DeviceInfo.Platform != DevicePlatform.Android, "Only on Android"); 129 | 130 | const string page = @"https://postman-echo.com/get"; 131 | var req = new HttpRequestMessage(HttpMethod.Get, page); 132 | req.Headers.Add("header1", "value1"); 133 | req.Headers.Add("header2", "value2"); 134 | req.Headers.Add("header3", "value3"); 135 | req.SetHeadersOrder("header3", "header2", "header1"); 136 | var result = await SendAsync(req).ReceiveString(); 137 | var json = JsonDocument.Parse(result); 138 | var headers = json.RootElement.GetProperty("headers").GetDictionary().Select(kv => kv.Key).ToList(); 139 | var index1 = headers.IndexOf("header1"); 140 | var index2 = headers.IndexOf("header2"); 141 | var index3 = headers.IndexOf("header3"); 142 | Assert.Equal(1, index2 - index3); 143 | Assert.Equal(1, index1 - index2); 144 | } 145 | 146 | [Fact] 147 | public async Task HttpTest_Utf8() 148 | { 149 | const string page = @"https://httpbingo.org/encoding/utf8"; 150 | var result = await GetAsync(page).ReceiveString(); 151 | Assert.Contains("∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i)", result); 152 | } 153 | 154 | [Fact] 155 | public async Task HttpTest_Redirect() 156 | { 157 | const string page = @"https://httpbingo.org/redirect/5"; // httpbingo replaces httpbin because of issue https://github.com/postmanlabs/httpbin/issues/617 158 | const string final = @"https://httpbingo.org/get"; 159 | 160 | var result = await GetAsync(page).ReceiveString(); 161 | var json = JsonDocument.Parse(result); 162 | var url = json.RootElement.GetProperty("url").GetString(); 163 | Assert.Equal(final, url); 164 | 165 | var request = new HttpRequestMessage(HttpMethod.Get, page); 166 | var response = await SendAsync(request); 167 | 168 | Assert.Equal(final, request.RequestUri.AbsoluteUri); 169 | Assert.Equal(final, response.RequestMessage.RequestUri.AbsoluteUri); 170 | } 171 | 172 | [Fact] 173 | public async Task HttpTest_DoNotFollowRedirects() 174 | { 175 | const string page = @"https://httpbingo.org/redirect/5"; // httpbingo replaces httpbin because of issue https://github.com/postmanlabs/httpbin/issues/617 176 | DoNotFollowRedirects(); 177 | var response = await GetAsync(page, false); 178 | Assert.Equal(HttpStatusCode.Found, response.StatusCode); 179 | Assert.Equal(page, response.RequestMessage.RequestUri.AbsoluteUri); 180 | } 181 | 182 | [Fact] 183 | public async Task HttpTest_Delay() 184 | { 185 | const string page = @"https://httpbingo.org/delay/5"; 186 | var result = await GetAsync(page).ReceiveString(); 187 | var json = JsonDocument.Parse(result); 188 | var url = json.RootElement.GetProperty("url").GetString(); 189 | Assert.Equal(page, url); 190 | } 191 | 192 | [Fact] 193 | public async Task HttpTest_Stream() 194 | { 195 | const string page = @"https://httpbingo.org/stream/50"; 196 | var result = await GetAsync(page).ReceiveString(); 197 | var nbLines = result.Split('\n').Length - 1; 198 | Assert.Equal(50, nbLines); 199 | } 200 | 201 | [Fact] 202 | public async Task HttpTest_Bytes() 203 | { 204 | const string page = @"https://httpbingo.org/bytes/1024"; 205 | var result = await GetAsync(page).ReceiveBytes(); 206 | Assert.Equal(1024, result.Length); 207 | } 208 | 209 | [Fact] 210 | public async Task HttpTest_StreamBytes() 211 | { 212 | const string page = @"https://httpbingo.org/stream-bytes/1024"; 213 | var result = await GetAsync(page).ReceiveBytes(); 214 | Assert.Equal(1024, result.Length); 215 | } 216 | 217 | [Fact] 218 | public async Task HttpTest_SetCookie() 219 | { 220 | const string page = @"https://httpbingo.org/cookies/set?k1=v1"; 221 | var result = await GetAsync(page).ReceiveString(); 222 | var json = JsonDocument.Parse(result); 223 | var cookies = json.RootElement.GetDictionary(); 224 | Assert.Contains(new KeyValuePair("k1", "v1"), cookies); 225 | } 226 | 227 | [Fact] 228 | public async Task HttpTest_SetCookieAgain() 229 | { 230 | const string page1 = @"https://httpbingo.org/cookies/set?k1=v1"; 231 | await GetAsync(page1); 232 | const string page2 = @"https://httpbingo.org/cookies/set?k1=v2"; 233 | var result = await GetAsync(page2).ReceiveString(); 234 | var json = JsonDocument.Parse(result); 235 | var cookies = json.RootElement.GetDictionary(); 236 | Assert.Contains(new KeyValuePair("k1", "v2"), cookies); 237 | } 238 | 239 | [Fact] 240 | public async Task HttpTest_SetCookies() 241 | { 242 | const string cookie1 = "k1=v1; Path=/; expires=Sat, 01-Jan-2050 00:00:00 GMT"; 243 | const string cookie2 = "k2=v2; Path=/; expires=Fri, 01-Jan-2049 00:00:00 GMT"; 244 | var page1 = $@"https://httpbingo.org/response-headers?Set-Cookie={WebUtility.UrlEncode(cookie1)}&Set-Cookie={WebUtility.UrlEncode(cookie2)}"; 245 | var response1 = await GetAsync(page1); 246 | response1.Headers.TryGetValues("set-cookie", out var respCookies); 247 | Assert.Equal(new List { cookie1, cookie2 }, respCookies); 248 | const string page2 = @"https://httpbingo.org/cookies"; 249 | var result = await GetAsync(page2).ReceiveString(); 250 | var json = JsonDocument.Parse(result); 251 | var cookies = json.RootElement.GetDictionary(); 252 | Assert.Contains(new KeyValuePair("k1", "v1"), cookies); 253 | Assert.Contains(new KeyValuePair("k2", "v2"), cookies); 254 | } 255 | 256 | [SkippableFact] 257 | public async Task HttpTest_DeleteCookie() 258 | { 259 | Skip.If(DeviceInfo.Platform == DevicePlatform.Android && DeviceInfo.Version.Major == 7, "Failing on Android 24-25"); 260 | 261 | const string page1 = @"https://httpbingo.org/cookies/set?k1=v1"; 262 | await GetAsync(page1); 263 | const string page2 = @"https://httpbingo.org/cookies/delete?k1"; 264 | var result = await GetAsync(page2).ReceiveString(); 265 | var json = JsonDocument.Parse(result); 266 | var cookies = json.RootElement.GetDictionary(); 267 | Assert.DoesNotContain(new KeyValuePair("k1", "v1"), cookies); 268 | } 269 | 270 | [Fact] 271 | public async Task HttpTest_DoNotUseCookies() 272 | { 273 | const string page = @"https://httpbingo.org/cookies/set?k1=v1"; 274 | DisableCookies(); 275 | var result = await GetAsync(page).ReceiveString(); 276 | var json = JsonDocument.Parse(result); 277 | var cookies = json.RootElement.GetDictionary(); 278 | Assert.Empty(cookies); 279 | } 280 | 281 | [SkippableFact] 282 | public async Task HttpTest_Protocol() 283 | { 284 | Skip.If(DeviceInfo.Platform == DevicePlatform.iOS, "Need help to get http version from NSHttpUrlResponse"); 285 | 286 | const string page = @"https://httpbingo.org/get"; 287 | var request = new HttpRequestMessage(HttpMethod.Get, page); 288 | if (DeviceInfo.Platform != DevicePlatform.Android && DeviceInfo.Platform != DevicePlatform.iOS) 289 | { 290 | request.Version = new Version(2, 0); 291 | } 292 | var response = await SendAsync(request); 293 | Assert.Equal(HttpVersion.Version20, response.Version); 294 | } 295 | 296 | [Fact] 297 | public async Task HttpTest_Timeout() 298 | { 299 | const string page = @"https://httpbingo.org/delay/5"; 300 | SetTimeout(1); 301 | await Assert.ThrowsAsync(() => GetAsync(page)); 302 | } 303 | 304 | [Fact] 305 | public async Task HttpTest_UnknownHost() 306 | { 307 | const string page = @"https://nosuchhostisknown/"; 308 | await Assert.ThrowsAsync(() => GetAsync(page)); 309 | } 310 | 311 | [SkippableFact] 312 | public async Task HttpTest_GetWithNonEmptyRequestBody() 313 | { 314 | Skip.If(DeviceInfo.Platform == DevicePlatform.Android || DeviceInfo.Platform == DevicePlatform.iOS, "GET method must not have a body"); 315 | 316 | const string page = @"https://httpbin.org/get"; 317 | var request = new HttpRequestMessage(HttpMethod.Get, page) 318 | { 319 | Content = new StringContent("test request body") 320 | }; 321 | var result = await SendAsync(request).ReceiveString(); 322 | var json = JsonDocument.Parse(result); 323 | var url = json.RootElement.GetProperty("url").GetString(); 324 | Assert.Equal(page, url); 325 | } 326 | 327 | [Fact] 328 | public async Task HttpTest_GetWithEmptyRequestBody() 329 | { 330 | const string page = @"https://httpbingo.org/get"; 331 | var request = new HttpRequestMessage(HttpMethod.Get, page) 332 | { 333 | Content = new StringContent("") 334 | }; 335 | var result = await SendAsync(request).ReceiveString(); 336 | var json = JsonDocument.Parse(result); 337 | var url = json.RootElement.GetProperty("url").GetString(); 338 | Assert.Equal(page, url); 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/SecureHttpClient.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/SpkiFingerprintTest.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | using System.Threading.Tasks; 3 | using Microsoft.Maui.Devices; 4 | using SecureHttpClient.CertificatePinning; 5 | using SecureHttpClient.Test.Helpers; 6 | using Xunit; 7 | 8 | namespace SecureHttpClient.Test 9 | { 10 | public class SpkiFingerprintTest : IClassFixture 11 | { 12 | [SkippableTheory] 13 | [InlineData("rsa_certificate.pem", "sha256/l6meTmH/OlRPWR/Mn3ncvtBS25C+uYFhP26IOMzAa/E=")] 14 | [InlineData("dsa_certificate.pem", "sha256/Vq9zQp9NSsMxsGYi3Q1lO3wxv8AP2hSUvtaURKWKAt4=")] 15 | [InlineData("ecdsa_certificate.pem", "sha256/sfMf1hmimBN4QW8AEMs2hXMX2aZEBTD4E9PTK0FntC0=")] 16 | public async Task SpkiFingerprintTest_RsaCertificate(string resource, string expected) 17 | { 18 | Skip.If(DeviceInfo.Platform == DevicePlatform.Android, "Not implemented on Android"); 19 | 20 | var certPem = await ResourceHelper.GetStringAsync(resource); 21 | var certBytes = System.Text.Encoding.ASCII.GetBytes(certPem); 22 | var certificate = X509CertificateLoader.LoadCertificate(certBytes); 23 | var actual = SpkiFingerprint.Compute(certificate); 24 | Assert.Equal(expected, actual); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/SslTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using Microsoft.Maui.Devices; 5 | using SecureHttpClient.Test.Helpers; 6 | using Xunit; 7 | 8 | namespace SecureHttpClient.Test 9 | { 10 | public class SslTest : TestBase, IClassFixture 11 | { 12 | public SslTest(TestFixture testFixture) : base(testFixture) 13 | { 14 | } 15 | 16 | [Fact] 17 | public async Task SslTest_ExpiredCertificate() 18 | { 19 | const string page = @"https://expired.badssl.com/"; 20 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(page)); 21 | } 22 | 23 | [Fact] 24 | public async Task SslTest_WrongHostCertificate() 25 | { 26 | const string page = @"https://wrong.host.badssl.com/"; 27 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(page)); 28 | } 29 | 30 | [Fact] 31 | public async Task SslTest_SelfSignedCertificate() 32 | { 33 | const string page = @"https://self-signed.badssl.com/"; 34 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(page)); 35 | } 36 | 37 | [Fact] 38 | public async Task SslTest_UntrustedRootCertificate() 39 | { 40 | const string page = @"https://untrusted-root.badssl.com/"; 41 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(page)); 42 | } 43 | 44 | [SkippableFact] 45 | public async Task SslTest_SpecificTrustedRootCertificate() 46 | { 47 | Skip.If(DeviceInfo.Platform == DevicePlatform.iOS, "Not working on iOS 13"); 48 | 49 | // NB: Using this feature on iOS 11 requires setting NSExceptionDomains in Info.plist, 50 | // particularly NSExceptionRequiresForwardSecrecy=NO : https://stackoverflow.com/q/46316604/5652125 51 | const string page = @"https://untrusted-root.badssl.com/"; 52 | var caCert = await ResourceHelper.GetStringAsync("untrusted_root_badssl_com_certificate.pem"); 53 | SetCaCertificate(caCert); 54 | await GetAsync(page); 55 | } 56 | 57 | [Fact] 58 | public async Task SslTest_OnlyTrustSpecificRootCertificate() 59 | { 60 | const string page = @"https://badssl.com"; // Has valid public cert, but not signed by our custom root 61 | var caCert = await ResourceHelper.GetStringAsync("untrusted_root_badssl_com_certificate.pem"); 62 | SetCaCertificate(caCert); 63 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(page)); 64 | } 65 | 66 | [SkippableFact] 67 | public async Task SslTest_RevokedCertificate() 68 | { 69 | Skip.IfNot(DeviceInfo.Platform == DevicePlatform.iOS, "Unsupported on Android and .Net"); 70 | 71 | const string page = @"https://revoked.badssl.com/"; 72 | await AssertExtensions.ThrowsTrustFailureAsync(() => GetAsync(page)); 73 | } 74 | 75 | [Fact] 76 | public async Task SslTest_Sha256Certificate() 77 | { 78 | const string page = @"https://sha256.badssl.com/"; 79 | await GetAsync(page); 80 | } 81 | 82 | [Fact(Skip = "Certificate has expired, badssl.com needs to fix it.")] 83 | public async Task SslTest_Sha384Certificate() 84 | { 85 | const string page = @"https://sha384.badssl.com/"; 86 | await GetAsync(page); 87 | } 88 | 89 | [Fact(Skip = "Certificate has expired, badssl.com needs to fix it.")] 90 | public async Task SslTest_Sha512Certificate() 91 | { 92 | const string page = @"https://sha512.badssl.com/"; 93 | await GetAsync(page); 94 | } 95 | 96 | [Fact] 97 | public async Task SslTest_Ecc256Certificate() 98 | { 99 | const string page = @"https://ecc256.badssl.com/"; 100 | await GetAsync(page); 101 | } 102 | 103 | [SkippableFact] 104 | public async Task SslTest_Ecc384Certificate() 105 | { 106 | Skip.If(DeviceInfo.Platform == DevicePlatform.Android && DeviceInfo.Version.Major == 7 && DeviceInfo.Version.Minor == 0, "Failing on Android 24"); 107 | 108 | const string page = @"https://ecc384.badssl.com/"; 109 | await GetAsync(page); 110 | } 111 | 112 | [Fact] 113 | public async Task SslTest_Rsa2048Certificate() 114 | { 115 | const string page = @"https://rsa2048.badssl.com/"; 116 | await GetAsync(page); 117 | } 118 | 119 | [Fact] 120 | public async Task SslTest_Rsa4096Certificate() 121 | { 122 | const string page = @"https://rsa4096.badssl.com/"; 123 | await GetAsync(page); 124 | } 125 | 126 | [Fact(Skip = "Certificate has expired, badssl.com need to fix it.")] 127 | public async Task SslTest_Rsa8192Certificate() 128 | { 129 | const string page = @"https://rsa8192.badssl.com/"; 130 | await GetAsync(page); 131 | } 132 | 133 | [Fact] 134 | public async Task SslTest_MissingClientCertificate() 135 | { 136 | const string page = @"https://client-cert-missing.badssl.com/"; 137 | await Assert.ThrowsAsync(() => GetAsync(page)); 138 | } 139 | 140 | [SkippableFact] 141 | public async Task SslTest_ClientCertificate() 142 | { 143 | const string page = @"https://client.badssl.com/"; 144 | var clientCert = await ResourceHelper.GetBytesAsync("badssl.com-client.p12"); 145 | const string certPass = "badssl.com"; 146 | SetClientCertificate(clientCert, certPass); 147 | await GetAsync(page); 148 | } 149 | 150 | [Fact] 151 | public async Task SslTest_HowsMySsl() 152 | { 153 | var expectedTlsVersion = (DeviceInfo.Platform == DevicePlatform.iOS || (DeviceInfo.Platform == DevicePlatform.Android && DeviceInfo.Version.Major >= 10)) ? "TLS 1.3" : "TLS 1.2"; 154 | const string expectedRating = "Probably Okay"; 155 | 156 | const string page = @"https://www.howsmyssl.com/a/check"; 157 | var result = await GetAsync(page).ReceiveString(); 158 | 159 | var json = JsonDocument.Parse(result); 160 | var actualTlsVersion = json.RootElement.GetProperty("tls_version").GetString(); 161 | var actualRating = json.RootElement.GetProperty("rating").GetString(); 162 | 163 | Assert.Equal(expectedTlsVersion, actualTlsVersion); 164 | Assert.Equal(expectedRating, actualRating); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace SecureHttpClient.Test 7 | { 8 | public class TestBase 9 | { 10 | private readonly SecureHttpClientHandler _secureHttpClientHandler; 11 | private readonly HttpClient _httpClient; 12 | 13 | protected TestBase(TestFixture fixture) 14 | { 15 | _secureHttpClientHandler = fixture.ServiceProvider.GetRequiredService() as SecureHttpClientHandler; 16 | _httpClient = new HttpClient(_secureHttpClientHandler); 17 | _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:115.0) Gecko/20100101 Firefox/115.0"); 18 | } 19 | 20 | protected async Task GetAsync(string page, bool ensureSuccessStatusCode = true) 21 | { 22 | var response = await _httpClient.GetAsync(page).ConfigureAwait(false); 23 | if (ensureSuccessStatusCode) 24 | { 25 | response.EnsureSuccessStatusCode(); 26 | } 27 | return response; 28 | } 29 | 30 | protected async Task SendAsync(HttpRequestMessage request, bool ensureSuccessStatusCode = true) 31 | { 32 | var response = await _httpClient.SendAsync(request).ConfigureAwait(false); 33 | if (ensureSuccessStatusCode) 34 | { 35 | response.EnsureSuccessStatusCode(); 36 | } 37 | return response; 38 | } 39 | 40 | protected void AddCertificatePinner(string hostname, string[] pins) 41 | { 42 | _secureHttpClientHandler.AddCertificatePinner(hostname, pins); 43 | } 44 | 45 | protected void SetClientCertificate(byte[] clientCert, string certPassword) 46 | { 47 | var provider = new ImportedClientCertificateProvider(); 48 | provider.Import(clientCert, certPassword); 49 | _secureHttpClientHandler.SetClientCertificates(provider); 50 | } 51 | 52 | protected void SetCaCertificate(string caCertEncoded) 53 | { 54 | var caCert = System.Text.Encoding.ASCII.GetBytes(caCertEncoded); 55 | _secureHttpClientHandler.SetTrustedRoots(caCert); 56 | } 57 | 58 | protected void DoNotFollowRedirects() 59 | { 60 | _secureHttpClientHandler.AllowAutoRedirect = false; 61 | } 62 | 63 | protected void SetTimeout(int timeout) 64 | { 65 | _httpClient.Timeout = TimeSpan.FromSeconds(timeout); 66 | } 67 | 68 | protected void DisableCookies() 69 | { 70 | _secureHttpClientHandler.UseCookies = false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/TestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Serilog; 5 | 6 | namespace SecureHttpClient.Test 7 | { 8 | public class TestFixture 9 | { 10 | public IServiceProvider ServiceProvider { get; } 11 | 12 | public TestFixture() 13 | { 14 | ServiceProvider = new ServiceCollection() 15 | .AddTransient() 16 | .AddLogging(configure => configure.AddSerilog()) 17 | .BuildServiceProvider(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SecureHttpClient.Test/res/badssl.com-client.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZitchCode/SecureHttpClient/d4713f4767ed87aaf9faccd6d30b83a1b186030c/SecureHttpClient.Test/res/badssl.com-client.p12 -------------------------------------------------------------------------------- /SecureHttpClient.Test/res/dsa_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDKTCCAuegAwIBAgIUbv4CsWWrSlTzGkObU8DNjYDL9TgwCwYJYIZIAWUDBAMC 3 | MEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ 4 | bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwIBcNMjIwOTI4MTE0NDEzWhgPMjEyMjA5 5 | MDQxMTQ0MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggG2MIIBKwYHKoZIzjgE 7 | ATCCAR4CgYEA5oyWuJ2OvjcrOtoNjLDd3ufbLBAOyk1mZls1cFbe0XOnsa7v/nHV 8 | 4jgJ5fpEqwoVu7pkjtuP+VtkfJPOwr0ae0owrTGkx/mQqRMIKODc/uXTnkmIz1VO 9 | nGNiAd8cPy4UbGZUAVHIAWCh+mlnJwrtzDSYBKeE4fJ6c8lyfJKcJFkCFQDKtMxC 10 | Ya3vt6OeV2AWPau6M+ZLtwKBgBosXv3jVnFBdv2gtI76jCJ/eK/ciOyrO6+Us9/V 11 | 1A74nZ9ufREuy454yCENgqQ5/WIQ71t3GcUAyh8GUvYjIHhFvR9N4piMQmtvyQ8S 12 | S+B9Ua//+7vrkYIT6YyR9caL/yWo0ybDHIORKAIA+QsRdw7PE6ovWarj0I5GjDEh 13 | 9EQJA4GEAAKBgDgYHBh2HnZjS6AEnCPAI9CHJfT+05yiAiMf2JhUiRDFJlvUy8ej 14 | A2AI1bxX9Zdc0NMJ8FcQgkLKa7f9AQ4qZ4ms0wrM6MngJ2B3FvWN0GNhCcd4tCHY 15 | 4tXEuYh1tdsJHTQN5mmMygoQaRNLFkNmfzX+Zacu9mQ1rqscFNIIw85Mo1MwUTAd 16 | BgNVHQ4EFgQUYwbQ6G+UsYFFC62QOHfmte/hXgUwHwYDVR0jBBgwFoAUYwbQ6G+U 17 | sYFFC62QOHfmte/hXgUwDwYDVR0TAQH/BAUwAwEB/zALBglghkgBZQMEAwIDLwAw 18 | LAIUDrQSRm8Q/FBMf5XBeRJTvdQtuC8CFBMewjrn7nxfUqdCVBhhGVfvD/3v 19 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /SecureHttpClient.Test/res/ecdsa_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB4jCCAYegAwIBAgIUXkOSgHNzp/t0Wnz5ccsJ+jddKWcwCgYIKoZIzj0EAwIw 3 | RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu 4 | dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMjA5MjgxMTQ0NTJaGA8yMTIyMDkw 5 | NDExNDQ1MlowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAf 6 | BgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBZMBMGByqGSM49AgEGCCqG 7 | SM49AwEHA0IABGAGor02x4y9xhay+kdVZWEu00kBALhTjFkMKu406ENx5hd7PmNL 8 | Aju/14kXtr1kt81fGfoTnLJhP+ZA0aOMOEqjUzBRMB0GA1UdDgQWBBSv/W+mafvR 9 | ygv4+aHS4Qwx3svT0zAfBgNVHSMEGDAWgBSv/W+mafvRygv4+aHS4Qwx3svT0zAP 10 | BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQDABTpK9Zs+NUKWN5zq 11 | ZlkF2FbyHayWJ9ShkKs2fFD1tgIhALt3ASAqmAbAOPcatZbY7AcOm92zgu0jwr7H 12 | 2MuQukD6 13 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /SecureHttpClient.Test/res/rsa_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFbTCCA1WgAwIBAgIUekMcHQbR8z0h0EvtzIjkcJB5x94wDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMjA5MjgxMTQzMjdaGA8yMTIy 5 | MDkwNDExNDMyN1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx 6 | ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBAOcnxdC9fbwPRlorPLJ9fW+ceZOhZgEhVvew8sk/ 8 | br0KE/mMs2g00ab0d1+7TsHaWiD6J+ZhYXBe92exQJTs6pyws1MVzLmBQABLgkYF 9 | vyDFsmFz4izoGv42ySJtPr/70P3yPI/FULPg/fkT3KxNEZaRlMk9ofkTg8mZn1Dm 10 | TNnZ+5E6Wk+2fxlbLKggDVdlOx+aaO1Sw+4/fk/g5Hr3lMIEjpDc7WKjZzhzDQbq 11 | ZbRL4bm2698tWoke1B+TeKZMCO9P0ryof77ZCiADGNYNzaqtzyG6cFTngjjJZgqK 12 | k9Rzo7FylLKmx+nWDuBvgq1Z5GwYnioF2qnV0rNSly6Xo/ujT89Ape2yhqjHQdw3 13 | ztaIHWC0c+HXx0OP+FTcr8VKDCuNvYcKhql2/xww/QkUSsnMcKIsmS5vSePSqih/ 14 | IdtuW3y3ZQFWZzWvRSWiFeDhLk9hogMbCjrqQidygF2TkBOSGk4C/lEmU8YqcVFV 15 | VWrwpUDFh4UPBkDgjf1F66KQCbzxiKZ6jk2MFTwblxF6fKIwKGNotnciJU0ZqOXS 16 | 8d/oG6GfuacIhAwB1anfRtwyzauU5sVdkxAzQ1bIOclcP+ScwnKUpZlyG1hz2J9F 17 | AVlT6gCtPMx2bqV2AyYzAEokiI6p5t45LuLvGBID9jrUhXGtRkR8RjxaX1Fy3asr 18 | s6uDAgMBAAGjUzBRMB0GA1UdDgQWBBSz6qwl47g4Q3WyLQlQJOHgawUfnzAfBgNV 19 | HSMEGDAWgBSz6qwl47g4Q3WyLQlQJOHgawUfnzAPBgNVHRMBAf8EBTADAQH/MA0G 20 | CSqGSIb3DQEBCwUAA4ICAQBe+7DrjwYHjpdrFzVZwbJydhtE7YQImHpAyk6iuRBj 21 | k2JGjEY61nhad8IcMFznaXGev4D+YgnyvfVJ0+fVNnExbWfqlwKrRYfbZxCb8GYI 22 | 3tyHxJIt1QJdnmsjl89BM7esOiRpsI44lrH+AHgBsFThtrcbpQhyl3T90FKF1O7z 23 | GlVND2z147NdKpWsIEikNH1rT2PjzhO2Q3uBizh3bicU8l4rgr/36FKN8grCgSCW 24 | H5R125IXAiE3ttAgZMbLb8kbiRkUoiv+JhOsOAbDqaJOcMYmvRKq5cSe/GevFOVh 25 | XLacX+4z2ZM3easwvQQbj/5iSJ3szJjGsl5Gqv1lIgDnYcoUiQG/uvwMyFLOccVW 26 | qUEt6oZKkeIHLE5oHBVtlerZpzsYf0b7TLzuFk64JKW60kgQaRG3vNrFUHNy36KT 27 | Lx5+qxItKlzpFxS1B9REjq5Ax9I7Xi+h9RdRmp2Tx+fhdSEBYP+P1DOa/k1o5jtU 28 | kT80cswCAj9ii6SMMAsnNHftqeleLX2hL1MHm/rZh0n1z7iW3dXG32rC0JVLRv4B 29 | Vj4kiAqmdiiv0tZG+P7YEfu+3PDZs/4Ry8agwew+6Vr1k1HL4dDo5lg3BsXq0mHZ 30 | /w/ZmHY+WFU5Sbt3Fj/oSwCxru1ks/LvHHDcL56mFm5pQ7Qvyeq5s9vSyjLci/8D 31 | nw== 32 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /SecureHttpClient.Test/res/untrusted_root_badssl_com_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGfjCCBGagAwIBAgIJAJeg/PrX5Sj9MA0GCSqGSIb3DQEBCwUAMIGBMQswCQYD 3 | VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j 4 | aXNjbzEPMA0GA1UECgwGQmFkU1NMMTQwMgYDVQQDDCtCYWRTU0wgVW50cnVzdGVk 5 | IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTE2MDcwNzA2MzEzNVoXDTM2 6 | MDcwMjA2MzEzNVowgYExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlh 7 | MRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQKDAZCYWRTU0wxNDAyBgNV 8 | BAMMK0JhZFNTTCBVbnRydXN0ZWQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw 9 | ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKQtPMhEH073gis/HISWAi 10 | bOEpCtOsatA3JmeVbaWal8O/5ZO5GAn9dFVsGn0CXAHR6eUKYDAFJLa/3AhjBvWa 11 | tnQLoXaYlCvBjodjLEaFi8ckcJHrAYG9qZqioRQ16Yr8wUTkbgZf+er/Z55zi1yn 12 | CnhWth7kekvrwVDGP1rApeLqbhYCSLeZf5W/zsjLlvJni9OrU7U3a9msvz8mcCOX 13 | fJX9e3VbkD/uonIbK2SvmAGMaOj/1k0dASkZtMws0Bk7m1pTQL+qXDM/h3BQZJa5 14 | DwTcATaa/Qnk6YHbj/MaS5nzCSmR0Xmvs/3CulQYiZJ3kypns1KdqlGuwkfiCCgD 15 | yWJy7NE9qdj6xxLdqzne2DCyuPrjFPS0mmYimpykgbPnirEPBF1LW3GJc9yfhVXE 16 | Cc8OY8lWzxazDNNbeSRDpAGbBeGSQXGjAbliFJxwLyGzZ+cG+G8lc+zSvWjQu4Xp 17 | GJ+dOREhQhl+9U8oyPX34gfKo63muSgo539hGylqgQyzj+SX8OgK1FXXb2LS1gxt 18 | VIR5Qc4MmiEG2LKwPwfU8Yi+t5TYjGh8gaFv6NnksoX4hU42gP5KvjYggDpR+NSN 19 | CGQSWHfZASAYDpxjrOo+rk4xnO+sbuuMk7gORsrl+jgRT8F2VqoR9Z3CEdQxcCjR 20 | 5FsfTymZCk3GfIbWKkaeLQIDAQABo4H2MIHzMB0GA1UdDgQWBBRvx4NzSbWnY/91 21 | 3m1u/u37l6MsADCBtgYDVR0jBIGuMIGrgBRvx4NzSbWnY/913m1u/u37l6MsAKGB 22 | h6SBhDCBgTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNV 23 | BAcMDVNhbiBGcmFuY2lzY28xDzANBgNVBAoMBkJhZFNTTDE0MDIGA1UEAwwrQmFk 24 | U1NMIFVudHJ1c3RlZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eYIJAJeg/PrX 25 | 5Sj9MAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IC 26 | AQBQU9U8+jTRT6H9AIFm6y50tXTg/ySxRNmeP1Ey9Zf4jUE6yr3Q8xBv9gTFLiY1 27 | qW2qfkDSmXVdBkl/OU3+xb5QOG5hW7wVolWQyKREV5EvUZXZxoH7LVEMdkCsRJDK 28 | wYEKnEErFls5WPXY3bOglBOQqAIiuLQ0f77a2HXULDdQTn5SueW/vrA4RJEKuWxU 29 | iD9XPnVZ9tPtky2Du7wcL9qhgTddpS/NgAuLO4PXh2TQ0EMCll5reZ5AEr0NSLDF 30 | c/koDv/EZqB7VYhcPzr1bhQgbv1dl9NZU0dWKIMkRE/T7vZ97I3aPZqIapC2ulrf 31 | KrlqjXidwrGFg8xbiGYQHPx3tHPZxoM5WG2voI6G3s1/iD+B4V6lUEvivd3f6tq7 32 | d1V/3q1sL5DNv7TvaKGsq8g5un0TAkqaewJQ5fXLigF/yYu5a24/GUD783MdAPFv 33 | gWz8F81evOyRfpf9CAqIswMF+T6Dwv3aw5L9hSniMrblkg+ai0K22JfoBcGOzMtB 34 | Ke/Ps2Za56dTRoY/a4r62hrcGxufXd0mTdPaJLw3sJeHYjLxVAYWQq4QKJQWDgTS 35 | dAEWyN2WXaBFPx5c8KIW95Eu8ShWE00VVC3oA4emoZ2nrzBXLrUScifY6VaYYkkR 36 | 2O2tSqU8Ri3XRdgpNPDWp8ZL49KhYGYo3R/k98gnMHiY5g== 37 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.Maui.Hosting; 3 | using Xunit.Runners.Maui; 4 | 5 | namespace SecureHttpClient.TestRunner.Maui 6 | { 7 | public static class MauiProgram 8 | { 9 | public static MauiApp CreateMauiApp() 10 | { 11 | return MauiApp 12 | .CreateBuilder() 13 | .ConfigureTests(new TestOptions 14 | { 15 | Assemblies = 16 | { 17 | typeof (Test.SslTest).GetTypeInfo().Assembly 18 | } 19 | }) 20 | .UseVisualRunner() 21 | .Build(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Microsoft.Maui; 4 | 5 | namespace SecureHttpClient.TestRunner.Maui 6 | { 7 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] 8 | public class MainActivity : MauiAppCompatActivity 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Android.App; 3 | using Android.Runtime; 4 | using Microsoft.Maui; 5 | using Microsoft.Maui.Hosting; 6 | using Serilog; 7 | using Serilog.Core; 8 | using Xunit; 9 | 10 | namespace SecureHttpClient.TestRunner.Maui 11 | { 12 | [Application] 13 | public class MainApplication : MauiApplication 14 | { 15 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 16 | : base(handle, ownership) 17 | { 18 | } 19 | 20 | protected override MauiApp CreateMauiApp() 21 | { 22 | Log.Logger = new LoggerConfiguration() 23 | .MinimumLevel.Information() 24 | .WriteTo.AndroidLog() 25 | .Enrich.WithProperty(Constants.SourceContextPropertyName, "TestRunner") 26 | .CreateLogger(); 27 | 28 | AndroidEnvironment.UnhandledExceptionRaiser += OnAndroidEnvironmentUnhandledExceptionRaiser; 29 | 30 | return MauiProgram.CreateMauiApp(); 31 | } 32 | 33 | private static void OnAndroidEnvironmentUnhandledExceptionRaiser(object sender, RaiseThrowableEventArgs e) 34 | { 35 | Log.Fatal(e.Exception, "AndroidEnvironment.UnhandledExceptionRaiser"); 36 | Assert.Fail("AndroidEnvironment.UnhandledExceptionRaiser"); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Android/Resources/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Maui; 2 | using Microsoft.Maui.Hosting; 3 | using Serilog; 4 | using Serilog.Core; 5 | 6 | // To learn more about WinUI, the WinUI project structure, 7 | // and more about our project templates, see: http://aka.ms/winui-project-info. 8 | 9 | namespace SecureHttpClient.TestRunner.Maui 10 | { 11 | /// 12 | /// Provides application-specific behavior to supplement the default Application class. 13 | /// 14 | public partial class App : MauiWinUIApplication 15 | { 16 | /// 17 | /// Initializes the singleton application object. This is the first line of authored code 18 | /// executed, and as such is the logical equivalent of main() or WinMain(). 19 | /// 20 | public App() 21 | { 22 | this.InitializeComponent(); 23 | } 24 | 25 | protected override MauiApp CreateMauiApp() 26 | { 27 | Log.Logger = new LoggerConfiguration() 28 | .MinimumLevel.Information() 29 | .WriteTo.Debug(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext:l}] {Message}{NewLine}{Exception}") 30 | .Enrich.WithProperty(Constants.SourceContextPropertyName, "TestRunner") 31 | .CreateLogger(); 32 | 33 | return MauiProgram.CreateMauiApp(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | $placeholder$ 12 | User Name 13 | $placeholder$.png 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | using Microsoft.Maui; 3 | using Microsoft.Maui.Hosting; 4 | using Serilog; 5 | using Serilog.Core; 6 | 7 | namespace SecureHttpClient.TestRunner.Maui 8 | { 9 | [Register("AppDelegate")] 10 | public class AppDelegate : MauiUIApplicationDelegate 11 | { 12 | protected override MauiApp CreateMauiApp() 13 | { 14 | Log.Logger = new LoggerConfiguration() 15 | .MinimumLevel.Information() 16 | .WriteTo.NSLog() 17 | .Enrich.WithProperty(Constants.SourceContextPropertyName, "TestRunner") 18 | .CreateLogger(); 19 | 20 | return MauiProgram.CreateMauiApp(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace SecureHttpClient.TestRunner.Maui 4 | { 5 | public class Program 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, typeof(AppDelegate)); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Maui/SecureHttpClient.TestRunner.Maui.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0-android35.0;net9.0-ios18.0 5 | $(TargetFrameworks);net9.0-windows10.0.19041.0 6 | Exe 7 | SecureHttpClient.TestRunner.Maui 8 | true 9 | true 10 | true 11 | true 12 | 13 | 14 | $(NoWarn);IL2026;IL2104 15 | 16 | 17 | SecureHttpClient.TestRunner.Maui 18 | 19 | 20 | com.companyname.securehttpclient.testrunner.maui 21 | 6CDE38DD-3432-4EE7-93C6-876DFC1E323F 22 | 23 | 24 | 1.0 25 | 1 26 | 27 | 15.0 28 | 21.0 29 | 10.0.17763.0 30 | 10.0.17763.0 31 | 32 | 33 | 34 | android-x64 35 | 36 | 37 | 38 | Platforms\Android\AndroidManifest.xml 39 | r8 40 | true 41 | False 42 | no-write-symbols,nodebug 43 | full 44 | true 45 | true 46 | 47 | 48 | 49 | True 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Net/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Reflection; 5 | using Serilog; 6 | using Serilog.Core; 7 | using Xunit.Runners; 8 | 9 | namespace SecureHttpClient.TestRunner.Net 10 | { 11 | internal class Program 12 | { 13 | // Use an event to know when we're done 14 | private static ManualResetEvent _finished; 15 | 16 | // Start out assuming success; we'll set this to 1 if we get a failed test 17 | private static int _result; 18 | 19 | private static int _totalTests; 20 | private static int _testsFailed; 21 | private static int _testsSkipped; 22 | private static decimal _executionTime; 23 | 24 | private static int Main(string[] args) 25 | { 26 | // Init logger 27 | Log.Logger = new LoggerConfiguration() 28 | .MinimumLevel.Information() 29 | .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext:l}] {Message}{NewLine}{Exception}") 30 | .Enrich.WithProperty(Constants.SourceContextPropertyName, "TestRunner") 31 | .CreateLogger(); 32 | 33 | // Tests to run 34 | var testAssemblies = new List 35 | { 36 | typeof (Test.SslTest).GetTypeInfo().Assembly 37 | }; 38 | 39 | // Loop on test assemblies 40 | foreach (var testAssembly in testAssemblies) 41 | { 42 | RunTests(testAssembly); 43 | } 44 | 45 | Log.Information($"Total: Finished: {_totalTests} tests in {Math.Round(_executionTime, 3)}s ({_testsFailed} failed, {_testsSkipped} skipped)"); 46 | 47 | Log.CloseAndFlush(); 48 | 49 | return _result; 50 | } 51 | 52 | private static void RunTests(Assembly testAssembly) 53 | { 54 | _finished = new ManualResetEvent(false); 55 | 56 | // Run tests 57 | using var runner = AssemblyRunner.WithoutAppDomain(testAssembly.Location); 58 | runner.OnDiscoveryComplete = OnDiscoveryComplete; 59 | runner.OnExecutionComplete = OnExecutionComplete; 60 | runner.OnTestFailed = OnTestFailed; 61 | runner.OnTestPassed = OnTestPassed; 62 | runner.OnTestSkipped = OnTestSkipped; 63 | 64 | Log.Debug($"Processing {testAssembly.FullName}..."); 65 | runner.Start(new AssemblyRunnerStartOptions()); 66 | 67 | _finished.WaitOne(); 68 | _finished.Dispose(); 69 | } 70 | 71 | private static void OnDiscoveryComplete(DiscoveryCompleteInfo info) 72 | { 73 | Log.Debug($"Running {info.TestCasesToRun} of {info.TestCasesDiscovered} tests..."); 74 | } 75 | 76 | private static void OnExecutionComplete(ExecutionCompleteInfo info) 77 | { 78 | Log.Information($"Finished: {info.TotalTests} tests in {Math.Round(info.ExecutionTime, 3)}s ({info.TestsFailed} failed, {info.TestsSkipped} skipped)"); 79 | 80 | _totalTests += info.TotalTests; 81 | _testsFailed += info.TestsFailed; 82 | _testsSkipped += info.TestsSkipped; 83 | _executionTime += info.ExecutionTime; 84 | 85 | _finished.Set(); 86 | } 87 | 88 | private static void OnTestFailed(TestFailedInfo info) 89 | { 90 | Log.Error($"[FAIL] {info.TestDisplayName}: {info.ExceptionMessage}"); 91 | if (info.ExceptionStackTrace != null) 92 | { 93 | Log.Error(info.ExceptionStackTrace); 94 | } 95 | _result = 1; 96 | } 97 | 98 | private static void OnTestPassed(TestPassedInfo info) 99 | { 100 | Log.Information($"[PASS] {info.TestDisplayName}"); 101 | } 102 | 103 | private static void OnTestSkipped(TestSkippedInfo info) 104 | { 105 | Log.Warning($"[SKIP] {info.TestDisplayName}: {info.SkipReason}"); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /SecureHttpClient.TestRunner.Net/SecureHttpClient.TestRunner.Net.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | Exe 6 | true 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SecureHttpClient.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.0.31808.319 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{08CA4EC8-8C67-4325-A09B-95E4B4ABFA05}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{98B921B7-5B2A-4E33-941F-119E72DF1908}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureHttpClient.Test", "SecureHttpClient.Test\SecureHttpClient.Test.csproj", "{503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureHttpClient", "SecureHttpClient\SecureHttpClient.csproj", "{F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}" 12 | ProjectSection(ProjectDependencies) = postProject 13 | {53117652-4EF0-4378-B285-BFDF42947291} = {53117652-4EF0-4378-B285-BFDF42947291} 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{660924B3-5808-46CA-A170-062605939D7A}" 17 | ProjectSection(SolutionItems) = preProject 18 | CHANGELOG.md = CHANGELOG.md 19 | scripts\clean.bat = scripts\clean.bat 20 | CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md 21 | Directory.Build.props = Directory.Build.props 22 | Directory.Build.targets = Directory.Build.targets 23 | global.json = global.json 24 | scripts\nuget_pack.bat = scripts\nuget_pack.bat 25 | README.md = README.md 26 | build\SecureHttpClient.nuspec = build\SecureHttpClient.nuspec 27 | version.txt = version.txt 28 | EndProjectSection 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureHttpClient.OkHttp", "SecureHttpClient.OkHttp\SecureHttpClient.OkHttp.csproj", "{53117652-4EF0-4378-B285-BFDF42947291}" 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureHttpClient.TestRunner.Maui", "SecureHttpClient.TestRunner.Maui\SecureHttpClient.TestRunner.Maui.csproj", "{0BB582EF-7699-4AA3-B6EE-D26F63A1A413}" 33 | EndProject 34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SecureHttpClient.TestRunner.Net", "SecureHttpClient.TestRunner.Net\SecureHttpClient.TestRunner.Net.csproj", "{54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}" 35 | EndProject 36 | Global 37 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 38 | Ad-Hoc|Any CPU = Ad-Hoc|Any CPU 39 | Ad-Hoc|ARM = Ad-Hoc|ARM 40 | Ad-Hoc|ARM64 = Ad-Hoc|ARM64 41 | Ad-Hoc|iPhone = Ad-Hoc|iPhone 42 | Ad-Hoc|iPhoneSimulator = Ad-Hoc|iPhoneSimulator 43 | Ad-Hoc|x64 = Ad-Hoc|x64 44 | Ad-Hoc|x86 = Ad-Hoc|x86 45 | AppStore|Any CPU = AppStore|Any CPU 46 | AppStore|ARM = AppStore|ARM 47 | AppStore|ARM64 = AppStore|ARM64 48 | AppStore|iPhone = AppStore|iPhone 49 | AppStore|iPhoneSimulator = AppStore|iPhoneSimulator 50 | AppStore|x64 = AppStore|x64 51 | AppStore|x86 = AppStore|x86 52 | Debug|Any CPU = Debug|Any CPU 53 | Debug|ARM = Debug|ARM 54 | Debug|ARM64 = Debug|ARM64 55 | Debug|iPhone = Debug|iPhone 56 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 57 | Debug|x64 = Debug|x64 58 | Debug|x86 = Debug|x86 59 | Release|Any CPU = Release|Any CPU 60 | Release|ARM = Release|ARM 61 | Release|ARM64 = Release|ARM64 62 | Release|iPhone = Release|iPhone 63 | Release|iPhoneSimulator = Release|iPhoneSimulator 64 | Release|x64 = Release|x64 65 | Release|x86 = Release|x86 66 | EndGlobalSection 67 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 68 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU 69 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU 70 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU 71 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU 72 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU 73 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU 74 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU 75 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU 76 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU 77 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU 78 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU 79 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|x64.Build.0 = Debug|Any CPU 80 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU 81 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Ad-Hoc|x86.Build.0 = Debug|Any CPU 82 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|Any CPU.ActiveCfg = Release|Any CPU 83 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|Any CPU.Build.0 = Release|Any CPU 84 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|ARM.ActiveCfg = Debug|Any CPU 85 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|ARM.Build.0 = Debug|Any CPU 86 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|ARM64.ActiveCfg = Debug|Any CPU 87 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|ARM64.Build.0 = Debug|Any CPU 88 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|iPhone.ActiveCfg = Release|Any CPU 89 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|iPhone.Build.0 = Release|Any CPU 90 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU 91 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU 92 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|x64.ActiveCfg = Debug|Any CPU 93 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|x64.Build.0 = Debug|Any CPU 94 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|x86.ActiveCfg = Debug|Any CPU 95 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.AppStore|x86.Build.0 = Debug|Any CPU 96 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 97 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|Any CPU.Build.0 = Debug|Any CPU 98 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|ARM.ActiveCfg = Debug|Any CPU 99 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|ARM.Build.0 = Debug|Any CPU 100 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|ARM64.ActiveCfg = Debug|Any CPU 101 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|ARM64.Build.0 = Debug|Any CPU 102 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|iPhone.ActiveCfg = Debug|Any CPU 103 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|iPhone.Build.0 = Debug|Any CPU 104 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 105 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 106 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|x64.ActiveCfg = Debug|Any CPU 107 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|x64.Build.0 = Debug|Any CPU 108 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|x86.ActiveCfg = Debug|Any CPU 109 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Debug|x86.Build.0 = Debug|Any CPU 110 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|Any CPU.ActiveCfg = Release|Any CPU 111 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|Any CPU.Build.0 = Release|Any CPU 112 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|ARM.ActiveCfg = Release|Any CPU 113 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|ARM.Build.0 = Release|Any CPU 114 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|ARM64.ActiveCfg = Release|Any CPU 115 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|ARM64.Build.0 = Release|Any CPU 116 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|iPhone.ActiveCfg = Release|Any CPU 117 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|iPhone.Build.0 = Release|Any CPU 118 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 119 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 120 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|x64.ActiveCfg = Release|Any CPU 121 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|x64.Build.0 = Release|Any CPU 122 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|x86.ActiveCfg = Release|Any CPU 123 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A}.Release|x86.Build.0 = Release|Any CPU 124 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU 125 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU 126 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU 127 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|ARM.Build.0 = Release|Any CPU 128 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|ARM64.ActiveCfg = Release|Any CPU 129 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|ARM64.Build.0 = Release|Any CPU 130 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU 131 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU 132 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU 133 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU 134 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU 135 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|x64.Build.0 = Release|Any CPU 136 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU 137 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Ad-Hoc|x86.Build.0 = Release|Any CPU 138 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU 139 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|Any CPU.Build.0 = Debug|Any CPU 140 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|ARM.ActiveCfg = Release|Any CPU 141 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|ARM.Build.0 = Release|Any CPU 142 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|ARM64.ActiveCfg = Release|Any CPU 143 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|ARM64.Build.0 = Release|Any CPU 144 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|iPhone.ActiveCfg = Debug|Any CPU 145 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|iPhone.Build.0 = Debug|Any CPU 146 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU 147 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU 148 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|x64.ActiveCfg = Release|Any CPU 149 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|x64.Build.0 = Release|Any CPU 150 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|x86.ActiveCfg = Release|Any CPU 151 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.AppStore|x86.Build.0 = Release|Any CPU 152 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 153 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 154 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|ARM.ActiveCfg = Debug|Any CPU 155 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|ARM.Build.0 = Debug|Any CPU 156 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|ARM64.ActiveCfg = Debug|Any CPU 157 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|ARM64.Build.0 = Debug|Any CPU 158 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|iPhone.ActiveCfg = Debug|Any CPU 159 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|iPhone.Build.0 = Debug|Any CPU 160 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 161 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 162 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|x64.ActiveCfg = Debug|Any CPU 163 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|x64.Build.0 = Debug|Any CPU 164 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|x86.ActiveCfg = Debug|Any CPU 165 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Debug|x86.Build.0 = Debug|Any CPU 166 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 167 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|Any CPU.Build.0 = Release|Any CPU 168 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|ARM.ActiveCfg = Release|Any CPU 169 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|ARM.Build.0 = Release|Any CPU 170 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|ARM64.ActiveCfg = Release|Any CPU 171 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|ARM64.Build.0 = Release|Any CPU 172 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|iPhone.ActiveCfg = Release|Any CPU 173 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|iPhone.Build.0 = Release|Any CPU 174 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 175 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 176 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|x64.ActiveCfg = Release|Any CPU 177 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|x64.Build.0 = Release|Any CPU 178 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|x86.ActiveCfg = Release|Any CPU 179 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB}.Release|x86.Build.0 = Release|Any CPU 180 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU 181 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU 182 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU 183 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU 184 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU 185 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU 186 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU 187 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU 188 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU 189 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU 190 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU 191 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|x64.Build.0 = Debug|Any CPU 192 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU 193 | {53117652-4EF0-4378-B285-BFDF42947291}.Ad-Hoc|x86.Build.0 = Debug|Any CPU 194 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU 195 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|Any CPU.Build.0 = Debug|Any CPU 196 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|ARM.ActiveCfg = Debug|Any CPU 197 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|ARM.Build.0 = Debug|Any CPU 198 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|ARM64.ActiveCfg = Debug|Any CPU 199 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|ARM64.Build.0 = Debug|Any CPU 200 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|iPhone.ActiveCfg = Debug|Any CPU 201 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|iPhone.Build.0 = Debug|Any CPU 202 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU 203 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU 204 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|x64.ActiveCfg = Debug|Any CPU 205 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|x64.Build.0 = Debug|Any CPU 206 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|x86.ActiveCfg = Debug|Any CPU 207 | {53117652-4EF0-4378-B285-BFDF42947291}.AppStore|x86.Build.0 = Debug|Any CPU 208 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 209 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|Any CPU.Build.0 = Debug|Any CPU 210 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|ARM.ActiveCfg = Debug|Any CPU 211 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|ARM.Build.0 = Debug|Any CPU 212 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|ARM64.ActiveCfg = Debug|Any CPU 213 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|ARM64.Build.0 = Debug|Any CPU 214 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|iPhone.ActiveCfg = Debug|Any CPU 215 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|iPhone.Build.0 = Debug|Any CPU 216 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 217 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 218 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|x64.ActiveCfg = Debug|Any CPU 219 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|x64.Build.0 = Debug|Any CPU 220 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|x86.ActiveCfg = Debug|Any CPU 221 | {53117652-4EF0-4378-B285-BFDF42947291}.Debug|x86.Build.0 = Debug|Any CPU 222 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|Any CPU.ActiveCfg = Release|Any CPU 223 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|Any CPU.Build.0 = Release|Any CPU 224 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|ARM.ActiveCfg = Release|Any CPU 225 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|ARM.Build.0 = Release|Any CPU 226 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|ARM64.ActiveCfg = Release|Any CPU 227 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|ARM64.Build.0 = Release|Any CPU 228 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|iPhone.ActiveCfg = Release|Any CPU 229 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|iPhone.Build.0 = Release|Any CPU 230 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 231 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 232 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|x64.ActiveCfg = Release|Any CPU 233 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|x64.Build.0 = Release|Any CPU 234 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|x86.ActiveCfg = Release|Any CPU 235 | {53117652-4EF0-4378-B285-BFDF42947291}.Release|x86.Build.0 = Release|Any CPU 236 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU 237 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU 238 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|Any CPU.Deploy.0 = Debug|Any CPU 239 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU 240 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU 241 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|ARM.Deploy.0 = Debug|Any CPU 242 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU 243 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU 244 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|ARM64.Deploy.0 = Debug|Any CPU 245 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU 246 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU 247 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|iPhone.Deploy.0 = Debug|Any CPU 248 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU 249 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU 250 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|Any CPU 251 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU 252 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|x64.Build.0 = Debug|Any CPU 253 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|x64.Deploy.0 = Debug|Any CPU 254 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU 255 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|x86.Build.0 = Debug|Any CPU 256 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Ad-Hoc|x86.Deploy.0 = Debug|Any CPU 257 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU 258 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|Any CPU.Build.0 = Debug|Any CPU 259 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|Any CPU.Deploy.0 = Debug|Any CPU 260 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|ARM.ActiveCfg = Debug|Any CPU 261 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|ARM.Build.0 = Debug|Any CPU 262 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|ARM.Deploy.0 = Debug|Any CPU 263 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|ARM64.ActiveCfg = Debug|Any CPU 264 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|ARM64.Build.0 = Debug|Any CPU 265 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|ARM64.Deploy.0 = Debug|Any CPU 266 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|iPhone.ActiveCfg = Debug|Any CPU 267 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|iPhone.Build.0 = Debug|Any CPU 268 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|iPhone.Deploy.0 = Debug|Any CPU 269 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU 270 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU 271 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|iPhoneSimulator.Deploy.0 = Debug|Any CPU 272 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|x64.ActiveCfg = Debug|Any CPU 273 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|x64.Build.0 = Debug|Any CPU 274 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|x64.Deploy.0 = Debug|Any CPU 275 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|x86.ActiveCfg = Debug|Any CPU 276 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|x86.Build.0 = Debug|Any CPU 277 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.AppStore|x86.Deploy.0 = Debug|Any CPU 278 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 279 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|Any CPU.Build.0 = Debug|Any CPU 280 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 281 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|ARM.ActiveCfg = Debug|Any CPU 282 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|ARM.Build.0 = Debug|Any CPU 283 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|ARM.Deploy.0 = Debug|Any CPU 284 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|ARM64.ActiveCfg = Debug|Any CPU 285 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|ARM64.Build.0 = Debug|Any CPU 286 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|ARM64.Deploy.0 = Debug|Any CPU 287 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|iPhone.ActiveCfg = Debug|Any CPU 288 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|iPhone.Build.0 = Debug|Any CPU 289 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|iPhone.Deploy.0 = Debug|Any CPU 290 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 291 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 292 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU 293 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|x64.ActiveCfg = Debug|Any CPU 294 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|x64.Build.0 = Debug|Any CPU 295 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|x64.Deploy.0 = Debug|Any CPU 296 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|x86.ActiveCfg = Debug|Any CPU 297 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|x86.Build.0 = Debug|Any CPU 298 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Debug|x86.Deploy.0 = Debug|Any CPU 299 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|Any CPU.ActiveCfg = Release|Any CPU 300 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|Any CPU.Build.0 = Release|Any CPU 301 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|Any CPU.Deploy.0 = Release|Any CPU 302 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|ARM.ActiveCfg = Release|Any CPU 303 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|ARM.Build.0 = Release|Any CPU 304 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|ARM.Deploy.0 = Release|Any CPU 305 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|ARM64.ActiveCfg = Release|Any CPU 306 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|ARM64.Build.0 = Release|Any CPU 307 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|ARM64.Deploy.0 = Release|Any CPU 308 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|iPhone.ActiveCfg = Release|Any CPU 309 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|iPhone.Build.0 = Release|Any CPU 310 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|iPhone.Deploy.0 = Release|Any CPU 311 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 312 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 313 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU 314 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|x64.ActiveCfg = Release|Any CPU 315 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|x64.Build.0 = Release|Any CPU 316 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|x64.Deploy.0 = Release|Any CPU 317 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|x86.ActiveCfg = Release|Any CPU 318 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|x86.Build.0 = Release|Any CPU 319 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413}.Release|x86.Deploy.0 = Release|Any CPU 320 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU 321 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU 322 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU 323 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU 324 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU 325 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU 326 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU 327 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU 328 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU 329 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU 330 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU 331 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|x64.Build.0 = Debug|Any CPU 332 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU 333 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Ad-Hoc|x86.Build.0 = Debug|Any CPU 334 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU 335 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|Any CPU.Build.0 = Debug|Any CPU 336 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|ARM.ActiveCfg = Debug|Any CPU 337 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|ARM.Build.0 = Debug|Any CPU 338 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|ARM64.ActiveCfg = Debug|Any CPU 339 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|ARM64.Build.0 = Debug|Any CPU 340 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|iPhone.ActiveCfg = Debug|Any CPU 341 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|iPhone.Build.0 = Debug|Any CPU 342 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU 343 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU 344 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|x64.ActiveCfg = Debug|Any CPU 345 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|x64.Build.0 = Debug|Any CPU 346 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|x86.ActiveCfg = Debug|Any CPU 347 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.AppStore|x86.Build.0 = Debug|Any CPU 348 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 349 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 350 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|ARM.ActiveCfg = Debug|Any CPU 351 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|ARM.Build.0 = Debug|Any CPU 352 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|ARM64.ActiveCfg = Debug|Any CPU 353 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|ARM64.Build.0 = Debug|Any CPU 354 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|iPhone.ActiveCfg = Debug|Any CPU 355 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|iPhone.Build.0 = Debug|Any CPU 356 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 357 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 358 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|x64.ActiveCfg = Debug|Any CPU 359 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|x64.Build.0 = Debug|Any CPU 360 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|x86.ActiveCfg = Debug|Any CPU 361 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Debug|x86.Build.0 = Debug|Any CPU 362 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 363 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|Any CPU.Build.0 = Release|Any CPU 364 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|ARM.ActiveCfg = Release|Any CPU 365 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|ARM.Build.0 = Release|Any CPU 366 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|ARM64.ActiveCfg = Release|Any CPU 367 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|ARM64.Build.0 = Release|Any CPU 368 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|iPhone.ActiveCfg = Release|Any CPU 369 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|iPhone.Build.0 = Release|Any CPU 370 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 371 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 372 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|x64.ActiveCfg = Release|Any CPU 373 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|x64.Build.0 = Release|Any CPU 374 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|x86.ActiveCfg = Release|Any CPU 375 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD}.Release|x86.Build.0 = Release|Any CPU 376 | EndGlobalSection 377 | GlobalSection(SolutionProperties) = preSolution 378 | HideSolutionNode = FALSE 379 | EndGlobalSection 380 | GlobalSection(NestedProjects) = preSolution 381 | {503B71DB-1CCA-49CA-8C2F-7FAA87B5520A} = {98B921B7-5B2A-4E33-941F-119E72DF1908} 382 | {F30E01E7-A9DB-4E37-9E23-2E8A38E309EB} = {08CA4EC8-8C67-4325-A09B-95E4B4ABFA05} 383 | {53117652-4EF0-4378-B285-BFDF42947291} = {08CA4EC8-8C67-4325-A09B-95E4B4ABFA05} 384 | {0BB582EF-7699-4AA3-B6EE-D26F63A1A413} = {98B921B7-5B2A-4E33-941F-119E72DF1908} 385 | {54235C0F-082D-4F6D-B173-A8DD2DC8A6BD} = {98B921B7-5B2A-4E33-941F-119E72DF1908} 386 | EndGlobalSection 387 | GlobalSection(ExtensibilityGlobals) = postSolution 388 | SolutionGuid = {2D808F29-FA94-47AE-A112-BEF721E47F86} 389 | EndGlobalSection 390 | GlobalSection(MonoDevelopProperties) = preSolution 391 | Policies = $0 392 | $0.DotNetNamingPolicy = $1 393 | $1.DirectoryNamespaceAssociation = PrefixedHierarchical 394 | $1.ResourceNamePolicy = FileFormatDefault 395 | $0.TextStylePolicy = $4 396 | $2.inheritsSet = null 397 | $2.scope = text/x-csharp 398 | $0.CSharpFormattingPolicy = $3 399 | $3.AutoPropertyFormatting = ForceOneLine 400 | $3.IndentPreprocessorDirectives = False 401 | $3.PropertyBraceStyle = NextLine 402 | $3.EventBraceStyle = NextLine 403 | $3.EmbeddedStatementPlacement = SameLine 404 | $3.ArrayInitializerBraceStyle = NextLine 405 | $3.BeforeMethodDeclarationParentheses = False 406 | $3.BeforeMethodCallParentheses = False 407 | $3.BeforeConstructorDeclarationParentheses = False 408 | $3.BeforeIndexerDeclarationBracket = False 409 | $3.BeforeDelegateDeclarationParentheses = False 410 | $3.AfterDelegateDeclarationParameterComma = True 411 | $3.NewParentheses = False 412 | $3.SpacesBeforeBrackets = False 413 | $3.AlignToFirstMethodDeclarationParameter = False 414 | $3.AlignToFirstIndexerDeclarationParameter = False 415 | $3.inheritsSet = Mono 416 | $3.inheritsScope = text/x-csharp 417 | $3.scope = text/x-csharp 418 | $4.FileWidth = 120 419 | $4.inheritsSet = VisualStudio 420 | $4.inheritsScope = text/plain 421 | $4.scope = text/plain 422 | EndGlobalSection 423 | EndGlobal 424 | -------------------------------------------------------------------------------- /SecureHttpClient/Abstractions/IClientCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | namespace SecureHttpClient.Abstractions 2 | { 3 | /// 4 | /// Interface for storing client certificates and private keys. 5 | /// 6 | public interface IClientCertificateProvider 7 | { 8 | // Implementation is fully platform-specific 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SecureHttpClient/Abstractions/ISecureHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | namespace SecureHttpClient.Abstractions 2 | { 3 | /// 4 | /// Interface for SecureHttpClientHandler 5 | /// 6 | public interface ISecureHttpClientHandler 7 | { 8 | /// 9 | /// Add certificate pins for a given hostname 10 | /// 11 | /// The hostname 12 | /// The array of certificate pins (example of pin string: "sha256/fiKY8VhjQRb2voRmVXsqI0xPIREcwOVhpexrplrlqQY=") 13 | void AddCertificatePinner(string hostname, string[] pins); 14 | 15 | /// 16 | /// Set the client certificate provider 17 | /// 18 | /// The provider for client certificates on this platform 19 | void SetClientCertificates(IClientCertificateProvider provider); 20 | 21 | /// 22 | /// Set certificates for the trusted Root Certificate Authorities 23 | /// 24 | /// Certificates for the CAs to trust 25 | void SetTrustedRoots(params byte[][] certificates); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SecureHttpClient/CertificatePinning/CertificatePinner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Security.Cryptography.X509Certificates; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace SecureHttpClient.CertificatePinning 7 | { 8 | internal class CertificatePinner 9 | { 10 | private readonly ConcurrentDictionary _pins; 11 | private readonly ILogger _logger; 12 | 13 | public CertificatePinner(ILogger logger = null) 14 | { 15 | _pins = new ConcurrentDictionary(); 16 | _logger = logger; 17 | } 18 | 19 | public void AddPins(string hostname, string[] pins) 20 | { 21 | _logger?.LogDebug($"Add CertificatePinner: hostname:{hostname}, pins:{string.Join("|", pins)}"); 22 | _pins[hostname] = pins; // Updates value if already existing 23 | } 24 | 25 | public bool HasPin(string hostname) 26 | { 27 | return _pins.ContainsKey(hostname); 28 | } 29 | 30 | public bool Check(string hostname, X509Certificate2 certificate) 31 | { 32 | // Get pins 33 | if (!_pins.TryGetValue(hostname, out var pins)) 34 | { 35 | _logger?.LogDebug($"No certificate pin found for {hostname}"); 36 | return true; 37 | } 38 | 39 | // Compute spki fingerprint 40 | var spkiFingerprint = SpkiFingerprint.Compute(certificate); 41 | 42 | // Check pin 43 | var match = Array.IndexOf(pins, spkiFingerprint) > -1; 44 | if (match) 45 | { 46 | _logger?.LogDebug($"Certificate pin is ok for {hostname}"); 47 | } 48 | else 49 | { 50 | _logger?.LogInformation($"Certificate pin error for {hostname}: found {spkiFingerprint}, expected {string.Join("|", pins)}"); 51 | } 52 | return match; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SecureHttpClient/CertificatePinning/SpkiFingerprint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | namespace SecureHttpClient.CertificatePinning 6 | { 7 | internal class SpkiFingerprint 8 | { 9 | public static string Compute(X509Certificate2 certificate) 10 | { 11 | // Extract SPKI (der-encoded) 12 | var spki = SpkiProvider.GetSpki(certificate); 13 | 14 | // Compute spki fingerprint (sha256) 15 | using var digester = SHA256.Create(); 16 | var digest = digester.ComputeHash(spki); 17 | var spkiFingerprint = Convert.ToBase64String(digest); 18 | 19 | return $"sha256/{spkiFingerprint}"; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SecureHttpClient/Extensions/RequestPropertiesExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace SecureHttpClient.Extensions 4 | { 5 | /// 6 | /// Request properties extensions 7 | /// 8 | public static class RequestPropertiesExtensions 9 | { 10 | private const string HeadersOrderPropertyKey = "headersOrder"; 11 | 12 | /// 13 | /// Set the headers order 14 | /// 15 | /// The request 16 | /// The ordered headers names 17 | public static void SetHeadersOrder(this HttpRequestMessage request, params string[] headers) 18 | { 19 | request.Options.Set(new HttpRequestOptionsKey(HeadersOrderPropertyKey), headers); 20 | } 21 | 22 | internal static string[] GetHeadersOrder(this HttpRequestMessage request) 23 | { 24 | if (request.Options.TryGetValue(new HttpRequestOptionsKey(HeadersOrderPropertyKey), out var value)) 25 | { 26 | return (string[])value; 27 | } 28 | return null; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Android/ClientCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | #if __ANDROID__ 2 | 3 | using System.IO; 4 | using Java.Security; 5 | 6 | namespace SecureHttpClient 7 | { 8 | /// 9 | /// IClientCertificateProvider for Android 10 | /// 11 | public interface IClientCertificateProvider : Abstractions.IClientCertificateProvider 12 | { 13 | /// 14 | /// The current collection of client certificates 15 | /// 16 | /// The key store. 17 | KeyStore KeyStore { get; } 18 | } 19 | 20 | /// 21 | /// Base Client certificate provider for Android 22 | /// 23 | public class ClientCertificateProvider : IClientCertificateProvider 24 | { 25 | /// 26 | /// The current collection of client certificates 27 | /// 28 | /// The key store. 29 | public virtual KeyStore KeyStore { get; private set; } 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The type of backing store for this certificate provider. 35 | public ClientCertificateProvider(string type) 36 | { 37 | KeyStore = KeyStore.GetInstance(type); 38 | KeyStore.Load(null); 39 | } 40 | } 41 | 42 | /// 43 | /// Client certificate provider for imported certificates and keys. 44 | /// 45 | public class ImportedClientCertificateProvider : ClientCertificateProvider 46 | { 47 | /// 48 | /// Initializes a new instance of the class 49 | /// with the default backing store. 50 | /// 51 | public ImportedClientCertificateProvider() : base("pkcs12") 52 | { 53 | } 54 | 55 | /// 56 | /// Import the specified certificate and its associated private key. 57 | /// 58 | /// The certificate and key, in PKCS12 format. 59 | /// The passphrase that protects the private key. 60 | public void Import(byte[] certificate, string passphrase) 61 | { 62 | KeyStore.Load(new MemoryStream(certificate), passphrase.ToCharArray()); 63 | } 64 | } 65 | } 66 | 67 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Android/SecureHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | #if __ANDROID__ 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Android.Runtime; 11 | using Java.Security; 12 | using Java.Security.Cert; 13 | using Java.Util.Concurrent; 14 | using Javax.Net.Ssl; 15 | using Square.OkHttp3; 16 | using SecureHttpClient.OkHttp; 17 | using Microsoft.Extensions.Logging; 18 | using System.Security.Authentication; 19 | using Java.Net; 20 | using SecureHttpClient.Extensions; 21 | 22 | namespace SecureHttpClient 23 | { 24 | /// 25 | /// Implementation of ISecureHttpClientHandler (Android implementation) 26 | /// 27 | public class SecureHttpClientHandler : HttpClientHandler, Abstractions.ISecureHttpClientHandler 28 | { 29 | private readonly Lazy _client; 30 | private readonly Lazy _certificatePinnerBuilder; 31 | private readonly ILogger _logger; 32 | private KeyManagerFactory _keyMgrFactory; 33 | private TrustManagerFactory _trustMgrFactory; 34 | private IX509TrustManager _x509TrustManager; 35 | private IKeyManager[] KeyManagers => _keyMgrFactory?.GetKeyManagers(); 36 | private ITrustManager[] TrustManagers => _trustMgrFactory?.GetTrustManagers(); 37 | 38 | /// 39 | /// SecureHttpClientHandler constructor (Android implementation) 40 | /// 41 | /// Logger 42 | public SecureHttpClientHandler(ILogger logger) 43 | { 44 | _logger = logger; 45 | _client = new Lazy(CreateOkHttpClientInstance); 46 | _certificatePinnerBuilder = new Lazy(); 47 | } 48 | 49 | /// 50 | /// Add certificate pins for a given hostname (Android implementation) 51 | /// 52 | /// The hostname 53 | /// The array of certifiate pins (example of pin string: "sha256/fiKY8VhjQRb2voRmVXsqI0xPIREcwOVhpexrplrlqQY=") 54 | public virtual void AddCertificatePinner(string hostname, string[] pins) 55 | { 56 | _logger?.LogDebug($"Add CertificatePinner: hostname:{hostname}, pins:{string.Join("|", pins)}"); 57 | _certificatePinnerBuilder.Value.Add(hostname, pins); 58 | } 59 | 60 | /// 61 | /// Set the client certificate provider (Android implementation) 62 | /// 63 | /// The provider for client certificates on this platform 64 | public virtual void SetClientCertificates(Abstractions.IClientCertificateProvider provider) 65 | { 66 | if (provider is IClientCertificateProvider androidProvider) 67 | { 68 | _keyMgrFactory = KeyManagerFactory.GetInstance("X509"); 69 | _keyMgrFactory.Init(androidProvider.KeyStore, null); 70 | } 71 | else 72 | { 73 | _keyMgrFactory = null; 74 | } 75 | } 76 | 77 | /// 78 | /// Set certificates for the trusted Root Certificate Authorities (Android implementation) 79 | /// 80 | /// Certificates for the CAs to trust 81 | public virtual void SetTrustedRoots(params byte[][] certificates) 82 | { 83 | if (certificates == null) 84 | { 85 | _trustMgrFactory = null; 86 | _x509TrustManager = null; 87 | return; 88 | } 89 | var keyStore = KeyStore.GetInstance(KeyStore.DefaultType); 90 | keyStore.Load(null); 91 | var certFactory = CertificateFactory.GetInstance("X.509"); 92 | foreach (var certificate in certificates) 93 | { 94 | var cert = (X509Certificate) certFactory.GenerateCertificate(new System.IO.MemoryStream(certificate)); 95 | keyStore.SetCertificateEntry(cert.SubjectDN.Name, cert); 96 | } 97 | 98 | _trustMgrFactory = TrustManagerFactory.GetInstance(TrustManagerFactory.DefaultAlgorithm); 99 | _trustMgrFactory.Init(keyStore); 100 | foreach (var trustManager in TrustManagers) 101 | { 102 | _x509TrustManager = trustManager.JavaCast(); 103 | if (_x509TrustManager != null) 104 | { 105 | break; 106 | } 107 | } 108 | } 109 | 110 | private OkHttpClient CreateOkHttpClientInstance() 111 | { 112 | var builder = new OkHttpClient.Builder() 113 | .ConnectTimeout(100, TimeUnit.Seconds) 114 | .WriteTimeout(100, TimeUnit.Seconds) 115 | .ReadTimeout(100, TimeUnit.Seconds) 116 | .FollowRedirects(AllowAutoRedirect) 117 | .AddInterceptor(new DecompressInterceptor()) 118 | .AddNetworkInterceptor(new HeadersOrderInterceptor()); 119 | 120 | if (UseCookies) 121 | { 122 | builder.CookieJar(new JavaNetCookieJar(new Java.Net.CookieManager())); 123 | } 124 | 125 | if (!UseProxy) 126 | { 127 | builder.Proxy(Java.Net.Proxy.NoProxy); 128 | } 129 | else if (Proxy is WebProxy webProxy) 130 | { 131 | var proxyAddress = new InetSocketAddress(webProxy.Address.Host, webProxy.Address.Port); 132 | builder.Proxy(new Proxy(Java.Net.Proxy.Type.Http, proxyAddress)); 133 | } 134 | 135 | if (_certificatePinnerBuilder.IsValueCreated) 136 | { 137 | builder.CertificatePinner(_certificatePinnerBuilder.Value.Build()); 138 | } 139 | 140 | if (_keyMgrFactory != null || _trustMgrFactory != null) 141 | { 142 | var context = SSLContext.GetInstance("TLS"); 143 | context.Init(KeyManagers, TrustManagers, null); 144 | builder.SslSocketFactory(context.SocketFactory, _x509TrustManager ?? TlsSslSocketFactory.GetSystemDefaultTrustManager()); 145 | } 146 | 147 | return builder.Build(); 148 | } 149 | 150 | /// 151 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 152 | { 153 | var javaUri = request.RequestUri.GetComponents(UriComponents.AbsoluteUri, UriFormat.UriEscaped); 154 | var url = new Java.Net.URL(javaUri); 155 | 156 | var body = default(RequestBody); 157 | if (request.Content != null) 158 | { 159 | var bytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); 160 | if (bytes.Length > 0 || request.Method != HttpMethod.Get) 161 | { 162 | var contentType = "text/plain"; 163 | if (request.Content.Headers.ContentType != null) 164 | { 165 | contentType = string.Join(" ", request.Content.Headers.GetValues("Content-Type")); 166 | } 167 | body = RequestBody.Create(bytes, MediaType.Parse(contentType)); 168 | } 169 | } 170 | 171 | var builder = new Request.Builder() 172 | .Method(request.Method.Method.ToUpperInvariant(), body) 173 | .Url(url); 174 | 175 | var keyValuePairs = request.Headers 176 | .Union(request.Content?.Headers ?? Enumerable.Empty>>()) 177 | .ToArray(); 178 | 179 | foreach (var (name, values) in keyValuePairs) 180 | { 181 | var headerSeparator = name == "User-Agent" ? " " : ","; 182 | builder.AddHeader(name, string.Join(headerSeparator, values)); 183 | } 184 | 185 | if (!keyValuePairs.Any(kv => kv.Key.Equals("Accept-Encoding", StringComparison.OrdinalIgnoreCase))) 186 | { 187 | builder.AddHeader("Accept-Encoding", "gzip, deflate, br"); 188 | } 189 | 190 | var headersOrder = request.GetHeadersOrder(); 191 | if (headersOrder != null) 192 | { 193 | builder.AddHeader("securehttpclient-headers-order", string.Join(';', headersOrder)); 194 | } 195 | 196 | cancellationToken.ThrowIfCancellationRequested(); 197 | 198 | var rq = builder.Build(); 199 | var call = _client.Value.NewCall(rq); 200 | 201 | // NB: Even closing a socket must be done off the UI thread. Cray! 202 | cancellationToken.Register(() => Task.Run(() => call.Cancel())); 203 | 204 | Response resp; 205 | try 206 | { 207 | resp = await call.ExecuteAsync().ConfigureAwait(false); 208 | } 209 | catch (Javax.Net.Ssl.SSLException ex) 210 | { 211 | throw new HttpRequestException(ex.Message, new AuthenticationException(ex.Message, ex)); 212 | } 213 | catch (Java.Net.SocketTimeoutException ex) 214 | { 215 | throw new TaskCanceledException(ex.Message, ex); 216 | } 217 | catch (Java.IO.IOException ex) 218 | { 219 | throw new HttpRequestException(ex.Message, ex); 220 | } 221 | 222 | var respBody = resp.Body(); 223 | 224 | cancellationToken.ThrowIfCancellationRequested(); 225 | 226 | var ret = new HttpResponseMessage((HttpStatusCode) resp.Code()) 227 | { 228 | RequestMessage = request, 229 | ReasonPhrase = resp.Message(), 230 | Version = GetVersion(resp.Protocol()) 231 | }; 232 | ret.RequestMessage.RequestUri = new Uri(resp.Request().Url().Url().ToString()); // should point to the request leading to the final response (in case of redirects) 233 | 234 | if (respBody != null) 235 | { 236 | ret.Content = new StreamContent(respBody.ByteStream()); 237 | } 238 | else 239 | { 240 | ret.Content = new ByteArrayContent(new byte[0]); 241 | } 242 | 243 | foreach (var (name, values) in resp.Headers().ToMultimap()) 244 | { 245 | // special handling for Set-Cookie because folding them into one header is strongly discouraged. 246 | // but adding them just folds them again so this is no option at the moment 247 | ret.Headers.TryAddWithoutValidation(name, values); 248 | ret.Content.Headers.TryAddWithoutValidation(name, values); 249 | } 250 | 251 | return ret; 252 | } 253 | 254 | private static Version GetVersion(Protocol protocol) 255 | { 256 | if (protocol == Protocol.Http10) 257 | { 258 | return HttpVersion.Version10; 259 | } 260 | if (protocol == Protocol.Http11) 261 | { 262 | return HttpVersion.Version11; 263 | } 264 | if (protocol == Protocol.Http2 || protocol == Protocol.H2PriorKnowledge) 265 | { 266 | return HttpVersion.Version20; 267 | } 268 | return HttpVersion.Unknown; 269 | } 270 | } 271 | } 272 | 273 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Android/SpkiProvider.cs: -------------------------------------------------------------------------------- 1 | #if __ANDROID__ 2 | 3 | using System; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | namespace SecureHttpClient 7 | { 8 | internal class SpkiProvider 9 | { 10 | public static byte[] GetSpki(X509Certificate2 certificate) 11 | { 12 | throw new NotImplementedException(); 13 | } 14 | } 15 | } 16 | 17 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Android/TlsSslSocketFactory.cs: -------------------------------------------------------------------------------- 1 | #if __ANDROID__ 2 | 3 | using Android.Runtime; 4 | using Java.Lang; 5 | using Java.Security; 6 | using Javax.Net.Ssl; 7 | 8 | namespace SecureHttpClient 9 | { 10 | internal static class TlsSslSocketFactory 11 | { 12 | public static IX509TrustManager GetSystemDefaultTrustManager() 13 | { 14 | IX509TrustManager x509TrustManager = null; 15 | try 16 | { 17 | var trustManagerFactory = TrustManagerFactory.GetInstance(TrustManagerFactory.DefaultAlgorithm); 18 | trustManagerFactory.Init((KeyStore)null); 19 | foreach (var trustManager in trustManagerFactory.GetTrustManagers()) 20 | { 21 | var manager = trustManager.JavaCast(); 22 | if (manager != null) 23 | { 24 | x509TrustManager = manager; 25 | break; 26 | } 27 | } 28 | } 29 | catch (Exception ex) when (ex is NoSuchAlgorithmException || ex is KeyStoreException) 30 | { 31 | // move along... 32 | } 33 | return x509TrustManager; 34 | } 35 | } 36 | } 37 | 38 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Net/ClientCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | #if (!__ANDROID__ && !__IOS__) 2 | 3 | using System.Security.Cryptography.X509Certificates; 4 | 5 | namespace SecureHttpClient 6 | { 7 | /// 8 | /// IClientCertificateProvider for Portable .Net 9 | /// 10 | public interface IClientCertificateProvider : Abstractions.IClientCertificateProvider 11 | { 12 | /// 13 | /// The current collection of client certificates. 14 | /// 15 | /// The certificates. 16 | X509CertificateCollection Certificates { get; } 17 | } 18 | 19 | /// 20 | /// Base Client certificate provider for Portable .Net 21 | /// 22 | public class ClientCertificateProvider : IClientCertificateProvider 23 | { 24 | /// 25 | /// The current collection of client certificates. 26 | /// 27 | /// The certificates. 28 | public virtual X509CertificateCollection Certificates { get; protected set; } = new (); 29 | } 30 | 31 | /// 32 | /// Client certificate provider for imported certificates and keys. 33 | /// 34 | public class ImportedClientCertificateProvider : ClientCertificateProvider 35 | { 36 | /// 37 | /// Import the specified certificate and its associated private key. 38 | /// 39 | /// The certificate and key, in PKCS12 format. 40 | /// The passphrase that protects the private key. 41 | public void Import(byte[] certificate, string passphrase) 42 | { 43 | Certificates.Add(X509CertificateLoader.LoadPkcs12(certificate, passphrase)); 44 | } 45 | } 46 | } 47 | 48 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Net/SecureHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | #if (!__ANDROID__ && !__IOS__) 2 | 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Security; 7 | using System.Security.Cryptography.X509Certificates; 8 | using Microsoft.Extensions.Logging; 9 | using SecureHttpClient.CertificatePinning; 10 | 11 | namespace SecureHttpClient 12 | { 13 | /// 14 | /// Implementation of ISecureHttpClientHandler (NetStandard implementation) 15 | /// 16 | public class SecureHttpClientHandler : HttpClientHandler, Abstractions.ISecureHttpClientHandler 17 | { 18 | private readonly Lazy _certificatePinner; 19 | private readonly ILogger _logger; 20 | private X509Certificate2Collection _trustedRoots; 21 | 22 | /// 23 | /// SecureHttpClientHandler constructor (NetStandard implementation) 24 | /// 25 | /// Logger 26 | public SecureHttpClientHandler(ILogger logger) 27 | { 28 | _logger = logger; 29 | 30 | _certificatePinner = new Lazy(() => new CertificatePinner(logger)); 31 | 32 | // Set Accept-Encoding headers and take care of decompression if needed 33 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli; 34 | MaxAutomaticRedirections = 10; 35 | } 36 | 37 | /// 38 | /// Add certificate pins for a given hostname (NetStandard implementation) 39 | /// 40 | /// The hostname 41 | /// The array of certifiate pins (example of pin string: "sha256/fiKY8VhjQRb2voRmVXsqI0xPIREcwOVhpexrplrlqQY=") 42 | public virtual void AddCertificatePinner(string hostname, string[] pins) 43 | { 44 | _certificatePinner.Value.AddPins(hostname, pins); 45 | ServerCertificateCustomValidationCallback = CheckServerCertificate; 46 | } 47 | 48 | /// 49 | /// Set the client certificate provider (NetStandard implementation) 50 | /// 51 | /// The provider for client certificates on this platform 52 | public virtual void SetClientCertificates(Abstractions.IClientCertificateProvider provider) 53 | { 54 | ClientCertificates.Clear(); 55 | if (provider is IClientCertificateProvider netProvider) 56 | { 57 | ClientCertificates.AddRange(netProvider.Certificates); 58 | } 59 | } 60 | 61 | /// 62 | /// Set certificates for the trusted Root Certificate Authorities (NetStandard implementation) 63 | /// 64 | /// Certificates for the CAs to trust 65 | public virtual void SetTrustedRoots(params byte[][] certificates) 66 | { 67 | if (certificates.Length == 0) 68 | { 69 | _trustedRoots = null; 70 | return; 71 | } 72 | _trustedRoots = new X509Certificate2Collection(); 73 | foreach (var cert in certificates) 74 | { 75 | _trustedRoots.Add(X509CertificateLoader.LoadCertificate(cert)); 76 | } 77 | ServerCertificateCustomValidationCallback = CheckServerCertificate; 78 | } 79 | 80 | private bool CheckServerCertificate(HttpRequestMessage httpRequestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 81 | { 82 | if (certificate == null) 83 | { 84 | _logger?.LogDebug("Missing certificate"); 85 | return false; 86 | } 87 | 88 | var good = sslPolicyErrors == SslPolicyErrors.None; 89 | if (_trustedRoots != null && (sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) == 0) 90 | { 91 | chain.ChainPolicy.ExtraStore.AddRange(_trustedRoots); 92 | chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; 93 | if (chain.Build(certificate)) 94 | { 95 | var root = chain.ChainElements[^1].Certificate; 96 | good = _trustedRoots.Find(X509FindType.FindByThumbprint, root.Thumbprint, false).Count > 0; 97 | } 98 | } 99 | 100 | if (!good) 101 | { 102 | _logger?.LogDebug($"SSL policy errors {sslPolicyErrors}"); 103 | return false; 104 | } 105 | 106 | if (_certificatePinner.IsValueCreated) 107 | { 108 | // Get request host 109 | var requestHost = httpRequestMessage?.RequestUri?.Host; 110 | if (string.IsNullOrEmpty(requestHost)) 111 | { 112 | _logger?.LogDebug("Failed to get host from request"); 113 | return false; 114 | } 115 | 116 | // Check pin 117 | good = _certificatePinner.Value.Check(requestHost, certificate); 118 | } 119 | return good; 120 | } 121 | } 122 | } 123 | 124 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/Net/SpkiProvider.cs: -------------------------------------------------------------------------------- 1 | #if (!__ANDROID__ && !__IOS__) 2 | 3 | using System.Security.Cryptography; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | namespace SecureHttpClient 7 | { 8 | internal class SpkiProvider 9 | { 10 | public static byte[] GetSpki(X509Certificate2 certificate) 11 | { 12 | // Extract SPKI (der-encoded) 13 | using var key = certificate.GetECDsaPublicKey() ?? certificate.GetRSAPublicKey() ?? (AsymmetricAlgorithm)certificate.GetDSAPublicKey(); 14 | var spki = key.ExportSubjectPublicKeyInfo(); 15 | return spki; 16 | } 17 | } 18 | } 19 | 20 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/AsyncLock.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using System.Threading; 6 | 7 | namespace SecureHttpClient 8 | { 9 | // Straight-up thieved from http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx 10 | internal sealed class AsyncLock 11 | { 12 | private readonly SemaphoreSlim _semaphore; 13 | private readonly Task _releaser; 14 | 15 | public static AsyncLock CreateLocked(out IDisposable releaser) 16 | { 17 | var asyncLock = new AsyncLock(true); 18 | releaser = asyncLock._releaser.Result; 19 | return asyncLock; 20 | } 21 | 22 | private AsyncLock(bool isLocked) 23 | { 24 | _semaphore = new SemaphoreSlim(isLocked ? 0 : 1, 1); 25 | _releaser = Task.FromResult((IDisposable)new Releaser(this)); 26 | } 27 | 28 | public Task LockAsync() 29 | { 30 | var wait = _semaphore.WaitAsync(); 31 | return wait.IsCompleted ? 32 | _releaser : 33 | wait.ContinueWith((_, state) => (IDisposable)state, 34 | _releaser.Result, CancellationToken.None, 35 | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 36 | } 37 | 38 | private sealed class Releaser : IDisposable 39 | { 40 | readonly AsyncLock _toRelease; 41 | internal Releaser(AsyncLock toRelease) { _toRelease = toRelease; } 42 | public void Dispose() { _toRelease._semaphore.Release(); } 43 | } 44 | } 45 | } 46 | 47 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/ByteArrayListStream.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace SecureHttpClient 10 | { 11 | internal class ByteArrayListStream : Stream 12 | { 13 | private Exception _exception; 14 | private IDisposable _lockRelease; 15 | private readonly AsyncLock _readStreamLock; 16 | private readonly List _bytes = new (); 17 | 18 | private bool _isCompleted; 19 | private long _maxLength; 20 | private long _position; 21 | private int _offsetInCurrentBuffer; 22 | 23 | public ByteArrayListStream() 24 | { 25 | // Initially we have nothing to read so Reads should be parked 26 | _readStreamLock = AsyncLock.CreateLocked(out _lockRelease); 27 | } 28 | 29 | public override bool CanRead => true; 30 | public override bool CanWrite => false; 31 | public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } 32 | public override void WriteByte(byte value) { throw new NotSupportedException(); } 33 | public override bool CanSeek => false; 34 | public override bool CanTimeout => false; 35 | public override void SetLength(long value) { throw new NotSupportedException(); } 36 | public override void Flush() { } 37 | 38 | public override long Seek(long offset, SeekOrigin origin) 39 | { 40 | throw new NotSupportedException(); 41 | } 42 | 43 | public override long Position 44 | { 45 | get => _position; 46 | set => throw new NotSupportedException(); 47 | } 48 | 49 | public override long Length => _maxLength; 50 | 51 | public override int Read(byte[] buffer, int offset, int count) 52 | { 53 | return ReadAsync(buffer, offset, count).Result; 54 | } 55 | 56 | /* OMG THIS CODE IS COMPLICATED 57 | * 58 | * Here's the core idea. We want to create a ReadAsync function that 59 | * reads from our list of byte arrays **until it gets to the end of 60 | * our current list**. 61 | * 62 | * If we're not there yet, we keep returning data, serializing access 63 | * to the underlying position pointer (i.e. we definitely don't want 64 | * people concurrently moving position along). If we try to read past 65 | * the end, we return the section of data we could read and complete 66 | * it. 67 | * 68 | * Here's where the tricky part comes in. If we're not Completed (i.e. 69 | * the caller still wants to add more byte arrays in the future) and 70 | * we're at the end of the current stream, we want to *block* the read 71 | * (not blocking, but async blocking whatever you know what I mean), 72 | * until somebody adds another byte[] to chew through, or if someone 73 | * rewinds the position. 74 | * 75 | * If we *are* completed, we should return zero to simply complete the 76 | * read, signalling we're at the end of the stream */ 77 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 78 | { 79 | retry: 80 | var bytesRead = 0; 81 | var buffersToRemove = 0; 82 | 83 | if (_isCompleted && _position == _maxLength) 84 | { 85 | return 0; 86 | } 87 | 88 | if (_exception != null) throw _exception; 89 | 90 | using (await _readStreamLock.LockAsync().ConfigureAwait(false)) 91 | { 92 | lock (_bytes) 93 | { 94 | foreach (var buf in _bytes) 95 | { 96 | cancellationToken.ThrowIfCancellationRequested(); 97 | if (_exception != null) throw _exception; 98 | 99 | var toCopy = Math.Min(count, buf.Length - _offsetInCurrentBuffer); 100 | Array.ConstrainedCopy(buf, _offsetInCurrentBuffer, buffer, offset, toCopy); 101 | 102 | count -= toCopy; 103 | offset += toCopy; 104 | bytesRead += toCopy; 105 | 106 | _offsetInCurrentBuffer += toCopy; 107 | 108 | if (_offsetInCurrentBuffer >= buf.Length) 109 | { 110 | _offsetInCurrentBuffer = 0; 111 | buffersToRemove++; 112 | } 113 | 114 | if (count <= 0) break; 115 | } 116 | 117 | // Remove buffers that we read in this operation 118 | _bytes.RemoveRange(0, buffersToRemove); 119 | 120 | _position += bytesRead; 121 | } 122 | } 123 | 124 | // If we're at the end of the stream and it's not done, prepare 125 | // the next read to park itself unless AddByteArray or Complete 126 | // posts 127 | if (_position >= _maxLength && !_isCompleted) 128 | { 129 | _lockRelease = await _readStreamLock.LockAsync().ConfigureAwait(false); 130 | } 131 | 132 | if (bytesRead == 0 && !_isCompleted) 133 | { 134 | // NB: There are certain race conditions where we somehow acquire 135 | // the lock yet are at the end of the stream, and we're not completed 136 | // yet. We should try again so that we can get stuck in the lock. 137 | goto retry; 138 | } 139 | 140 | if (cancellationToken.IsCancellationRequested) 141 | { 142 | Interlocked.Exchange(ref _lockRelease, EmptyDisposable.Instance).Dispose(); 143 | cancellationToken.ThrowIfCancellationRequested(); 144 | } 145 | 146 | if (_exception != null) 147 | { 148 | Interlocked.Exchange(ref _lockRelease, EmptyDisposable.Instance).Dispose(); 149 | throw _exception; 150 | } 151 | 152 | if (_isCompleted && _position < _maxLength) 153 | { 154 | // NB: This solves a rare deadlock 155 | // 156 | // 1. ReadAsync called (waiting for lock release) 157 | // 2. AddByteArray called (release lock) 158 | // 3. AddByteArray called (release lock) 159 | // 4. Complete called (release lock the last time) 160 | // 5. ReadAsync called (lock released at this point, the method completed successfully) 161 | // 6. ReadAsync called (deadlock on LockAsync(), because the lock is block, and there is no way to release it) 162 | // 163 | // Current condition forces the lock to be released in the end of 5th point 164 | 165 | Interlocked.Exchange(ref _lockRelease, EmptyDisposable.Instance).Dispose(); 166 | } 167 | 168 | return bytesRead; 169 | } 170 | 171 | public void AddByteArray(byte[] arrayToAdd) 172 | { 173 | if (_exception != null) throw _exception; 174 | if (_isCompleted) throw new InvalidOperationException("Can't add byte arrays once Complete() is called"); 175 | 176 | lock (_bytes) 177 | { 178 | _maxLength += arrayToAdd.Length; 179 | _bytes.Add(arrayToAdd); 180 | } 181 | 182 | Interlocked.Exchange(ref _lockRelease, EmptyDisposable.Instance).Dispose(); 183 | } 184 | 185 | public void Complete() 186 | { 187 | _isCompleted = true; 188 | Interlocked.Exchange(ref _lockRelease, EmptyDisposable.Instance).Dispose(); 189 | } 190 | 191 | public void SetException(Exception ex) 192 | { 193 | _exception = ex; 194 | Complete(); 195 | } 196 | } 197 | } 198 | 199 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/CancellableStreamContent.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace SecureHttpClient 9 | { 10 | internal class CancellableStreamContent : ProgressStreamContent 11 | { 12 | private Action _onDispose; 13 | 14 | public CancellableStreamContent(Stream source, Action onDispose) : base(source, CancellationToken.None) 15 | { 16 | _onDispose = onDispose; 17 | } 18 | 19 | protected override void Dispose(bool disposing) 20 | { 21 | var disp = Interlocked.Exchange(ref _onDispose, null); 22 | disp?.Invoke(); 23 | 24 | // EVIL HAX: We have to let at least one ReadAsync of the underlying 25 | // stream fail with OperationCancelledException before we can dispose 26 | // the base, or else the exception coming out of the ReadAsync will 27 | // be an ObjectDisposedException from an internal MemoryStream. This isn't 28 | // the Ideal way to fix this, but #yolo. 29 | Task.Run(() => base.Dispose(disposing)); 30 | } 31 | } 32 | } 33 | 34 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/ClientCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using Foundation; 4 | using ObjCRuntime; 5 | using Security; 6 | 7 | namespace SecureHttpClient 8 | { 9 | /// 10 | /// IClientCertificateProvider for iOS 11 | /// 12 | public interface IClientCertificateProvider : Abstractions.IClientCertificateProvider 13 | { 14 | /// 15 | /// The current client certificate 16 | /// 17 | /// The credential. 18 | NSUrlCredential Credential { get; } 19 | } 20 | 21 | /// 22 | /// Base Client certificate provider for iOS 23 | /// 24 | public class ClientCertificateProvider : IClientCertificateProvider 25 | { 26 | /// 27 | /// The current client certificate 28 | /// 29 | /// The credential. 30 | public NSUrlCredential Credential { get; protected set; } 31 | } 32 | 33 | /// 34 | /// Client certificate provider for imported certificates and keys. 35 | /// 36 | public class ImportedClientCertificateProvider : ClientCertificateProvider 37 | { 38 | /// 39 | /// Import the specified certificate and its associated private key. 40 | /// 41 | /// The certificate and key, in PKCS12 format. 42 | /// The passphrase that protects the private key. 43 | public void Import(byte[] certificate, string passphrase) 44 | { 45 | NSDictionary opt; 46 | if (string.IsNullOrEmpty(passphrase)) 47 | { 48 | opt = new NSDictionary(); 49 | } 50 | else 51 | { 52 | opt = NSDictionary.FromObjectAndKey(new NSString(passphrase), SecImportExport.Passphrase); 53 | } 54 | 55 | var status = SecImportExport.ImportPkcs12(certificate, opt, out NSDictionary[] array); 56 | 57 | if (status == SecStatusCode.Success) 58 | { 59 | var identity = Runtime.GetINativeObject(array[0]["identity"].Handle, false); 60 | NSArray chain = array[0]["chain"] as NSArray; 61 | SecCertificate[] certs = new SecCertificate[chain.Count]; 62 | for (nuint i = 0; i < chain.Count; i++) 63 | certs[i] = chain.GetItem(i); 64 | Credential = new NSUrlCredential(identity, certs, NSUrlCredentialPersistence.ForSession); 65 | } 66 | } 67 | } 68 | } 69 | 70 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/DataTaskDelegate.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Security.Authentication; 7 | using Foundation; 8 | using SecureHttpClient.CertificatePinning; 9 | using System.Security.Cryptography.X509Certificates; 10 | 11 | namespace SecureHttpClient 12 | { 13 | internal class DataTaskDelegate : NSUrlSessionDataDelegate 14 | { 15 | private readonly SecureHttpClientHandler _secureHttpClientHandler; 16 | private readonly CertificatePinner _certificatePinner; 17 | private readonly X509Certificate2Collection _trustedRoots; 18 | 19 | public DataTaskDelegate(SecureHttpClientHandler secureHttpClientHandler, CertificatePinner certificatePinner, X509Certificate2Collection trustedRoots) 20 | { 21 | _secureHttpClientHandler = secureHttpClientHandler; 22 | _certificatePinner = certificatePinner; 23 | _trustedRoots = trustedRoots; 24 | } 25 | 26 | public override void DidReceiveResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action completionHandler) 27 | { 28 | var data = GetResponseForTask(dataTask); 29 | 30 | try 31 | { 32 | if (data.CancellationToken.IsCancellationRequested) 33 | { 34 | dataTask.Cancel(); 35 | } 36 | 37 | var resp = (NSHttpUrlResponse)response; 38 | 39 | var content = new CancellableStreamContent(data.ResponseBody, () => 40 | { 41 | if (!data.IsCompleted) 42 | { 43 | dataTask.Cancel(); 44 | } 45 | data.IsCompleted = true; 46 | 47 | data.ResponseBody.SetException(new OperationCanceledException()); 48 | }) 49 | { 50 | Progress = data.Progress 51 | }; 52 | 53 | 54 | // NB: The double cast is because of a Xamarin compiler bug 55 | var status = (int)resp.StatusCode; 56 | var ret = new HttpResponseMessage((HttpStatusCode)status) 57 | { 58 | Content = content, 59 | RequestMessage = data.Request, 60 | }; 61 | ret.RequestMessage.RequestUri = new Uri(resp.Url.AbsoluteString); 62 | 63 | foreach (var v in resp.AllHeaderFields) 64 | { 65 | // NB: Cocoa trolling us so hard by giving us back dummy dictionary entries 66 | if (v.Key == null || v.Value == null) continue; 67 | 68 | var headerKey = v.Key.ToString(); 69 | 70 | if (headerKey.ToLower() == "set-cookie") 71 | { 72 | var splitter = new SetCookieHeaderSplitter(v.Value.ToString()); 73 | while (splitter.HasNext()) 74 | { 75 | var setCookieHeaderValue = splitter.Next(); 76 | ret.Headers.TryAddWithoutValidation(headerKey, setCookieHeaderValue); 77 | ret.Content.Headers.TryAddWithoutValidation(headerKey, setCookieHeaderValue); 78 | } 79 | } 80 | else 81 | { 82 | ret.Headers.TryAddWithoutValidation(headerKey, v.Value.ToString()); 83 | ret.Content.Headers.TryAddWithoutValidation(headerKey, v.Value.ToString()); 84 | } 85 | } 86 | 87 | data.FutureResponse.TrySetResult(ret); 88 | } 89 | catch (Exception ex) 90 | { 91 | data.FutureResponse.TrySetException(ex); 92 | } 93 | 94 | completionHandler(NSUrlSessionResponseDisposition.Allow); 95 | } 96 | 97 | public override void WillCacheResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSCachedUrlResponse proposedResponse, Action completionHandler) 98 | { 99 | completionHandler(proposedResponse); 100 | } 101 | 102 | public override void DidCompleteWithError(NSUrlSession session, NSUrlSessionTask task, NSError error) 103 | { 104 | var data = GetResponseForTask(task); 105 | data.IsCompleted = true; 106 | 107 | if (error != null || data.Error != null) 108 | { 109 | var ex = CreateExceptionForNsError(data.Error ?? error); 110 | 111 | // Pass the exception to the response 112 | data.FutureResponse.TrySetException(ex); 113 | data.ResponseBody.SetException(ex); 114 | return; 115 | } 116 | 117 | data.ResponseBody.Complete(); 118 | 119 | lock (_secureHttpClientHandler.InflightRequests) 120 | { 121 | _secureHttpClientHandler.InflightRequests.Remove(task); 122 | } 123 | } 124 | 125 | public override void DidReceiveData(NSUrlSession session, NSUrlSessionDataTask dataTask, NSData byteData) 126 | { 127 | var data = GetResponseForTask(dataTask); 128 | var bytes = byteData.ToArray(); 129 | 130 | // NB: If we're cancelled, we still might have one more chunk of data that attempts to be delivered 131 | if (data.IsCompleted) return; 132 | 133 | data.ResponseBody.AddByteArray(bytes); 134 | } 135 | 136 | private InflightOperation GetResponseForTask(NSUrlSessionTask task) 137 | { 138 | lock (_secureHttpClientHandler.InflightRequests) 139 | { 140 | return _secureHttpClientHandler.InflightRequests[task]; 141 | } 142 | } 143 | 144 | public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action completionHandler) 145 | { 146 | if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodNTLM) 147 | { 148 | if (_secureHttpClientHandler.Credentials != null) 149 | { 150 | NetworkCredential credentialsToUse; 151 | if (_secureHttpClientHandler.Credentials is NetworkCredential credentials) 152 | { 153 | credentialsToUse = credentials; 154 | } 155 | else 156 | { 157 | var uri = GetResponseForTask(task).Request.RequestUri; 158 | credentialsToUse = _secureHttpClientHandler.Credentials.GetCredential(uri, "NTLM"); 159 | } 160 | var credential = new NSUrlCredential(credentialsToUse.UserName, credentialsToUse.Password, NSUrlCredentialPersistence.ForSession); 161 | completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential); 162 | return; 163 | } 164 | } 165 | 166 | if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) 167 | { 168 | challenge.ProtectionSpace.ServerSecTrust.SetAnchorCertificates(_trustedRoots); 169 | 170 | var hostname = task.CurrentRequest.Url.Host; 171 | if (_certificatePinner != null && _certificatePinner.HasPin(hostname)) 172 | { 173 | var serverTrust = challenge.ProtectionSpace.ServerSecTrust; 174 | var status = serverTrust.Evaluate(out _); 175 | if (status) 176 | { 177 | var serverCertificate = serverTrust.GetCertificateChain()[0]; 178 | var x509Certificate = serverCertificate.ToX509Certificate2(); 179 | var match = _certificatePinner.Check(hostname, x509Certificate); 180 | if (match) 181 | { 182 | completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, NSUrlCredential.FromTrust(serverTrust)); 183 | } 184 | else 185 | { 186 | var inflightRequest = GetResponseForTask(task); 187 | inflightRequest.Error = new NSError(NSError.NSUrlErrorDomain, (nint)(long)NSUrlError.ServerCertificateUntrusted); 188 | completionHandler(NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, null); 189 | } 190 | return; 191 | } 192 | } 193 | } 194 | 195 | if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate) 196 | { 197 | var certificate = _secureHttpClientHandler.ClientCertificate; 198 | if (certificate == null) 199 | { 200 | var url = task.CurrentRequest.Url; 201 | var space = new NSUrlProtectionSpace(url.Host, url.Port, url.Scheme, null, NSUrlProtectionSpace.AuthenticationMethodClientCertificate); 202 | certificate = NSUrlCredentialStorage.SharedCredentialStorage.GetDefaultCredential(space); 203 | } 204 | if (certificate != null) 205 | { 206 | completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, certificate); 207 | return; 208 | } 209 | } 210 | 211 | completionHandler(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential); 212 | } 213 | 214 | public override void WillPerformHttpRedirection(NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action completionHandler) 215 | { 216 | var nextRequest = _secureHttpClientHandler.AllowAutoRedirect ? newRequest : null; 217 | completionHandler(nextRequest); 218 | } 219 | 220 | private static Exception CreateExceptionForNsError(NSError error) 221 | { 222 | var innerException = new NSErrorException(error); 223 | 224 | if ((error.Domain == NSError.NSUrlErrorDomain) || (error.Domain == NSError.CFNetworkErrorDomain)) 225 | { 226 | switch ((NSUrlError)(long)error.Code) 227 | { 228 | case NSUrlError.Cancelled: 229 | case NSUrlError.UserCancelledAuthentication: 230 | case (NSUrlError)NSNetServicesStatus.CancelledError: 231 | return new OperationCanceledException(error.LocalizedDescription, innerException); 232 | 233 | case NSUrlError.SecureConnectionFailed: 234 | case NSUrlError.ServerCertificateHasBadDate: 235 | case NSUrlError.ServerCertificateHasUnknownRoot: 236 | case NSUrlError.ServerCertificateNotYetValid: 237 | case NSUrlError.ServerCertificateUntrusted: 238 | case NSUrlError.ClientCertificateRejected: 239 | case NSUrlError.ClientCertificateRequired: 240 | return new HttpRequestException(error.LocalizedDescription, 241 | new AuthenticationException(error.LocalizedDescription, innerException)); 242 | } 243 | } 244 | 245 | return new HttpRequestException(error.LocalizedDescription, innerException); 246 | } 247 | } 248 | } 249 | 250 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/EmptyDisposable.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | 5 | namespace SecureHttpClient 6 | { 7 | internal class EmptyDisposable : IDisposable 8 | { 9 | public static IDisposable Instance { get; } = new EmptyDisposable(); 10 | 11 | private EmptyDisposable() { } 12 | public void Dispose() { } 13 | } 14 | } 15 | 16 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/InflightOperation.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using System.Threading; 6 | using Foundation; 7 | 8 | namespace SecureHttpClient 9 | { 10 | internal class InflightOperation 11 | { 12 | public HttpRequestMessage Request { get; set; } 13 | public TaskCompletionSource FutureResponse { get; set; } 14 | public ProgressDelegate Progress { get; set; } 15 | public ByteArrayListStream ResponseBody { get; set; } 16 | public CancellationToken CancellationToken { get; set; } 17 | public bool IsCompleted { get; set; } 18 | public NSError Error { get; set; } 19 | } 20 | } 21 | 22 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/ProgressStreamContent.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using System.IO; 7 | using System.Net; 8 | using System.Threading; 9 | 10 | namespace SecureHttpClient 11 | { 12 | internal delegate void ProgressDelegate(long bytes, long totalBytes, long totalBytesExpected); 13 | 14 | internal class ProgressStreamContent : StreamContent 15 | { 16 | public ProgressStreamContent(Stream stream, CancellationToken token) 17 | : this(new ProgressStream(stream, token)) 18 | { 19 | } 20 | 21 | public ProgressStreamContent(Stream stream, int bufferSize) 22 | : this(new ProgressStream(stream, CancellationToken.None), bufferSize) 23 | { 24 | } 25 | 26 | private ProgressStreamContent(ProgressStream stream) 27 | : base(stream) 28 | { 29 | Init(stream); 30 | } 31 | 32 | private ProgressStreamContent(ProgressStream stream, int bufferSize) 33 | : base(stream, bufferSize) 34 | { 35 | Init(stream); 36 | } 37 | 38 | private void Init(ProgressStream stream) 39 | { 40 | stream.ReadCallback = ReadBytes; 41 | 42 | Progress = delegate { }; 43 | } 44 | 45 | private void Reset() 46 | { 47 | _totalBytes = 0L; 48 | } 49 | 50 | private long _totalBytes; 51 | private long _totalBytesExpected = -1; 52 | 53 | private void ReadBytes(long bytes) 54 | { 55 | if (_totalBytesExpected == -1) 56 | _totalBytesExpected = Headers.ContentLength ?? -1; 57 | 58 | if (_totalBytesExpected == -1 && TryComputeLength(out var computedLength)) 59 | _totalBytesExpected = computedLength == 0 ? -1 : computedLength; 60 | 61 | // If less than zero still then change to -1 62 | _totalBytesExpected = Math.Max(-1, _totalBytesExpected); 63 | _totalBytes += bytes; 64 | 65 | Progress(bytes, _totalBytes, _totalBytesExpected); 66 | } 67 | 68 | ProgressDelegate _progress; 69 | public ProgressDelegate Progress 70 | { 71 | get => _progress; 72 | set { _progress = value ?? delegate { }; } 73 | } 74 | 75 | protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) 76 | { 77 | Reset(); 78 | return base.SerializeToStreamAsync(stream, context); 79 | } 80 | 81 | protected override bool TryComputeLength(out long length) 82 | { 83 | var result = base.TryComputeLength(out length); 84 | _totalBytesExpected = length; 85 | return result; 86 | } 87 | 88 | private class ProgressStream : Stream 89 | { 90 | private readonly CancellationToken _token; 91 | 92 | public ProgressStream(Stream stream, CancellationToken token) 93 | { 94 | ParentStream = stream; 95 | _token = token; 96 | 97 | ReadCallback = delegate { }; 98 | WriteCallback = delegate { }; 99 | } 100 | 101 | public Action ReadCallback { private get; set; } 102 | 103 | private Action WriteCallback { get; } 104 | 105 | private Stream ParentStream { get; } 106 | 107 | public override bool CanRead => ParentStream.CanRead; 108 | 109 | public override bool CanSeek => ParentStream.CanSeek; 110 | 111 | public override bool CanWrite => ParentStream.CanWrite; 112 | 113 | public override bool CanTimeout => ParentStream.CanTimeout; 114 | 115 | public override long Length => ParentStream.Length; 116 | 117 | public override void Flush() 118 | { 119 | ParentStream.Flush(); 120 | } 121 | 122 | public override Task FlushAsync(CancellationToken cancellationToken) 123 | { 124 | return ParentStream.FlushAsync(cancellationToken); 125 | } 126 | 127 | public override long Position 128 | { 129 | get => ParentStream.Position; 130 | set => ParentStream.Position = value; 131 | } 132 | 133 | public override int Read(byte[] buffer, int offset, int count) 134 | { 135 | _token.ThrowIfCancellationRequested(); 136 | 137 | var readCount = ParentStream.Read(buffer, offset, count); 138 | ReadCallback(readCount); 139 | return readCount; 140 | } 141 | 142 | public override long Seek(long offset, SeekOrigin origin) 143 | { 144 | _token.ThrowIfCancellationRequested(); 145 | return ParentStream.Seek(offset, origin); 146 | } 147 | 148 | public override void SetLength(long value) 149 | { 150 | _token.ThrowIfCancellationRequested(); 151 | ParentStream.SetLength(value); 152 | } 153 | 154 | public override void Write(byte[] buffer, int offset, int count) 155 | { 156 | _token.ThrowIfCancellationRequested(); 157 | ParentStream.Write(buffer, offset, count); 158 | WriteCallback(count); 159 | } 160 | 161 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 162 | { 163 | _token.ThrowIfCancellationRequested(); 164 | var linked = CancellationTokenSource.CreateLinkedTokenSource(_token, cancellationToken); 165 | 166 | var readCount = await ParentStream.ReadAsync(buffer, offset, count, linked.Token); 167 | 168 | ReadCallback(readCount); 169 | return readCount; 170 | } 171 | 172 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 173 | { 174 | _token.ThrowIfCancellationRequested(); 175 | 176 | var linked = CancellationTokenSource.CreateLinkedTokenSource(_token, cancellationToken); 177 | var task = ParentStream.WriteAsync(buffer, offset, count, linked.Token); 178 | 179 | WriteCallback(count); 180 | return task; 181 | } 182 | 183 | protected override void Dispose(bool disposing) 184 | { 185 | if (disposing) 186 | { 187 | ParentStream.Dispose(); 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/SecureHttpClientHandler.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using System.Threading; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Http; 10 | using Foundation; 11 | using Microsoft.Extensions.Logging; 12 | using SecureHttpClient.CertificatePinning; 13 | using System.Security.Cryptography.X509Certificates; 14 | 15 | namespace SecureHttpClient 16 | { 17 | /// 18 | /// Implementation of ISecureHttpClientHandler (iOS implementation) 19 | /// 20 | public class SecureHttpClientHandler : HttpClientHandler, Abstractions.ISecureHttpClientHandler 21 | { 22 | internal readonly Dictionary InflightRequests; 23 | internal NSUrlCredential ClientCertificate { get; private set; } 24 | private X509Certificate2Collection _trustedRoots = null; 25 | private readonly Lazy _certificatePinner; 26 | private NSUrlSession _session; 27 | 28 | /// 29 | /// SecureHttpClientHandler constructor (iOS implementation) 30 | /// 31 | /// Logger 32 | public SecureHttpClientHandler(ILogger logger) 33 | { 34 | InflightRequests = new Dictionary(); 35 | _certificatePinner = new Lazy(() => new CertificatePinner(logger)); 36 | } 37 | 38 | /// 39 | /// Add certificate pins for a given hostname (iOS implementation) 40 | /// 41 | /// The hostname 42 | /// The array of certifiate pins (example of pin string: "sha256/fiKY8VhjQRb2voRmVXsqI0xPIREcwOVhpexrplrlqQY=") 43 | public virtual void AddCertificatePinner(string hostname, string[] pins) 44 | { 45 | _certificatePinner.Value.AddPins(hostname, pins); 46 | } 47 | 48 | /// 49 | /// Set the client certificate provider (iOS implementation) 50 | /// 51 | /// The provider for client certificates on this platform 52 | public virtual void SetClientCertificates(Abstractions.IClientCertificateProvider provider) 53 | { 54 | ClientCertificate = (provider as IClientCertificateProvider)?.Credential; 55 | } 56 | 57 | /// 58 | /// Set certificates for the trusted Root Certificate Authorities (iOS implementation) 59 | /// 60 | /// Certificates for the CAs to trust 61 | public virtual void SetTrustedRoots(params byte[][] certificates) 62 | { 63 | if (certificates.Length == 0) 64 | { 65 | _trustedRoots = null; 66 | return; 67 | } 68 | _trustedRoots = new X509Certificate2Collection(); 69 | foreach (var cert in certificates) 70 | { 71 | _trustedRoots.Add(X509CertificateLoader.LoadCertificate(cert)); 72 | } 73 | } 74 | 75 | private void InitSession() 76 | { 77 | using var configuration = NSUrlSessionConfiguration.DefaultSessionConfiguration; 78 | if (!UseProxy) 79 | { 80 | configuration.ConnectionProxyDictionary = new NSDictionary(); 81 | } 82 | #pragma warning disable CA1416 83 | else if (Proxy is WebProxy webProxy) 84 | #pragma warning restore CA1416 85 | { 86 | configuration.ConnectionProxyDictionary = GetProxyDictionary(webProxy); 87 | } 88 | if (!UseCookies) 89 | { 90 | configuration.HttpCookieStorage = null; 91 | } 92 | var nsUrlSessionDelegate = (INSUrlSessionDelegate)new DataTaskDelegate(this, _certificatePinner.IsValueCreated ? _certificatePinner.Value : null, _trustedRoots); 93 | _session = NSUrlSession.FromConfiguration(configuration, nsUrlSessionDelegate, null); 94 | } 95 | 96 | private static NSDictionary GetProxyDictionary(WebProxy webProxy) 97 | { 98 | var host = webProxy.Address.Host; 99 | var port = webProxy.Address.Port; 100 | var values = new [] 101 | { 102 | NSObject.FromObject(host), 103 | NSNumber.FromInt32(port), 104 | NSNumber.FromInt32(1), 105 | NSObject.FromObject(host), 106 | NSNumber.FromInt32(port), 107 | NSNumber.FromInt32(1) 108 | }; 109 | var keys = new [] 110 | { 111 | NSObject.FromObject("HTTPProxy"), 112 | NSObject.FromObject("HTTPPort"), 113 | NSObject.FromObject("HTTPEnable"), 114 | NSObject.FromObject("HTTPSProxy"), 115 | NSObject.FromObject("HTTPSPort"), 116 | NSObject.FromObject("HTTPSEnable") 117 | }; 118 | var proxyDictionary = NSDictionary.FromObjectsAndKeys(values, keys); 119 | return proxyDictionary; 120 | } 121 | 122 | /// 123 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 124 | { 125 | if (_session == null) 126 | { 127 | InitSession(); 128 | } 129 | 130 | var headers = request.Headers as IEnumerable>>; 131 | 132 | var body = default(NSData); 133 | if (request.Content != null) 134 | { 135 | var bytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); 136 | if (bytes.Length > 0 || request.Method != HttpMethod.Get) 137 | { 138 | body = NSData.FromArray(bytes); 139 | headers = headers.Union(request.Content.Headers).ToArray(); 140 | } 141 | } 142 | 143 | var rq = new NSMutableUrlRequest 144 | { 145 | AllowsCellularAccess = true, 146 | Body = body, 147 | CachePolicy = NSUrlRequestCachePolicy.UseProtocolCachePolicy, 148 | Headers = headers.Aggregate(new NSMutableDictionary(), (acc, x) => { 149 | acc.Add(new NSString(x.Key), new NSString(string.Join(x.Key == "User-Agent" ? " " : ",", x.Value))); 150 | return acc; 151 | }), 152 | HttpMethod = request.Method.ToString().ToUpperInvariant(), 153 | Url = NSUrl.FromString(request.RequestUri.AbsoluteUri), 154 | }; 155 | 156 | var op = _session.CreateDataTask(rq); 157 | 158 | cancellationToken.ThrowIfCancellationRequested(); 159 | 160 | var ret = new TaskCompletionSource(); 161 | cancellationToken.Register(() => ret.TrySetCanceled()); 162 | 163 | lock (InflightRequests) 164 | { 165 | InflightRequests[op] = new InflightOperation 166 | { 167 | FutureResponse = ret, 168 | Request = request, 169 | ResponseBody = new ByteArrayListStream(), 170 | CancellationToken = cancellationToken, 171 | }; 172 | } 173 | 174 | op.Resume(); 175 | return await ret.Task.ConfigureAwait(false); 176 | } 177 | } 178 | } 179 | 180 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/SetCookieHeaderSplitter.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | namespace SecureHttpClient 4 | { 5 | /// 6 | /// Splits a string with one or more cookies separated with a comma (set-cookie folding). 7 | /// Since some attribute values can also have commas, like the expires date field, this does a look ahead to detect where it's a separator or not. 8 | /// Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 9 | /// 10 | internal class SetCookieHeaderSplitter 11 | { 12 | private readonly string _s; 13 | private int _pos; 14 | 15 | public SetCookieHeaderSplitter(string setCookieHeaderString) 16 | { 17 | _s = setCookieHeaderString; 18 | _pos = 0; 19 | } 20 | 21 | public bool HasNext() 22 | { 23 | return _pos < _s.Length; 24 | } 25 | 26 | public string Next() 27 | { 28 | var start = _pos; 29 | while (SkipWhiteSpace()) 30 | { 31 | if (_s[_pos] == ',') 32 | { 33 | var lastComma = _pos++; 34 | SkipWhiteSpace(); 35 | var nextStart = _pos; 36 | while (_pos < _s.Length && _s[_pos] != '=' && _s[_pos] != ';' && _s[_pos] != ',') 37 | { 38 | _pos++; 39 | } 40 | if (_pos < _s.Length && _s[_pos] == '=') 41 | { 42 | // pos is inside the next cookie, so back up and return it. 43 | _pos = nextStart; 44 | return _s[start..lastComma]; 45 | } 46 | _pos = lastComma; 47 | } 48 | _pos++; 49 | } 50 | return _s[start..]; 51 | } 52 | 53 | // Skip whitespace, returning true if there are more chars to read. 54 | private bool SkipWhiteSpace() 55 | { 56 | while (_pos < _s.Length && char.IsWhiteSpace(_s[_pos])) 57 | { 58 | _pos++; 59 | } 60 | return _pos < _s.Length; 61 | } 62 | } 63 | } 64 | 65 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/Platforms/iOS/SpkiProvider.cs: -------------------------------------------------------------------------------- 1 | #if __IOS__ 2 | 3 | using System.Security.Cryptography.X509Certificates; 4 | using Org.BouncyCastle.Asn1; 5 | using Org.BouncyCastle.Asn1.X509; 6 | 7 | namespace SecureHttpClient 8 | { 9 | internal class SpkiProvider 10 | { 11 | public static byte[] GetSpki(X509Certificate2 certificate) 12 | { 13 | // We have to use BouncyCastle, because iOS does not support DSA, so .net does not support DSACertificateExtensions for iOS 14 | // https://github.com/dotnet/runtime/issues/76305 15 | 16 | // Load ASN.1 encoded certificate structure 17 | var certAsn1 = Asn1Object.FromByteArray(certificate.RawData); 18 | var certStruct = X509CertificateStructure.GetInstance(certAsn1); 19 | 20 | // Extract SPKI and DER-encode it 21 | var spki = certStruct.SubjectPublicKeyInfo; 22 | var spkiDer = spki.GetDerEncoded(); 23 | 24 | return spkiDer; 25 | } 26 | } 27 | } 28 | 29 | #endif -------------------------------------------------------------------------------- /SecureHttpClient/SecureHttpClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net9.0-android35.0;net9.0-ios18.0 5 | 15.0 6 | 21.0 7 | false 8 | true 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /build/SecureHttpClient.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | SecureHttpClient 5 | $version$ 6 | SecureHttpClient 7 | tranb3r 8 | false 9 | MIT 10 | https://github.com/ZitchCode/SecureHttpClient 11 | Cross-platform HttpClientHandler with additional security features (certificate pinning, TLS1.2+, client certificates) 12 | https://github.com/ZitchCode/SecureHttpClient/releases 13 | Copyright 2024 ZitchCode 14 | .net dotnet-maui xamarin dotnet maui cross-platform ios android windows httpclient security certificate pinning 15 | 16 | README.md 17 | icon.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.100" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZitchCode/SecureHttpClient/d4713f4767ed87aaf9faccd6d30b83a1b186030c/icon.png -------------------------------------------------------------------------------- /scripts/clean.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo List of folders to remove: 3 | for /d /r .. %%d in (bin,obj) do @if exist "%%d" echo "%%d" 4 | set /p temp= Hit enter to continue 5 | for /d /r .. %%d in (bin,obj) do @if exist "%%d" rd /s/q "%%d" 6 | echo Done ! 7 | -------------------------------------------------------------------------------- /scripts/nuget_pack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SETLOCAL 4 | 5 | echo -- INIT ---------------------------------------------------------------------------------------------------------------------------------------------------- 6 | set Z_LibName=SecureHttpClient 7 | echo LibName: %Z_LibName% 8 | set /p Z_Version=<..\version.txt 9 | echo Version: %Z_Version% 10 | set Z_AndroidSdkDirectory=D:\android\sdk 11 | echo AndroidSdkDirectory: %Z_AndroidSdkDirectory% 12 | 13 | echo -- CLEAN --------------------------------------------------------------------------------------------------------------------------------------------------- 14 | dotnet clean -v m ../%Z_LibName%/%Z_LibName%.csproj -c Release 15 | 16 | echo -- BUILD --------------------------------------------------------------------------------------------------------------------------------------------------- 17 | dotnet build -v m ../%Z_LibName%/%Z_LibName%.csproj -c Release -p:AndroidSdkDirectory=%Z_AndroidSdkDirectory% -p:Version=%Z_Version% -p:AssemblyVersion=%Z_Version% -p:AssemblyFileVersion=%Z_Version% -p:ContinuousIntegrationBuild=true 18 | 19 | echo -- PACK ---------------------------------------------------------------------------------------------------------------------------------------------------- 20 | dotnet pack -v m ../%Z_LibName%/%Z_LibName%.csproj -c Release -p:AndroidSdkDirectory=%Z_AndroidSdkDirectory% --no-build -o ..\ -p:PackageVersion=%Z_Version% -p:NuspecFile=..\build\SecureHttpClient.nuspec -p:NuspecProperties=version=%Z_Version% -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg 21 | 22 | echo -- DONE !! ------------------------------------------------------------------------------------------------------------------------------------------------- 23 | ENDLOCAL 24 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 2.3.1 --------------------------------------------------------------------------------