├── .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 | [](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