From e95fd6a0419b7a62586585abe062ca9cfd646fdb Mon Sep 17 00:00:00 2001 From: valentinbreiz Date: Sun, 4 Oct 2026 11:37:37 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9A=9A=20Start=20from=20nanoFramework?= =?UTF-8?q?.System.Net.Http's=20sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the client with nanoframework/System.Net.Http (384a926): its nanoFramework.System.Net.Http, .Client and .Server projects copied as is into src/Cosmos.Network.Http, .Client and .Server. Only the project folder links in the .nfproj and the AssemblyInfo title are renamed. --- src/Cosmos.Network.Http.Client/Http/.gitkeep | 0 .../Http/Headers/.gitkeep | 0 .../Properties/.gitkeep | 0 .../System.Net.Http.Client.nfproj | 248 ++ .../packages.config | 11 + .../packages.lock.json | 55 + src/Cosmos.Network.Http.Server/Http/.gitkeep | 0 .../Properties/.gitkeep | 0 .../System.Net.Http.Server.nfproj | 174 ++ .../packages.config | 11 + .../packages.lock.json | 55 + .../CertificateValidator.cs | 568 ----- src/Cosmos.Network.Http/ChunkedBody.cs | 150 -- src/Cosmos.Network.Http/Friends.cs | 8 + .../Http/ByteArrayContent.cs | 107 + .../Http/ClientCertificateOption.cs | 24 + .../Http/Headers/HeaderDescriptor.cs | 35 + .../Http/Headers/HttpContentHeaders.cs | 80 + .../Http/Headers/HttpGeneralHeaders.cs | 24 + .../Http/Headers/HttpHeaderType.cs | 22 + .../Http/Headers/HttpHeaders.cs | 81 + .../Http/Headers/HttpRequestHeaders.cs | 99 + .../Http/Headers/HttpResponseHeaders.cs | 19 + .../Http/Headers/KnownHeader.cs | 34 + .../Http/Headers/MediaTypeHeaderValue.cs | 131 ++ src/Cosmos.Network.Http/Http/HttpClient.cs | 486 ++++ .../Http/HttpClientHandler.cs | 425 ++++ .../Http/HttpCompletionOption.cs | 24 + src/Cosmos.Network.Http/Http/HttpContent.cs | 227 ++ .../Http/HttpMessageHandler.cs | 50 + .../Http/HttpMessageInvoker.cs | 93 + src/Cosmos.Network.Http/Http/HttpMethod.cs | 86 + .../Http/HttpRequestException.cs | 41 + .../Http/HttpRequestMessage.cs | 171 ++ .../Http/HttpResponseMessage.cs | 201 ++ .../Http/ReadOnlyStream.cs | 130 ++ src/Cosmos.Network.Http/Http/StreamContent.cs | 176 ++ src/Cosmos.Network.Http/Http/StringContent.cs | 93 + .../Http/System.Net.AuthenticationType.cs | 26 + .../Http/System.Net.Http.Constants.cs | 21 + .../Http/System.Net.HttpListener.cs | 703 ++++++ .../Http/System.Net.HttpListenerContext.cs | 203 ++ .../Http/System.Net.HttpListenerRequest.cs | 454 ++++ .../Http/System.Net.HttpListenerResponse.cs | 572 +++++ .../Http/System.Net.HttpStatusCode.cs | 295 +++ .../Http/System.Net.HttpUtility.cs | 367 +++ .../Http/System.Net.HttpVersion.cs | 26 + .../Http/System.Net.HttpWebRequest.cs | 2070 +++++++++++++++++ .../Http/System.Net.HttpWebResponse.cs | 343 +++ .../Http/System.Net.IKnowWhenDone.cs | 22 + .../Http/System.Net.IWebRequestCreate.cs | 27 + ...em.Net.Internal.WebRequestPrefixElement.cs | 20 + .../Http/System.Net.Internal.cs | 328 +++ .../Http/System.Net.NetworkCredential.cs | 94 + .../System.Net.ProtocolViolationException.cs | 35 + .../Http/System.Net.WebException.cs | 120 + .../Http/System.Net.WebHeaders.cs | 742 ++++++ .../Http/System.Net.WebRequest.cs | 419 ++++ .../Http/System.Net.WebResponse.cs | 136 ++ .../Http/System.Net.WebSocketContext.cs | 45 + .../Http/System.Net.WebStatus.cs | 62 + .../Http/System.Net._HeaderInfo.cs | 36 + .../Http/System.Net._HeaderInfoTable.cs | 171 ++ .../Http/System.Net._HttpDateParse.cs | 489 ++++ .../System.Net._InputNetworkStreamWrapper.cs | 807 +++++++ .../System.Net._OutputNetworkStreamWrapper.cs | 295 +++ .../Http/System.Net._ValidationHelper.cs | 133 ++ .../Http/System.Net.iwebproxy.cs | 35 + .../Http/System.Net.webproxy.cs | 230 ++ src/Cosmos.Network.Http/Http/System.Uri.cs | 1393 +++++++++++ src/Cosmos.Network.Http/HttpConnection.cs | 177 -- src/Cosmos.Network.Http/HttpException.cs | 29 - src/Cosmos.Network.Http/HttpRequest.cs | 338 --- src/Cosmos.Network.Http/HttpResponse.cs | 142 -- src/Cosmos.Network.Http/HttpUrl.cs | 257 -- .../Properties/AssemblyInfo.cs | 22 + src/Cosmos.Network.Http/ResponseReader.cs | 298 --- src/Cosmos.Network.Http/ServerCertificate.cs | 94 - src/Cosmos.Network.Http/SocketTransport.cs | 114 - .../System.Net.Http.nfproj | 148 ++ src/Cosmos.Network.Http/TlsTransport.cs | 483 ---- src/Cosmos.Network.Http/TrustedRoots.cs | 158 -- src/Cosmos.Network.Http/key.snk | Bin 0 -> 596 bytes src/Cosmos.Network.Http/packages.config | 11 + src/Cosmos.Network.Http/packages.lock.json | 55 + 85 files changed, 14376 insertions(+), 2808 deletions(-) create mode 100644 src/Cosmos.Network.Http.Client/Http/.gitkeep create mode 100644 src/Cosmos.Network.Http.Client/Http/Headers/.gitkeep create mode 100644 src/Cosmos.Network.Http.Client/Properties/.gitkeep create mode 100644 src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj create mode 100644 src/Cosmos.Network.Http.Client/packages.config create mode 100644 src/Cosmos.Network.Http.Client/packages.lock.json create mode 100644 src/Cosmos.Network.Http.Server/Http/.gitkeep create mode 100644 src/Cosmos.Network.Http.Server/Properties/.gitkeep create mode 100644 src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj create mode 100644 src/Cosmos.Network.Http.Server/packages.config create mode 100644 src/Cosmos.Network.Http.Server/packages.lock.json delete mode 100644 src/Cosmos.Network.Http/CertificateValidator.cs delete mode 100644 src/Cosmos.Network.Http/ChunkedBody.cs create mode 100644 src/Cosmos.Network.Http/Friends.cs create mode 100644 src/Cosmos.Network.Http/Http/ByteArrayContent.cs create mode 100644 src/Cosmos.Network.Http/Http/ClientCertificateOption.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs create mode 100644 src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpClient.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpClientHandler.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpCompletionOption.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpContent.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpMessageHandler.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpMethod.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpRequestException.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpRequestMessage.cs create mode 100644 src/Cosmos.Network.Http/Http/HttpResponseMessage.cs create mode 100644 src/Cosmos.Network.Http/Http/ReadOnlyStream.cs create mode 100644 src/Cosmos.Network.Http/Http/StreamContent.cs create mode 100644 src/Cosmos.Network.Http/Http/StringContent.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.Internal.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.WebException.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Net.webproxy.cs create mode 100644 src/Cosmos.Network.Http/Http/System.Uri.cs delete mode 100644 src/Cosmos.Network.Http/HttpConnection.cs delete mode 100644 src/Cosmos.Network.Http/HttpException.cs delete mode 100644 src/Cosmos.Network.Http/HttpRequest.cs delete mode 100644 src/Cosmos.Network.Http/HttpResponse.cs delete mode 100644 src/Cosmos.Network.Http/HttpUrl.cs create mode 100644 src/Cosmos.Network.Http/Properties/AssemblyInfo.cs delete mode 100644 src/Cosmos.Network.Http/ResponseReader.cs delete mode 100644 src/Cosmos.Network.Http/ServerCertificate.cs delete mode 100644 src/Cosmos.Network.Http/SocketTransport.cs create mode 100644 src/Cosmos.Network.Http/System.Net.Http.nfproj delete mode 100644 src/Cosmos.Network.Http/TlsTransport.cs delete mode 100644 src/Cosmos.Network.Http/TrustedRoots.cs create mode 100644 src/Cosmos.Network.Http/key.snk create mode 100644 src/Cosmos.Network.Http/packages.config create mode 100644 src/Cosmos.Network.Http/packages.lock.json diff --git a/src/Cosmos.Network.Http.Client/Http/.gitkeep b/src/Cosmos.Network.Http.Client/Http/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Cosmos.Network.Http.Client/Http/Headers/.gitkeep b/src/Cosmos.Network.Http.Client/Http/Headers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Cosmos.Network.Http.Client/Properties/.gitkeep b/src/Cosmos.Network.Http.Client/Properties/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj b/src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj new file mode 100644 index 0000000..28370c4 --- /dev/null +++ b/src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj @@ -0,0 +1,248 @@ + + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 6c6ee1b2-20f0-4f0e-8085-7ecd1e692567 + Library + Properties + 512 + System.Net.Http + System.Net.Http + v1.0 + True + bin\$(Configuration)\System.Net.Http.xml + true + true + + + true + + + ..\Cosmos.Network.Http\key.snk + + + false + + + + bin\$(Configuration)\Stubs + System.Net.Http + + + + false + + + + + + Http\ByteArrayContent.cs + + + Http\ClientCertificateOption.cs + + + Http\Headers\HeaderDescriptor.cs + + + Http\Headers\HttpContentHeaders.cs + + + Http\Headers\HttpGeneralHeaders.cs + + + Http\Headers\HttpHeaders.cs + + + Http\Headers\HttpHeaderType.cs + + + Http\Headers\HttpRequestHeaders.cs + + + Http\Headers\HttpResponseHeaders.cs + + + Http\Headers\KnownHeader.cs + + + Http\Headers\MediaTypeHeaderValue.cs + + + Http\HttpClient.cs + + + Http\HttpClientHandler.cs + + + Http\HttpCompletionOption.cs + + + Http\HttpContent.cs + + + Http\HttpMessageHandler.cs + + + Http\HttpMessageInvoker.cs + + + Http\HttpMethod.cs + + + Http\HttpRequestException.cs + + + Http\HttpRequestMessage.cs + + + Http\HttpResponseMessage.cs + + + + Http\StreamContent.cs + + + Http\StringContent.cs + + + Http\System.Net.Http.Constants.cs + + + Http\System.Net.IKnowWhenDone.cs + + + Http\System.Net.Internal.WebRequestPrefixElement.cs + + + Http\System.Net.AuthenticationType.cs + + + Http\System.Net.HttpStatusCode.cs + + + Http\System.Net.HttpVersion.cs + + + Http\System.Net.HttpWebRequest.cs + + + Http\System.Net.HttpWebResponse.cs + + + Http\System.Net.Internal.cs + + + Http\System.Net.iwebproxy.cs + + + Http\System.Net.IWebRequestCreate.cs + + + Http\System.Net.NetworkCredential.cs + + + Http\System.Net.ProtocolViolationException.cs + + + Http\System.Net.WebException.cs + + + Http\System.Net.WebHeaders.cs + + + Http\System.Net.webproxy.cs + + + Http\System.Net.WebRequest.cs + + + Http\System.Net.WebResponse.cs + + + Http\System.Net.WebStatus.cs + + + Http\System.Net._HeaderInfo.cs + + + Http\System.Net._HeaderInfoTable.cs + + + Http\System.Net._HttpDateParse.cs + + + Http\System.Net._InputNetworkStreamWrapper.cs + + + Http\System.Net._OutputNetworkStreamWrapper.c + + + Http\System.Net._ValidationHelper.cs + + + Http\System.Uri.cs + + + Properties\AssemblyInfo.cs + + + Http\System.Net.HttpUtility.cs + + + + + + + + + + + + + + + + ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\packages\nanoFramework.Runtime.Events.1.11.39\lib\nanoFramework.Runtime.Events.dll + + + ..\packages\nanoFramework.System.Collections.1.5.75\lib\nanoFramework.System.Collections.dll + + + ..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\packages\nanoFramework.System.Net.1.11.64\lib\System.Net.dll + + + ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Client/packages.config b/src/Cosmos.Network.Http.Client/packages.config new file mode 100644 index 0000000..211bede --- /dev/null +++ b/src/Cosmos.Network.Http.Client/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Client/packages.lock.json b/src/Cosmos.Network.Http.Client/packages.lock.json new file mode 100644 index 0000000..6b18a4c --- /dev/null +++ b/src/Cosmos.Network.Http.Client/packages.lock.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.39, 1.11.39]", + "resolved": "1.11.39", + "contentHash": "qNcgsk1QU5lTj2MgMYyF4iTx85YjFcAfJP9sYYydUlEXL1JaO2MnKGI3oBG4p20dJ1Fqi1ClIgKXJfAp9z80tA==" + }, + "nanoFramework.System.Collections": { + "type": "Direct", + "requested": "[1.5.75, 1.5.75]", + "resolved": "1.5.75", + "contentHash": "OiMpKo8RWbjZq1594YjdRnOGNN2aT4nowa+ET7scK0frGCux0a8LGaPkG56mmSfBCKzLlVs13LIlla3XLhXaMA==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.64, 1.11.64]", + "resolved": "1.11.64", + "contentHash": "jyC28lSWbBs1aajgp12HlHn6U0o00OMGzFP95SAeJ1CJ6BdmK+bq+peTvsfzpqyT0Babuaqq/x9G94XLaGapGw==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.10.94, 3.10.94]", + "resolved": "3.10.94", + "contentHash": "sn6gFIx6bwFj3Me0XGmek3gLZi/MZC8C4tRLwslXGeBcXcVxvFpZyDrQA93G8TiYgxTS7J7triKB7hj5wXzSxA==" + } + } + } +} \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Server/Http/.gitkeep b/src/Cosmos.Network.Http.Server/Http/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Cosmos.Network.Http.Server/Properties/.gitkeep b/src/Cosmos.Network.Http.Server/Properties/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj b/src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj new file mode 100644 index 0000000..62700ab --- /dev/null +++ b/src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj @@ -0,0 +1,174 @@ + + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 3b91a15a-40b1-4d57-93f2-eb96af0e7949 + Library + Properties + 512 + System.Net.Http + System.Net.Http + v1.0 + True + bin\$(Configuration)\System.Net.Http.xml + true + true + + + true + + + ..\Cosmos.Network.Http\key.snk + + + false + + + + bin\$(Configuration)\Stubs + System.Net.Http + + + + false + + + + + + Http\System.Net.AuthenticationType.cs + + + Http\System.Net.Http.Constants.cs + + + Http\System.Net.HttpListener.cs + + + Http\System.Net.HttpListenerContext.cs + + + Http\System.Net.HttpListenerRequest.cs + + + Http\System.Net.HttpListenerResponse.cs + + + Http\System.Net.HttpStatusCode.cs + + + Http\System.Net.HttpVersion.cs + + + Http\System.Net.IKnowWhenDone.cs + + + Http\System.Net.Internal.cs + + + Http\System.Net.iwebproxy.cs + + + Http\System.Net.NetworkCredential.cs + + + Http\System.Net.ProtocolViolationException.cs + + + Http\System.Net.WebException.cs + + + Http\System.Net.WebHeaders.cs + + + \Http\System.Net.WebResponse.cs + + + Http\System.Net.WebStatus.cs + + + Http\System.Net._HeaderInfo.cs + + + Http\System.Net._HeaderInfoTable.cs + + + Http\System.Net._HttpDateParse.cs + + + Http\System.Net._InputNetworkStreamWrapper.cs + + + Http\System.Net._OutputNetworkStreamWrapper.cs + + + Http\System.Net._ValidationHelper.cs + + + Http\System.Uri.cs + + + Properties\AssemblyInfo.cs + + + Http\System.Net.HttpUtility.cs + + + Http\System.Net.WebSocketContext.cs + + + + + + + + + + + + + + + ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\packages\nanoFramework.Runtime.Events.1.11.39\lib\nanoFramework.Runtime.Events.dll + + + ..\packages\nanoFramework.System.Collections.1.5.75\lib\nanoFramework.System.Collections.dll + + + ..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\packages\nanoFramework.System.Net.1.11.64\lib\System.Net.dll + + + ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Server/packages.config b/src/Cosmos.Network.Http.Server/packages.config new file mode 100644 index 0000000..211bede --- /dev/null +++ b/src/Cosmos.Network.Http.Server/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Server/packages.lock.json b/src/Cosmos.Network.Http.Server/packages.lock.json new file mode 100644 index 0000000..6b18a4c --- /dev/null +++ b/src/Cosmos.Network.Http.Server/packages.lock.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.39, 1.11.39]", + "resolved": "1.11.39", + "contentHash": "qNcgsk1QU5lTj2MgMYyF4iTx85YjFcAfJP9sYYydUlEXL1JaO2MnKGI3oBG4p20dJ1Fqi1ClIgKXJfAp9z80tA==" + }, + "nanoFramework.System.Collections": { + "type": "Direct", + "requested": "[1.5.75, 1.5.75]", + "resolved": "1.5.75", + "contentHash": "OiMpKo8RWbjZq1594YjdRnOGNN2aT4nowa+ET7scK0frGCux0a8LGaPkG56mmSfBCKzLlVs13LIlla3XLhXaMA==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.64, 1.11.64]", + "resolved": "1.11.64", + "contentHash": "jyC28lSWbBs1aajgp12HlHn6U0o00OMGzFP95SAeJ1CJ6BdmK+bq+peTvsfzpqyT0Babuaqq/x9G94XLaGapGw==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.10.94, 3.10.94]", + "resolved": "3.10.94", + "contentHash": "sn6gFIx6bwFj3Me0XGmek3gLZi/MZC8C4tRLwslXGeBcXcVxvFpZyDrQA93G8TiYgxTS7J7triKB7hj5wXzSxA==" + } + } + } +} \ No newline at end of file diff --git a/src/Cosmos.Network.Http/CertificateValidator.cs b/src/Cosmos.Network.Http/CertificateValidator.cs deleted file mode 100644 index 754914a..0000000 --- a/src/Cosmos.Network.Http/CertificateValidator.cs +++ /dev/null @@ -1,568 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Text; -using Org.BouncyCastle.Asn1; -using Org.BouncyCastle.Asn1.X509; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Pkix; -using Org.BouncyCastle.Tls; -using Org.BouncyCastle.Utilities; -using Org.BouncyCastle.X509; - -namespace Cosmos.Network.Http; - -/// -/// Decides whether the certificates an https:// server presented prove it is -/// the host it was asked for: a chain from its certificate to a trusted root, -/// every link signed by the next with a strong algorithm, valid now and -/// allowed to issue what it issued, and a certificate that names the host. -/// -/// -/// -/// What the TLS handshake proves on its own is that the server holds the key -/// of the certificate it sent, and that the key may sign (BouncyCastle checks -/// both). The rest is here, close to what browsers check, and short of it in -/// a few ways: there is no revocation (neither OCSP nor CRLs), no certificate -/// policy, no fetching of an intermediate certificate the server leaves out, -/// and the roots' own name constraints are not applied (none of Mozilla's -/// roots has any). Like browsers, the host is only looked for in the subject -/// alternative names, never in the subject's common name. -/// -/// -/// The chain is built from the certificates the server sent, in any order, -/// and stops at the first certificate a trusted root signed, so a chain that -/// ends with a cross-signed copy of a root still leads to the root itself. -/// -/// -/// The dates are checked last, so a date being the problem means nothing -/// else is: a that -/// forgives a wrong clock forgives nothing more. -/// -/// -internal static class CertificateValidator -{ - /// The most intermediate certificates a chain may go through. - private const int MaxIntermediates = 8; - - /// The names listed when the certificate names another host. - private const int MaxListedNames = 4; - - /// The shortest RSA key a certificate below the root may have, in bits, as browsers require. - private const int MinRsaKeyBits = 2048; - - private static readonly DerObjectIdentifier[] s_understoodCriticalExtensions = - [ - X509Extensions.BasicConstraints, - X509Extensions.KeyUsage, - X509Extensions.ExtendedKeyUsage, - X509Extensions.SubjectAlternativeName, - X509Extensions.NameConstraints, - // Policies constrain what a certificate may be used for beyond - // TLS server authentication; none is required here, as in browsers. - X509Extensions.CertificatePolicies, - X509Extensions.PolicyConstraints, - X509Extensions.InhibitAnyPolicy, - ]; - - /// - /// The algorithms a certificate below the root may be signed with: SHA-2 - /// with RSA (PKCS #1 v1.5 or PSS) or ECDSA, and EdDSA. Not SHA-1 or MD5, - /// whose signatures can be forged. A root's own signature is not checked. - /// - private static readonly string[] s_strongSignatureAlgorithms = - [ - "1.2.840.113549.1.1.11", // sha256WithRSAEncryption - "1.2.840.113549.1.1.12", // sha384WithRSAEncryption - "1.2.840.113549.1.1.13", // sha512WithRSAEncryption - "1.2.840.113549.1.1.10", // RSASSA-PSS - "1.2.840.10045.4.3.2", // ecdsa-with-SHA256 - "1.2.840.10045.4.3.3", // ecdsa-with-SHA384 - "1.2.840.10045.4.3.4", // ecdsa-with-SHA512 - "1.3.101.112", // Ed25519 - "1.3.101.113", // Ed448 - ]; - - /// - /// Checks the chain for . - /// - /// The certificates the server sent, its own first. - /// The host name or IP address the request connected to. - /// The roots to trust. - /// The time the certificates must be valid at, in UTC. - /// The TLS alert that tells the server why, when the chain is not trusted. - /// Why the chain is not trusted, the first problem found; when it is trusted. - public static string? Validate(IReadOnlyList presented, string host, TrustedRoots roots, DateTime now, out short alert) - { - alert = AlertDescription.bad_certificate; - if (presented.Count == 0) - { - return "the server sent no certificate"; - } - - try - { - return Check(presented, host, roots, now, ref alert); - } - catch (Exception exception) - { - // A malformed extension, name or key, which BouncyCastle throws on. - alert = AlertDescription.bad_certificate; - return $"the certificates could not be read: {exception.Message}"; - } - } - - private static string? Check(IReadOnlyList presented, string host, TrustedRoots roots, DateTime now, ref short alert) - { - X509Certificate leaf = presented[0]; - - string? error = CheckHost(leaf, host) - ?? CheckServerUsage(leaf, Name(leaf, leaf)) - ?? CheckCriticalExtensions(leaf, Name(leaf, leaf), ref alert); - if (error is not null) - { - return error; - } - - // The chain from the server's certificate up, without the root. - List path = [leaf]; - bool[] used = new bool[presented.Count]; - used[0] = true; - - // A root that signed the chain but has expired, and how far up the - // chain it signed: kept in case no other way leads to a root. - X509Certificate? expiredRoot = null; - int expiredRootPath = 0; - - X509Certificate current = leaf; - while (true) - { - X509Certificate? root = FindRoot(roots, current, now, out X509Certificate? expired); - if (root is not null) - { - return CheckPath(path, root, host, now, ref alert); - } - - if (expired is not null && expiredRoot is null) - { - expiredRoot = expired; - expiredRootPath = path.Count; - } - - int index = path.Count > MaxIntermediates ? -1 : FindIssuer(presented, used, current); - if (index < 0) - { - if (expiredRoot is not null) - { - return CheckPath(path.GetRange(0, expiredRootPath), expiredRoot, host, now, ref alert); - } - - if (path.Count > MaxIntermediates) - { - return $"the chain goes through more than {MaxIntermediates} intermediate certificates"; - } - - alert = AlertDescription.unknown_ca; - if (current == leaf) - { - return IsSelfIssued(leaf) - ? "the certificate is self-signed" - : $"the certificate is issued by {leaf.IssuerDN}, which is not a trusted root: the server may have left out an intermediate certificate"; - } - - return IsSelfIssued(current) - ? $"the chain ends with {current.SubjectDN}, which is not a trusted root" - : $"the chain goes up to {current.IssuerDN}, which is not a trusted root"; - } - - used[index] = true; - X509Certificate issuer = presented[index]; - string name = Name(issuer, leaf); - - error = CheckAuthority(issuer, name, path.Count - 1) - ?? CheckCriticalExtensions(issuer, name, ref alert); - if (error is not null) - { - return error; - } - - path.Add(issuer); - current = issuer; - } - } - - /// - /// What is left to check once signed the top of - /// : signature algorithms and keys, name - /// constraints, then the dates of everything, the root's included. - /// - private static string? CheckPath(List path, X509Certificate root, string host, DateTime now, ref short alert) - { - X509Certificate leaf = path[0]; - foreach (X509Certificate certificate in path) - { - string? error = CheckSignatureAlgorithm(certificate, Name(certificate, leaf)) - ?? CheckKey(certificate, Name(certificate, leaf)); - if (error is not null) - { - return error; - } - } - - string? constraints = CheckNameConstraints(path, host); - if (constraints is not null) - { - return constraints; - } - - foreach (X509Certificate certificate in path) - { - string? error = CheckDates(certificate, Name(certificate, leaf), now, ref alert); - if (error is not null) - { - return error; - } - } - - return CheckDates(root, $"the root {root.SubjectDN}", now, ref alert); - } - - /// How a message calls : "the certificate" for the server's own, its subject otherwise. - private static string Name(X509Certificate certificate, X509Certificate leaf) => - certificate == leaf ? "the certificate" : certificate.SubjectDN.ToString(); - - /// - /// A trusted root whose subject is the issuer of - /// and whose key verifies its signature, valid now; - /// is one that would do but for its dates, when there is no such root. - /// - private static X509Certificate? FindRoot(TrustedRoots roots, X509Certificate certificate, DateTime now, out X509Certificate? expired) - { - expired = null; - foreach (X509Certificate candidate in roots.FindBySubject(certificate.IssuerDN)) - { - if (!IsSignedBy(certificate, candidate)) - { - continue; - } - - if (now >= candidate.NotBefore && now <= candidate.NotAfter) - { - return candidate; - } - - expired ??= candidate; - } - - return null; - } - - /// - /// The index in of an unused certificate - /// named as the issuer of and whose key - /// verifies its signature, or -1. - /// - private static int FindIssuer(IReadOnlyList presented, bool[] used, X509Certificate certificate) - { - byte[] issuerName = certificate.IssuerDN.GetEncoded(); - for (int i = 0; i < presented.Count; i++) - { - if (!used[i] - && Arrays.AreEqual(presented[i].SubjectDN.GetEncoded(), issuerName) - && IsSignedBy(certificate, presented[i])) - { - return i; - } - } - - return -1; - } - - private static bool IsSignedBy(X509Certificate certificate, X509Certificate issuer) - { - try - { - return certificate.IsSignatureValid(issuer.GetPublicKey()); - } - catch (Exception) - { - // A key or signature algorithm the other does not go with. - return false; - } - } - - private static bool IsSelfIssued(X509Certificate certificate) => - Arrays.AreEqual(certificate.SubjectDN.GetEncoded(), certificate.IssuerDN.GetEncoded()); - - private static string? CheckSignatureAlgorithm(X509Certificate certificate, string name) => - Array.IndexOf(s_strongSignatureAlgorithms, certificate.SigAlgOid) >= 0 - ? null - : $"{name} is signed with {certificate.SigAlgName}, which is too weak to trust"; - - private static string? CheckKey(X509Certificate certificate, string name) => - certificate.GetPublicKey() is RsaKeyParameters rsa && rsa.Modulus.BitLength < MinRsaKeyBits - ? $"{name} has a {rsa.Modulus.BitLength}-bit RSA key, shorter than the {MinRsaKeyBits} bits required" - : null; - - private static string? CheckDates(X509Certificate certificate, string name, DateTime now, ref short alert) - { - if (now < certificate.NotBefore) - { - alert = AlertDescription.certificate_unknown; - return $"{name} is not valid before {Format(certificate.NotBefore)} (is the clock right? it reads {Format(now)})"; - } - - if (now > certificate.NotAfter) - { - alert = AlertDescription.certificate_expired; - return $"{name} expired on {Format(certificate.NotAfter)}"; - } - - return null; - } - - private static string Format(DateTime time) => - time.ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture); - - /// - /// Whether may issue the chain below it: a - /// certificate authority, allowed to sign certificates, whose path length - /// constraint leaves room for the - /// intermediates under it, and not restricted to other uses than TLS - /// servers. - /// - private static string? CheckAuthority(X509Certificate issuer, string name, int intermediatesBelow) - { - int pathLength = issuer.GetBasicConstraints(); - if (pathLength < 0) - { - return $"{name} issued part of the chain but is not a certificate authority"; - } - - if (intermediatesBelow > pathLength) - { - return $"{name} allows {pathLength} intermediate certificates under it, and the chain has {intermediatesBelow}"; - } - - bool[]? keyUsage = issuer.GetKeyUsage(); - if (keyUsage is not null && !keyUsage[5]) - { - return $"{name} issued part of the chain but its key may not sign certificates"; - } - - return CheckServerUsage(issuer, name); - } - - /// Whether the extended key usage, if any, allows TLS servers: browsers apply it to intermediates too. - private static string? CheckServerUsage(X509Certificate certificate, string name) - { - IList? usages = certificate.GetExtendedKeyUsage(); - if (usages is null || usages.Contains(KeyPurposeID.id_kp_serverAuth) || usages.Contains(KeyPurposeID.AnyExtendedKeyUsage)) - { - return null; - } - - return $"{name} is not for TLS servers"; - } - - private static string? CheckCriticalExtensions(X509Certificate certificate, string name, ref short alert) - { - ISet? critical = certificate.GetCriticalExtensionOids(); - if (critical is null) - { - return null; - } - - foreach (string oid in critical) - { - bool understood = false; - foreach (DerObjectIdentifier known in s_understoodCriticalExtensions) - { - if (known.Id == oid) - { - understood = true; - break; - } - } - - if (!understood) - { - alert = AlertDescription.unsupported_certificate; - return $"{name} has a critical extension that is not understood ({oid})"; - } - } - - return null; - } - - /// - /// Applies the name constraints of the intermediates on - /// (the server's certificate first) to the - /// certificates below each, from the top down (RFC 5280, section 6.1), - /// and to itself: a wildcard name could otherwise - /// cover a host its issuer may not certify. - /// - private static string? CheckNameConstraints(List path, string host) - { - PkixNameConstraintValidator validator = new(); - bool constrained = false; - for (int i = path.Count - 1; i >= 0; i--) - { - X509Certificate certificate = path[i]; - bool isLeaf = i == 0; - - // Self-issued intermediates are left out of the check (section 4.2.1.10). - if (constrained && (isLeaf || !IsSelfIssued(certificate))) - { - try - { - validator.CheckDN(certificate.SubjectDN); - GeneralNames? names = certificate.GetSubjectAlternativeNameExtension(); - if (names is not null) - { - foreach (GeneralName name in names.GetNames()) - { - validator.CheckName(name); - } - } - - if (isLeaf) - { - validator.CheckName(IPAddress.TryParse(host, out _) - ? new GeneralName(GeneralName.IPAddress, host) - : new GeneralName(GeneralName.DnsName, host.TrimEnd('.'))); - } - } - catch (PkixNameConstraintValidatorException exception) - { - return $"{certificate.SubjectDN} is outside the names its issuer may certify: {exception.Message}"; - } - } - - if (isLeaf) - { - break; - } - - Asn1Object? value = certificate.GetExtensionParsedValue(X509Extensions.NameConstraints); - if (value is null) - { - continue; - } - - NameConstraints constraints = NameConstraints.GetInstance(value); - if (constraints.PermittedSubtreesValue is GeneralSubtrees permitted) - { - validator.IntersectPermittedSubtree(permitted.Elements); - constrained = true; - } - - if (constraints.ExcludedSubtreesValue is GeneralSubtrees excluded) - { - foreach (GeneralSubtree subtree in excluded.GetElements()) - { - validator.AddExcludedSubtree(subtree); - } - - constrained = true; - } - } - - return null; - } - - /// - /// Whether the certificate's subject alternative names name - /// : as an IP address when the host is one, - /// otherwise as a DNS name (RFC 6125), with a wildcard allowed as the - /// whole left-most label. - /// - private static string? CheckHost(X509Certificate certificate, string host) - { - GeneralNames? names = certificate.GetSubjectAlternativeNameExtension(); - if (names is null) - { - return $"the certificate names no host (it has no subject alternative names), so not {host}"; - } - - bool isAddress = IPAddress.TryParse(host, out IPAddress? address); - List listed = []; - foreach (GeneralName name in names.GetNames()) - { - if (name.TagNo == GeneralName.IPAddress) - { - byte[] bytes = Asn1OctetString.GetInstance(name.Name).GetOctets(); - if (isAddress && Arrays.AreEqual(bytes, address!.GetAddressBytes())) - { - return null; - } - - if (bytes.Length is 4 or 16) - { - listed.Add(new IPAddress(bytes).ToString()); - } - } - else if (name.TagNo == GeneralName.DnsName) - { - string pattern = ((IAsn1String)name.Name).GetString(); - if (!isAddress && MatchesDnsName(pattern, host)) - { - return null; - } - - listed.Add(pattern); - } - } - - if (listed.Count == 0) - { - return $"the certificate names no host, so not {host}"; - } - - StringBuilder message = new("the certificate is for "); - for (int i = 0; i < listed.Count && i < MaxListedNames; i++) - { - message.Append(i == 0 ? "" : ", ").Append(listed[i]); - } - - if (listed.Count > MaxListedNames) - { - message.Append(" and ").Append(listed.Count - MaxListedNames).Append(" more"); - } - - return message.Append(", not ").Append(host).ToString(); - } - - /// - /// Whether , a DNS name from a certificate, - /// names : alike but for case, or with a - /// * as the whole left-most label standing for exactly one label - /// of the host, under at least two labels (never *.com). There is - /// no public suffix list: that *.co.uk is never issued is left to - /// the certificate authorities, which may not issue it. - /// - internal static bool MatchesDnsName(string pattern, string host) - { - pattern = pattern.TrimEnd('.'); - host = host.TrimEnd('.'); - if (pattern.Length == 0 || host.Length == 0) - { - return false; - } - - if (!pattern.StartsWith("*.", StringComparison.Ordinal)) - { - return pattern.Equals(host, StringComparison.OrdinalIgnoreCase); - } - - string suffix = pattern.Substring(1); - int firstDot = host.IndexOf('.'); - return suffix.IndexOf('.', 1) > 0 - && suffix.IndexOf('*') < 0 - && firstDot > 0 - && host.Substring(firstDot).Equals(suffix, StringComparison.OrdinalIgnoreCase); - } -} diff --git a/src/Cosmos.Network.Http/ChunkedBody.cs b/src/Cosmos.Network.Http/ChunkedBody.cs deleted file mode 100644 index d5fb09b..0000000 --- a/src/Cosmos.Network.Http/ChunkedBody.cs +++ /dev/null @@ -1,150 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System.IO; - -namespace Cosmos.Network.Http; - -/// -/// The chunked transfer coding (RFC 9112, section 7.1): chunks, each after -/// its size in hexadecimal, up to a chunk of size 0 and a trailer section. -/// Chunk extensions and trailers carry nothing a client needs and are -/// skipped. -/// -internal static class ChunkedBody -{ - /// - /// Whether the chunked body that starts at - /// has arrived whole: its last chunk and the trailer section that ends it. - /// moves past each whole chunk, so the next - /// call resumes there rather than walking the body again. - /// - /// A chunk size is malformed. - public static bool FindEnd(byte[] data, int length, ref int position) - { - while (true) - { - int lineEnd = IndexOfLineEnd(data, position, length); - if (lineEnd < 0) - { - return false; - } - - int size = ParseSize(data, position, lineEnd); - if (size == 0) - { - // The trailer section: field lines up to an empty one. - int line = lineEnd + 2; - while (true) - { - int end = IndexOfLineEnd(data, line, length); - if (end < 0) - { - return false; - } - - if (end == line) - { - return true; - } - - line = end + 2; - } - } - - long next = (long)lineEnd + 2 + size + 2; - if (next > length) - { - return false; - } - - position = (int)next; - } - } - - /// The data of the chunked body that starts at , chunk sizes, extensions and trailers left out. - /// The body ends before its last chunk, or a chunk size is malformed. - public static byte[] Decode(byte[] data, int start, int length) - { - MemoryStream body = new(); - int position = start; - while (true) - { - int lineEnd = IndexOfLineEnd(data, position, length); - if (lineEnd < 0) - { - throw Truncated(); - } - - int size = ParseSize(data, position, lineEnd); - if (size == 0) - { - return body.ToArray(); - } - - int chunkStart = lineEnd + 2; - if ((long)chunkStart + size > length) - { - throw Truncated(); - } - - body.Write(data, chunkStart, size); - position = chunkStart + size + 2; - } - } - - /// The size on a chunk line, ignoring the extensions after it. - private static int ParseSize(byte[] data, int start, int end) - { - int size = 0; - int digits = 0; - int i = start; - for (; i < end; i++) - { - int digit = HexValue(data[i]); - if (digit < 0) - { - break; - } - - if (size > (int.MaxValue >> 4)) - { - throw new HttpException("The response has a chunk larger than 2 GB."); - } - - size = (size << 4) | digit; - digits++; - } - - // What follows the digits, if anything, is whitespace or a ";name=value" extension. - if (digits == 0 || (i < end && data[i] is not ((byte)';' or (byte)' ' or (byte)'\t'))) - { - throw new HttpException("The response has a malformed chunk size."); - } - - return size; - } - - private static int HexValue(byte c) => c switch - { - >= (byte)'0' and <= (byte)'9' => c - '0', - >= (byte)'a' and <= (byte)'f' => c - 'a' + 10, - >= (byte)'A' and <= (byte)'F' => c - 'A' + 10, - _ => -1, - }; - - /// The index of the next CRLF from , or -1. - private static int IndexOfLineEnd(byte[] data, int start, int length) - { - for (int i = start; i + 1 < length; i++) - { - if (data[i] == '\r' && data[i + 1] == '\n') - { - return i; - } - } - - return -1; - } - - private static HttpException Truncated() => new("The server closed the connection in the middle of a chunked body."); -} diff --git a/src/Cosmos.Network.Http/Friends.cs b/src/Cosmos.Network.Http/Friends.cs new file mode 100644 index 0000000..ca99e4c --- /dev/null +++ b/src/Cosmos.Network.Http/Friends.cs @@ -0,0 +1,8 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System.Runtime.CompilerServices; + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("System.Net.Websockets, PublicKey=00240000048000009400000006020000002400005253413100040000010001001120aa3e809b3da4f65e1b1f65c0a3a1bf6335c39860ca41acb3c48de278c6b63c5df38239ec1f2e32d58cb897c8c174a5f8e78a9c0b6087d3aef373d7d0f3d9be67700fc2a5a38de1fb71b5b6f6046d841ff35abee2e0b0840a6291a312be184eb311baff5fef0ff6895b9a5f2253aed32fb06b819134f6bb9d531488a87ea2")] diff --git a/src/Cosmos.Network.Http/Http/ByteArrayContent.cs b/src/Cosmos.Network.Http/Http/ByteArrayContent.cs new file mode 100644 index 0000000..592b709 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/ByteArrayContent.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.IO; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content based on a byte array. + /// + public class ByteArrayContent : HttpContent + { + private readonly byte[] _content; + private readonly int _offset; + private readonly int _count; + + /// + /// Initializes a new instance of the class. + /// + /// The content used to initialize the . + /// The parameter is . + public ByteArrayContent(byte[] content) + { + if (content is null) + { + throw new ArgumentNullException(); + } + + _content = content; + _count = content.Length; + } + + /// + /// Initializes a new instance of the class. + /// + /// The content used to initialize the . + /// + /// + /// The parameter is . + /// + /// + /// The parameter is less than zero. + /// + /// + /// -or- + /// + /// + /// The parameter is greater than the length of content specified by the parameter. + /// + /// + /// -or- + /// + /// + /// The parameter is less than zero. + /// + /// + /// -or- + /// + /// + /// The parameter is greater than the length of content specified by the parameter - minus the parameter. + /// + /// + /// + /// Only the range specified by the parameter and the parameter is used as content. Syntax + /// + public ByteArrayContent( + byte[] content, + int offset, + int count) + { + if (content is null) + { + throw new ArgumentNullException(); + } + + if ((offset < 0) || (offset > content.Length)) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if ((count < 0) || (count > (content.Length - offset))) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + _content = content; + _offset = offset; + _count = count; + } + + /// + protected override void SerializeToStream(Stream stream) + { + stream.Write(_content, _offset, _count); + } + + /// + protected internal override bool TryComputeLength(out long length) + { + length = _count; + return true; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs b/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs new file mode 100644 index 0000000..ca32713 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http +{ + /// + /// Specifies how client certificates are provided. + /// + public enum ClientCertificateOption + { + /// + /// The application manually provides the client certificates to the WebRequestHandler. This value is the default. + /// + Manual = 0, + + /// + /// The will attempt to provide all available client certificates automatically. + /// + Automatic + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs b/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs new file mode 100644 index 0000000..dd7dd2b --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http.Headers +{ + // This struct represents a particular named header -- + // if the header is one of our known headers, then it contains a reference to the KnownHeader object; + // otherwise, for custom headers, it just contains a string for the header name. + // Use HeaderDescriptor.TryGet to resolve an arbitrary header name to a HeaderDescriptor. + internal readonly struct HeaderDescriptor + { + /// + /// Either a or . + /// + private readonly object _descriptor; + + // This should not be used directly; use static TryGet below + internal HeaderDescriptor( + string headerName, + bool customHeader = false) + { + _descriptor = headerName; + } + + public string Name => _descriptor is KnownHeader header ? header.Name : (_descriptor as string)!; + + public HttpHeaderType HeaderType => _descriptor is KnownHeader knownHeader ? knownHeader.HeaderType : HttpHeaderType.Custom; + + public KnownHeader KnownHeader => _descriptor as KnownHeader; + + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs new file mode 100644 index 0000000..99d0a0c --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Net.Http.Http.Headers; + +namespace System.Net.Http.Headers +{ + /// + /// Represents the collection of Content Headers as defined in RFC 2616. + /// + public sealed class HttpContentHeaders : HttpHeaders + { + private readonly HttpContent _content; + + /// + /// Gets or sets the value of the Content-Length content header on an HTTP response. + /// + /// The value of the Content-Length content header on an HTTP response. + /// + /// In .NET nanoFramework this property is read-only. + /// + public long ContentLength + { + get + { + if (_content.Headers is not null + && _content.Headers._headerStore is not null) + { + var contentLengthValue = _content.Headers._headerStore.GetValues(HttpKnownHeaderNames.ContentLength); + + if (contentLengthValue is not null + && contentLengthValue.Length > 0) + { + return Convert.ToInt64(contentLengthValue[0]); + } + + if(_content.TryComputeLength(out long computedOrBufferLength)) + { + _content.Headers._headerStore.SetAddVerified(HttpKnownHeaderNames.ContentLength, computedOrBufferLength.ToString()); + return computedOrBufferLength; + } + } + + return -1; + } + + set + { + throw new PlatformNotSupportedException(); + } + } + + /// + /// Gets or sets the value of the Content-Type content header on an HTTP response. + /// + /// The value of the Content-Type content header on an HTTP response. + public MediaTypeHeaderValue ContentType + { + get + { + return MediaTypeHeaderValue.Parse(_headerStore[HttpKnownHeaderNames.ContentType]); + } + + set + { + // build header value, OK to add ; even if CharSet is empty + _headerStore.Add(HttpKnownHeaderNames.ContentType, value.ToString()); + } + } + + internal HttpContentHeaders(HttpContent parent) + : base(HttpHeaderType.Content | HttpHeaderType.Custom, HttpHeaderType.None) + { + _content = parent; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs new file mode 100644 index 0000000..c68a965 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; + +namespace System.Net.Http.Headers +{ + // The purpose of this type is to extract the handling of general headers in one place rather than duplicating + // functionality in both HttpRequestHeaders and HttpResponseHeaders. + internal sealed class HttpGeneralHeaders + { + private readonly HttpHeaders _parent; + + internal HttpGeneralHeaders(HttpHeaders parent) + { + Debug.Assert(parent != null); + + _parent = parent; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs b/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs new file mode 100644 index 0000000..76cb40f --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http.Headers +{ + [Flags] + internal enum HttpHeaderType : byte + { + General = 0b0000_0001, + Request = 0b0000_0010, + Response = 0b0000_0100, + Content = 0b0000_1000, + Custom = 0b0001_0000, + NonTrailing = 0b0010_0000, + + All = 0b0011_1111, + None = 0 + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs new file mode 100644 index 0000000..5bc064e --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; + +namespace System.Net.Http.Headers +{ + /// + /// Key/value pairs of headers. The value is either a raw or a . + /// + internal struct HeaderEntry + { + public HeaderDescriptor Key; + public object Value; + + public HeaderEntry( + HeaderDescriptor key, + object value) + { + Key = key; + Value = value; + } + } + + /// + /// A collection of headers and their values as defined in RFC 2616. + /// + public abstract class HttpHeaders + { + internal WebHeaderCollection _headerStore = new WebHeaderCollection(true); + + private readonly HttpHeaderType _allowedHeaderTypes; + private readonly HttpHeaderType _treatAsCustomHeaderTypes; + + /// + /// Initializes a new instance of the HttpHeaders class. + /// + protected HttpHeaders() + : this( + HttpHeaderType.All, + HttpHeaderType.None) + { + } + + internal HttpHeaders( + HttpHeaderType allowedHeaderTypes, + HttpHeaderType treatAsCustomHeaderTypes) + { + // Should be no overlap + Debug.Assert((allowedHeaderTypes & treatAsCustomHeaderTypes) == 0); + + _allowedHeaderTypes = allowedHeaderTypes & ~HttpHeaderType.NonTrailing; + _treatAsCustomHeaderTypes = treatAsCustomHeaderTypes & ~HttpHeaderType.NonTrailing; + } + + + /// + /// Adds the specified header and its value into the collection. + /// + /// The header to add to the collection. + /// The content of the header. + /// The values cannot be or empty. + public void Add( + string name, + string value) + { + _headerStore.Add(name, value); + } + + internal virtual void AddHeaders(HttpHeaders sourceHeaders) + { + foreach (var headerKey in sourceHeaders._headerStore.AllKeys) + { + _headerStore.AddInternal(headerKey, sourceHeaders._headerStore[headerKey]); + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs new file mode 100644 index 0000000..78bc9e5 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; + +namespace System.Net.Http.Headers +{ + /// + /// Represents the collection of Request Headers as defined in RFC 2616. + /// + public sealed class HttpRequestHeaders : HttpHeaders + { + #region General Headers + + /// + /// Gets the value of the Connection header for an HTTP request. + /// + /// The value of the Connection header for an HTTP request. + public string Connection + { + get + { + var connectionHeader = _headerStore.GetValues(HttpKnownHeaderNames.Connection); + if (connectionHeader is not null) + { + return connectionHeader[0]; + } + else + { + return string.Empty; + } + } + } + + /// + /// Gets or sets a value that indicates if the header for an HTTP request contains Close. + /// + /// if the header contains Close, otherwise . + public bool ConnectionClose + { + get + { + return Connection.ToLower().Contains("close"); + } + + set + { + var connectionHeader = _headerStore.GetValues(HttpKnownHeaderNames.Connection); + if (connectionHeader is not null) + { + connectionHeader[0] = value ? "close" : "keep-alive"; + } + else + { + _headerStore.AddInternal(HttpKnownHeaderNames.Connection, value ? "close" : "keep-alive"); + } + } + } + + /// + /// Gets or sets a value that indicates if the Transfer-Encoding header for an HTTP request contains chunked. + /// + /// if the Transfer-Encoding header contains chunked, otherwise . + public bool TransferEncodingChunked + { + get + { + var header = _headerStore[HttpKnownHeaderNames.TransferEncoding]; + + if (header is not null) + { + return header.Contains("chunked"); + } + + return false; + } + + set + { + throw new NotImplementedException(); + } + } + + #endregion + + internal HttpRequestHeaders() + : base() + { + } + + internal override void AddHeaders(HttpHeaders headers) + { + base.AddHeaders(headers); + } + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs new file mode 100644 index 0000000..746ab29 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http.Headers +{ + /// + /// + /// + public sealed class HttpResponseHeaders : HttpHeaders + { + internal HttpResponseHeaders() + : base() + { + } + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs b/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs new file mode 100644 index 0000000..1cdc292 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Diagnostics; + +namespace System.Net.Http.Headers +{ + internal sealed partial class KnownHeader + { + public string Name { get; } + + public HttpHeaderType HeaderType { get; } + + /// + /// If a raw string is a known value, this instance will be returned rather than allocating a new string. + /// + public string[] KnownValues { get; } + + public KnownHeader( + string name, + HttpHeaderType headerType, + string[] knownValues = null) + { + Debug.Assert(!string.IsNullOrEmpty(name)); + + Name = name; + HeaderType = headerType; + KnownValues = knownValues; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs b/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs new file mode 100644 index 0000000..4cd14d0 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http.Http.Headers +{ + /// + /// Represents a media type used in a Content-Type header as defined in the RFC 2616. + /// + /// + /// The MediaTypeHeaderValue class provides support for the media type used in a Content-Type header as defined in RFC 2616 by the IETF. + /// An example of a media-type would be "text/plain; charset=iso-8859-5". + /// + public class MediaTypeHeaderValue + { + private const string _CharSetLabel = "charset="; + private const int _CharSetLabelLenght = 8; + + /// + /// Gets or sets the character set. + /// + /// The character set. + public string CharSet { get; set; } + + /// + /// Gets or sets the media-type header value. + /// + /// The media-type header value. + /// + /// The media-type is used in the Content-Type and Accept header fields in order to provide open and extensible data typing and type negotiation. + /// + public string MediaType { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The source represented as a string to initialize the new instance. + /// If parameter is null or empty + /// If parameter contains invalid value for . + public MediaTypeHeaderValue(string mediaType) + { + if (string.IsNullOrEmpty(mediaType)) + { + throw new ArgumentException(); + } + + // sanity check for invalid content in media type + if (mediaType.Contains(_CharSetLabel) + || mediaType.Contains(";") + || !mediaType.Contains("/")) + { + throw new FormatException(); + } + + MediaType = mediaType; + } + + /// + /// Converts a string to an instance. + /// + /// A string that represents media type header value information. + /// A MediaTypeHeaderValue instance. + /// input is not valid media type header value information. + /// input is a null reference. + public static MediaTypeHeaderValue Parse(string input) + { + if (input is null) + { + throw new ArgumentNullException(); + } + + MediaTypeHeaderValue value = null; + + // format should be similar to: + // "text/plain" + // "text/plain; charset=utf-8" + + var values = input.Split(';'); + + // remove leading and trailing spaces + var tempValue = values[0].Trim(' '); + + if (string.IsNullOrEmpty(tempValue)) + { + throw new FormatException(); + } + + // assume it's MediaType + value = new MediaTypeHeaderValue(tempValue); + + if (values.Length > 1 && values.Length <= 2) + { + // remove leading and trailing spaces + tempValue = values[1].Trim(' '); + + // assume 2nd part, if present is charset + int indexOfCharSet = tempValue.IndexOf(_CharSetLabel); + + // check for misplaced start of charset + if (indexOfCharSet != 0) + { + throw new FormatException(); + } + + value.CharSet = tempValue.Substring(indexOfCharSet + _CharSetLabelLenght); + } + + if (values.Length > 2) + { + throw new FormatException(); + } + + return value; + } + + /// + public override string ToString() + { + string headerValue = MediaType; + + if (CharSet is not null) + { + headerValue += $"; {_CharSetLabel}{CharSet}"; + } + + return headerValue; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpClient.cs b/src/Cosmos.Network.Http/Http/HttpClient.cs new file mode 100644 index 0000000..3f26189 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpClient.cs @@ -0,0 +1,486 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.IO; +using System.Net.Http.Headers; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Http +{ + /// + /// Initializes a new instance of the HttpClient class. + /// + /// + /// The HttpClient class instance acts as a session to send HTTP requests. + /// An HttpClient instance is a collection of settings applied to all requests executed by that instance. + /// In addition, every HttpClient instance uses its own connection pool, + /// isolating its requests from requests executed by other HttpClient instances. + /// + /// HttpClient is intended to be instantiated once and reused throughout the life of an application. + /// + public partial class HttpClient : HttpMessageInvoker + { + private const HttpCompletionOption DefaultCompletionOption = HttpCompletionOption.ResponseContentRead; + private Version _defaultRequestVersion = HttpRequestMessage.DefaultRequestVersion; + + private bool _operationStarted; + private bool _disposed; + + private HttpRequestHeaders _headers; + private Uri _baseAddress; + private TimeSpan _timeout; + + /// + /// Gets the headers which should be sent with each request. + /// + /// + /// The headers which should be sent with each request. + /// + /// + /// Headers set on this property don't need to be set on request messages again. should not be modified while there are outstanding requests, because it is not thread-safe. + /// + public HttpRequestHeaders DefaultRequestHeaders => _headers ??= new HttpRequestHeaders(); + + /// + /// Gets or sets the base address of Uniform Resource Identifier (URI) of the Internet resource used when sending requests. + /// + /// + /// The base address of Uniform Resource Identifier (URI) of the Internet resource used when sending requests. + /// + /// Value is null or it not an absolute Uniform Resource Identifier (URI). + /// An operation has already been started on the current instance. + /// The current instance has been disposed. + public Uri BaseAddress + { + get => _baseAddress; + + set + { + // It's OK to not have a base address specified, but if one is, it needs to be absolute. + if (value is not null + && !value.IsAbsoluteUri) + { + throw new ArgumentException(); + } + + CheckDisposedOrStarted(); + + _baseAddress = value; + } + } + + /// + /// Gets or sets the timespan to wait before the request times out. + /// + /// + /// The timespan to wait before the request times out. + /// + /// + /// The default value is 100,000 milliseconds (100 seconds). + /// To set an infinite timeout, set the property value to . + /// + /// A Domain Name System (DNS) query may take up to 15 seconds to return or time out. If your request contains a host name that requires resolution and you set to a value less than 15 seconds, it may take 15 seconds or more before a is thrown to indicate a timeout on your request. + /// + /// + /// The same timeout will apply for all requests using this instance. You may also set different timeouts for individual requests using a CancellationTokenSource on a task.Note that only the shorter of the two timeouts will apply. + /// + /// + /// Value is null or it not an absolute Uniform Resource Identifier (URI). + /// An operation has already been started on the current instance. + /// The current instance has been disposed. + public TimeSpan Timeout + { + get => _timeout; + + set + { + if (value != Threading.Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value.TotalMilliseconds > int.MaxValue)) + { + throw new ArgumentOutOfRangeException(); + } + + CheckDisposedOrStarted(); + + _timeout = value; + } + } + + /// + /// Gets or sets the root CA certificate used to authenticate with https servers. + /// This certificate is used only for https connections; http connections do not require this. + /// + /// + /// This property is an extension from the full .NET required by nanoFramework. + /// + public X509Certificate HttpsAuthentCert { get; set; } + + /// + /// Gets or sets the TLS/SSL protocol used by the class. + /// + /// + /// One of the values defined in the enumeration. Default value is . + /// + /// + /// This property is an extension from the full .NET required by nanoFramework. + /// + public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls12; + + /// + /// Gets or sets the TLS/SSL verification mode used by the class. + /// + /// + /// Default value is . + /// + public SslVerification SslVerification { get; set; } = SslVerification.CertificateRequired; + + #region Constructors + + /// + /// Initializes a new instance of the class using a that is disposed when this instance is disposed. + /// + public HttpClient() : base( + new HttpClientHandler(), + true) + { + _timeout = Threading.Timeout.InfiniteTimeSpan; + + // The default in REST API is to close the connection after each request. + DefaultRequestHeaders.ConnectionClose = true; + } + + #endregion Constructors + + #region REST Overloads + + /// + /// Send a DELETE request to the specified Uri as a synchronous operation. + /// + /// The the request is sent to. + /// The object resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of DeleteAsync. + /// + /// + public HttpResponseMessage Delete(string requestUri) => Send(new HttpRequestMessage(HttpMethod.Delete, requestUri), DefaultCompletionOption); + + /// + /// Sends a GET request to the specified . + /// + /// The the request is sent to. + /// The object resulting from the HTTP request. + /// Request operation has already started. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of GetAsync. + /// + /// + public HttpResponseMessage Get(string requestUri) => Send(new HttpRequestMessage(HttpMethod.Get, requestUri), DefaultCompletionOption); + + /// + /// Send a GET request to the specified Uri. + /// + /// The the request is sent to. + /// + /// The object resulting from the HTTP request. + /// Request operation has already started. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of GetAsync. + /// + /// + public HttpResponseMessage Get(string requestUri, HttpCompletionOption completionOption) => Send(new HttpRequestMessage(HttpMethod.Get, requestUri), completionOption); + + /// + /// Sends a PATCH request as a synchronous operation. + /// + /// The the request is sent to. + /// The HTTP request content sent to the server. + /// The object resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of PatchAsync. + /// + /// + public HttpResponseMessage Patch(string requestUri, HttpContent content) => Send(new HttpRequestMessage(HttpMethod.Patch, requestUri) { Content = content }, DefaultCompletionOption); + + /// + /// Send a POST request as a synchronous operation. + /// + /// The the request is sent to. + /// The HTTP request content sent to the server. + /// The object resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of PostAsync. + /// + /// + public HttpResponseMessage Post(string requestUri, HttpContent content) => Send(new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }, DefaultCompletionOption); + + /// + /// Send a PUT request as a synchronous operation. + /// + /// The the request is sent to. + /// The HTTP request content sent to the server. + /// The object resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of PutAsync. + /// + /// + public HttpResponseMessage Put(string requestUri, HttpContent content) => Send(new HttpRequestMessage(HttpMethod.Put, requestUri) { Content = content }, DefaultCompletionOption); + + #endregion + + /// + /// Sends a GET request to the specified Uri and return the response body as a byte array in an synchronous operation. + /// + /// The the request is sent to. + /// A byte array resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. It returns after the whole response body is read. + /// + /// + /// This is the .NET nanoFramework equivalent of GetByteArrayAsync. + /// + /// + public byte[] GetByteArray(string requestUri) + { + using var resp = Get(requestUri, HttpCompletionOption.ResponseContentRead); + resp.EnsureSuccessStatusCode(); + + return resp.Content.ReadAsByteArray(); + } + + /// + /// Send a GET request to the specified Uri and return the response body as a stream in a synchronous operation. + /// + /// The the request is sent to. + /// A Stream resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// As of now, this method reads and buffers the entire response body (see ). + /// If the response needs to be streamed, one of the methods that return should be used + /// with and instead. + /// + /// + /// This is the .NET nanoFramework equivalent of GetStreamAsync. + /// + /// + public Stream GetStream(string requestUri) + { + var resp = Get(requestUri, HttpCompletionOption.ResponseHeadersRead); + resp.EnsureSuccessStatusCode(); + + return resp.Content.ReadAsStream(); + } + + /// + /// Send a GET request to the specified Uri and return the response body as a string in an synchronous operation. + /// + /// The the request is sent to. + /// A string resulting from the HTTP request. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + /// + /// + /// This operation will block. + /// + /// + /// This operation will block. It returns after the whole response body is read. + /// + /// + /// This is the .NET nanoFramework equivalent of GetStringAsync. + /// + /// + public string GetString(string requestUri) + { + using HttpResponseMessage resp = Get(requestUri, HttpCompletionOption.ResponseContentRead); + resp.EnsureSuccessStatusCode(); + + return resp.Content.ReadAsString(); + } + + #region Advanced Send Overloads + + /// + /// Sends an HTTP request with the specified request. + /// + /// The HTTP request message to send. + /// The HTTP response message. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + public override HttpResponseMessage Send(HttpRequestMessage request) + { + return Send(request, DefaultCompletionOption); + } + + /// + /// Sends an HTTP request with the specified request. + /// + /// The HTTP request message to send. + /// One of the enumeration values that specifies when the operation should complete (as soon as a response is available or after reading the response content). + /// The HTTP response message. + /// The request is . + /// The request message was already sent by the instance. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, or server certificate validation. + public HttpResponseMessage Send( + HttpRequestMessage request, + HttpCompletionOption completionOption) + { + if (request == null) + { + throw new ArgumentNullException(); + } + + if (request.SetIsUsed()) + { + throw new InvalidOperationException(); + } + + var uri = request.RequestUri; + + if (uri == null) + { + if (_baseAddress == null) + { + throw new InvalidOperationException(); + } + + request.RequestUri = _baseAddress; + } + else if (!uri.IsAbsoluteUri) + { + if (_baseAddress == null) + { + throw new InvalidOperationException(); + } + + request.RequestUri = new Uri(_baseAddress, uri.OriginalString); + } + + if (_headers != null) + { + request.Headers.AddHeaders(_headers); + } + + return SendWorker(request, completionOption); + } + + private HttpResponseMessage SendWorker(HttpRequestMessage request, HttpCompletionOption completionOption) + { + // need to pass to the HttpWebRequest: + // - timeout + // - SSL protocol + // - CA root certs + if (_handler is HttpClientHandler clientHandler) + { + clientHandler.SetWebRequestTimeout(_timeout); + clientHandler.SetWebRequestSslProcol(SslProtocols); + clientHandler.SetWebRequestHttpAuthCert(HttpsAuthentCert); + clientHandler.SetWebRequestSslVerification(SslVerification); + } + + HttpResponseMessage response = base.Send(request); + + // Read the content when default HttpCompletionOption.ResponseContentRead is set + if (response.Content != null && completionOption == HttpCompletionOption.ResponseContentRead) + { + response.Content.LoadIntoBuffer(); + } + + return response; + } + + #endregion + + #region helper methods + + private void SetOperationStarted() + { + // This method flags the HttpClient instances as "active". I.e. we executed at least one request (or are + // in the process of doing so). This information is used to lock-down all property setters. Once a + // Send operation is started, no property can be changed. + if (!_operationStarted) + { + _operationStarted = true; + } + } + + private Uri CreateUri(string uri) => string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute); + + private HttpRequestMessage CreateRequestMessage(HttpMethod method, string uri) => new HttpRequestMessage(method, uri) { Version = _defaultRequestVersion }; + + private void CheckDisposedOrStarted() + { + CheckDisposed(); + + if (_operationStarted) + { + throw new InvalidOperationException(); + } + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(); + } + } + + #endregion + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpClientHandler.cs b/src/Cosmos.Network.Http/Http/HttpClientHandler.cs new file mode 100644 index 0000000..e4ca4ce --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpClientHandler.cs @@ -0,0 +1,425 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Net.Http.Headers; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Http +{ + /// + /// The default message handler used by HttpClient in .NET nanoFramework. + /// + public partial class HttpClientHandler : HttpMessageHandler + { + private NetworkCredential _credentials; + private IWebProxy _proxy; + private bool _useProxy = false; + private TimeSpan _timeout = TimeSpan.Zero; + + private bool _disposed; + private bool _sentRequest; + private SslProtocols _sslProtocols; + private X509Certificate _caCert; + private X509Certificate _clientCert; + private ClientCertificateOption _clientCertificateOptions = ClientCertificateOption.Manual; + private SslVerification _sslVerification; + + /// + /// Gets or sets a value that indicates if the certificate is automatically picked from the certificate store or if the caller is allowed to pass in a specific client certificate. + /// + /// The collection of security certificates associated with this handler. + public ClientCertificateOption ClientCertificateOptions + { + get + { + return _clientCertificateOptions; + } + + set + { + EnsureModifiability(); + _clientCertificateOptions = value; + } + } + + /// + /// Gets the collection of security certificates that are associated with requests to the server. + /// + /// The that is presented to the server when performing certificate based client authentication. + /// + /// .NET nanoFramework only supports using one client certificate on the request, therefore this is not a collection has it happens with the equivalent .NET property. + /// + public X509Certificate ClientCertificate + { + get + { + if (ClientCertificateOptions != ClientCertificateOption.Manual) + { + throw new InvalidOperationException(); + } + + return _clientCert; + } + } + + /// + /// Gets or sets authentication information used by this handler. + /// + /// The authentication credentials associated with the handler. The default is null. + public NetworkCredential Credentials + { + get + { + return _credentials; + } + + set + { + EnsureModifiability(); + _credentials = value; + } + } + + /// + /// Gets or sets the TLS/SSL protocol used by the class. + /// + /// + /// One of the values defined in the enumeration. + /// + public SslProtocols SslProtocols + { + get + { + return _sslProtocols; + } + + set + { + EnsureModifiability(); + _sslProtocols = value; + } + } + + /// + /// Gets or sets proxy information used by the handler. + /// + /// The proxy information used by the handler. The default value is null. + public IWebProxy Proxy + { + get + { + return _proxy; + } + + set + { + EnsureModifiability(); + + if (!_useProxy) + { + throw new InvalidOperationException(); + } + + _proxy = value; + } + } + + /// + /// Gets a value that indicates whether the handler supports automatic response content decompression. + /// + /// if the if the handler supports automatic response content decompression; otherwise . The default value is . + /// + /// The default value for .NET nanoFramework is . + /// + public virtual bool SupportsAutomaticDecompression => false; + + /// + /// Gets a value that indicates whether the handler supports proxy settings. + /// + /// if the if the handler supports proxy settings; otherwise . The default value is . + public virtual bool SupportsProxy => true; + + /// + /// Gets a value that indicates whether the handler supports configuration settings for the AllowAutoRedirect and MaxAutomaticRedirections properties. + /// + /// if the if the handler supports configuration settings for the AllowAutoRedirect and MaxAutomaticRedirections properties; otherwise . The default value is . + /// + /// The default value for .NET nanoFramework is . + /// + public virtual bool SupportsRedirectConfiguration => false; + + /// + /// Gets or sets a value that indicates whether the handler uses the CookieContainer property to store server cookies and uses these cookies when sending requests. + /// + /// if the if the handler supports uses the CookieContainer property to store server cookies and uses these cookies when sending requests; otherwise . The default value is . + /// + /// The default value for .NET nanoFramework is . + /// + public bool UseCookies => false; + + /// + /// Gets or sets a value that indicates whether the handler uses a proxy for requests. + /// + /// if the handler should use a proxy for requests; otherwise . The default value is . + public bool UseProxy + { + get + { + return _useProxy; + } + + set + { + EnsureModifiability(); + _useProxy = value; + } + } + + /// + /// Creates an instance of a HttpClientHandler class. + /// + public HttpClientHandler() + { + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + } + + base.Dispose(disposing); + } + + /// + /// + /// + protected internal override HttpResponseMessage Send(HttpRequestMessage request) + { + if (_disposed) + { + throw new ObjectDisposedException(); + } + + var webRequest = CreateWebRequest(request); + + HttpWebResponse wresponse = null; + + try + { + var content = request.Content; + + if (content != null) + { + var headers = webRequest.Headers; + + foreach (var headerKey in content.Headers._headerStore.AllKeys) + { + headers.AddInternal(headerKey, content.Headers._headerStore[headerKey]); + } + + if (request.Headers.TransferEncodingChunked) + { + webRequest.SendChunked = true; + } + + // set content length + // if it can't be computed and Transfer-Encoding: chunked isn't set, + // webRequest.GetRequestStream throws an exception (so we don't have to validate that here). + if (request.Content.TryComputeLength(out long length)) + { + webRequest.ContentLength = length; + } + + + // set request sent flag + _sentRequest = true; + + var stream = webRequest.GetRequestStream(); + + request.Content.CopyTo(stream); + } + else if (MethodHasBody(request.Method)) + { + // Explicitly set this to make sure we're sending a "Content-Length: 0" header. + webRequest.ContentLength = 0; + + // set request sent flag + _sentRequest = true; + } + + wresponse = (HttpWebResponse)webRequest.GetResponse(); + } + catch (WebException we) + { + if (we.Status != WebExceptionStatus.RequestCanceled) + { + throw new HttpRequestException("An error occurred while sending the request", we); + } + } + catch (IO.IOException ex) + { + throw new HttpRequestException("An error occurred while sending the request", ex); + } + + try + { + return CreateResponseMessage(wresponse, request); + } + catch + { + // wresponse isn't referenced anywhere else yet; if building the HttpResponseMessage + // fails here, it must be disposed explicitly or its socket leaks until finalized. + if (wresponse != null) + { + wresponse.Dispose(); + } + + throw; + } + } + + private HttpWebRequest CreateWebRequest(HttpRequestMessage request) + { + var wr = new HttpWebRequest(request.RequestUri); + + wr.AllowWriteStreamBuffering = false; + + if (request.Version == HttpVersion.Version10) + { + wr.ProtocolVersion = HttpVersion.Version10; + } + else + { + wr.ProtocolVersion = request.Version; + } + + wr.Method = request.Method.Method; + + if (wr.ProtocolVersion == HttpVersion.Version10) + { + wr.KeepAlive = GetConnectionKeepAlive(request.Headers); + } + else + { + wr.KeepAlive = request.Headers.ConnectionClose != true; + } + + wr.Credentials = _credentials; + + if (_useProxy) + { + wr.Proxy = _proxy; + } + + if (_timeout != TimeSpan.Zero) + { + wr.Timeout = (int)_timeout.TotalMilliseconds; + } + + wr.SslProtocols = _sslProtocols; + wr.HttpsAuthentCert = _caCert; + wr.SslVerification = _sslVerification; + + if (ClientCertificateOptions == ClientCertificateOption.Manual) + { + wr._clientCert = ClientCertificate; + } + + // Add request headers + var headers = wr.Headers; + foreach (var headerKey in request.Headers._headerStore.AllKeys) + { + headers.AddInternal(headerKey, request.Headers._headerStore[headerKey]); + } + + return wr; + } + + HttpResponseMessage CreateResponseMessage(HttpWebResponse wr, HttpRequestMessage requestMessage) + { + var response = new HttpResponseMessage(wr.StatusCode) + { + RequestMessage = requestMessage, + ReasonPhrase = wr.StatusDescription + }; + + // set content + response.Content = new StreamContent(wr.GetResponseStream()); + + // Tie wr's lifetime to the response, otherwise it's unreachable once this method + // returns and can be finalized (closing its shared socket) mid-read. + response.WebResponse = wr; + + var headers = wr.Headers; + + foreach (var headerKey in headers.AllKeys) + { + response.Headers._headerStore.AddInternal(headerKey, headers[headerKey]); + response.Content.Headers._headerStore.AddInternal(headerKey, headers[headerKey]); + } + + requestMessage.RequestUri = wr.ResponseUri; + + return response; + } + + bool GetConnectionKeepAlive(HttpRequestHeaders headers) + { + // In theory, the value should be lower case but it can with upper case. + return headers.Connection.ToLower().Equals("keep-alive"); + } + + internal void EnsureModifiability() + { + // This instance has already started one or more requests. + // Properties can only be modified before sending the first request. + + if (_sentRequest) + { + throw new InvalidOperationException(); + } + } + + static bool MethodHasBody(HttpMethod method) + { + switch (method.Method) + { + case "HEAD": + case "GET": + case "MKCOL": + case "CONNECT": + case "TRACE": + return false; + default: + return true; + } + } + + internal void SetWebRequestTimeout(TimeSpan timeout) + { + _timeout = timeout; + } + + internal void SetWebRequestSslProcol(SslProtocols sslProtocols) + { + _sslProtocols = sslProtocols; + } + + internal void SetWebRequestHttpAuthCert(X509Certificate certificate) + { + _caCert = certificate; + } + + internal void SetWebRequestSslVerification(SslVerification sslVerification) + { + _sslVerification = sslVerification; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs b/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs new file mode 100644 index 0000000..2d1f68a --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http +{ + /// + /// Indicates if operations should be considered completed either as soon as a response is available, or after reading the entire response message including the content. + /// + public enum HttpCompletionOption + { + /// + /// The operation should complete after reading the entire response including the content. + /// + ResponseContentRead = 0, + + /// + /// The operation should complete as soon as a response is available and headers are read. The content is not read yet. + /// + ResponseHeadersRead, + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpContent.cs b/src/Cosmos.Network.Http/Http/HttpContent.cs new file mode 100644 index 0000000..2a074bd --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpContent.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.IO; +using System.Net.Http.Headers; +using System.Text; + +namespace System.Net.Http +{ + /// + /// A base class representing an HTTP entity body and content headers. + /// + public abstract class HttpContent : IDisposable + { + private HttpContentHeaders _headers; + private MemoryStream _buffer; + + private bool _disposed; + + internal static readonly Encoding DefaultStringEncoding = Encoding.UTF8; + + /// + /// Gets the HTTP content headers as defined in RFC 2616. + /// + /// The content headers as defined in RFC 2616. + public HttpContentHeaders Headers + { + get + { + if (_headers == null) + { + _headers = new HttpContentHeaders(this); + } + + return _headers; + } + } + + /// + /// Initializes a new instance of the HttpContent class. + /// + protected HttpContent() + { + } + + /// + /// Serializes the HTTP content into a stream of bytes and copies it to . + /// + /// The target stream. + /// The was . + /// + /// + /// This is the .NET nanoFramework equivalent of HttpContent.CopyTo(Stream, TransportContext, CancellationToken). + /// + /// + public void CopyTo(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(); + } + + if (_buffer != null) + { + _buffer.Position = 0; + _buffer.CopyTo(stream); + return; + } + + SerializeToStream(stream); + } + + /// + /// Serialize the HTTP content to a memory buffer. + /// + /// The Stream with the HTTP content. + /// + /// + /// This operation will block. + /// + /// + /// After content is serialized to a memory buffer, calls to one of the CopyTo methods will copy the content of the memory buffer to the target stream. + /// + /// + /// This is the .NET nanoFramework equivalent of LoadIntoBufferAsync. + /// + /// + /// If the object has been disposed. + public void LoadIntoBuffer() + { + if (_disposed) + { + throw new ObjectDisposedException(); + } + + if (_buffer != null) + { + return; + } + + var buffer = new MemoryStream(); + SerializeToStream(buffer); + _buffer = buffer; + } + + /// + /// Serializes the HTTP content and returns a stream that represents the content. + /// + /// The stream that represents the HTTP content. + /// If the object has been disposed. + public Stream ReadAsStream() + { + return CreateContentReadStream(); + } + + /// + /// Serialize the HTTP content to a byte array as an synchronous operation. + /// + /// A byte array with the HTTP content. + /// + /// + /// This operation will block. + /// + /// + /// This is the .NET nanoFramework equivalent of ReadAsByteArrayAsync. + /// + /// + public byte[] ReadAsByteArray() + { + LoadIntoBuffer(); + + return _buffer.ToArray(); + } + + /// + /// Serialize the HTTP content to a string as an synchronous operation. + /// + /// A string with the HTTP content. + /// + /// + /// This operation will block. + /// + /// + /// This supports only encoding. + /// + /// + /// This is the .NET nanoFramework equivalent of ReadAsStringAsync. + /// + /// + public string ReadAsString() + { + LoadIntoBuffer(); + + if (_buffer.Length == 0) + { + return string.Empty; + } + + // we only support UTF-8 + return Encoding.UTF8.GetString( + _buffer.GetBuffer(), + 0, + (int)_buffer.Length); + } + + /// + /// Determines whether the HTTP content has a valid length in bytes. + /// + /// The length in bytes of the HTTP content. + /// if is a valid length; otherwise, . + protected internal abstract bool TryComputeLength(out long length); + + /// + public void Dispose() + { + Dispose(true); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + if (_buffer != null) + { + _buffer.Dispose(); + } + } + } + + /// + /// When overridden in a derived class, serializes the HTTP content to a stream. Otherwise, throws a . + /// + /// The target stream. + /// + /// The method is not overridden in the derived class. + /// + /// + /// This is the .NET nanoFramework equivalent of SerializeToStream(Stream, TransportContext, CancellationToken). + /// + /// + protected abstract void SerializeToStream(Stream stream); + + /// + /// If overridden, returns the HTTP content as a stream. + /// Otherwise, is called. + /// + /// + /// + /// + /// This is the .NET nanoFramework equivalent of CreateContentReadStreamAsync(). + /// + /// + protected virtual Stream CreateContentReadStream() + { + LoadIntoBuffer(); + + _buffer.Seek(0, SeekOrigin.Begin); + return new ReadOnlyStream(_buffer); + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs b/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs new file mode 100644 index 0000000..55b8ea6 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http +{ + /// + /// A base type for HTTP message handlers. + /// + /// + /// There are various HTTP message handlers that can be used. These include the following. + /// + /// HttpClientHandler - A class that operates at the bottom of the handler chain that actually handles the HTTP transport operations. + /// + /// + /// If developers derive classes from and override the method, they must make sure that can get called concurrently by different threads. + /// + /// + public abstract class HttpMessageHandler : IDisposable + { + /// + /// When overridden in a derived class, sends an HTTP request with the specified request. Otherwise, throws a . + /// + /// The HTTP request message to send. + /// The HTTP response message. + /// + /// This is the .NET nanoFramework equivalent of Send(HttpRequestMessage). + /// + protected internal abstract HttpResponseMessage Send(HttpRequestMessage request); + + #region IDisposable Members + + /// + protected virtual void Dispose(bool disposing) + { + // Nothing to do in base class. + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs b/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs new file mode 100644 index 0000000..877139a --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http +{ + /// + /// A specialty class that allows applications to call the (HttpRequestMessage) method on an HTTP handler chain. + /// + public class HttpMessageInvoker : IDisposable + { + private bool _disposed; + private readonly bool _disposeHandler; + internal readonly HttpMessageHandler _handler; + + /// + /// Initializes an instance of a class with a specific . + /// + /// The responsible for processing the HTTP response messages. + public HttpMessageInvoker(HttpMessageHandler handler) + : this(handler, true) + { + } + + /// + /// Initializes an instance of a class with a specific . + /// + /// The responsible for processing the HTTP response messages. + /// if the inner handler should be disposed of by , if you intend to reuse the inner handler. + /// + public HttpMessageInvoker( + HttpMessageHandler handler, + bool disposeHandler) + { + if (handler is null) + { + throw new ArgumentNullException(); + } + + _handler = handler; + _disposeHandler = disposeHandler; + } + + /// + /// Sends an HTTP request. + /// + /// The HTTP request message to send. + /// The HTTP response message. + /// The was . + public virtual HttpResponseMessage Send(HttpRequestMessage request) + { + if (request is null) + { + throw new ArgumentNullException(); + } + + CheckDisposed(); + + return _handler.Send(request); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + if (_disposeHandler) + { + _handler.Dispose(); + } + } + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(); + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpMethod.cs b/src/Cosmos.Network.Http/Http/HttpMethod.cs new file mode 100644 index 0000000..1a6cc67 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpMethod.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http +{ + /// + /// A helper class for retrieving and comparing standard HTTP methods and for creating new HTTP methods. + /// + /// + /// The most common usage of is to use one of the static properties on this class. However, if an app needs a different value for the HTTP method, the constructor initializes a new instance of the with an HTTP method that the app specifies. + /// + public partial class HttpMethod + { + private readonly string _method; + + private static readonly HttpMethod s_getMethod = new("GET"); + private static readonly HttpMethod s_putMethod = new("PUT"); + private static readonly HttpMethod s_postMethod = new("POST"); + private static readonly HttpMethod s_deleteMethod = new("DELETE"); + private static readonly HttpMethod s_headMethod = new("HEAD"); + private static readonly HttpMethod s_optionsMethod = new("OPTIONS"); + private static readonly HttpMethod s_patchMethod = new("PATCH"); + + /// + /// Represents an HTTP GET protocol method. + /// + public static HttpMethod Get => s_getMethod; + + /// + /// Represents an HTTP PUT protocol method. + /// + public static HttpMethod Put => s_putMethod; + + /// + /// Represents an HTTP POST protocol method. + /// + public static HttpMethod Post => s_postMethod; + + /// + /// Represents an HTTP DELETE protocol method. + /// + public static HttpMethod Delete => s_deleteMethod; + + /// + /// Represents an HTTP HEAD protocol method. + /// + public static HttpMethod Head => s_headMethod; + + /// + /// Represents an HTTP OPTIONS protocol method. + /// + public static HttpMethod Options => s_optionsMethod; + + /// + /// Represents an HTTP PATCH protocol method. + /// + public static HttpMethod Patch => s_patchMethod; + + /// + /// An HTTP method. + /// + /// An HTTP method represented as a . + public string Method => _method; + + /// + /// Initializes a new instance of the HttpMethod class with a specific HTTP method. + /// + /// The HTTP method. + /// + /// + /// If an app needs a different value for the HTTP method from one of the static properties, the HttpMethod constructor initializes a new instance of the HttpMethod with an HTTP method that the app specifies. + /// + public HttpMethod(string method) + { + if (string.IsNullOrEmpty(method)) + { + throw new ArgumentException(); + } + + _method = method; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpRequestException.cs b/src/Cosmos.Network.Http/Http/HttpRequestException.cs new file mode 100644 index 0000000..04a7a9d --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpRequestException.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net.Http +{ + /// + /// A base class for exceptions thrown by the and HttpMessageHandler classes. + /// + [Serializable] + public class HttpRequestException : Exception + { + /// + /// Initializes a new instance of the HttpRequestException class. + /// + public HttpRequestException() + { + } + + /// + /// Initializes a new instance of the HttpRequestException class. + /// + /// A message that describes the current exception. + public HttpRequestException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the HttpRequestException class. + /// + /// A message that describes the current exception. + /// The inner exception. + public HttpRequestException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs b/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs new file mode 100644 index 0000000..12dd663 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Net.Http.Headers; + +namespace System.Net.Http +{ + /// + /// Represents a HTTP request message. + /// + public class HttpRequestMessage : IDisposable + { + internal static Version DefaultRequestVersion => HttpVersion.Version11; + + private const int MessageNotYetSent = 0; + private const int MessageAlreadySent = 1; + private const int MessageIsRedirect = 2; + + // Track whether the message has been sent. + // The message shouldn't be sent again if this field is equal to MessageAlreadySent. + private int _sendStatus = MessageNotYetSent; + + private HttpMethod _method; + private Uri _requestUri; + private HttpRequestHeaders _headers; + private Version _version; + private bool _disposed; + private bool _isUsed; + + /// + /// Gets or sets the HTTP message version. + /// + /// The HTTP message version. The default value is 1.1. + public Version Version + { + get { return _version; } + + set + { + if (value is null) + { + throw new ArgumentNullException(); + } + + CheckDisposed(); + + _version = value; + } + } + + /// + /// Gets or sets the HTTP method used by the HTTP request message. + /// + /// The HTTP method used by the request message. The default is the GET method. + public HttpMethod Method + { + get { return _method; } + + set + { + if (value is null) + { + throw new ArgumentNullException(); + } + + CheckDisposed(); + + _method = value; + } + } + + /// + /// Gets or sets the Uri used for the HTTP request. + /// + /// The used for the HTTP request. + /// + /// If the request Uri is a relative Uri, it will be combined with the . + /// + public Uri RequestUri + { + get { return _requestUri; } + + set + { + CheckDisposed(); + + _requestUri = value; + } + } + + /// + /// Gets the collection of HTTP request headers. + /// + /// The collection of HTTP request headers. + public HttpRequestHeaders Headers => _headers ??= new HttpRequestHeaders(); + + /// + /// Gets or sets the contents of the HTTP message. + /// + public HttpContent Content { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public HttpRequestMessage() + : this(HttpMethod.Get, null) + { + } + + /// + /// Initializes a new instance of the class with an HTTP method and a request Uri. + /// + /// The HTTP method. + /// A string that represents the request . + public HttpRequestMessage( + HttpMethod method, + string requestUri) + { + // It's OK to have a 'null' request Uri. If HttpClient is used, the 'BaseAddress' will be added. + // If there is no 'BaseAddress', sending this request message will throw. + // Note that we also allow the string to be empty: null and empty are considered equivalent. + _method = method; + _requestUri = string.IsNullOrEmpty(requestUri) ? null : new Uri(requestUri, UriKind.RelativeOrAbsolute); + _version = DefaultRequestVersion; + } + + #region IDisposable Members + + /// + protected virtual void Dispose(bool disposing) + { + // The reason for this type to implement IDisposable is that it contains instances of types that implement + // IDisposable (content). + if (disposing && !_disposed) + { + _disposed = true; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(); + } + } + + internal bool SetIsUsed() + { + if (_isUsed) + { + return true; + } + + _isUsed = true; + return false; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs b/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs new file mode 100644 index 0000000..0e3fa40 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs @@ -0,0 +1,201 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Net.Http.Headers; + +namespace System.Net.Http +{ + /// + /// Represents a HTTP response message including the status code and data. + /// + public class HttpResponseMessage : IDisposable + { + private static Version DefaultResponseVersion => HttpVersion.Version11; + + private HttpStatusCode _statusCode; + private HttpResponseHeaders _headers; + private HttpRequestMessage _requestMessage; + private Version _version; + private HttpContent _content; + private bool _disposed; + private IDisposable _webResponse; + + /// + /// Gets or sets the content of a HTTP response message. + /// + /// The content of the HTTP response message. + public HttpContent Content + { + get { return _content; } + + set + { + CheckDisposed(); + + _content = value; + } + } + + /// + /// Gets the collection of HTTP response headers. + /// + /// The collection of HTTP response headers. + public HttpResponseHeaders Headers + { + get + { + return _headers ?? (_headers = new HttpResponseHeaders()); + } + } + + /// + /// Gets a value that indicates if the HTTP response was successful. + /// + /// if was in the range 200-299; otherwise, . + public bool IsSuccessStatusCode + { + get { return ((int)_statusCode >= 200) && ((int)_statusCode <= 299); } + } + + /// + /// Gets or sets the reason phrase which typically is sent by servers together with the status code. + /// + /// The reason phrase sent by the server. + public string ReasonPhrase { get; set; } + + /// + /// Gets or sets the request message which led to this response message. + /// + /// The request message which led to this response message. + /// + /// This property is set to the request message which led to this response message. In the case of a request sent using HttpClient, this property will point to the actual request message leading to the final response. Note that this may not be the same message the user provided when sending the request. This is typically the case if the request needs to be resent due to redirects or authentication. This property can be used to determine what URL actually created the response (useful in case of redirects). + /// + public HttpRequestMessage RequestMessage + { + get { return _requestMessage; } + + set + { + CheckDisposed(); + + _requestMessage = value; + } + } + + /// + /// Gets or sets the status code of the HTTP response. + /// + /// The status code of the HTTP response. + public HttpStatusCode StatusCode => _statusCode; + + /// + /// The underlying this message was created from. + /// + /// + /// Kept alive and disposed with this instance, so it can't be finalized while + /// is still reading from its shared socket. + /// + internal IDisposable WebResponse + { + set { _webResponse = value; } + } + + /// + /// Gets or sets the HTTP message version. + /// + /// The HTTP message version. The default is 1.1. + public Version Version + { + get { return _version; } + + set + { + CheckDisposed(); + + _version = value; + } + } + + /// + /// Initializes a new instance of the HttpResponseMessage class. + /// + public HttpResponseMessage() + : this(HttpStatusCode.OK) + { + } + + /// + /// Initializes a new instance of the HttpResponseMessage class. + /// + /// The status code of the HTTP response. + /// If has an invalid value. + public HttpResponseMessage(HttpStatusCode statusCode) + { + if (((int)statusCode < 0) || ((int)statusCode > 999)) + { + throw new ArgumentOutOfRangeException(); + } + + _statusCode = statusCode; + _version = DefaultResponseVersion; + } + + /// + /// Throws an exception if the property for the HTTP response is . + /// + /// The HTTP response message if the call is successful. + /// + public HttpResponseMessage EnsureSuccessStatusCode() + { + if (!IsSuccessStatusCode) + { + throw new HttpRequestException(); + } + + return this; + } + + #region IDisposable Members + + /// + protected virtual void Dispose(bool disposing) + { + // The reason for this type to implement IDisposable is that it contains instances of types that implement + // IDisposable (content). + if (disposing && !_disposed) + { + _disposed = true; + if (_content != null) + { + _content.Dispose(); + } + + if (_webResponse != null) + { + _webResponse.Dispose(); + _webResponse = null; + } + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(); + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs b/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs new file mode 100644 index 0000000..0fc9187 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs @@ -0,0 +1,130 @@ +using System.IO; + +namespace System.Net.Http +{ + /// + /// A wrapper stream that provides read-only access to an underlying stream. + /// + public class ReadOnlyStream : Stream + { + private readonly Stream _innerStream; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying stream to wrap. + /// Thrown when is null. + public ReadOnlyStream(Stream innerStream) + { + _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); + } + + /// + public override bool CanRead + { + get { return _innerStream.CanRead; } + } + + /// + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } + + /// + public override long Length + { + get { return _innerStream.Length; } + } + + /// + /// This stream is read-only and always returns false. + public override bool CanWrite => false; + + /// + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } + + /// + public override int ReadTimeout + { + get { return _innerStream.ReadTimeout; } + set { _innerStream.ReadTimeout = value; } + } + + /// + public override bool CanTimeout + { + get { return _innerStream.CanTimeout; } + } + + /// + /// Setting or getting this property is not supported for a read-only stream. + /// Always thrown when accessing this property. + public override int WriteTimeout + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerStream.Dispose(); + } + base.Dispose(disposing); + } + + /// + /// Flushing of a read-only stream is not supported. + /// Always thrown when called. + public override void Flush() => throw new NotSupportedException(); + + /// + public override int Read(SpanByte buffer) + { + return _innerStream.Read(buffer); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + return _innerStream.Read(buffer, offset, count); + } + + /// + public override int ReadByte() + { + return _innerStream.ReadByte(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + /// + /// Setting the length of a read-only stream is not supported. + /// Always thrown when called. + public override void SetLength(long value) => + throw new NotSupportedException(); + + /// + /// Writing to a read-only stream is not supported. + /// Always thrown when called. + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + + /// + /// Writing a byte to a read-only stream is not supported. + /// Always thrown when called. + public override void WriteByte(byte value) => + throw new NotSupportedException(); + } +} diff --git a/src/Cosmos.Network.Http/Http/StreamContent.cs b/src/Cosmos.Network.Http/Http/StreamContent.cs new file mode 100644 index 0000000..8b18c1b --- /dev/null +++ b/src/Cosmos.Network.Http/Http/StreamContent.cs @@ -0,0 +1,176 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.IO; +using System.Threading; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content based on a stream. + /// + public class StreamContent : HttpContent + { + private readonly Stream _content; + private readonly int _bufferSize; + private readonly long _startPosition; + private bool _contentConsumed; + + /// + /// Creates a new instance of the class. + /// + /// The content used to initialize the . + /// + /// The object calls on the provided Stream object when is called. + /// + public StreamContent(Stream content) + : this(content, 4 * 1024) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The content used to initialize the . + /// The size, in bytes, of the buffer for the . + /// The was . + /// The was less than or equal to zero. + /// + /// The object calls on the provided Stream object when is called. + /// + public StreamContent( + Stream content, + int bufferSize) + { + _content = content ?? throw new ArgumentNullException(); + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(); + } + + _bufferSize = bufferSize; + + if (content.CanSeek) + { + _startPosition = content.Position; + } + } + + /// + protected override void SerializeToStream(Stream stream) + { + PrepareContent(); + + if (_content is IKnowWhenDone knowWhenDone) + { + // special case for InputNetworkStreamWrapper (which implements IKnowWhenDone), because Read() + // returns 0 when there is no data available (rather than blocking, which would be standard), so + // something like CopyTo would stop reading before the response is finished. + + byte[] buffer = new byte[_bufferSize]; + int read; + int totalRead = 0; + long contentLength = Headers.ContentLength; + + // occurs when there is no Content-Length header (i.e. chunked response) + if (contentLength < 0) + { + if (TryComputeLength(out long possibleLength)) + { + contentLength = possibleLength; + } + else + { + contentLength = int.MaxValue; + } + } + + bool isDone = false; + while ((totalRead < contentLength) && !isDone) + { + read = _content.Read(buffer, 0, _bufferSize); + isDone = knowWhenDone.IsDone; + + if (read == 0 && !isDone) + { + // need to let the native layer get more data + Thread.Sleep(10); + } + else if (read > 0) + { + totalRead += read; + stream.Write(buffer, 0, read); + } + } + } + else + { + _content.CopyTo(stream); + } + } + + /// + protected override Stream CreateContentReadStream() + { + if (_content is IKnowWhenDone) + { + // Special case for InputNetworkStreamWrapper: + // Call the base method which buffers the entire response. + // Due to the way InputNetworkStreamWrapper works, we can't really return the stream directly + // (see comment in SerializeToStream) + return base.CreateContentReadStream(); + } + else + { + PrepareContent(); + return new ReadOnlyStream(_content); + } + } + + private void PrepareContent() + { + if (_contentConsumed) + { + if (!_content.CanSeek) + { + throw new InvalidOperationException(); + } + + _content.Seek(_startPosition, SeekOrigin.Begin); + } + else + { + _contentConsumed = true; + } + } + + /// + protected internal override bool TryComputeLength(out long length) + { + if (!_content.CanSeek) + { + length = 0; + return false; + } + + length = _content.Length - _startPosition; + + return true; + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _content.Dispose(); + } + + base.Dispose(disposing); + } + } +} diff --git a/src/Cosmos.Network.Http/Http/StringContent.cs b/src/Cosmos.Network.Http/Http/StringContent.cs new file mode 100644 index 0000000..dce12bb --- /dev/null +++ b/src/Cosmos.Network.Http/Http/StringContent.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Net.Http.Http.Headers; +using System.Text; + +namespace System.Net.Http +{ + /// + /// Provides HTTP content based on a string. + /// + public class StringContent : ByteArrayContent + { + private const string DefaultMediaType = "text/plain"; + private const string EncodingUTF8WebName = "utf-8"; + + /// + /// Creates a new instance of the class. + /// + /// The content used to initialize the . + /// + /// The media type for the created defaults to text/plain. + /// + public StringContent(string content) + : this( + content, + null, + null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The content used to initialize the . + /// The encoding to use for the content. + /// + /// The media type for the created defaults to text/plain. + /// + public StringContent( + string content, + Encoding encoding) + : this( + content, + encoding, + null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The content used to initialize the . + /// The encoding to use for the content. + /// The media type to use for the content. + public StringContent( + string content, + Encoding encoding, + string mediaType) + : base(GetContentByteArray( + content, + encoding)) + { + Headers.ContentType = new MediaTypeHeaderValue(mediaType ?? DefaultMediaType) + { + CharSet = EncodingUTF8WebName + }; + } + + // A StringContent is essentially a ByteArrayContent. We serialize the string into a byte-array in the + // constructor using encoding information provided by the caller (if any). When this content is sent, the + // Content-Length can be retrieved easily (length of the array). + private static byte[] GetContentByteArray( + string content, + Encoding encoding) + { + if (content is null) + { + throw new ArgumentNullException(); + } + + // In this case we treat 'null' strings different from string.Empty in order to be consistent with our + // other *Content constructors: 'null' throws, empty values are allowed. + // we only support UTF8 encoding + encoding ??= Encoding.UTF8; + + return encoding.GetBytes(content); + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs b/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs new file mode 100644 index 0000000..1bcebbd --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Network authentication type. + /// Currently supports: + /// Basic Authentication + /// Microsoft Live Id Delegate Authentication + /// + public enum AuthenticationType + { + /// + /// + /// + Basic, + /// + /// + /// + WindowsLive + }; +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs b/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs new file mode 100644 index 0000000..c63926c --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + internal class HttpConstants + { + /// + /// Default time (ms) to keep a persistent connection open + /// + internal const int DefaultKeepAliveMilliseconds = 600000; + + /// + /// maximum length of the line in reponse line + /// + internal const int maxHTTPLineLength = 4000; + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs new file mode 100644 index 0000000..ffca5d9 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs @@ -0,0 +1,703 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System.Collections; + using System.Diagnostics; + using System.Net.Security; + using System.Net.Sockets; + using System.Security.Cryptography.X509Certificates; + using System.Threading; + + /// + /// Provides a simple, programmatically controlled HTTP protocol listener. + /// This class cannot be inherited. + /// + /// + /// This class enables using a socket to receive data that uses the HTTP + /// protocol. + /// + public class HttpListener + { + private readonly object lockObj; + + /// + /// Indicates whether the listener is waiting on an http or https + /// connection. + /// + bool m_IsHttpsConnection; + + /// + /// The certificate to send during https authentication. + /// + X509Certificate m_httpsCert; + + /// + /// This value is the number of connections that can be ready but are + /// not retrieved through the Accept call. + /// + /// + /// This value is passed to the Listen method of the socket. + /// + private const int MaxCountOfPendingConnections = 10; + + /// + /// Server socket for incoming connections. + /// + private Socket m_listener; + + /// + /// The MAXIMUM length, in kilobytes (1024 bytes), of the request + /// headers. + /// + internal int m_maxResponseHeadersLen; + + /// + /// Event that indicates arrival of new event from client. + /// + private AutoResetEvent m_RequestArrived; + + /// + /// The queue of connected networks streams with pending client data. + /// + Queue m_InputStreamsQueue; + + /// + /// Port number for the server socket. + /// + private int m_Port; + + /// + /// the local endpoint to bind the socket to. if Null the default is used + /// + private IPAddress m_localEndpointIP; + + /// + /// Indicates whether the listener is started and is currently accepting + /// connections. + /// + private bool m_ServiceRunning; + + /// + /// Indicates whether the listener has been closed + /// + private bool m_Closed; + + /// + /// Array of connected client sockets. + /// + private ArrayList m_ClientStreams; + + /// + /// Http Thread for accepting new connections. + /// + private Thread m_thAccept; + + /// + /// SslProtocol which shall be used. + /// + private SslProtocols m_sslProtocols = SslProtocols.None; + +#pragma warning disable S2292 // Trivial properties should be auto-implemented + /// + /// Gets or sets the TLS/SSL protocol used by the class. + /// + /// + /// One of the values defined in the enumeration. + /// + /// + /// This property is specific to nanoFramework. There is no equivalent in the .NET API. + /// + public SslProtocols SslProtocols +#pragma warning restore S2292 // Trivial properties should be auto-implemented + // nanoFramework doesn't support auto-properties + { + get { return m_sslProtocols; } + set { m_sslProtocols = value; } + } + + /// + /// Creates an HTTP or HTTPS listener on the standard ports. + /// + /// Prefix ( http or https ) to start listen + /// In the desktop version of .NET, the constructor for this + /// class has no arguments. + public HttpListener(string prefix) + { + lockObj = new object(); + + InitListener(prefix, -1); + } + + /// + /// Creates an HTTP or HTTPS listener on the specified port. + /// + /// The prefix for the service, either "http" or + /// "https". + /// The port to start listening on. If -1, the + /// default port is used (port 80 for http, or port 443 for https). + /// + /// The local endpoint to bind the socket to. If Null the default is used + /// + /// In the desktop version of .NET, the constructor for this + /// class has no arguments. + public HttpListener(string prefix, int port, IPAddress localEndpointIP = null) + { + lockObj = new object(); + + InitListener(prefix, port, localEndpointIP); + } + + /// + /// Initializes the listener. + /// + /// The prefix for the service, either "http" or + /// "https". + /// The port to start listening on. If -1, the + /// default port is used (port 80 for http, or port 443 for https). + /// + private void InitListener(string prefix, int port, IPAddress localEndpointIp = null) + { + switch (prefix.ToLower()) + { + case Uri.UriSchemeHttp: + case Uri.UriSchemeWs: + { + m_IsHttpsConnection = false; + m_Port = Uri.HttpDefaultPort; + break; + } + case Uri.UriSchemeHttps: + case Uri.UriSchemeWss: + { + m_IsHttpsConnection = true; + m_Port = Uri.HttpsDefaultPort; + break; + } + default: throw new ArgumentException("Prefix should be http or https"); + } + + if (port != -1) + { + m_Port = port; + } + + if (localEndpointIp != null) + { + m_localEndpointIP = localEndpointIp; + } + // Default members initialization + m_maxResponseHeadersLen = 4; + m_RequestArrived = new AutoResetEvent(false); + m_InputStreamsQueue = new Queue(); + m_ClientStreams = new ArrayList(); + } + + /// + /// Adds a new output stream to the list of connected streams. + /// + /// This is an internal function, not visible to the user. + /// + /// The stream to add. + internal void AddClientStream(OutputNetworkStreamWrapper clientStream) + { + lock (m_ClientStreams) + { + m_ClientStreams.Add(clientStream); + } + } + + /// + /// Removes the specified output stream from the list of connected + /// streams. + /// + /// The stream to remove. + internal void RemoveClientStream(OutputNetworkStreamWrapper clientStream) + { + lock (m_ClientStreams) + { + for (int i = 0; i < m_ClientStreams.Count; i++) + { + if (clientStream == m_ClientStreams[i]) + { + m_ClientStreams.RemoveAt(i); + break; + } + } + } + } + + /// + /// Packages together an HttpListener and a socket. + /// + /// This class is used to package together an HttpListener and a socket. + /// We need to start new thread and pass 2 parameters - instance of listener and socket. + /// For that purpose we create class that keeps references to both listerner and socket and + /// start thread using member function of this class as delegate. + /// Internal class not visible to user. + private class HttpListernerAndStream + { + internal HttpListernerAndStream(HttpListener listener, OutputNetworkStreamWrapper outputStream) + { + m_listener = listener; + m_outStream = outputStream; + } + + internal HttpListener m_listener; + internal OutputNetworkStreamWrapper m_outStream; + + // Forwards to waiting function of the listener. + internal void AddToWaitingConnections() + { + m_listener.WaitingConnectionThreadFunc(m_outStream); + } + } + + internal void AddToWaitingConnections(OutputNetworkStreamWrapper outputStream) + { + // Create a thread that blocks onsocket.Poll - basically waits for new data from client. + HttpListernerAndStream listAndSock = new HttpListernerAndStream(this, outputStream); + + // Creates new thread to wait on data + Thread thWaitData = new Thread(listAndSock.AddToWaitingConnections); + thWaitData.Start(); + } + + /// + /// Waits for new data from the client. + /// + private void WaitingConnectionThreadFunc(OutputNetworkStreamWrapper outputStream) + { + try + { + // This is a blocking call waiting for more data. + outputStream.m_Socket.Poll(HttpConstants.DefaultKeepAliveMilliseconds * 1000, SelectMode.SelectRead); + } + catch (Exception ex) + { + // Poll failed (e.g. connection reset) - outputStream isn't queued anywhere else, so it + // must be disposed here or its socket leaks. + Debug.WriteLine(ex.Message); + outputStream.Dispose(); + return; + } + + if (outputStream.m_Socket.Available > 0) + { + + // Add this connected stream to the list. + lock (m_InputStreamsQueue) + { + m_InputStreamsQueue.Enqueue(outputStream); + } + + // Set event that client stream or exception is added to the queue. + m_RequestArrived.Set(); + } + else // If no data available - means connection was close on other side or timed out. + { + outputStream.Dispose(); + } + } + + /// + /// Shuts down the HttpListener object immediately, + /// discarding all currently queued requests. + /// + /// This method disposes of all resources held by this + /// listener. Any pending requests are unable to complete. To shut + /// down the HttpListener object after processing + /// currently queued requests, use the + /// method. + /// + /// After calling this method, you will receive an + /// if you attempt to use this + /// HttpListener. + /// + /// + public void Abort() + { + lock (lockObj) + { + // First we shut down the service. + Close(); + + // Now we need to go through list of all client sockets and close all of them. + // This will cause exceptions on read/write operations on these sockets. + foreach (OutputNetworkStreamWrapper netStream in m_ClientStreams) + { + netStream.Close(); + } + m_ClientStreams.Clear(); + } + + if (m_thAccept != null) + { + m_thAccept.Join(); + } + } + + /// + /// Waits for new connections from the client. + /// + /// On new connections, this method enques the new input + /// network stream and sets an event that a new connection is available. + /// + private void AcceptThreadFunc() + { + Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; + // If there was no exception up to this point, means we succeded to start listening. + m_ServiceRunning = true; + int retry = 0; + + // The Start function is waiting on this event. We set it to indicate that + // thread that waits for connections is already started. + m_RequestArrived.Set(); + + // The value of m_serviceStarted normally is changed from other thread by calling Stop. + while (m_ServiceRunning) + { + Socket clientSock; + // Need to create NetworkStream or SSL stream depending on protocol used. + NetworkStream netStream = null; + + try + { + // It is important that multithread access to m_listener.Accept(); is not locked. + // If it was locked - then Close or Stop would be blocked potnetially for ever while waiting for connection. + // This is a blocking call waiting for connection. + clientSock = m_listener.Accept(); + + retry = 0; + try + { + // set NoDelay to increase HTTP(s) response times + clientSock.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + } + catch + { + // empty on purpose + } + } + catch (SocketException) + { + if (++retry > 5) + { + // If request to stop listener flag is set or locking call is interupted return + // On exception we stop the service and record the exception. + if (m_ServiceRunning && !m_Closed) + { + Stop(); + } + + // Set event to unblock thread waiting for accept. + m_RequestArrived.Set(); + + break; + } + + continue; + } + catch + { + // If request to stop listener flag is set or locking call is interupted return + // On exception we stop the service and record the exception. + if (m_ServiceRunning && !m_Closed) + { + Stop(); + } + + // Set event to unblock thread waiting for accept. + m_RequestArrived.Set(); + + break; + } + + try + { + if (!m_IsHttpsConnection) + { + // This is case of normal HTTP. Create network stream. + netStream = new NetworkStream(clientSock, true); + } + else + { + // This is the case of https. + // Once connection established need to create secure stream and authenticate server. + netStream = new SslStream(clientSock); + + // Throws exception if this fails + // pass the server certificate + // do not require client certificate + ((SslStream)netStream).AuthenticateAsServer(m_httpsCert, false, m_sslProtocols); + + netStream.ReadTimeout = 10000; + } + } + catch + { + if (netStream != null) + { + netStream.Dispose(); + } + else + { + clientSock.Close(); + } + + m_RequestArrived.Set(); + + // try again + continue; + } + + // Add this connected stream to the list. + lock (m_InputStreamsQueue) + { + m_InputStreamsQueue.Enqueue(new OutputNetworkStreamWrapper(clientSock, netStream)); + } + + // Set event that client stream or exception is added to the queue. + m_RequestArrived.Set(); + } + } + + /// + /// Allows this instance to receive incoming requests. + /// + /// This method must be called before you call the + /// method. If + /// the service was already started, the call has no effect. After you + /// have started an HttpListener object, you can use + /// the method to stop it. + /// + public void Start() + { + lock (lockObj) + { + if (m_Closed) throw new ObjectDisposedException(); + + // If service was already started, the call has no effect. + if (m_ServiceRunning) + { + return; + } + + m_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + // set NoDelay to increase HTTP(s) response times + m_listener.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + } + catch + { + // empty on purpose + } + + try + { + // Start server socket to accept incoming connections. + m_listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + } + catch + { + // empty on purpose + } + + IPAddress addr = m_localEndpointIP ?? IPAddress.GetDefaultLocalAddress(); + + IPEndPoint endPoint = new IPEndPoint(addr, m_Port); + m_listener.Bind(endPoint); + + // Starts to listen to maximum of 10 connections. + m_listener.Listen(MaxCountOfPendingConnections); + + // Create a thread that blocks on m_listener.Accept() - basically waits for connection from client. + m_thAccept = new Thread(AcceptThreadFunc); + m_thAccept.Start(); + + // Waits for thread that calls Accept to start. + m_RequestArrived.WaitOne(); + } + } + + /// + /// Shuts down the HttpListener after processing all + /// currently queued requests. + /// + /// After calling this method, you can no longer use the + /// HttpListener object. To temporarily pause an + /// HttpListener object, use the + /// method. + public void Close() + { + lock (lockObj) + { + // close does not throw + try + { + Stop(); + } + catch + { + // empty on purpose to catch any exceptions thrown when calling the Stop above + } + + m_Closed = true; + } + } + + /// + /// Causes this instance to stop receiving incoming requests. + /// + /// If this instance is already stopped, calling this method + /// has no effect. + /// + /// After you have stopped an HttpListener object, + /// you can use the method + /// to restart it. + /// + /// + public void Stop() + { + // Need to lock access to object, because Stop can be called from a + // different thread. + lock (lockObj) + { + if (m_Closed) throw new ObjectDisposedException(); + + m_ServiceRunning = false; + + // We close the server socket that listen for incoming connection. + // Connections that already accepted are processed. + // Connections that has been in queue for server socket, but not accepted, are lost. + if (m_listener != null) + { + m_listener.Close(); + m_listener = null; + + m_RequestArrived.Set(); + } + } + } + + /// + /// Waits for an incoming request and returns when one is received. + /// + /// + /// An object that + /// represents a client request. + /// + /// A socket call failed. Check the + /// exception's ErrorCode property to determine the cause of the exception. + /// This object has not been started or is + /// currently stopped or The HttpListener does not have any Uniform Resource Identifier + /// (URI) prefixes to respond to. + /// This object is closed. + /// This example shows how to call the + /// GetContext method. + /// + /// HttpListener myListener = new HttpListener("http", -1); + /// myListener.Start(); + /// while (true) + /// { + /// HttpListenerResponse response = null; + /// try + /// { + /// Debug.Print("Waiting for requests"); + /// HttpListenerContext context = myListener.GetContext(); + /// + /// + public HttpListenerContext GetContext() + { + // Protects access for simultaneous call for GetContext and Close or Stop. + lock (lockObj) + { + if (m_Closed) throw new ObjectDisposedException(); + + if (!m_ServiceRunning) throw new InvalidOperationException(); + } + + // Try to get context until service is running. + while (m_ServiceRunning) + { + // Before waiting for event we need to look for pending connections. + lock (m_InputStreamsQueue) + { + if (m_InputStreamsQueue.Count > 0) + { + OutputNetworkStreamWrapper outputStreamWrap = m_InputStreamsQueue.Dequeue() as OutputNetworkStreamWrapper; + if (outputStreamWrap != null) + { + return new HttpListenerContext(outputStreamWrap, this); + } + } + } + + // Waits for new connection to arrive on new or existing socket. + m_RequestArrived.WaitOne(); + } + + return null; + } + + /// + /// Gets whether the HttpListener service was started + /// and is waiting for client connections. + /// + /// true if the + /// HttpListener was started; otherwise, + /// false. + public bool IsListening + { + get { return m_ServiceRunning; } + } + + /// + /// Gets or sets the maximum allowed length of the response headers, in + /// KB. + /// + /// The length, in kilobytes (1024 bytes), of the response + /// headers. + /// + /// The length of the response header includes the response status line + /// and any extra control characters that are received as part of the + /// HTTP protocol. A value of -1 means no limit is imposed on the + /// response headers; a value of 0 means that all requests fail. If + /// this property is not explicitly set, it defaults to 4 (KB). + /// + public int MaximumResponseHeadersLength + { + get { return m_maxResponseHeadersLen; } + set + { + if (value <= 0 && value != -1) + { +#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one + throw new ArgumentOutOfRangeException(); +#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one + // can't add description as that would increase the deployment image size + } + + m_maxResponseHeadersLen = value; + } + } + +#pragma warning disable S2292 // Trivial properties should be auto-implemented + /// + /// The certificate used if implements an https server. + /// + public X509Certificate HttpsCert +#pragma warning restore S2292 // Trivial properties should be auto-implemented + // nanoFramework doesn't support auto-properties + { + get { return m_httpsCert; } + set { m_httpsCert = value; } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs new file mode 100644 index 0000000..925a26e --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs @@ -0,0 +1,203 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System.Net.Sockets; + + /// + /// Provides access to the request and response objects used by the + /// HttpListener class. This class cannot be inherited. + /// + public class HttpListenerContext + { + /// + /// A flag that indicates whether an HTTP request was parsed. + /// + /// + /// The HTTP request is parsed upon the first access to the Request to + /// Response property. Access to that property might be done from a + /// different thread than the thread that is used for construction of + /// the HttpListenerContext. + /// + bool m_IsHTTPRequestParsed; + + /// + /// Member with network stream connected to client. + /// This stream is used for writing data. + /// This stream owns the socket. + /// + internal OutputNetworkStreamWrapper m_clientOutputStream; + + /// + /// Member with network stream connected to client. + /// This stream is used for Reading data. + /// This stream does not own the socket. + /// + internal InputNetworkStreamWrapper m_clientInputStream; + + /// + /// Instance of the request from client. + /// it is a server side representation of HttpWebRequest. + /// It is the same data, but instead of composing request we parse it. + /// + private HttpListenerRequest m_ClientRequest; + + /// + /// Instance of the response to client. + /// + /// + internal HttpListenerResponse m_ResponseToClient; + + /// + /// Internal constructor, used each time client connects. + /// + /// The stream that is connected to the client. A stream is needed, to + /// provide information about the connected client. + /// See also the class. + /// + /// TBD + internal HttpListenerContext(OutputNetworkStreamWrapper clientStream, HttpListener httpListener) + { + // Saves the stream. + m_clientOutputStream = clientStream; + + // Input stream does not own socket. + m_clientInputStream = new InputNetworkStreamWrapper(clientStream.m_Stream, clientStream.m_Socket, false, null); + + // Constructs request and response classes. + m_ClientRequest = new HttpListenerRequest(m_clientInputStream, httpListener.m_maxResponseHeadersLen); + + // Closing reponse to client causes removal from clientSocketsList. + // Thus we need to pass clientSocketsList to client response. + m_ResponseToClient = new HttpListenerResponse(m_clientOutputStream, httpListener); + + // There is incoming connection HTTP connection. Add new Socket to the list of connected sockets + // The socket is removed from this array after correponding HttpListenerResponse is closed. + httpListener.AddClientStream(m_clientOutputStream); + + // Set flag that HTTP request was not parsed yet. + // It will be parsed on first access to m_ClientRequest or m_ResponseToClient + m_IsHTTPRequestParsed = false; + } + + public void Reset() + { + m_IsHTTPRequestParsed = false; + m_ClientRequest.Reset(); + } + + /// + /// Gets the HttpListenerRequest that represents a + /// client's request for a resource. + /// + /// An HttpListenerRequest object that + /// represents the client request. + public HttpListenerRequest Request + { + get + { + if (!m_IsHTTPRequestParsed) + { + m_ClientRequest.ParseHTTPRequest(); + // After request parsed check for "transfer-ecoding" header. If it is chunked, change stream property. + // If m_EnableChunkedDecoding is set to true, then readig from stream automatically processing chunks. + string chunkedVal = m_ClientRequest.Headers[HttpKnownHeaderNames.TransferEncoding]; + if (chunkedVal != null && chunkedVal.ToLower() == "chunked") + { + m_clientInputStream.m_EnableChunkedDecoding = true; + } + + m_IsHTTPRequestParsed = true; + } + + return m_ClientRequest; + } + } + + /// + /// Gets the HttpListenerResponse object that will be + /// sent to the client in response to the client's request. + /// + /// An HttpListenerResponse object used to + /// send a response back to the client. + public HttpListenerResponse Response + { + get + { + if (!m_IsHTTPRequestParsed) + { + m_ClientRequest.ParseHTTPRequest(); + m_IsHTTPRequestParsed = true; + } + + return m_ResponseToClient; + } + } + + /// + /// Get WebsocketContext for WebsocketServer. + /// This will release all bindings and resources from the HttpListner rendering HttpListnerContext unusable. + /// + internal WebSocketContext GetWebsocketContext() + { + var webSocketContext = new WebSocketContext(m_clientOutputStream.m_Socket, m_clientOutputStream.m_Stream, Request.Headers); + + m_ResponseToClient.m_Listener.RemoveClientStream(m_ResponseToClient.m_clientStream); + + m_ResponseToClient = null; + m_clientOutputStream = null; + m_clientInputStream = null; + + + + return webSocketContext; + } + + /// + /// Closes the stream attached to this listener context. + /// + public void Close() + { + try + { + if (m_clientOutputStream != null) + { + try + { + if (m_clientOutputStream.m_Socket != null) + { + m_clientOutputStream.m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); + } + } + catch { } + } + + if (m_ResponseToClient != null) + { + m_ResponseToClient.Close(); + m_ResponseToClient = null; + } + + // Close the underlying stream + if (m_clientOutputStream != null) + { + m_clientOutputStream.Dispose(); + m_clientOutputStream = null; + } + + if (m_clientInputStream != null) + { + m_clientInputStream.Dispose(); + m_clientInputStream = null; + } + } + catch + { + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs new file mode 100644 index 0000000..54d9c6d --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs @@ -0,0 +1,454 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + using System.IO; + using System.Net.Sockets; + + /// + /// Describes an incoming HTTP request to an + /// object. + /// + /// + /// When a client makes a request to a Uniform Resource Identifier (URI) + /// handled by an HttpListener object, the + /// HttpListener provides an + /// object that contains + /// information about the sender, the request, and the response that is sent + /// to the client. The + /// property returns the HttpListenerRequest object that + /// describes the request. + /// + /// The HttpListenerRequest object contains information + /// about the request, such as the request + /// string, + /// string, and + /// request body data (see the + /// + /// property). + /// + /// To reply to the request, you must get the associated response using the + /// property. + /// + public class HttpListenerRequest + { + /// + /// The original string with the request. For example, + /// "GET /Index.htm HTTP/1.1". + /// + private string m_RequestString; + + /// + /// The maximum length of the request headers, in KB (1024 bytes). + /// + private int m_maxResponseHeadersLen; + + /// + /// The verb of the request parsed from m_RequestString. + /// + private string m_requestVerb; + + /// + /// URL of request parsed from m_RequestString. + /// + private string m_rawURL; + + /// + /// HTTP version from m_RequestString. + /// + private Version m_requestHttpVer; + + /// + /// Indicates whether the client requests a persistent connection. + /// If client did not specify "Connection" header - default is false. + /// + private bool m_KeepAlive = false; + + /// + /// The request Headers From HTTP client. + /// + private WebHeaderCollection m_httpRequestHeaders = new WebHeaderCollection(true); + + /// + /// Member with network stream connected to client. + /// + /// + private InputNetworkStreamWrapper m_clientStream; + + /// + /// The length of the content in the body of the request, if a body is + /// present. + /// + long m_contentLength; + + /// + /// Keep NetworkCredential if user have send user name and password. + /// + private NetworkCredential m_NetworkCredentials; + + /// + /// Constructs a HttpListenerRequest is created by HttpListenerContext. + /// + /// Network stream to the client. + /// TBD + internal HttpListenerRequest(InputNetworkStreamWrapper clientStream, int maxHeaderLen) + { + m_clientStream = clientStream; + + // maxHeaderLen is in kilobytes (Desktop designer decided so). If -1 just maximum integer value + m_maxResponseHeadersLen = maxHeaderLen == -1 ? 0x7FFFFFFF : maxHeaderLen * 1024; + // If not set, default for content length is -1 + m_contentLength = -1; + } + + public void Reset() + { + m_httpRequestHeaders = new WebHeaderCollection(true); + m_contentLength = -1; + } + + /// + /// Parses request from client. + /// Fills + /// - HTTP Verb. + /// - HTTP version. + /// - Content Length. + /// - Fills generic value name pair in WEB header collection. + /// + internal void ParseHTTPRequest() + { + try + { + // This is the request line. + m_RequestString = m_clientStream.Read_HTTP_Line(HttpConstants.maxHTTPLineLength).Trim(); + } + catch (SocketException) + { + //This is for handling with transitory or bad requests and the server continues to function properly + return; + } + catch + { + throw; + } + + + // Split request line into 3 strings - VERB, URL and HTTP version. + char[] delimiter = { ' ' }; + string[] requestStr = m_RequestString.Split(delimiter); + // requestStr should consist of 3 parts. + if (requestStr.Length < 3) + { + throw new ProtocolViolationException("Invalid HTTP request String: " + m_RequestString); + } + + // We have at least 3 strings. Fills the proper fields + m_requestVerb = requestStr[0]; + m_rawURL = requestStr[1]; + + // Process third string. It should be either http/1.1 or http/1.0 + string httpVerLowerCase = requestStr[2].ToLower(); + if (httpVerLowerCase.Equals("http/1.1")) + { + m_requestHttpVer = HttpVersion.Version11; + } + else if (httpVerLowerCase.Equals("http/1.0")) + { + m_requestHttpVer = HttpVersion.Version10; + } + else + { + throw new ProtocolViolationException("Unsupported HTTP version: " + requestStr[2]); + } + + // Now it is list of HTTP headers: + string line; + int headersLen = m_maxResponseHeadersLen; + while ((line = m_clientStream.Read_HTTP_Header(HttpConstants.maxHTTPLineLength)).Length > 0) + { + // line.Length is used for the header. Substruct it. + headersLen -= line.Length; + // If total length used for header is exceeded, we break + + if (headersLen < 0) + { + throw new ProtocolViolationException("Http Headers exceeding: " + m_maxResponseHeadersLen); + } + + int sepIdx = line.IndexOf(':'); + if (sepIdx == -1) + { + throw new ProtocolViolationException("Invalid HTTP Header: " + line); + } + + string headerName = line.Substring(0, sepIdx).Trim(); + string headerValue = line.Substring(sepIdx + 1).Trim(); + string matchableHeaderName = headerName.ToLower(); + // Adds new header to collection. + m_httpRequestHeaders.AddInternal(headerName, headerValue); + + // Now we check the value - name pair. For some of them we need to initilize member variables. + headerName = headerName.ToLower(); + // If it is connection header + if (headerName == "connection") + { + // If value is "Keep-Alive" ( lower case now ), set m_KeepAlive to true; + headerValue = headerValue.ToLower(); + m_KeepAlive = headerValue == "keep-alive"; + } + + // If user supplied user name and password - parse it and store in m_NetworkCredentials + if (headerName == "authorization") + { + int sepSpace = headerValue.IndexOf(' '); + // Authorization header value must contain an auth scheme followed by a space and its parameter(s), e.g. "Basic xxx" or "Bearer xxx". If not, ignore. + if (sepSpace > 0) + { + string authType = headerValue.Substring(0, sepSpace); + if (authType.ToLower() == "basic") + { + string authInfo = headerValue.Substring(sepSpace + 1); + // authInfo is base64 encoded username and password. + byte[] authInfoDecoded = Convert.FromBase64String(authInfo); + char[] authInfoDecChar = System.Text.Encoding.UTF8.GetChars(authInfoDecoded); + string strAuthInfo = new string(authInfoDecChar); + // The strAuthInfo comes in format username:password. Parse it. + int sepColon = strAuthInfo.IndexOf(':'); + if (sepColon != -1) + { + m_NetworkCredentials = new NetworkCredential(strAuthInfo.Substring(0, sepColon), strAuthInfo.Substring(sepColon + 1)); + } + } + } + + } + } + + // Http headers were processed. Now we search for content length. + string strContentLen = m_httpRequestHeaders[HttpKnownHeaderNames.ContentLength]; + if (strContentLen != null) + { + try + { + m_contentLength = Convert.ToInt32(strContentLen); + } + catch (Exception) + { + throw new ProtocolViolationException("Invalid content length in request: " + strContentLen); + } + } + } + + /// + /// Gets the HTTP method specified by the client. + /// + /// A String that contains the method used in + /// the request. + public string HttpMethod + { + get { return m_requestVerb; } + } + + /// + /// Gets the URL information (without the host and port) requested by + /// the client. + /// + /// A String that contains the raw URL for + /// this request. + /// + /// This URL information is the URL requested in the first request line. + /// + public string RawUrl + { + get { return m_rawURL; } + } + + /// + /// Gets the MIME types accepted by the client. + /// + /// A String array that contains the type + /// names specified in the request's Accept header, or a null reference + /// if the client request did not include an Accept header. + public string[] AcceptTypes + { + get { return m_httpRequestHeaders.GetValues(HttpKnownHeaderNames.Accept); } + } + + /// + /// Gets the length of the body data included in the request. + /// + /// + /// The Content-Length header expresses the length, in bytes, of the + /// body data that accompanies the request. + /// enumeration. + /// + /// The value from the request's Content-Length header. This + /// value is -1 if the content length is not known. + public long ContentLength64 + { + get { return m_contentLength; } + } + + /// + /// Gets the MIME type of the body data included in the request. + /// + /// A String that contains the text of the + /// request's Content-Type header. + public string ContentType + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.ContentType]; } + } + + /// + /// Gets the collection of header name/value pairs sent in the request. + /// + /// A WebHeaderCollection that contains the + /// HTTP headers included in the request. + public WebHeaderCollection Headers + { + get { return m_httpRequestHeaders; } + } + + /// + /// Gets a stream that contains the body data sent by the client. + /// + /// A readable Stream object that contains the + /// bytes sent by the client in the body of the request. This property + /// returns Null if no data is sent with the request. + /// + public Stream InputStream + { + get { return m_clientStream; } + } + + /// + /// Gets a Boolean value that indicates whether the client sending this + /// request is authenticated. + /// + /// + /// Because authentication is not supported, returns + /// false. + /// + /// Because authentication is not supported, returns + /// false. + public bool IsAuthenticated + { + get { return false; } + } + + /// + /// Gets a value that indicates whether the + /// client requests a persistent connection. + /// + /// + /// This property is set during parsing of HTTP header. + /// + /// true if the connection should be kept + /// open; otherwise, false. + public bool KeepAlive + { + get { return m_KeepAlive; } + } + + /// + /// Gets the server IP address and port number to which the request is + /// directed. Not currently supported. + /// + /// An IPEndPoint that represents the IP + /// address that the request is sent to. + /// + public IPEndPoint LocalEndPoint + { + get { return (IPEndPoint)m_clientStream.m_Socket.LocalEndPoint; } + } + + /// + /// Gets the HTTP version used by the requesting client. + /// + /// + /// The capabilities of different HTTP versions are specified in the + /// documents available at http://www.rfc-editor.org. + /// + /// A Version that identifies the client's + /// version of HTTP. + public Version ProtocolVersion + { + get { return m_requestHttpVer; } + } + + /// + /// Gets the client IP address and port number from which the request + /// originated. + /// + /// An IPEndPoint that represents the IP + /// address and port number from which the request originated. + public IPEndPoint RemoteEndPoint + { + get { return (IPEndPoint)m_clientStream.m_Socket.RemoteEndPoint; } + } + + /// + /// Gets the Uri object requested by the client. Not currently + /// supported. + /// + public Uri Url { get { return new Uri(m_rawURL, UriKind.Relative); } } + + /// + /// Gets the user agent presented by the client. + /// + /// A String object that contains the text of + /// the request's User-Agent header. + /// + /// + public string UserAgent + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.UserAgent]; } + } + + /// + /// Gets the server IP address and port number to which the request is + /// directed. + /// + /// A String that contains the host address + /// information. + public string UserHostAddress + { + get { return ((IPEndPoint)m_clientStream.m_Socket.LocalEndPoint).Address.ToString(); } + } + + /// + /// Gets the DNS name and, if provided, the port number specified by the + /// client. + /// + /// A String value that contains the text of the request's Host header. + public string UserHostName + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.UserAgent]; } + } + + /// + /// Return NetworkCredential if user have send user name and password. + /// + public NetworkCredential Credentials + { + get { return m_NetworkCredentials; } + } + + /// + /// Gets the natural languages that are preferred for the response. + /// + /// A String array that contains the languages + /// specified in the request's AcceptLanguage header, + /// or null if the client request did not include an + /// AcceptLanguage header. + public string[] UserLanguages + { + get { return m_httpRequestHeaders.GetValues(HttpKnownHeaderNames.AcceptLanguage); } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs new file mode 100644 index 0000000..3856ee2 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs @@ -0,0 +1,572 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + using System.IO; + using System.Text; + + /// + /// Represents a response to a request being handled by an + /// object. + /// + public sealed class HttpListenerResponse : IDisposable + { + /// + /// A flag that indicates whether the response was already sent. + /// + private bool m_WasResponseSent = false; + + /// + /// A flag that indicates that the response was closed. + /// Writing to client is not allowed after response is closed. + /// + private bool m_IsResponseClosed = false; + + /// + /// The length of the content of the response. + /// + long m_ContentLength = -1; + + /// + /// The response headers from the HTTP client. + /// + private WebHeaderCollection m_httpResponseHeaders = new WebHeaderCollection(true); + + /// + /// The HTTP version for the response. + /// + private Version m_version = new Version(1, 1); + + /// + /// Indicates whether the server requests a persistent connection. + /// Persistent connection is used if KeepAlive is true + /// in both the request and the response. + /// + private bool m_KeepAlive = false; + + /// + /// Encoding for this response's OutputStream. + /// + private Encoding m_Encoding = Encoding.UTF8; + + /// + /// Keeps content type for the response, set by user application. + /// + private string m_contentType; + + /// + /// Response status code. + /// + private int m_ResponseStatusCode = (int)HttpStatusCode.OK; + + /// + /// Array of connected client streams + /// + internal HttpListener m_Listener; + + /// + /// Member with network stream connected to client. + /// After call to Close() the stream is closed, no further writing allowed. + /// + internal OutputNetworkStreamWrapper m_clientStream; + + /// + /// The value of the HTTP Location header in this response. + /// + private string m_redirectLocation; + + /// + /// Response uses chunked transfer encoding. + /// + private bool m_sendChunked = false; + + /// + /// text description of the HTTP status code returned to the client. + /// + private string m_statusDescription; + + /// + /// Throws InvalidOperationException is HTTP response was sent. + /// Called before setting of properties. + /// + private void ThrowIfResponseSent() + { + if (m_WasResponseSent) + { + throw new InvalidOperationException(); + } + } + + /// + /// HttpListenerResponse is created by HttpListenerContext + /// + /// Network stream to the client + /// TBD + internal HttpListenerResponse(OutputNetworkStreamWrapper clientStream, HttpListener httpListener) + { + // Sets the delegate, so SendHeaders will be called on first write. + clientStream.HeadersDelegate = new OutputNetworkStreamWrapper.SendHeadersDelegate(SendHeaders); + // Saves network stream as member. + m_clientStream = clientStream; + // Saves list of client streams. m_clientStream is removed from clientStreamsList during Close(). + m_Listener = httpListener; + } + + /// + /// Updates the HTTP WEB header collection to prepare it for request. + /// For each property set it adds member to m_httpResponseHeaders. + /// m_httpResponseHeaders is serializes to string and sent to client. + /// + private void PrepareHeaders() + { + // Adds content length if it was present. + if (m_ContentLength != -1) + { + m_httpResponseHeaders.ChangeInternal(HttpKnownHeaderNames.ContentLength, m_ContentLength.ToString()); + } + + // Since we do not support persistent connection, send close always. + string connection = m_KeepAlive ? "Keep-Alive" : "Close"; + m_httpResponseHeaders.ChangeInternal(HttpKnownHeaderNames.Connection, connection); + + // Adds content type if user set it: + if (m_contentType != null) + { + m_httpResponseHeaders.AddWithoutValidate(HttpKnownHeaderNames.ContentType, m_contentType); + } + + if (m_redirectLocation != null) + { + m_httpResponseHeaders.AddWithoutValidate(HttpKnownHeaderNames.Location, m_redirectLocation); + m_ResponseStatusCode = (int)HttpStatusCode.Redirect; + } + + if (m_sendChunked) + { + m_httpResponseHeaders.AddWithoutValidate(HttpKnownHeaderNames.TransferEncoding, "chunked"); + } + } + + /// + /// Composes HTTP response line based on + /// + /// + private string ComposeHTTPResponse() + { + // Starts with HTTP + string resp = "HTTP/"; + // Adds version of HTTP + resp += m_version.ToString(); + // Add status code. + resp += " " + m_ResponseStatusCode; + // Adds description + if (m_statusDescription == null) + { + resp += " " + GetStatusDescription(m_ResponseStatusCode); + } + else // User provided description is present. + { + resp += " " + m_statusDescription; + } + + // Add line termindation. + resp += "\r\n"; + return resp; + } + + /// + /// Sends HTTP status and headers to client. + /// + private void SendHeaders() + { + // As first step we disable the callback to SendHeaders, so m_clientStream.Write would not call + // SendHeaders() again. + m_clientStream.HeadersDelegate = null; + + // Creates encoder, generates headers and sends the data. + Encoding encoder = Encoding.UTF8; + + byte[] statusLine = encoder.GetBytes(ComposeHTTPResponse()); + m_clientStream.m_Stream.Write(statusLine, 0, statusLine.Length); + + // Prepares/Updates WEB header collection. + PrepareHeaders(); + + // Serialise WEB header collection to byte array. + byte[] pHeaders = m_httpResponseHeaders.ToByteArray(); + + // Sends the headers + m_clientStream.m_Stream.Write(pHeaders, 0, pHeaders.Length); + + m_WasResponseSent = true; + } + + /// + /// Gets or sets the HTTP status code to be returned to the client. + /// + /// An Int32 value that specifies the + /// for the requested resource. + /// The default is OK, indicating that the server + /// successfully processed the client's request and included the + /// requested resource in the response body. + public int StatusCode + { + get { return m_ResponseStatusCode; } + set + { + ThrowIfResponseSent(); + m_ResponseStatusCode = value; + } + } + + /// + /// Gets or sets the number of bytes in the body data included in the + /// response. + /// + /// The value of the response's Content-Length + /// header. + public long ContentLength64 + { + get { return m_ContentLength; } + + set + { + ThrowIfResponseSent(); + m_ContentLength = value; + } + } + + /// + /// Gets a value indicating whether response headers have been sent to the client. + /// + public bool HasStarted => m_WasResponseSent; + + /// + /// Gets or sets the collection of header name/value pairs that is + /// returned by the server. + /// + /// A WebHeaderCollection instance that + /// contains all the explicitly set HTTP headers to be included in the + /// response. + public WebHeaderCollection Headers + { + get { return m_httpResponseHeaders; } + set + { + ThrowIfResponseSent(); + m_httpResponseHeaders = value; + } + } + + /// + /// Gets or sets whether the server requests a persistent connection. + /// + /// true if the server requests a persistent + /// connection; otherwise, false. The default is + /// true. + public bool KeepAlive + { + get { return m_KeepAlive; } + + set { m_KeepAlive = value; } + } + + /// + /// Gets a Stream object to which a response can be + /// written. + /// + /// A Stream object to which a response can be + /// written. + /// + /// The first write to the output stream sends a response to the client. + /// + public Stream OutputStream + { + get + { + if (m_IsResponseClosed) + { + throw new ObjectDisposedException("Response has been sent"); + } + + return m_clientStream; + } + } + + /// + /// Gets or sets the HTTP version that is used for the response. + /// + /// A Version object indicating the version of + /// HTTP used when responding to the client. This property is obsolete. + /// + public Version ProtocolVersion + { + get { return m_version; } + set + { + ThrowIfResponseSent(); + m_version = value; + } + } + + /// + /// Gets or sets the value of the HTTP Location + /// header in this response. + /// + /// A String that contains the absolute URL to + /// be sent to the client in the Location header. + /// + public string RedirectLocation + { + get { return m_redirectLocation; } + set + { + ThrowIfResponseSent(); + m_redirectLocation = value; + } + } + + /// + /// Gets or sets whether the response uses chunked transfer encoding. + /// + /// true if the response is set to use chunked + /// transfer encoding; otherwise, false. The default + /// is false. + public bool SendChunked + { + get { return m_sendChunked; } + set + { + ThrowIfResponseSent(); + m_sendChunked = value; + m_clientStream.m_enableChunkedEncoding = value; + } + } + + /// + /// Gets or sets the encoding for this response's + /// OutputStream. + /// + /// An Encoding object suitable for use with + /// the data in the + /// property, + /// or null reference if no encoding is specified. + /// + /// + /// Only UTF8 encoding is supported. + /// + public Encoding ContentEncoding + { + get { return m_Encoding; } + set + { + ThrowIfResponseSent(); + m_Encoding = value; + } + } + + /// + /// Gets or sets the MIME type of the returned content. + /// + /// A String instance that contains the text + /// of the response's Content-Type header. + public string ContentType + { + get { return m_contentType; } + set + { + ThrowIfResponseSent(); + m_contentType = value; + } + } + + /// + /// Gets or sets a text description of the HTTP status code that is + /// returned to the client. + /// + /// The text description of the HTTP status code returned to the + /// client. + public string StatusDescription + { + get { return m_statusDescription; } + set + { + ThrowIfResponseSent(); + m_statusDescription = value; + } + } + + public void Detach() + { + if (!m_IsResponseClosed) + { + if (!m_WasResponseSent) + { + SendHeaders(); + } + + m_IsResponseClosed = true; + } + } + + /// + /// Sends the response to the client and releases the resources held by + /// this HttpListenerResponse instance. + /// + /// + /// This method flushes data to the client and closes the network + /// connection. + /// + public void Close() + { + if (!m_IsResponseClosed) + { + try + { + if (!m_WasResponseSent) + { + SendHeaders(); + } + } + finally + { + // Removes from the list of streams and closes the socket. + ((IDisposable)this).Dispose(); + } + } + } + + /// + /// Closes the socket and sends the response if it was not done earlier + /// and the socket is present. + /// + void IDisposable.Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the resources held by this instance. + /// + /// + /// when called from ; + /// when called from the finalizer, in which case other managed objects (like ) + /// may already be finalized, so only the socket itself is released. + /// + private void Dispose(bool disposing) + { + if (m_IsResponseClosed) + { + return; + } + + m_IsResponseClosed = true; + + if (disposing) + { + try + { + // Iterates over list of client connections and remove its stream from it. + m_Listener.RemoveClientStream(m_clientStream); + + m_clientStream.m_Stream.Flush(); + + // If KeepAlive is true, + if (m_KeepAlive) + { // Then socket is tramsferred to the list of waiting for new data. + m_Listener.AddToWaitingConnections(m_clientStream); + } + else // If not KeepAlive then close + { + m_clientStream.Dispose(); + } + } + catch { } + } + else + { + try + { + m_clientStream.Dispose(); + } + catch { } + } + } + + /// + /// Called to close the socket if necessary. + /// + ~HttpListenerResponse() + { + Dispose(false); + } + + /// + /// Return default Description based in response status code. + /// + /// HTTP status code + /// + /// Default string with description. + /// + internal static string GetStatusDescription(int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + + return ""; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs new file mode 100644 index 0000000..e9c71a4 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs @@ -0,0 +1,295 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + // Any int can be cast to a HttpStatusCode to allow checking for non http1.1 + // codes. + + /// + /// Contains the values of status codes defined for HTTP. + /// + /// + ///

Status codes indicate categories, as follows:

+ ///

1xx -- Informational.

+ ///

2xx -- Successful.

+ ///

3xx -- Redirection.

+ ///

4xx -- Client Error.

+ ///

5xx -- Server Error.

+ ///
+ public enum HttpStatusCode + { + /// Informational -- 1xx. + /// Equivalent to HTTP status 100. Indicates that the client can continue with its + /// request. + Continue = 100, + /// Equivalent to HTTP status 101. Indicates that the protocol version or protocol + /// is being changed. + SwitchingProtocols = 101, + + /// Successful -- 2xx. + /// Equivalent to HTTP status 200. Indicates that the request succeeded and that + /// the requested information is in the response. This is the most common status code to + /// receive. + OK = 200, + /// Equivalent to HTTP status 201. Indicates that the request resulted in a new + /// resource created before the response was sent. + Created = 201, + /// Equivalent to HTTP status 202. Indicates that the request has been accepted for + /// further processing. + Accepted = 202, + /// Equivalent to HTTP status 203. Indicates that the returned metainformation is + /// from a cached copy instead of the origin server and therefore may be incorrect. + NonAuthoritativeInformation = 203, + /// Equivalent to HTTP status 204. Indicates that the request has been successfully + /// processed and that the response is intentionally blank. + NoContent = 204, + /// Equivalent to HTTP status 205. Indicates that the client should reset (not + /// reload) the current resource. + ResetContent = 205, + /// Equivalent to HTTP status 206. Indicates that the response is a + /// partial response as requested by a GET request that includes a byte range. + PartialContent = 206, + + /// Redirection -- 3xx. + /// Equivalent to HTTP status 300. Indicates that the requested information has + /// multiple representations. The default action is to treat this status as a redirect and + /// follow the contents of the Location header associated with this response. + /// If the property is + /// false, MultipleChoices will cause an exception to + /// be thrown. + /// MultipleChoices is a synonym for Ambiguous. + MultipleChoices = 300, + /// Equivalent to HTTP status 300. Indicates that the requested + /// information has multiple representations. The default action is to treat this status as + /// a redirect and follow the contents of the Location header associated with this response. + /// If the property is + /// false, Ambiguous will cause an exception to be + /// thrown. + /// Ambiguous is a synonym for MultipleChoices. + Ambiguous = 300, + /// Equivalent to HTTP status 301. Indicates that the requested information has + /// been moved to the URI specified in the Location header. The default action when this + /// status is received is to follow the Location header associated with the response. + /// MovedPermanently is a synonym for Moved. + MovedPermanently = 301, + /// Equivalent to HTTP status 301. Indicates that the requested information + /// has been moved to the URI specified in the Location header. The default action when this + /// status is received is to follow the Location header associated with the response. When + /// the original request method was POST, the redirected request will use the GET method. + /// Moved is a synonym for MovedPermanently. + Moved = 301, + /// Equivalent to HTTP status 302. Indicates that the requested information is + /// located at the URI specified in the Location header. The default action when this status + /// is received is to follow the Location header associated with the response. When the + /// original request method was POST, the redirected request will use the GET method. + /// If the property is + /// false, Found will cause an exception to be thrown. + /// Found is a synonym for Redirect. + Found = 302, + /// Equivalent to HTTP status 302. Indicates that the requested information is + /// located at the URI specified in the Location header. The default action when this status + /// is received is to follow the Location header associated with the response. When the + /// original request method was POST, the redirected request will use the GET method. + /// If the property is + /// false, Redirect will cause an exception to be + /// thrown. + /// Redirect is a synonym for Found. + Redirect = 302, + /// Equivalent to HTTP status 303. Automatically redirects the client to + /// the URI specified in the Location header as the result of a POST. The request to the + /// resource specified by the Location header will be made with a GET. + /// If the property is + /// false, SeeOther will cause an exception to be + /// thrown. + /// SeeOther is a synonym for RedirectMethod. + SeeOther = 303, + /// Equivalent to HTTP status 303. Automatically redirects the + /// client to the URI specified in the Location header as the result of a POST. The request + /// to the resource specified by the Location header will be made with a GET. + /// If the property is + /// false, RedirectMethod will cause an exception to + /// be thrown. + /// RedirectMethod is a synonym for SeeOther. + RedirectMethod = 303, + /// Equivalent to HTTP status 304. Indicates that the client's cached copy is + /// up-to-date. The contents of the resource are not transferred. + NotModified = 304, + /// Equivalent to HTTP status 305. Indicates that the request should use the proxy + /// server at the URI specified in the Location header. + UseProxy = 305, + /// Equivalent to HTTP status 306. This value is a proposed extension to the HTTP/1.1 + /// specification that is not fully specified. + Unused = 306, + /// Equivalent to HTTP status 307. Indicates that the request information is + /// located at the URI specified in the Location header. The default action when this status + /// is received is to follow the Location header associated with the response. When the + /// original request method was POST, the redirected request will also use the POST method. + /// If the property is + /// false, TemporaryRedirect will cause an exception + /// to be thrown. + /// TemporaryRedirect is a synonym for RedirectKeepVerb. + TemporaryRedirect = 307, + /// Equivalent to HTTP status 307. Indicates that the request + /// information is located at the URI specified in the Location header. The default action + /// when this status is received is to follow the Location header associated with the + /// response. When the original request method was POST, the redirected request will also + /// use the POST method. + /// If the property is + /// false, RedirectKeepVerb will cause an exception to + /// be thrown. + /// RedirectKeepVerb is a synonym for TemporaryRedirect. + RedirectKeepVerb = 307, + + /// Client Error -- 4xx. + /// Equivalent to HTTP status 400. Indicates that the request could not be + /// understood by the server. BadRequest is sent when no other error is + /// applicable, or if the exact error is unknown or does not have its own error code. + BadRequest = 400, + /// Equivalent to HTTP status 401. Indicates that the requested + /// resource requires authentication. The WWW-Authenticate header contains the details of + /// how to perform the authentication. + Unauthorized = 401, + /// Equivalent to HTTP status 402. Reserved for future use. + PaymentRequired = 402, + /// Equivalent to HTTP status 403. Indicates that the server refuses to + /// fulfill the request. + Forbidden = 403, + /// Equivalent to HTTP status 404. Indicates that the requested resource + /// does not exist on the server. + NotFound = 404, + /// Equivalent to HTTP status 405. Indicates that the request + /// method (POST or GET) is not allowed on the requested resource. + MethodNotAllowed = 405, + /// Equivalent to HTTP status 406. Indicates that the client has + /// indicated with Accept headers that it will not accept any of the available + /// representations of the resource. + NotAcceptable = 406, + /// Equivalent to HTTP status 407. Indicates that the + /// requested proxy requires authentication. The Proxy-authenticate header contains the + /// details of how to perform the authentication. + ProxyAuthenticationRequired = 407, + /// Equivalent to HTTP status 408. Indicates that the client did not + /// send a request within the time the server was expecting the request. + RequestTimeout = 408, + /// Equivalent to HTTP status 409. Indicates that the request could not be + /// carried out because of a conflict on the server. + Conflict = 409, + /// Equivalent to HTTP status 410. Indicates that the requested resource is no + /// longer available. + Gone = 410, + /// Equivalent to HTTP status 411. Indicates that the required + /// Content-length header is missing. + LengthRequired = 411, + /// Equivalent to HTTP status 412. Indicates that a condition + /// set for this request failed, and the request cannot be carried out. Conditions are set + /// with conditional request headers like If-Match, If-None-Match, or If-Unmodified-Since. + PreconditionFailed = 412, + /// Equivalent to HTTP status 413. Indicates that the request + /// is too large for the server to process. + RequestEntityTooLarge = 413, + /// Equivalent to HTTP status 414. Indicates that the URI is too + /// long. + RequestUriTooLong = 414, + /// Equivalent to HTTP status 415. Indicates that the request + /// is an unsupported type. + UnsupportedMediaType = 415, + /// Equivalent to HTTP status 416. Indicates that the + /// range of data requested from the resource cannot be returned, either because the + /// beginning of the range is before the beginning of the resource, or the end of the range + /// is after the end of the resource. + RequestedRangeNotSatisfiable = 416, + /// Equivalent to HTTP status 417. Indicates that an expectation + /// given in an Expect header could not be met by the server. + ExpectationFailed = 417, + + /// Server Error -- 5xx. + /// Equivalent to HTTP status 500. Indicates that a generic + /// error has occurred on the server. + InternalServerError = 500, + /// Equivalent to HTTP status 501. Indicates that the server does + /// not support the requested function. + NotImplemented = 501, + /// Equivalent to HTTP status 502. Indicates that an intermediate proxy + /// server received a bad response from another proxy or the origin server. + BadGateway = 502, + /// Equivalent to HTTP status 503. Indicates that the server is + /// temporarily unavailable, usually due to high load or maintenance. + ServiceUnavailable = 503, + /// Equivalent to HTTP status 504. Indicates that an intermediate + /// proxy server timed out while waiting for a response from another proxy or the origin + /// server. + GatewayTimeout = 504, + /// Equivalent to HTTP status 505. Indicates that the + /// requested HTTP version is not supported by the server. + HttpVersionNotSupported = 505, + + }; + + /// + /// Range for the HTTP status codes. + /// + internal enum HttpStatusRange + { + /// TBD + MaxOkStatus = 299, + /// TBD + MaxRedirectionStatus = 399 + }; // enum HttpStatusRange + + /* + Fielding, et al. Standards Track [Page 3] + + RFC 2616 HTTP/1.1 June 1999 + + 10.1 Informational 1xx ...........................................57 + 10.1.1 100 Continue .............................................58 + 10.1.2 101 Switching Protocols ..................................58 + 10.2 Successful 2xx ..............................................58 + 10.2.1 200 OK ...................................................58 + 10.2.2 201 Created ..............................................59 + 10.2.3 202 Accepted .............................................59 + 10.2.4 203 Non-Authoritative Information ........................59 + 10.2.5 204 No Content ...........................................60 + 10.2.6 205 Reset Content ........................................60 + 10.2.7 206 Partial Content ......................................60 + 10.3 Redirection 3xx .............................................61 + 10.3.1 300 Multiple Choices .....................................61 + 10.3.2 301 Moved Permanently ....................................62 + 10.3.3 302 Found ................................................62 + 10.3.4 303 See Other ............................................63 + 10.3.5 304 Not Modified .........................................63 + 10.3.6 305 Use Proxy ............................................64 + 10.3.7 306 (Unused) .............................................64 + 10.3.8 307 Temporary Redirect ...................................65 + 10.4 Client Error 4xx ............................................65 + 10.4.1 400 Bad Request .........................................65 + 10.4.2 401 Unauthorized ........................................66 + 10.4.3 402 Payment Required ....................................66 + 10.4.4 403 Forbidden ...........................................66 + 10.4.5 404 Not Found ...........................................66 + 10.4.6 405 Method Not Allowed ..................................66 + 10.4.7 406 Not Acceptable ......................................67 + 10.4.8 407 Proxy Authentication Required .......................67 + 10.4.9 408 Request Timeout .....................................67 + 10.4.10 409 Conflict ............................................67 + 10.4.11 410 Gone ................................................68 + 10.4.12 411 Length Required .....................................68 + 10.4.13 412 Precondition Failed .................................68 + 10.4.14 413 Request Entity Too Large ............................69 + 10.4.15 414 Request-URI Too Long ................................69 + 10.4.16 415 Unsupported Media Type ..............................69 + 10.4.17 416 Requested Range Not Satisfiable .....................69 + 10.4.18 417 Expectation Failed ..................................70 + 10.5 Server Error 5xx ............................................70 + 10.5.1 500 Internal Server Error ................................70 + 10.5.2 501 Not Implemented ......................................70 + 10.5.3 502 Bad Gateway ..........................................70 + 10.5.4 503 Service Unavailable ..................................70 + 10.5.5 504 Gateway Timeout ......................................71 + 10.5.6 505 HTTP Version Not Supported ...........................71 + */ +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs new file mode 100644 index 0000000..79f44d7 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs @@ -0,0 +1,367 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Text; + +namespace System.Web +{ + /// + /// Utilities to encode and decode URLs. + /// + public class HttpUtility + { + /// + /// Encodes a URL string. + /// + /// The text to encode. + /// An encoded string. + public static string UrlEncode(string str) + { + if ((str == null) || (str == string.Empty)) + { + return string.Empty; + } + + return new string(Encoding.UTF8.GetChars(UrlEncodeToBytes(str, Encoding.UTF8))); + } + + /// + /// Encodes a URL string using the specified encoding object. + /// + /// The text to encode. + /// The object that specifies the encoding scheme. + /// An encoded string. + public static string UrlEncode( + string str, + Encoding e) + { + if ((str == null) || (str == string.Empty)) + { + return string.Empty; + } + + return new string(e.GetChars(UrlEncodeToBytes(str, e))); + } + + /// + /// Converts a byte array into a URL-encoded string, starting at the specified position in the array and continuing for the specified number of bytes. + /// + /// The array of bytes to encode. + /// The position in the byte array at which to begin encoding. + /// The number of bytes to encode. + /// An encoded string. + public static string UrlEncode( + byte[] bytes, + int offset, + int count) + { + return new string(Encoding.UTF8.GetChars(UrlEncodeBytesToBytesInternal(bytes, offset, count, false))); + } + + /// + /// Converts a byte array into an encoded URL string. + /// + /// The array of bytes to encode. + /// An encoded string. + public static string UrlEncode(byte[] bytes) => UrlEncode(bytes, 0, bytes.Length); + + /// + /// Converts a string into a URL-encoded array of bytes using the specified encoding object. + /// + /// The string to encode. + /// The that specifies the encoding scheme. + /// An encoded array of bytes. + public static byte[] UrlEncodeToBytes(string str, Encoding e) + { + if (str == null) + { + return null; + } + + var bytes = e.GetBytes(str); + + return UrlEncodeBytesToBytesInternal(bytes, 0, bytes.Length, false); + } + + /// + /// Converts a URL-encoded array of bytes into a decoded array of bytes. + /// + /// The array of bytes to encode + /// A decoded array of bytes. + public static byte[] UrlEncodeToBytes(byte[] bytes) => UrlEncodeBytesToBytesInternal(bytes, 0, bytes.Length, false); + + /// + /// Converts an array of bytes into a URL-encoded array of bytes, starting at the specified position in the array and continuing for the specified number of bytes. + /// + /// The array of bytes to encode + /// The position in the byte array at which to begin encoding + /// The number of bytes to encode + /// An encoded array of bytes. + public static byte[] UrlEncodeToBytes( + byte[] bytes, + int offset, + int count) => UrlEncodeBytesToBytesInternal( + bytes, + offset, + count, + false); + + private static byte[] UrlEncodeBytesToBytesInternal( + byte[] bytes, + int offset, + int count, + bool alwaysCreateReturnValue) + { + var num = 0; + var num2 = 0; + + for (var i = 0; i < count; i++) + { + var ch = (char)bytes[offset + i]; + + if (ch == ' ') + { + num++; + } + else if (!IsSafe(ch)) + { + num2++; + } + } + if ((!alwaysCreateReturnValue && (num == 0)) + && (num2 == 0)) + { + return bytes; + } + + var buffer = new byte[count + (num2 * 2)]; + var num4 = 0; + + for (var j = 0; j < count; j++) + { + var num6 = bytes[offset + j]; + var ch2 = (char)num6; + + if (IsSafe(ch2)) + { + buffer[num4++] = num6; + } + else if (ch2 == ' ') + { + buffer[num4++] = 0x2b; + } + else + { + buffer[num4++] = 0x25; + buffer[num4++] = (byte)IntToHex((num6 >> 4) & 15); + buffer[num4++] = (byte)IntToHex(num6 & 15); + } + } + + return buffer; + } + + private static char IntToHex(int n) + { + if (n <= 9) + { + return (char)(n + 0x30); + } + + return (char)((n - 10) + 0x41); + } + + private static bool IsSafe(char ch) + { + if ((((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z'))) + || ((ch >= '0') && (ch <= '9'))) + { + return true; + } + + return ch switch + { + '~' or '-' or '.' or '_' => true, + _ => false, + }; + } + + /// + /// Converts a string that has been encoded for transmission in a URL into a decoded string. + /// + /// The string to decode. + /// The decoded URL + public static string UrlDecode(string str) + { + if (string.IsNullOrEmpty(str)) + { + return string.Empty; + } + + var data = Encoding.UTF8.GetBytes(str); + + return new string(Encoding.UTF8.GetChars(UrlDecodeToBytes(data, 0, data.Length))); + } + + /// + /// Converts a URL-encoded string into a decoded string, using the specified encoding object. + /// + /// The string to decode. + /// The that specifies the decoding scheme. + /// A decoded string. + public static string UrlDecode(string str, Encoding e) + { + if ((str == null) || (str == string.Empty)) + { + return string.Empty; + } + + var data = e.GetBytes(str); + return new string(e.GetChars(UrlDecodeToBytes(data, 0, data.Length))); + } + + /// + /// Converts a URL-encoded byte array into a decoded string using the specified encoding object, starting at the specified position in the array, and continuing for the specified number of bytes. + /// + /// The array of bytes to decode. + /// The position in the byte to begin decoding + /// The number of bytes to decode. + /// The object that specifies the encoding scheme. + /// A decoded string. + public static string UrlDecode( + byte[] bytes, + int offset, + int count, + Encoding e) => new(e.GetChars(UrlDecodeToBytes( + bytes, + offset, + count))); + + /// + /// Converts a URL-encoded byte array into a decoded string using the specified decoding object. + /// + /// The array of bytes to decode. + /// The object that specifies the encoding scheme. + /// A decoded string. + public static string UrlDecode( + byte[] bytes, + Encoding e) => new string(e.GetChars(UrlDecodeToBytes( + bytes, + 0, + bytes.Length))); + + /// + /// Converts a URL-encoded array of bytes into a decoded array of bytes, starting at the specified position in the array and continuing for the specified number of bytes. + /// + /// The array of bytes to decode. + /// The position in the byte array at which to begin decoding. + /// The number of bytes to decode. + /// A decoded array of bytes. + public static byte[] UrlDecodeToBytes( + byte[] bytes, + int offset, + int count) + { + var length = 0; + var sourceArray = new byte[count]; + + for (var i = 0; i < count; i++) + { + var index = offset + i; + var num4 = bytes[index]; + + if (num4 == 0x2b) + { + num4 = 0x20; + } + else if ((num4 == 0x25) + && (i < (count - 2))) + { + var num5 = HexToInt((char)bytes[index + 1]); + var num6 = HexToInt((char)bytes[index + 2]); + + if ((num5 >= 0) + && (num6 >= 0)) + { + num4 = (byte)((num5 << 4) | num6); + i += 2; + } + } + sourceArray[length++] = num4; + } + + if (length < sourceArray.Length) + { + var destinationArray = new byte[length]; + Array.Copy(sourceArray, destinationArray, length); + sourceArray = destinationArray; + } + + return sourceArray; + } + + /// + /// Converts a URL-encoded string into a decoded array of bytes using the specified decoding object. + /// + /// The string to encode. + /// The object that specifies the encoding scheme. + /// A decoded array of bytes. + public static byte[] UrlDecodeToBytes( + string str, + Encoding e) + { + var data = e.GetBytes(str); + return UrlDecodeToBytes(data, 0, data.Length); + } + + /// + /// Converts a URL-encoded string into a decoded array of bytes. + /// + /// The string to decode. + /// A decoded array of bytes. + public static byte[] UrlDecodeToBytes(string str) => UrlDecodeToBytes( + str, + Encoding.UTF8); + + /// + /// Converts a URL-encoded array of bytes into a decoded array of bytes. + /// + /// The array of bytes to decode. + /// A decoded array of bytes. + public static byte[] UrlDecodeToBytes(byte[] bytes) => UrlDecodeToBytes( + bytes, + 0, + bytes.Length); + + /// + /// Get the int value of a char. + /// + /// The to convert. + /// The value of the . + public static int HexToInt(char h) + { + if ((h >= '0') && + (h <= '9')) + { + return (h - '0'); + } + + if ((h >= 'a') && + (h <= 'f')) + { + return ((h - 'a') + 10); + } + + if ((h >= 'A') && + (h <= 'F')) + { + return ((h - 'A') + 10); + } + + return -1; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs new file mode 100644 index 0000000..229e5b8 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Defines the HTTP version numbers that are supported by the + /// and + /// classes. + /// + public class HttpVersion + { + /// + /// Defines a instance for HTTP 1.0. + /// + public static readonly Version Version10 = new Version(1, 0); + + /// + /// Defines a instance for HTTP 1.1. + /// + public static readonly Version Version11 = new Version(1, 1); + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs new file mode 100644 index 0000000..afc775a --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs @@ -0,0 +1,2070 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + using System.Collections; + using System.IO; + using System.Net.Security; + using System.Net.Sockets; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Threading; + using System.Diagnostics; + + /// + /// This is the class that we use to create HTTP and requests. + /// Used to register prefix "http" with WEB Request class. + /// + internal class HttpRequestCreator : IWebRequestCreate + { + internal HttpRequestCreator() + { + } + + /// + /// Creates an HttpWebRequest. We register + /// for http, https, ws and wss URLs, and this method is called when a request + /// needs to be created for one of those. + /// + /// Url for request being created. + /// The newly created HttpWebRequest. + public WebRequest Create(Uri Url) + { + + return new HttpWebRequest(Url); + } + + } // class HttpRequestCreator + + /// + /// Provides an HTTP-specific implementation of the class. + /// + /// This class does the main work of the request: it collects the header information + /// from the user, exposes the Stream for outgoing entity data, and processes the incoming + /// request. + public class HttpWebRequest : WebRequest + { + + /// + /// Array list of connected streams. + /// This is static list, keeps all "stay live" sockets. + /// + internal static ArrayList m_ConnectedStreams; + + /// + /// Timer that checks on open connections and closes them if they are + /// idle for a long time. + /// + static Timer m_DropOldConnectionsTimer; + + /// + /// If a response was created then Dispose on the Request will not dispose the underlying stream. + /// + private bool m_responseCreated; + + /// + /// Timer callback. Called periodically and closes all connections that + /// are idle for long time. + /// + /// Unused + static private void CheckPersistentConnections(object unused) + { + // Persistent connections have not been properly implemented yet. + int count = m_ConnectedStreams.Count; + + // The fastest way to exit out - if there are no sockets in the list - exit out. + if (count > 0) + { + DateTime curTime = DateTime.UtcNow; + + lock (m_ConnectedStreams) + { + count = m_ConnectedStreams.Count; + + for (int i = count - 1; i >= 0; i--) + { + InputNetworkStreamWrapper streamWrapper = (InputNetworkStreamWrapper)m_ConnectedStreams[i]; + + TimeSpan timePassed = curTime - streamWrapper.m_lastUsed; + + // If the socket is old, then close and remove from the list. + if (timePassed.TotalMilliseconds > HttpConstants.DefaultKeepAliveMilliseconds) + { + m_ConnectedStreams.RemoveAt(i); + + // Closes the socket to release resources. + streamWrapper.Dispose(); + } + } + + // Keep the timer going for another DefaultKeepAliveMilliseconds if we still have persistent connections in m_ConnectedStreams. + // Otherwise, do nothing. The timer won't be fired again. + if (m_ConnectedStreams.Count > 0) + { + m_DropOldConnectionsTimer.Change(HttpConstants.DefaultKeepAliveMilliseconds, System.Threading.Timeout.Infinite); + } + + } + } + } + + /// + /// Registers HttpRequestCreator as the creator for the "http" prefix. + /// + static HttpWebRequest() + { + // The static constructor of the base class does not get called before the derived static constructor + // so you have to make sure the base class gets initialized in any case if you want to use "RegisterPrefix" + Initialize(); + + // Creates instance of HttpRequestCreator. HttpRequestCreator creates HttpWebRequest + HttpRequestCreator Creator = new HttpRequestCreator(); + + // Register prefix. HttpWebRequest handles both http and https + RegisterPrefix("http:", Creator); + RegisterPrefix("https:", Creator); + + if (m_ConnectedStreams == null) + { + // Creates new list for connected sockets. + m_ConnectedStreams = new ArrayList(); + m_DropOldConnectionsTimer = new Timer(CheckPersistentConnections, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + } + } + + /// + /// Closes a response stream, if present. + /// + /// Not used. + protected override void Dispose(bool disposing) + { + if (m_requestStream != null) + { + if (!m_responseCreated) + { + RemoveStreamFromPool(m_requestStream); + + m_requestStream.Dispose(); + } + } + + base.Dispose(disposing); + } + + + /// + /// The length in KB of the default maximum for response headers + /// received. + /// + /// + /// The default configuration file sets this value to 4 kilobytes. + /// + static int defaultMaxResponseHeadersLen = 4; + + /// + /// Default delay on the Stream.Read and Stream.Write operations + /// + private const int DefaultReadWriteTimeout = 5 * 60 * 1000; // 5 minutes + + /// + /// Delegate that can be called on Continue Response + /// + private HttpContinueDelegate m_continueDelegate; + + /// + /// HTTP verb. + /// + private string m_method; + + /// + /// The Headers for the HTTP request. + /// + private WebHeaderCollection m_httpRequestHeaders; + + /// + /// Controls how writes are handled. + /// + private HttpWriteMode m_httpWriteMode; + + /// + /// The URI that we do the request for. + /// + private Uri m_originalUrl; + + /// + /// Content length of the request message on POST. + /// + private long m_contentLength; + + /// + /// The HTTP version for this request. + /// + private Version m_version; + + /// + /// Timeout for Read And Write on the Stream that we return through + /// GetResponse().GetResponseStream() and GetRequestStream() + /// + private int m_readWriteTimeout; + + /// + /// Proxy to use for connection. + /// + private IWebProxy m_proxy; + + /// + /// Select to be used for requests. The default is to force setting it. + /// + private SslProtocols m_sslProtocols = SslProtocols.None; + + /// + /// Whether to use persistent connections. + /// + private bool m_keepAlive; + + /// + /// An array of certificates used to verify servers that support https. + /// + /// + /// The client application sets these certificates to the + /// HttpWebRequest. When the server certificate is received, it + /// is validated with certificates in this array. + /// + X509Certificate m_caCert; + + /// + /// Client certificate used to authenticate with server. + /// + internal X509Certificate _clientCert; + + /// + /// The number of people using the connection. Must reference-count this + /// stuff. Except reference counting is apparently insufficient. I'm going to flag each section + /// that uses the parser with a constant, and twiddle the flags for + /// adding and removing connections. + /// + internal const int k_noConnection = 0x0; + private const int k_parserFlag = 0x1; + private const int k_writeStreamFlag = 0x2; + private const int k_readStreamFlag = 0x4; + private const int k_abortFlag = 0x8; + internal int m_connectionUsers = 0; + + /// + /// Static instance of decoder to convert received bytes from network + /// stream into text of the response line and WEB headers. + /// + static private Decoder UTF8decoder = System.Text.Encoding.UTF8.GetDecoder(); + + /// + /// Invalid characters that cannot be found in a valid method-verb. + /// + private static readonly char[] k_invalidMethodChars = + new char[]{' ', + '\r', + '\t', + '\n'}; + + /// + /// Exposes this property from . + /// + private SslVerification _sslVerification; + + /// + /// The maximum length, in kilobytes (1024 bytes), of the response + /// headers. + /// + private int m_maxResponseHeadersLen = defaultMaxResponseHeadersLen; + + /// + /// The response from the server. + /// + private HttpStatusCode m_responseStatus = (HttpStatusCode)0; + + /// + /// true if we have a response, or a transport error while constructing the response + /// + private bool m_responseComplete = false; + + /// + /// This is non-null if there was an error. If this is true, then there is no valid HttpWebResponse. + /// + private WebException m_errorResponse = null; + + /// + /// Buffer size for reading from the server + /// + private const int k_readBlockLength = 2048; + + /// + /// This is the maximum amount of data which can be buffered at any time + /// and have a failed match. In other words, if we receive this much + /// data, and can't parse it in any useful way, assume an error. + /// + private const int k_maximumBufferSize = 8192; + + /// + /// True if the request has been started, false otherwise. Disables + /// setting of many header properties. + /// + private bool m_requestSent; + + /// + /// This is the request stream, if it has been created. + /// + private InputNetworkStreamWrapper m_requestStream; + + /// + /// Whether or not data should be buffered when sent. + /// Data is always buffered though (given redirects and stuff). + /// + private bool m_allowWriteStreamBuffering; + + /// + /// The timeout value for this request. + /// + private int m_timeout; + + /// + /// Keep NetworkCredential if user have send user name and password. + /// + private NetworkCredential m_NetworkCredentials; + + /// + /// Gets or sets the timeout value in milliseconds for the + /// and + /// methods. + /// + /// The number of milliseconds to wait before the request times + /// out. The default is 100,000 milliseconds (100 seconds). + /// + /// Overrides the property + /// of WebRequest. + public override int Timeout + { + get + { + return m_timeout; + } + + set + { + if (value < 0 && value != System.Threading.Timeout.Infinite) + { + throw new ArgumentOutOfRangeException("value"); + } + + m_timeout = value; + } + } + + /// + /// Set or Get NetworkCredential if user have send user name and password. + /// + public NetworkCredential Credentials + { + get { return m_NetworkCredentials; } + set { m_NetworkCredentials = value; } + } + + /// + /// Gets or sets the root CA certificate used to authenticate with https + /// servers. This certificate is used only for https connections; + /// http connections do not require this. + /// + public X509Certificate HttpsAuthentCert + { + get { return m_caCert; } + set { m_caCert = value; } + } + + /// + /// Gets or sets + /// + public SslVerification SslVerification + { + get { return _sslVerification; } + set { _sslVerification = value; } + } + + /// + /// Gets or sets the TLS/SSL protocol used by the class. + /// + /// + /// One of the values defined in the enumeration. + /// + /// + /// Setting this property is mandatory when performing HTTPS requests, otherwise the authentication will fail. + /// + /// This property is specific to nanoFramework. There is no equivalent in the .NET API. + /// + public SslProtocols SslProtocols + { + get { return m_sslProtocols; } + set { m_sslProtocols = value; } + } + + /// + /// Gets or sets a timeout in milliseconds when writing to or reading + /// from a stream. + /// + /// The number of milliseconds before the writing or reading + /// times out. The default value is 300,000 milliseconds (5 minutes). + /// + /// This property is used to control the timeout when calling + /// and . + /// This property affects Streams returned from + /// GetResponse().() + /// and + /// GetResponse().(). + /// + public int ReadWriteTimeout + { + get + { + return m_readWriteTimeout; + } + + set + { + // we can't change timeouts after the request has been sent + if (m_requestSent) + throw new InvalidOperationException("Cannot change timeout after request submitted "); + if (value <= 0 && value != System.Threading.Timeout.Infinite) + { + throw new ArgumentOutOfRangeException("value"); + } + + m_readWriteTimeout = value; + } + } + + /// + /// The HTTP status code returned by the server. + /// + internal HttpStatusCode ResponseStatusCode + { + get + { + return m_responseStatus; + } + } + + /// + /// Return if error is present in response. + /// + /// true if error happened, false otherwise + internal bool hasError() + { + return m_errorResponse != null; + } + + /// + /// Gets the original Uniform Resource Identifier (URI) of the request. + /// + /// + /// The URI object was created by the constructor and is always + /// non-null. The URI object will always be the base URI, because + /// automatic re-directs aren't supported. + /// + /// A Uri that contains the URI of the Internet resource passed + /// to the WebRequest. method. + /// + public override Uri RequestUri + { + get + { + return m_originalUrl; + } + } + + /// + /// Gets the URI for this request. + /// + /// A Uri that identifies the Internet + /// resource that actually responds to the request. The default is the + /// URI used by the + /// WebRequest. method to + /// initialize the request. + /// + /// + /// This value is always the same as the + /// + /// property, because automatic re-direction isn't supported. + /// + public Uri Address + { + get + { + return m_originalUrl; + } + } + + /// + /// Gets or sets a value that indicates whether to buffer the data sent + /// to the Internet resource. + /// + /// true to enable buffering of the data sent + /// to the Internet resource; false to disable buffering. The + /// default is true. + public bool AllowWriteStreamBuffering + { + + get + { + return m_allowWriteStreamBuffering; + } + + set + { + m_allowWriteStreamBuffering = value; + } + } + + /// + /// Gets or sets the Content-Length of the request entity body. + /// + /// The number of bytes of data to send to the Internet resource. + /// The default is -1, which indicates the property has not been set and + /// that there is no request data to send. + /// + /// Getting this property returns the last value set, or -1 if no value + /// has been set. Setting it sets the content length, and the + /// application must write that much data to the stream. This property + /// interacts with + /// HttpWebRequest.. + /// + public override long ContentLength + { + get + { + return m_contentLength; + } + + set + { + //no race. Don't need interlocked + if (true == m_requestSent) + throw new InvalidOperationException(); + + if (value < 0) + throw new ArgumentOutOfRangeException("Content length cannot be negative: " + value); + + m_contentLength = value; + //if a content length is set, then we cannot send chunked data. + m_httpWriteMode = HttpWriteMode.Write; + } + } + + /// + /// Gets or sets the delegate used to signal on Continue callback. + /// + /// A delegate that implements the callback method that executes + /// when an HTTP Continue response is returned from the Internet + /// resource. The default value is null. + /// + /// This property gets or sets the delegate method called when an HTTP + /// 100-continue response is received from the Internet resource. + /// + public HttpContinueDelegate ContinueDelegate + { + get { return m_continueDelegate; } + set { m_continueDelegate = value; } + } + + /// + /// Gets a value that indicates whether the request should follow + /// redirection responses. This value is always + /// false, because Autodirect isn't supported. + /// + /// This value is always false, because + /// Autodirect isn't supported. + public bool AllowAutoRedirect + { + get + { + return false; + } + } + + /// + /// Gets the maximum number of automatic redirections. This value is + /// always zero, because auto-redirection isn't supported. + /// + /// This value is always zero, because auto-redirection isn't + /// supported. + public int MaximumAutomaticRedirections + { + get + { + return 0; + } + } + + /// + /// Gets or sets the HTTP method of this request. + /// + /// The request method to use to contact the Internet resource. + /// The default value is GET. + /// + /// This method represents the initial origin verb, which is unchanged + /// and unaffected by redirects. + /// + public override string Method + { + get + { + return m_method; + } + + set + { + if (ValidationHelper.IsBlankString(value)) + { + throw new ArgumentException("Blank Method Set: " + value); + } + + if (value.IndexOfAny(k_invalidMethodChars) != -1) + { + throw new ArgumentException("Invalid Method Set: " + value); + } + + m_method = value; + } + } + + /// + /// Gets or sets whether to use a persistent connection, if available. + /// + /// true if the request to the Internet resource should + /// contain a Connection HTTP header with the value Keep-alive; + /// otherwise, false. The default is true. + public bool KeepAlive + { + get + { + return m_keepAlive; + } + + set + { + m_keepAlive = value; + } + } + + /// + /// Gets or sets the maximum allowed length of the response headers. + /// + /// The length, in kilobytes (1024 bytes), of the response + /// headers. + /// + /// The length of the response header includes the response status line + /// and any extra control characters that are received as part of HTTP + /// protocol. A value of -1 means no limit is imposed on the response + /// headers; a value of 0 means that all requests fail. If this + /// property is not explicitly set, it defaults to the value of the + /// + /// property. + /// + public int MaximumResponseHeadersLength + { + get { return m_maxResponseHeadersLen; } + set + { + if (value <= 0 && value != -1) + { + throw new ArgumentOutOfRangeException(); + } + + m_maxResponseHeadersLen = value; + } + } + + /// + /// Gets or sets the default maximum allowed length of the response + /// headers. + /// + /// The default maximum allowed length of the response headers. + /// + /// + /// On creation of an HttpWebRequest instance, this + /// value is used for the + /// + /// property. + /// + public static int DefaultMaximumResponseHeadersLength + { + get { return defaultMaxResponseHeadersLen; } + set + { + if (value <= 0 && value != -1) + { + throw new ArgumentOutOfRangeException(); + } + defaultMaxResponseHeadersLen = value; + } + } + + /// + /// A collection of HTTP headers stored as name/value pairs. + /// + /// A WebHeaderCollection that contains the name/value + /// pairs that make up the headers for the HTTP request. + /// + /// The following header values are set through properties on the + /// HttpWebRequest class: Accept, Connection, + /// Content-Length, Content-Type, Expect, Range, Referer, + /// Transfer-Encoding, and User-Agent. Trying to set these header + /// values by using + /// WebHeaderCollection.() + /// will raise an exception. Date and Host are set internally. + /// + public override WebHeaderCollection Headers + { + get + { + return m_httpRequestHeaders; + } + + set + { + // we can't change headers after they've already been sent + if (m_requestSent) + throw new InvalidOperationException("Cannot change headers after request submitted"); + + WebHeaderCollection webHeaders = value; + WebHeaderCollection newWebHeaders = + new WebHeaderCollection(true); + + // Copy And Validate - + // Handle the case where their object tries to change + // name, value pairs after they call set, so therefore, + // we need to clone their headers. + for (int i = 0; i < webHeaders.AllKeys.Length; i++) + { + newWebHeaders.Add(webHeaders.AllKeys[i], webHeaders[webHeaders.AllKeys[i]]); + } + + m_httpRequestHeaders = newWebHeaders; + } + } + + /// + /// Gets or sets the proxy for the request. + /// + /// The object to use to proxy + /// the request. null indicates that no proxy will be used. + public override IWebProxy Proxy + { + get { return m_proxy; } + set + { + if (m_requestSent) + throw new InvalidOperationException("Cannot change proxy after request submitted"); + if (value == null) + throw new ArgumentNullException(); + + m_proxy = value; + } + } + + /// + /// Gets or sets the state of chunk transfer send mode. + /// + /// true to send data to the Internet resource in + /// segments; otherwise, false. The default value is + /// false. + /// + /// If true, bits are uploaded and written using the + /// Chunked property of HttpWriteMode. + /// + public bool SendChunked + { + get { return m_httpWriteMode == HttpWriteMode.Chunked; } + set + { + //no race. Don't need interlocked + if (true == m_requestSent) + { + throw new InvalidOperationException("Cannot set \"chunked\" after request submitted"); + } + + if (value) + { + m_httpWriteMode = HttpWriteMode.Chunked; + } + else + { + if (m_contentLength >= 0) + { + m_httpWriteMode = HttpWriteMode.Write; + } + else + { + m_httpWriteMode = HttpWriteMode.None; + } + } + } + } + + /// + /// Gets or sets the HTTP protocol version for this request. + /// + /// The HTTP version to use for the request. The default is + /// . + public Version ProtocolVersion + { + get + { + return m_version; + } + + set + { + if (!value.Equals(HttpVersion.Version10) && + !value.Equals(HttpVersion.Version11)) + { + throw new ArgumentException("Invalid HTTP Verion: " + value); + } + + m_version = new Version(value.Major, value.Minor); + } + } + + /// + /// Private method for removing duplicate code which removes and adds + /// headers that are marked private. + /// + /// The name of the HTTP header. + /// The value of the HTTP header. + private void SetSpecialHeaders(String HeaderName, String value) + { + value = WebHeaderCollection.CheckBadChars(value, true); + + m_httpRequestHeaders.RemoveInternal(HeaderName); + if (value != null && value.Length != 0) + { + m_httpRequestHeaders.AddInternal(HeaderName, value); + } + } + + /// + /// Gets or sets the type of the entity body (the value of the content + /// type). + /// + /// The value of the Content-type HTTP header. The + /// default value is null. + /// + /// Setting to null clears the content-type. + /// + public override String ContentType + { + get + { + return m_httpRequestHeaders[HttpKnownHeaderNames.ContentType]; + } + + set + { + SetSpecialHeaders(HttpKnownHeaderNames.ContentType, value); + } + } + + /// + /// Gets or sets the TransferEncoding HTTP header. + /// + /// The value of the Transfer-encoding HTTP header. The + /// default value is null. + /// + /// null clears the transfer encoding except for the + /// Chunked setting. + /// + public String TransferEncoding + { + get + { + return m_httpRequestHeaders[HttpKnownHeaderNames.TransferEncoding]; + } + + set + { + bool fChunked; + + // on blank string, remove current header + if (ValidationHelper.IsBlankString(value)) + { + // if the value is blank, then remove the header + m_httpRequestHeaders.RemoveInternal(HttpKnownHeaderNames.TransferEncoding); + + return; + } + + // if not, check if the user is trying to set chunked: + string newValue = value.ToLower(); + + fChunked = (newValue.IndexOf("chunked") != -1); + + // prevent them from adding chunked, or from adding an Encoding + // without turing on chunked, the reason is due to the HTTP + // Spec which prevents additional encoding types from being + // used without chunked + if (fChunked) + { + throw new ArgumentException("Cannot add \"Encoding\" and set \"chunked\""); + } + else if (m_httpWriteMode != HttpWriteMode.Chunked) + { + throw new InvalidOperationException("Need HttpWriteMode.Chunked to be current mode"); + } + else + { + m_httpRequestHeaders.CheckUpdate(HttpKnownHeaderNames.TransferEncoding, value); + } + } + + } + + /// + /// Gets or sets the value of the Accept HTTP header. + /// + /// The value of the Accept HTTP header. The default + /// value is null. + public String Accept + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.Accept]; } + set { SetSpecialHeaders(HttpKnownHeaderNames.Accept, value); } + } + + /// + /// Gets or sets the value of the Referer HTTP header. + /// + /// The value of the Referer HTTP header. The default + /// value is null. + /// This header value is misspelled intentionally. + public String Referer + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.Referer]; } + set + { + SetSpecialHeaders(HttpKnownHeaderNames.Referer, value); + } + } + + /// + /// Gets or sets the value of the User-Agent HTTP header. + /// + /// The value of the User-agent HTTP header. The default + /// value is null. + public String UserAgent + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.UserAgent]; } + set + { + SetSpecialHeaders(HttpKnownHeaderNames.UserAgent, value); + } + } + + /// + /// Gets or sets the value of the Expect HTTP header. + /// + /// The contents of the Expect HTTP header. The default + /// value is null. + /// When setting this property, null clears the + /// Expect (except for the 100-continue value). + public String Expect + { + get { return m_httpRequestHeaders[HttpKnownHeaderNames.Expect]; } + set + { + // on blank string, remove current header + if (ValidationHelper.IsBlankString(value)) + { + m_httpRequestHeaders.RemoveInternal(HttpKnownHeaderNames.Expect); + return; + } + + m_httpRequestHeaders.CheckUpdate(HttpKnownHeaderNames.Expect, value); + } + } + + /// + /// Gets the IfModifiedSince value of + /// HttpKnownHeaderNames. + /// + /// A that contains the contents of + /// the If-Modified-Since HTTP header. The default value is the + /// current date and time. + /// + /// The setter for this property isn't supported, because a function + /// that formats the time isn't implemented. + /// + /// null clears the + /// IfModifiedSince header. + /// + public DateTime IfModifiedSince + { + get + { + string ifmodHeaderValue = m_httpRequestHeaders[HttpKnownHeaderNames.IfModifiedSince]; + + if (ifmodHeaderValue == null) + { + return DateTime.UtcNow; + } + + return HttpProtocolUtils.string2date(ifmodHeaderValue); + } + + // Set is not supported at this moment. It is needed for server. + //set + //{ + // SetSpecialHeaders(HttpKnownHeaderNames.IfModifiedSince, + // HttpProtocolUtils.date2string(value)); + //} + } + + /// + /// Constructs an instance of the HTTP Protocol class and initalizes it + /// to the basic header state. + /// + /// The Url object for which we're creating. + internal HttpWebRequest(Uri Url) + { + m_requestSent = false; + m_originalUrl = Url; + SendChunked = false; + m_keepAlive = true; + m_httpRequestHeaders = new WebHeaderCollection( + true, + Url.Scheme == Uri.UriSchemeWs || Url.Scheme == Uri.UriSchemeWss); + m_httpWriteMode = HttpWriteMode.None; + + m_contentLength = -1; + m_version = HttpVersion.Version11; + + m_allowWriteStreamBuffering = false; + + Method = "GET"; + + m_timeout = WebRequest.DefaultTimeout; + m_readWriteTimeout = DefaultReadWriteTimeout; + + // set the default proxy initially (this can be overriden by the Proxy property) + m_proxy = WebRequest.DefaultWebProxy; + + m_responseCreated = false; + } + + /// + /// Reset + /// + public void Reset() + { + m_requestSent = false; + m_responseCreated = false; + m_contentLength = -1; + m_httpWriteMode = HttpWriteMode.None; + m_httpRequestHeaders = new WebHeaderCollection(true); + } + + /// + /// Gets whether a response has been received from an Internet resource. + /// + /// true if a response has been received; otherwise, + /// false. + public bool HaveResponse + { + get { return (m_responseComplete); } + } + + /// + /// Adds a byte range header to the request for a specified range. + /// + /// The start of the range. + /// The end of the range. + public void AddRange(int from, int to) + { + AddRange("bytes", from, to); + } + + /// + /// Adds a range header to a request for a specific range from the + /// beginning or end of the requested data. + /// + /// Start of the range. The end of the range is the + /// end of the existing data. + public void AddRange(int range) + { + AddRange("bytes", range); + } + + /// + /// Adds a range header to a request for a specified range. + /// + /// The description of the range, such as + /// "bytes". + /// The start of the range. + /// The end of the range. + /// + /// would normally be + /// specified as "bytes", since this is the only range specifier + /// recognized by most HTTP servers. Setting + /// to some other string allows + /// support for custom range specifiers other than bytes. The + /// byte-range specifier is defined in RFC 2616 by the IETF. + public void AddRange(string rangeSpecifier, int from, int to) + { + + // Do some range checking before assembling the header + if ((from < 0) || (to < 0) || (from > to)) + { + throw new ArgumentOutOfRangeException(); + } + + // Add it + if (!AddRange(rangeSpecifier, from.ToString(), to.ToString())) + { + throw new InvalidOperationException(); + } + } + + /// + /// Adds a range header to a request for a specific range from the + /// beginning or end of the requested data. + /// + /// The description of the range, such as + /// "bytes". + /// The range value. + public void AddRange(string rangeSpecifier, int range) + { + if (!AddRange(rangeSpecifier, range.ToString(), (range >= 0) ? "" : null)) + { + throw new InvalidOperationException(); + } + } + + /// + /// Adds or extends a range header. + /// + /// Range specifier + /// Start of range + /// End of range + /// TBD + /// + /// Various range types can be specified via + /// , but only one type of Range + /// request will be made; for example, a byte-range request, or a + /// row-range request. Range types cannot be mixed. + /// + private bool AddRange(string rangeSpecifier, string from, string to) + { + // Checks for NULL rangeSpecifier + if (rangeSpecifier == null) + { + throw new ArgumentNullException(); + } + + // Checks for Valid characters in rangeSpecifier + if (!WebHeaderCollection.IsValidToken(rangeSpecifier)) + { + throw new ArgumentException(); + } + + // Add range specifier or appends to existing one + string curRange = m_httpRequestHeaders[HttpKnownHeaderNames.Range]; + + if ((curRange == null) || (curRange.Length == 0)) + { + curRange = rangeSpecifier + "="; + } + else + { + if (String.Compare(curRange.Substring(0, curRange.IndexOf('=')), rangeSpecifier) != 0) + { + return false; + } + + curRange = string.Empty; + } + + curRange += from.ToString(); + if (to != null) + { + curRange += "-" + to; + } + + m_httpRequestHeaders.SetAddVerified(HttpKnownHeaderNames.Range, curRange); + return true; + } + + /// + /// This function is called first in GetRequestStream() and throws exception + /// if conditions are not correct. + /// + private void ValidateGetRequestStream() + { + // TransferEncoding is set to a value and SendChunked is false. + if (TransferEncoding != null && SendChunked == false) + { + throw new InvalidOperationException(); + } + + // ProtocolViolationException The Method property is GET or HEAD. + if (m_method == "GET" || m_method == "HEAD") + { + throw new ProtocolViolationException("HTTP Method is incorrect: " + Method); + } + + // Condition for exception - KeepAlive is true, AllowWriteStreamBuffering is false, + // ContentLength is -1, SendChunked is false. + if (m_method == "PUT" || m_method == "POST") + { + if (ContentLength == -1 && SendChunked == false) + { + throw new ProtocolViolationException("Content length must be present for this request"); + } + } + } + + /// + /// Updates the HTTP WEB header collection to prepare it for request. + /// + private void PrepareHeaders() + { + // Depending on protocol version we update HTTP headers collection. + if (!(m_version.Equals(HttpVersion.Version10))) + { + if (m_httpWriteMode == HttpWriteMode.Write) + { + m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.ContentLength, m_contentLength.ToString()); + } + else if (m_httpWriteMode == HttpWriteMode.Chunked) + { + m_httpRequestHeaders.AddInternal(HttpKnownHeaderNames.TransferEncoding, "chunked"); + } + + // Set keepAlive header, we always send it, do not rely in defaults. + // Do not override it, if it's already in the request. This is used for websockets to request the upgrade of the connection + // Otherwise: we send "Connection:Close" or "Connection:Keep-Alive" + if (m_httpRequestHeaders[HttpKnownHeaderNames.Connection] == null) + { + string connectionValue; + if (m_keepAlive) + { + // According to RFC, should be lower case + connectionValue = "keep-alive"; + } + else + { + // According to RFC, should be lower case + connectionValue = "close"; + } + + m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.Connection, connectionValue); + } + } + + //1.0 path + else + { + //1.0 doesn't support chunking + SendChunked = false; + + //1.0 doesn't support keep alive + m_keepAlive = false; + + if (m_httpWriteMode == HttpWriteMode.Write) + { + m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.ContentLength, m_contentLength.ToString()); + } + } + + m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.Host, m_originalUrl.Host); + + // Adds user name and password for basic Http authentication. + if (m_NetworkCredentials != null && m_NetworkCredentials.AuthenticationType == AuthenticationType.Basic) + { // If credentials are supplied, we need to add header like "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" + // where QWxhZGRpbjpvcGVuIHNlc2FtZQ== is b64 encoded user name and password orginating as username:password. + string userInfo = ""; + if (m_NetworkCredentials.UserName != null) + { + userInfo += m_NetworkCredentials.UserName; + } + + userInfo += ":"; + if (m_NetworkCredentials.Password != null) + { + userInfo += m_NetworkCredentials.Password; + } + + // Encode user info. + byte[] buffer = Encoding.UTF8.GetBytes(userInfo); + string userNameAndPassEncoded = buffer != null ? Convert.ToBase64String(buffer) : ""; + string authValue = "Basic " + userNameAndPassEncoded; + m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.Authorization, authValue); + } + + m_requestSent = true; + } + + /// + /// Removes the given stream from the connection pool + /// + internal static void RemoveStreamFromPool(InputNetworkStreamWrapper stream) + { + lock (m_ConnectedStreams) + { + if (m_ConnectedStreams.Contains(stream)) + { + m_ConnectedStreams.Remove(stream); + } + } + } + + /// + /// Returns network stream connected to server. It could be a proxy or a + /// real server Uri. + /// + /// Uri that describes the proxy server. + /// Uri that describes the target (real) server. + /// Nerwork stream connected to server. + private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targetServer) + { + InputNetworkStreamWrapper retStream = null; + + // Create a socket and set reuse true. + // But before creating new socket we look in the list of existing sockets. If socket for this host already + // exist - use it. No need to create new socket. + string remoteServer = targetServer.Host + ":" + targetServer.Port; + lock (m_ConnectedStreams) + { + ArrayList removeStreamList = new ArrayList(); + + for (int i = 0; i < m_ConnectedStreams.Count; i++) + { + InputNetworkStreamWrapper inputStream = (InputNetworkStreamWrapper)m_ConnectedStreams[i]; + + if (inputStream.m_rmAddrAndPort == remoteServer && !inputStream.m_InUse) + { + // Re-use the connected socket. + // But first we need to know that socket is not closed. + try + { + // Non-blocking liveness check: Available == 0 after a ready SelectRead poll means the peer closed the connection. + bool peerClosed = inputStream.m_Socket.Poll(0, SelectMode.SelectRead) && inputStream.m_Socket.Available == 0; + if (!peerClosed) + { + // No exception, good we can condtinue and re-use connected stream. + // Control flow returning here means we're now using a persistent connection. + inputStream.m_InUse = true; + inputStream.m_lastUsed = DateTime.UtcNow; + + retStream = inputStream; + break; + } + else + { + removeStreamList.Add(inputStream); + } + } + catch (Exception) + { + removeStreamList.Add(inputStream); + } + + } + } + + for (int i = 0; i < removeStreamList.Count; i++) + { + InputNetworkStreamWrapper removeStream = (InputNetworkStreamWrapper)removeStreamList[i]; + + // Means socket was closed. Remove it from the list. + m_ConnectedStreams.Remove(removeStream); + + removeStream.Dispose(); + } + } + + if (retStream == null) + { + // No persistent connection found. Need to establish new one. + IPAddress address = null; + UriHostNameType hostNameType = proxyServer.HostNameType; + if (hostNameType == UriHostNameType.IPv4) + { + address = IPAddress.Parse(proxyServer.Host); + } + else if (hostNameType == UriHostNameType.Dns) + { + IPHostEntry hostEntry = null; + + try + { + hostEntry = Dns.GetHostEntry(proxyServer.Host); + } + catch (SocketException se) + { + throw new WebException("host not available", se, WebExceptionStatus.ConnectFailure, null); + } + + int addressListSize = hostEntry.AddressList.Length; + for (int i = 0; i < addressListSize; i++) + { + if ((address = hostEntry.AddressList[i]) != null) + { + break; + } + } + + if (address == null) + { + throw new WebException("Unable to resolve Dns entry to valid IPv4 Address", WebExceptionStatus.NameResolutionFailure); + } + } + else + { + throw new WebException("Only IPv4 or Dns host names allowed."); + } + + // If socket was not found in waiting connections, then we create new one. + Socket socket = null; + socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + } + catch (Exception) + { + // We can safely ignore exceptions + } + + try + { + socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); + } + catch (Exception) + { + // We can safely ignore exceptions + } + + try + { + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, m_keepAlive); + } + catch (Exception) + { + // We can safely ignore exceptions + } + + + // Connect to remote endpoint + try + { + IPEndPoint remoteEP = new IPEndPoint(address, proxyServer.Port); + socket.Connect((EndPoint)remoteEP); + } + catch (SocketException e) + { + // need to close socket, otherwise this will cause an out of memory exception + socket.Close(); + + throw new WebException("connection failed", e, WebExceptionStatus.ConnectFailure, null); + } + + bool isSecured = false; + + if (m_originalUrl.Scheme == Uri.UriSchemeHttps + || m_originalUrl.Scheme == Uri.UriSchemeWss) + { + isSecured = true; + } + + // We have a connected socket. Create request stream + retStream = new InputNetworkStreamWrapper(new NetworkStream(socket), socket, true, proxyServer.Host + ":" + proxyServer.Port); + + // For Secured connectrions, proxy works differently + if (isSecured) + { + try + { + // If proxy is set, then for https/wss we need to send "CONNECT" command to proxy. + // Once this command is send, the socket from proxy works as if it is the socket to the destination server. + if (proxyServer != targetServer) + { + String request = "CONNECT " + remoteServer + " HTTP/" + ProtocolVersion + "\r\n\r\n"; + Byte[] bytesToSend = Encoding.UTF8.GetBytes(request); + retStream.Write(bytesToSend, 0, bytesToSend.Length); + + // Now proxy should respond with the connected status. If it is successul, then we are good to go. + CoreResponseData respData = ParseHTTPResponse(retStream, m_keepAlive); + if (respData.m_statusCode != (int)HttpStatusCode.OK) + { + throw new WebException("Proxy returned " + respData.m_statusCode, WebExceptionStatus.ConnectFailure); + } + } + + // Once connection established need to create secure stream and authenticate server. + SslStream sslStream = new SslStream(retStream.m_Socket); + + sslStream.SslVerification = _sslVerification; + + // Throws exception if it fails + sslStream.AuthenticateAsClient(m_originalUrl.Host, null, m_caCert, m_sslProtocols); + + // Changes the stream to SSL stream. + retStream.m_Stream = sslStream; + + // Changes the address. Originally socket was connected to proxy, now as if it connected to m_originalUrl.Host on m_originalUrl.Port + retStream.m_rmAddrAndPort = m_originalUrl.Host + ":" + m_originalUrl.Port; + } + catch + { + // Proxy CONNECT failure or SSL handshake failure: retStream is not usable and + // isn't referenced anywhere else yet, so it must be closed here or the socket leaks. + retStream.Dispose(); + throw; + } + } + + // Check keepAlive before creating a persistent connection + if (m_keepAlive) + { + lock (m_ConnectedStreams) + { + m_ConnectedStreams.Add(retStream); + + // if the current stream list was empty then start the timer that drops unused connections. + if (m_ConnectedStreams.Count == 1) + { + m_DropOldConnectionsTimer.Change(HttpConstants.DefaultKeepAliveMilliseconds, System.Threading.Timeout.Infinite); + } + } + } + } + + return retStream; + } + + /// + /// Submits request to the WEB server. + /// + private void SubmitRequest() + { + // We have connected socket. Create request stream + // If proxy is set - connect to proxy server. + if (m_requestStream == null) + { + if (m_proxy == null) + { + // Direct connection to target server. + m_requestStream = EstablishConnection(m_originalUrl, m_originalUrl); + } + else + { + // Connection through proxy. We create network stream connected to proxy + Uri proxyUri = m_proxy.GetProxy(m_originalUrl); + + if (m_originalUrl.Scheme == Uri.UriSchemeHttps + || m_originalUrl.Scheme == Uri.UriSchemeWss) + { + // For HTTPs we still need to know the target name to decide on persistent connection. + m_requestStream = EstablishConnection(proxyUri, m_originalUrl); + } + else + { + // For normal HTTP all requests go to proxy + m_requestStream = EstablishConnection(proxyUri, proxyUri); + } + } + } + // Call EstablishConnection() for the case where (m_requestStream != null) + // Look for a persistent connection + else + { + m_requestStream = EstablishConnection(m_originalUrl, m_originalUrl); + } + + + + if (m_requestStream == null) + { + // Connection could not be established + m_requestSent = false; + return; + } + + // We have connected stream. Set the timeout from HttpWebRequest + m_requestStream.WriteTimeout = m_readWriteTimeout; + m_requestStream.ReadTimeout = m_readWriteTimeout; + + // Now we need to write headers. First we update headers. + PrepareHeaders(); + + // Now send request string and headers. + byte[] dataToSend = GetHTTPRequestData(); + +#if DEBUG // In debug mode print the request. It helps a lot to troubleshoot the issues. + int byteUsed, charUsed; + bool completed = false; + char[] charBuf = new char[dataToSend.Length]; + UTF8decoder.Convert(dataToSend, 0, dataToSend.Length, charBuf, 0, charBuf.Length, true, out byteUsed, out charUsed, out completed); + string strSend = new string(charBuf); + Debug.WriteLine(strSend); +#endif + // Writes this data to the network stream. + m_requestStream.Write(dataToSend, 0, dataToSend.Length); + m_requestSent = true; + } + + + /// + /// Reads and parses HTTP response from server. + /// After return of function HTTP response is read. + /// + /// Network stream connected to server. + /// TBD + /// CoreResponseData that describes server response. + private CoreResponseData ParseHTTPResponse(InputNetworkStreamWrapper inStream, bool defaultKeepAlive) + { + // CoreResponseData keeps all the information of the response. + CoreResponseData ret = new CoreResponseData(); + // maximumHeadersLength is maximum total length of http header. Basically this is amount + // of memory used for headers. + int headersLength = m_maxResponseHeadersLen == -1 ? 0x7FFFFFFF : m_maxResponseHeadersLen * 1024; + + ret.m_shouldClose = !defaultKeepAlive; + // Parse the request line. + string line = inStream.Read_HTTP_Line(HttpConstants.maxHTTPLineLength).Trim(); + + // Cutoff white spaces + int currentOffset = 0; + for (; currentOffset < line.Length && ' ' != line[currentOffset]; ++currentOffset) ; + + // find HTTP version, read http/1.x + string httpVersionString = line.Substring(0, currentOffset).ToLower(); + if (httpVersionString.Equals("http/1.1")) + { + ret.m_version = HttpVersion.Version11; + } + else if (httpVersionString.Equals("http/1.0")) + { + ret.m_version = HttpVersion.Version10; + } + else + { + ret.m_status = WebExceptionStatus.ServerProtocolViolation; + ret.m_exceptionMessage = "Unknown http version: " + httpVersionString; + return ret; + } + + //advance to the status code + for (; currentOffset < line.Length && ' ' == line[currentOffset]; ++currentOffset) ; + + // Read the status code + int codeStart = currentOffset; + for (; currentOffset < line.Length && ' ' != line[currentOffset]; ++currentOffset) ; + int statusCode = -1; + try + { + string statusCodeStr = + line.Substring(codeStart, + currentOffset - codeStart); + statusCode = Convert.ToInt32(statusCodeStr); + } + catch (Exception e) + { + ret.m_status = WebExceptionStatus.ServerProtocolViolation; + ret.m_exceptionMessage = "Missing status code in HTTP reply"; + ret.m_innerException = e; + return ret; + } + + // If we get here - status code should be read. + ret.m_statusCode = statusCode; + + // Advance to the status message. The message is optional + for (; currentOffset < line.Length && ' ' != line[currentOffset]; ++currentOffset) ; + ret.m_statusDescription = line.Substring(currentOffset); + + ret.m_headers = new WebHeaderCollection(true); + ret.m_chunked = false; + ret.m_contentLength = -1; + + while ((line = inStream.Read_HTTP_Header(HttpConstants.maxHTTPLineLength)).Length > 0) + { + // line.Length is used for the header. Substruct it. + headersLength -= line.Length; + // If total length used for header is exceeded, we break + if (headersLength < 0) + { + ret.m_status = WebExceptionStatus.ServerProtocolViolation; + ret.m_exceptionMessage = "Headers size exceed limit"; + return ret; + } + + // Now parse the header. + int sepIdx = line.IndexOf(':'); + if (sepIdx == -1) + { + ret.m_status = WebExceptionStatus.ServerProtocolViolation; + ret.m_exceptionMessage = "Illegal header format: " + line; + return ret; + } + + string headerName = line.Substring(0, sepIdx); + string headerValue = line.Substring(sepIdx + 1).TrimStart(null); + string matchableHeaderName = headerName.ToLower(); + + ret.m_headers.AddInternal(headerName, headerValue); + if (matchableHeaderName.Equals("content-length")) + { + try + { + ret.m_contentLength = Convert.ToInt32(headerValue); + + // set the response stream length for the input stream, so that an EOF will be read + // if the caller tries to read base the response content length + inStream.m_BytesLeftInResponse = ret.m_contentLength; + } + catch (Exception e) + { + ret.m_status = + WebExceptionStatus.ServerProtocolViolation; + ret.m_exceptionMessage = "Content length NAN: " + headerValue; + ret.m_innerException = e; + return ret; + } + } + else if (matchableHeaderName.Equals("transfer-encoding")) + { + if (headerValue.ToLower().IndexOf("chunked") != -1) + { + ret.m_chunked = true; + } + } + else if (matchableHeaderName.Equals("connection")) + { + if (headerValue.ToLower().IndexOf(HttpKnownHeaderValues.close) != -1) + { + ret.m_shouldClose = true; + } + } + } + return ret; + } + + /// + /// Event handler for the web request timeout. This handler will be invoked if the response takes longer than the value + /// indicated by the property Timeout. + /// + /// + private void OnRequestTimeout(object arg) + { + if (m_requestStream != null && m_requestStream.m_Socket != null) + { + try + { + // Close the socket to kill the operation + m_requestStream.m_Socket.Close(); + } + catch + { + } + finally + { + m_requestStream.m_InUse = false; + } + } + } + + /// + /// Returns a response from an Internet resource. Overrides the + /// WebRequest. + /// method. + /// + /// The response from the Internet resource. + public override WebResponse GetResponse() + { + HttpWebResponse response = null; + + try + { + // If response was not sent, Submit the request. + if (!m_requestSent) + { + SubmitRequest(); + } + + // Need to check m_requestSent after SubmitRequest() + if (!m_requestSent) + { + return response; + } + + CoreResponseData respData = null; + + // reset the total response bytes for the new request. + m_requestStream.m_BytesLeftInResponse = -1; + + // create the request timeout timer. This will kill the operation if it takes longer than specified by the Timeout property. + // The underlying socket will be closed to end the web request + using (Timer tmr = new Timer(new TimerCallback(OnRequestTimeout), null, m_timeout, System.Threading.Timeout.Infinite)) + { + // Processes response from server. Request stream should already be there. + + respData = ParseHTTPResponse(m_requestStream, m_keepAlive); + + if (respData.m_statusCode == (int)HttpStatusCode.Continue) + { + if (m_continueDelegate != null) + { + m_continueDelegate(respData.m_statusCode, respData.m_headers); + } + else + { + respData = ParseHTTPResponse(m_requestStream, m_keepAlive); + } + } + } + + response = new HttpWebResponse(m_method, m_originalUrl, respData, this); + + // Now we look if response has chunked encoding. If it is chunked, we need to set flag in m_requestStream we return. + m_requestStream.m_EnableChunkedDecoding = respData.m_chunked; + + // Currently the request and response are the same network streams, but we optimize later. + response.SetResponseStream(m_requestStream); + + m_responseStatus = response.StatusCode; + + m_responseCreated = true; + m_requestStream.m_InUse = false; // Persistent connections are not yet supported, but they wouldn't work without this. + } + catch (SocketException se) + { + if (m_requestStream != null) + { + m_requestStream.m_InUse = false; + + if (m_requestStream.m_Socket != null) + { + this.m_requestStream.m_Socket.Close(); + } + } + throw new WebException("GetResponse() failed", se); + } + catch (Exception e) + { + // Same cleanup as above: without it, any failure between the connection being + // established and the response being fully constructed (bad status line, malformed + // headers, a throwing continue-delegate, etc.) leaks the connection/socket. + if (m_requestStream != null) + { + m_requestStream.m_InUse = false; + + if (m_requestStream.m_Socket != null) + { + this.m_requestStream.m_Socket.Close(); + } + } + throw new WebException("GetResponse() failed", e); + } + + + return response; + } + + /// + /// Submits a request with HTTP headers to the server, and returns a + /// Stream object to use to write request data. + /// + /// A Stream to use to write request data. + /// Used for POST of PUT requests. + public override Stream GetRequestStream() + { + // Validates the call to GetRequestStream. Throws exception on errors. + ValidateGetRequestStream(); + + // Submits the request. + try + { + SubmitRequest(); + } + catch + { + if (m_requestStream != null) + { + RemoveStreamFromPool(m_requestStream); + m_requestStream.Dispose(); + } + throw; + } + + // Return the stream + return m_requestStream.CloneStream(); + } + + /// + /// Constucts WEB exception if error is detected during parsing. + /// + /// Inner exception network exception + /// Partially constructed HttpWebResponse + /// WebException instance + private static WebException protocolError(Exception inner, + HttpWebResponse resp) + { + HttpStatusCode statusCode = resp.StatusCode; + int sr = (int)statusCode; + string message = "(" + ((int)statusCode) + ")"; + string description; + description = resp.StatusDescription; + if (description != null && description.Length > 0) + message += " " + description; + message = "Server returned error" + message; + return new WebException(message, inner, + WebExceptionStatus.ProtocolError, resp); + } + + internal bool m_sentHeaders = false; + + private bool hasEntityData() + { + if (m_httpWriteMode != HttpWriteMode.None) + return true; + else + return false; + } + + private bool canWrite() + { + return !KnownVerbs.GetHttpVerbType(Method).m_ContentBodyNotAllowed; + } + + /// + /// Retrieves HTTP request as bytes array. Used to create a request + /// message. + /// + /// Byte array with HTTP request. This data is sent through network. + private byte[] GetHTTPRequestData() + { + //step 1 - compute the length of the headers. + + string statusLine; + + // Connect verbs require CONNECT host:port + if (Method.ToUpper().Equals("CONNECT")) + { + statusLine = "CONNECT " + Address.Host + ":" + Address.Port + " HTTP/" + ProtocolVersion + "\r\n"; + } + else if (m_proxy != null + && m_originalUrl.Scheme != Uri.UriSchemeHttps + && m_originalUrl.Scheme != Uri.UriSchemeWss) + { + statusLine = Method + " " + Address.AbsoluteUri + " HTTP/" + ProtocolVersion + "\r\n"; + } + else + { + // statusLine = Method + " " + Address.AbsolutePath + " HTTP/" + ProtocolVersion + "\r\n"; // .PathAndQuery + statusLine = Method + " " + Address.PathAndQuery + " HTTP/" + ProtocolVersion + "\r\n"; + } + + //most intrinsic headers are stored in the webheaders class + //content length is not. + int headersLength = statusLine.Length; + + //extra header lengths. Includes extension headers. + headersLength += Headers.byteLength(); + + byte[] headerBytes = new byte[headersLength]; + + int currentOffset = 0; + //store the request line + currentOffset += copyString(statusLine, headerBytes, + currentOffset); + + //now for the general headers + currentOffset += Headers.copyTo(headerBytes, currentOffset); + + return headerBytes; + } + + /// + /// Converts array to string by casting bytes to chars. + /// + /// Array with byte data. + /// Offset to start convertion. + /// Count of bytes to convert to string. + /// String converted from byte array. + private static string toEAscii(byte[] data, int offset, int count) + { + char[] output = new char[count]; + for (int i = 0; i < count; ++i) + { + output[i] = (char)data[offset + i]; + } + + return new string(output, 0, count); + } + + /// + /// Convert string to array of bytes + /// + /// string to convert + /// array of bytes converted from string + internal static byte[] fromEAscii(string data) + { + byte[] ret = new byte[data.Length]; + for (int i = 0; i < data.Length; ++i) + { + ret[i] = (byte)data[i]; + } + + return ret; + } + + /// + /// This function returns true if the response code from the server + /// (i.e. 304) MUST NOT have any entity data. I will artificially set + /// the content length in the stream to zero, so that reading... + /// + /// HTTP response code + /// true if the specified response code is one of the + /// defined values; otherwise, false. + private bool setContentLengthToZero(HttpStatusCode responseCode) + { + switch (responseCode) + { + case HttpStatusCode.SwitchingProtocols: + case HttpStatusCode.ResetContent: + case HttpStatusCode.NotModified: + case HttpStatusCode.NoContent: + case HttpStatusCode.UseProxy: + return true; + default: + return false; + } + } + + /// + /// Copies a string into an array of bytes. + /// + /// A String to copy. + /// Output array. + /// Offset to start placing data in array. + /// Count of bytes copied + private static int copyString(String src, byte[] bytes, int offset) + { + int i; + for (i = 0; i < src.Length; ++i) + bytes[offset + i] = (byte)src[i]; + return i; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs new file mode 100644 index 0000000..45790af --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs @@ -0,0 +1,343 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + using System.IO; + + /// + /// Handles retrieval of HTTP Response headers, and handles data reads. + /// + /// This class should never be created directly, but rather should + /// be created by the + /// HttpWebRequest. + /// method. + /// + public class HttpWebResponse : WebResponse + { + /// + /// The Web request object that was used for this response. + /// We need it to access KeepAlive property. + /// + private HttpWebRequest m_httpWebRequest; + + /// + /// Response URI generated by the request. + /// + private Uri m_url; + + /// + /// response Method gernated by the request + /// + private string m_method; + + /// + /// ConnectStream - for reading actual data + /// + private InputNetworkStreamWrapper m_responseStream; + + /// + /// Collection of HTTP headers returned by server + /// + private WebHeaderCollection m_httpResponseHeaders; + + /// + /// Content Length needed for semantics, -1 if chunked + /// + private long m_contentLength = -1; + + /// + /// The HTTP version for the response. + /// + private Version m_version; + + /// + /// The status code from the response. + /// + private int m_statusCode; + + /// + /// the description of the status returned by the server. + /// + private String m_statusDescription; + + /// + /// Retrieves a response header object. + /// + /// A WebHeaderCollection that contains the header + /// information returned with the response. + public override WebHeaderCollection Headers + { + get + { + return m_httpResponseHeaders; + } + } + + /// + /// Gets the length of the content returned by the request. + /// + /// + /// This property contains the value of the Content-Length header + /// that is returned with the response. If the Content-Length + /// header is not set in the response, this property is set to -1. + /// + /// The number of bytes returned by the request. Content length + /// does not include header information. + public override long ContentLength + { get { return m_contentLength; } } + + /// + /// TBD + /// + /// TBD + internal long InternalContentLength + { set { m_contentLength = value; } } + + /// + /// Gets the method that is used to encode the body of the response. + /// + /// + /// This property contains the value of the Content-Encoding + /// header returned with the response; that is, the encoding used for + /// the response. + /// + /// A string that describes the method that is used to encode the + /// body of the response. + public String ContentEncoding + { + get + { + return GetResponseHeader(HttpKnownHeaderNames.ContentEncoding); + } + } + + /// + /// Gets the content type of the response. + /// + /// A string that contains the content type of the response. + /// + /// + /// This property contains the value of the Content-Type header + /// returned with the response. + /// + public override string ContentType + { + get + { + return GetResponseHeader(HttpKnownHeaderNames.ContentType); + } + } + + /// + /// Gets the name of the server that sent the response. + /// + /// A string that contains the name of the server that sent the + /// response. + public string Server + { + get + { + return GetResponseHeader(HttpKnownHeaderNames.Server); + } + } + + /// + /// Gets the value of the Last-Modified header, which indicates the last + /// time the document was modified. + /// + /// A that contains the date and + /// time that the contents of the response were modified. + public DateTime LastModified + { + get + { + + string lastmodHeaderValue = + m_httpResponseHeaders[HttpKnownHeaderNames.LastModified]; + if (lastmodHeaderValue == null) + { + return DateTime.UtcNow; + } + + return HttpProtocolUtils.string2date(lastmodHeaderValue); + } + } + + /// + /// Gets the status of the HTTP response, as a number. + /// + /// + /// For status code values, see . + /// + /// One of the HttpStatusCode values. + public HttpStatusCode StatusCode + { + get { return (HttpStatusCode)m_statusCode; } + } + + /// + /// Gets the status description returned with the response. + /// + /// A string that describes the status of the response. + public string StatusDescription + { + get + { + return m_statusDescription; + } + } + + /// + /// Gets the version of the HTTP protocol that is used in the response. + /// + /// A Version that contains the HTTP protocol version of + /// the response. + public Version ProtocolVersion + { + get { return m_version; } +#if DEBUG + set { m_version = value; } +#endif + } + + /// + /// Gets the stream used for reading the body of the response from the + /// server. + /// + /// A network stream to read body of the message. + public override Stream GetResponseStream() + { + Stream retVal = m_responseStream.CloneStream(); + + m_responseStream.m_dataStart = m_responseStream.m_dataEnd = 0; + + return retVal; + } + + /// + /// Sets the response stream. + /// + /// + /// + /// Used internally during creation of HttpWebResponse. + /// + internal void SetResponseStream(InputNetworkStreamWrapper stream) + { + m_responseStream = stream; + } + + /// + /// Creates WEB response based on information known just after parsing the status line. + /// + /// Http Verb + /// TBD + /// Response data + /// TBD + internal HttpWebResponse(string method, Uri responseUrl, + CoreResponseData data, HttpWebRequest httpWebReq) + { + m_httpWebRequest = httpWebReq; + m_method = method; + m_url = responseUrl; + m_version = data.m_version; + m_statusCode = data.m_statusCode; + m_statusDescription = data.m_statusDescription; + + m_httpResponseHeaders = data.m_headers; + + m_contentLength = data.m_contentLength; + } + + /// + /// Gets the contents of a header that was returned with the response. + /// + /// HTTP header to search for matching header on. + /// The matched entry, if found. + /// + public string GetResponseHeader(string headerName) + { + string headerValue = m_httpResponseHeaders[headerName]; + return ((headerValue == null) ? String.Empty : headerValue); + } + + /// + /// Gets the final Response URI, that includes any + /// changes that may have transpired from the orginal Request. + /// + /// A Uri that contains the URI of the Internet resource + /// that responded to the request. + public override Uri ResponseUri { get { return m_url; } } + + /// + /// Gets the method that is used to return the response. + /// + /// A string that contains the HTTP method that is used to return + /// the response. + public string Method { get { return m_method; } } + + /// + /// Closes a response stream, if present. + /// + /// Not used. + protected override void Dispose(bool disposing) + { + if (m_responseStream != null) + { + bool closeConnection = true; + if (m_httpWebRequest.KeepAlive) + { + string connValue = null; + + // Check if server have sent use "Connection:Close" + if (m_httpResponseHeaders != null) connValue = m_httpResponseHeaders[HttpKnownHeaderNames.Connection]; + + // If server had not send this header or value is not "close", then we keep connection. + closeConnection = connValue == null || connValue.ToLower() == HttpKnownHeaderValues.close; + } + + // If it is not in the list - Add it + if (closeConnection) + { + HttpWebRequest.RemoveStreamFromPool(m_responseStream); + + // Closing connection socket + m_responseStream.Dispose(); + } + else + { + m_responseStream.ReleaseStream(); + } + + // Set flag that we already completed work on this stream. + m_responseStream = null; + } + + base.Dispose(disposing); + } + + /// + /// Closes the response stream. + /// + /// + /// The method closes the response stream and releases the connection to the resource for reuse by other requests. + /// You should not access any properties of the object after the call to the method. + /// You must call either the or the method to close the stream and release the connection for reuse. It is not necessary to call both and , but doing so does not cause an error. Failure to close the stream can cause your application to run out of connections. + /// + public override void Close() + { + if (m_responseStream != null) + { + // Closing connection socket + m_responseStream.Dispose(); + + // Set flag that we already completed work on this stream. + m_responseStream = null; + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs b/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs new file mode 100644 index 0000000..bbcab69 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// The interface to let its consumer know work is done + /// + internal interface IKnowWhenDone + { + /// + /// The property reflects if work is done + /// + /// + /// True when work is done. + /// + bool IsDone { get; } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs b/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs new file mode 100644 index 0000000..1bae096 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// The interface for creating class + /// objects. + /// + public interface IWebRequestCreate + { + /// + /// Creates an instance of a class derived from + /// WebRequest. + /// + /// The URI for initialization of the class that is + /// derived from WebRequest. + /// + /// An instance of the class that is derived from + /// WebRequest. + /// + WebRequest Create(Uri uri); + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs b/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs new file mode 100644 index 0000000..5a26c99 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + internal class WebRequestPrefixElement + { + public string Prefix; + public IWebRequestCreate Creator; + + public WebRequestPrefixElement(string P, IWebRequestCreate C) + { + Prefix = P; + Creator = C; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.Internal.cs b/src/Cosmos.Network.Http/Http/System.Net.Internal.cs new file mode 100644 index 0000000..bb53448 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.Internal.cs @@ -0,0 +1,328 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Used during parsing to capture all the information contained in the http + /// status line and headers. + /// + internal class CoreResponseData + { + // Basic response information. + internal int m_statusCode; + internal string m_statusDescription; + internal WebHeaderCollection m_headers; + internal Version m_version; + + // Variables for the end of entity mark. + internal bool m_chunked; + internal long m_contentLength; + internal bool m_shouldClose; + + // The web status. + internal WebExceptionStatus m_status; + //here is the error message string associated with the status code. + //Its used for an exception message. + internal string m_exceptionMessage; + //here's the inner exception from the parse (for the WebException) + internal Exception m_innerException; + } + + /// + /// Contains known HTTP header names. + /// + public class HttpKnownHeaderNames + { + /// The Cache-Control HTTP header. + public const string CacheControl = "Cache-Control"; + /// The Connection HTTP header. + public const string Connection = "Connection"; + /// The Date HTTP header. + public const string Date = "Date"; + /// The Keep-Alive HTTP header. + public const string KeepAlive = "Keep-Alive"; + /// The Pragma HTTP header. + public const string Pragma = "Pragma"; + /// The Proxy-Connection HTTP header. + public const string ProxyConnection = "Proxy-Connection"; + /// The Trailer HTTP header. + public const string Trailer = "Trailer"; + /// The Transfer-Encoding HTTP header. + public const string TransferEncoding = "Transfer-Encoding"; + /// The Upgrade HTTP header. + public const string Upgrade = "Upgrade"; + /// The Via HTTP header. + public const string Via = "Via"; + /// The Warning HTTP header. + public const string Warning = "Warning"; + /// The Content-Length HTTP header. + public const string ContentLength = "Content-Length"; + /// The Content-Type HTTP header. + public const string ContentType = "Content-Type"; + /// The Content-ID HTTP header. + public const string ContentID = "Content-ID"; + /// The Content-Encoding HTTP header. + public const string ContentEncoding = "Content-Encoding"; + /// The Content-Transfer-Encoding HTTP header. + public const string ContentTransferEncoding = "Content-Transfer-Encoding"; + /// The Content-Language HTTP header. + public const string ContentLanguage = "Content-Language"; + /// The Content-Location HTTP header. + public const string ContentLocation = "Content-Location"; + /// The Content-Range HTTP header. + public const string ContentRange = "Content-Range"; + /// The Expires HTTP header. + public const string Expires = "Expires"; + /// The Last-Modified HTTP header. + public const string LastModified = "Last-Modified"; + /// The Age HTTP header. + public const string Age = "Age"; + /// The Location HTTP header. + public const string Location = "Location"; + /// The Proxy-Authenticate HTTP header. + public const string ProxyAuthenticate = "Proxy-Authenticate"; + /// The Retry-After HTTP header. + public const string RetryAfter = "Retry-After"; + /// The Server HTTP header. + public const string Server = "Server"; + /// The Set-Cookie HTTP header. + public const string SetCookie = "Set-Cookie"; + /// The Set-Cookie2 HTTP header. + public const string SetCookie2 = "Set-Cookie2"; + /// The Vary HTTP header. + public const string Vary = "Vary"; + /// The WWW-Authenticate HTTP header. + public const string WWWAuthenticate = "WWW-Authenticate"; + /// The Accept HTTP header. + public const string Accept = "Accept"; + /// The Accept-Charset HTTP header. + public const string AcceptCharset = "Accept-Charset"; + /// The Accept-Encoding HTTP header. + public const string AcceptEncoding = "Accept-Encoding"; + /// The Accept-Language HTTP header. + public const string AcceptLanguage = "Accept-Language"; + /// The Authorization HTTP header. + public const string Authorization = "Authorization"; + /// The Cookie HTTP header. + public const string Cookie = "Cookie"; + /// The Cookie2 HTTP header. + public const string Cookie2 = "Cookie2"; + /// The Expect HTTP header. + public const string Expect = "Expect"; + /// The From HTTP header. + public const string From = "From"; + /// The Host HTTP header. + public const string Host = "Host"; + /// The If-Match HTTP header. + public const string IfMatch = "If-Match"; + /// The If-Modified-Since HTTP header. + public const string IfModifiedSince = "If-Modified-Since"; + /// The If-None-Match HTTP header. + public const string IfNoneMatch = "If-None-Match"; + /// The If-Range HTTP header. + public const string IfRange = "If-Range"; + /// The If-Unmodified-Since HTTP header. + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + /// The Max-Forwards HTTP header. + public const string MaxForwards = "Max-Forwards"; + /// The Proxy-Authorization HTTP header. + public const string ProxyAuthorization = "Proxy-Authorization"; + /// The Referer HTTP header. + public const string Referer = "Referer"; + /// The Range HTTP header. + public const string Range = "Range"; + /// The User-Agent HTTP header. + public const string UserAgent = "User-Agent"; + /// The Content-MD5 HTTP header. + public const string ContentMD5 = "Content-MD5"; + /// The ETag HTTP header. + public const string ETag = "ETag"; + /// The TE HTTP header. + public const string TE = "TE"; + /// The Allow HTTP header. + public const string Allow = "Allow"; + /// The Accept-Ranges HTTP header. + public const string AcceptRanges = "Accept-Ranges"; + /// The MIME-Version HTTP header. + public const string MimeVersion = "MIME-Version"; + /// + /// The Sec-WebSocket-Accept HTTP header. + /// + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + /// + /// The Sec-WebSocket-Protocol HTTP header. + /// + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + /// + /// The Sec-WebSocket-Version HTTP header. + /// + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; + /// + /// The Sec-WebSocket-Key HTTP header. + /// + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + + } + + /// + /// TBD + /// + internal class HttpKnownHeaderValues + { + /// TBD + public const string close = "close"; + } + + /* + File: httpreq.cs + + Summary: Basic HTTP Protocol support for HttpWeb request Class. + Contains the implimention of various HTTP primitives. + + Classes: HttpWebReques + + Functions: + + ---------------------------------------------------------------------------- + This file is part of the Microsoft COM+ Netclasses. + + Copyright (C) 1998-1999 Microsoft Corporation. All rights reserved. + ==========================================================================+*/ + + // - seperate HTTP header names/header data + // - improve/check var/func naming + // - stress parsering cases + // - Chunked transfer needs a better algorithm, to prevent over copying + // - keep-alive + + /// + /// Represents the method that notifies callers when a continue response is + /// received by the client. + /// + /// The numeric value of the HTTP status from the + /// server. + /// The headers returned with the 100-continue + /// response from the server. + public delegate void HttpContinueDelegate(int StatusCode, WebHeaderCollection httpHeaders); + + /// + /// Controls the way an entity body is posted. + /// + internal enum HttpWriteMode + { + Chunked = 1, + Write = 2, + None = 0, + Prebuffer = 3 + } + + /// + /// Known Verbs are verbs that require special handling. + /// + internal class KnownVerbs + { + + // This is a placeholder for Verb properties. The following two bools can most likely be + // combined into a single Enum type. And the Verb can be incorporated. + + internal struct HttpVerb + { + // require content body to be sent + internal bool m_RequireContentBody; + // not allowed to send content body + internal bool m_ContentBodyNotAllowed; + // special semantics for a connect request + internal bool m_ConnectRequest; + // response will not have content body + internal bool m_ExpectNoContentResponse; + + /* + * XXX + * * Wed 10/10/2001 + * This should only be used by KnownVerbs + * */ + internal string m_name; + internal HttpVerb(string name, bool RequireContentBody, bool ContentBodyNotAllowed, bool ConnectRequest, bool ExpectNoContentResponse) + { + + m_name = name; + m_RequireContentBody = RequireContentBody; + m_ContentBodyNotAllowed = ContentBodyNotAllowed; + m_ConnectRequest = ConnectRequest; + m_ExpectNoContentResponse = ExpectNoContentResponse; + } + } + + // Force an an init, before we use them + private static HttpVerb[] m_knownVerbs; + static KnownVerbs() + { + m_knownVerbs = new HttpVerb[5]; + m_knownVerbs[0] = new HttpVerb("GET", false, true, false, false); + m_knownVerbs[1] = new HttpVerb("POST", true, false, false, false); + m_knownVerbs[2] = new HttpVerb("HEAD", false, true, false, true); + m_knownVerbs[3] = new HttpVerb("PUT", true, false, false, false); + /* + * XXX + * * Mon 02/25/2002 + * I've changed this from the desktop. There is no entity response + * in a connect request. It won't be there, and don't close it. + * */ + m_knownVerbs[4] = new HttpVerb("CONNECT", false, true, true, true); + + // default Verb + DefaultVerb = new HttpVerb("", false, false, false, false); + } + + // default verb, contains default properties for an unidentifable verb. + private static HttpVerb DefaultVerb; + + internal static HttpVerb GetHttpVerbType(String name) + { + for (int i = 0; i < m_knownVerbs.Length; ++i) + { + HttpVerb v = m_knownVerbs[i]; + if (0 == string.Compare(v.m_name, name)) + return v; + } + + return DefaultVerb; + } + } + + /// + /// A collection of utility functions for HTTP usage. + /// + internal class HttpProtocolUtils + { + private HttpProtocolUtils() + { + } + + /// + /// Parse String to DateTime format. + /// + /// String with date. + /// DateTime object that represent the same value as in input string. + internal static DateTime + string2date(String S) + { + DateTime dtOut; + + if (HttpDateParse.ParseHttpDate( + S, + out dtOut)) + { + return dtOut; + } + else + { + throw new Exception("Invalid Date in HTTP header"); + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs b/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs new file mode 100644 index 0000000..03563c9 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Class that keeps user name and password. + /// + public class NetworkCredential + { + private string m_userName; + private string m_password; + private AuthenticationType m_authenticationType; + + /// + /// Construct class with empty user name and password + /// + public NetworkCredential() + { + } + + /// + /// Constructs credientials and initializes them by provided user name and pssword + /// + /// + /// + public NetworkCredential(string userName, string password) + : this(userName, password, AuthenticationType.Basic) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the user. + /// The password. + /// Type of the authentication. + public NetworkCredential(string userName, string password, AuthenticationType authenticationType) + { + UserName = userName; + Password = password; + AuthenticationType = authenticationType; + } + + /// + /// Set or get user name. + /// + public string UserName + { + get + { + return m_userName; + } + set + { + m_userName = value; + } + } + + /// + /// Set or get password. + /// + public string Password + { + get + { + return m_password; + } + set + { + m_password = value; + } + } + + /// + /// Gets or sets the type of the authentication. + /// + /// The type of the authentication. + public AuthenticationType AuthenticationType + { + get + { + return m_authenticationType; + } + set + { + m_authenticationType = value; + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs b/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs new file mode 100644 index 0000000..d2eb103 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// The exception that is thrown when an error is made while using a network + /// protocol. + /// + public class ProtocolViolationException : InvalidOperationException + { + // constructors + /// + /// Initializes a new instance of the + /// ProtocolViolationException class. + /// + public ProtocolViolationException() + { + } + + /// + /// Initializes a new instance of the + /// ProtocolViolationException class with the + /// specified message. + /// + /// The error message string. + public ProtocolViolationException(string message) + : base(message) + { + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebException.cs b/src/Cosmos.Network.Http/Http/System.Net.WebException.cs new file mode 100644 index 0000000..9408a10 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.WebException.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + + /// + /// Defines the exception that is thrown by + /// instances when an error occurs. + /// + /// + /// This class is a subclass of InvalidOperationException + /// that contains a WebExceptionStatus and possibly a + /// reference to a WebResponse. The + /// WebResponse is only present if there is a response + /// from the remote server. + /// + public class WebException : InvalidOperationException + { + + private WebExceptionStatus m_Status; + private WebResponse m_Response; + + /// + /// The default constructor. + /// + public WebException() + { + + } + + /// + /// Constructs a WebException based on the specified + /// message string. + /// + /// The message string for the exception. + public WebException(string message) + : base(message) + { + } + + /// + /// Constructs a WebException based on the specified + /// message string and inner exception. + /// + /// The message string for the exception. + /// The exception that caused this + /// exception. + public WebException(string message, Exception innerException) : + base(message, innerException) + { + + } + + /// + /// Constructs a WebException based on the specified + /// message string and WebExceptionStatus. + /// + /// The message string for the exception. + /// The network status of the exception. + public WebException(string message, WebExceptionStatus status) + : base(message) + { + m_Status = status; + } + + /// + /// Constructs a WebException based on the specified + /// message string, inner exception, + /// , and + /// . + /// + /// Message string for exception. + /// The exception that caused this exception. + /// + /// The network status of the exception. + /// The WebResponse we have. + /// + public WebException(string message, + Exception inner, + WebExceptionStatus status, + WebResponse response) + : base(message, inner) + { + m_Status = status; + m_Response = response; + } + + /// + /// Gets the WebExceptionStatus code. + /// + /// One of the WebExceptionStatus values. + public WebExceptionStatus Status + { + get + { + return m_Status; + } + } + + /// + /// Gets the response that the remote host returned. + /// + /// If a response is available from the Internet resource, a + /// WebResponse instance that contains the error + /// response from an Internet resource; otherwise, + /// null. + public WebResponse Response + { + get + { + return m_Response; + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs b/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs new file mode 100644 index 0000000..9587186 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs @@ -0,0 +1,742 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System.Collections; + using System.Text; + + /// + /// Contains a name/value pair that is used in an HTTP header. + /// + internal class HeaderValuePair + { + public HeaderValuePair(string hd, string val) + { + headerAsKey = hd; + value = val; + } + + // Pair of value and header. + public string headerAsKey; + public string value; + } + + /// + /// Keeps an array of name/value pairs that are in HTTP headers. + /// + internal class HeaderValueCollection : ArrayList + { + /// + /// Searches in the collection for the header with the same name. + /// + /// The header to seach for. + /// The name/value pair if found; otherwise, null. + /// + /// Header comparison is case-insensitive. + /// + /// In the desktop and Compact Framework versions of .NET, the array is + /// represented by a hash table. + /// + /// + public HeaderValuePair GetValuePair(string header) + { + string lowerHeader = header.ToLower(); + for (int i = 0; i < this.Count; i++) + { + if (((HeaderValuePair)this[i]).headerAsKey.ToLower() == lowerHeader) + { + return (HeaderValuePair)this[i]; + } + } + + return null; + } + + /// + /// Adds a header and a value for the header into the collection. + /// + /// String for header + /// String for value + /// + /// If the specified header is already present, the value is appended to + /// that header. + /// + public void Add(string header, string value) + { + // Checks is we already have the header. + HeaderValuePair pair = GetValuePair(header); + // If found, adds valus to existing valies. + if (pair != null) + { + pair.value += "," + value; + } + else // if not found - then we add it. + { + base.Add(new HeaderValuePair(header, value)); + } + } + + /// + /// Replaces the previous values for the header with the new value. + /// + /// Header name + /// New value + public void Set(string header, string value) + { + // Remove previous header. + RemoveHeader(header); + + // Add new value(s) + base.Add(new HeaderValuePair(header, value)); + } + + /// + /// Removes the header that has the specified name. + /// + /// + /// + public bool RemoveHeader(string header) + { + int totalElemCount = Count; + string lowerHeader = header.ToLower(); + for (int i = 0; i < totalElemCount; i++) + { // If name matches - remove this header. + if (((HeaderValuePair)this[i]).headerAsKey.ToLower() == lowerHeader) + { + RemoveAt(i); + return true; + } + } + + return false; + } + + } + + /// + /// Contains protocol headers associated with a request or response. + /// Manages name-value pairs for HTTP headers. + /// + /// + /// This class includes additional methods, including HTTP parsing of a + /// collection into a buffer that can be sent. + /// + /// Headers are validated when attempting to add them. + /// + /// + public class WebHeaderCollection + { + /// + /// Data and constants. + /// + private const int ApproxAveHeaderLineSize = 30; + private static readonly HeaderInfoTable HInfo = new HeaderInfoTable(); + + /// + /// Array list of headers and values + /// + private HeaderValueCollection head_val_coll = new HeaderValueCollection(); + + /// + /// true if this object is created for internal use, in this case + /// we turn on checking when adding special headers. + /// + private bool m_IsHttpWebHeaderObject = false; + + /// + /// true is this collection is created for a WebSocket request + /// + private bool _isWebSocket; + + /// + /// Adds header name/value pair to collection. Does not check if + /// multiple values are allowed. + /// + /// Name in header + /// Value in header + internal void AddWithoutValidate(string headerName, string headerValue) + { + headerName = CheckBadChars(headerName, false); + headerValue = CheckBadChars(headerValue, true); + + head_val_coll.Add(headerName, headerValue); + } + + /// + /// Adds header name/value pair to collection. + /// If multi value allowed for this header name - adds new one + /// If multi value is not allowed, replace the old value with new one + /// + /// Name in header + /// Value in header + internal void SetAddVerified(string name, string value) + { + if (HInfo[name].AllowMultiValues) + { + head_val_coll.Add(name, value); + } + else + { + head_val_coll.Set(name, value); + } + } + + // The below 3 methods are for fast headers manipulation, bypassing all + // the checks. + + /// + /// Just internal fast add. + /// + /// + /// + internal void AddInternal(string headerName, string headerValue) + { + head_val_coll.Add(headerName, headerValue); + } + + /// + /// Internal fast channge + /// + /// + /// + internal void ChangeInternal(string name, string value) + { + head_val_coll.Set(name, value); + } + + /// + /// Internal remove of header. + /// + /// + internal void RemoveInternal(string name) + { + head_val_coll.RemoveHeader(name); + } + + /// + /// Changes to new value. Check for illegal characters first. + /// + /// + /// + internal void CheckUpdate(string name, string value) + { + value = CheckBadChars(value, true); + ChangeInternal(name, value); + } + + /// + /// Throws an error if invalid chars are found in the header name or + /// value. + /// + /// The header name or header value string to + /// check. + /// Whether the name parameter is a header + /// name or a header value. + /// + internal static string CheckBadChars(string name, bool isHeaderValue) + { + + if (name == null || name.Length == 0) + { + // empty name is invlaid + if (!isHeaderValue) + { + throw new ArgumentException(); + } + + // empty value is OK + return string.Empty; + } + + if (isHeaderValue) + { + // VALUE check + // Trim spaces from both ends + name = name.Trim(); + + // First, check for correctly formed multi-line value + // Second, check for absence of CTL characters + bool crlf = false; + for (int i = 0; i < name.Length; ++i) + { + char c = name[i]; + if (c == 127 || (c < ' ' && !(c == '\t' || c == '\r' || c == '\n'))) + { + throw new ArgumentException(); + } + + if (crlf) + { + if (!(c == ' ' || c == '\t')) + { + throw new ArgumentException(); + } + + crlf = false; + } + else + { + if (c == '\n') + { + crlf = true; + } + } + } + } + else + { + // NAME check + // First, check for absence of separators and spaces + if (name.IndexOfAny(ValidationHelper.InvalidParamChars) != -1) + { + throw new ArgumentException(); + } + + // Second, check for non CTL ASCII-7 characters (32-126) + if (ContainsNonAsciiChars(name)) + { + throw new ArgumentException(); + } + } + + return name; + } + + internal static bool IsValidToken(string token) + { + return (token.Length > 0) + && (token.IndexOfAny(ValidationHelper.InvalidParamChars) == -1) + && !ContainsNonAsciiChars(token); + } + + internal static bool ContainsNonAsciiChars(string token) + { + for (int i = 0; i < token.Length; ++i) + { + if ((token[i] < 0x20) || (token[i] > 0x7e)) + { + return true; + } + } + + return false; + } + + /// + /// Throws an exception if the user passed in a reserved string as the + /// header name. + /// + /// + internal void ThrowOnRestrictedHeader(string headerName) + { + if (m_IsHttpWebHeaderObject && HInfo[headerName].IsRestricted) + { + // check if this is a WebSocket request trying to set + if (_isWebSocket && headerName == HttpKnownHeaderNames.Connection) + { + // OK for WebSocket to set this header + return; + } + + throw new ArgumentException("Cannot update restricted header: " + headerName); + } + } + + // Our Public METHOD set, most are inherited from NameValueCollection, + // not all methods from NameValueCollection are listed, even though + // usable. + // + // This includes: + // Add(name, value) + // Add(header) + // this[name] {set, get} + // Remove(name), returns bool + // Remove(name), returns void + // Set(name, value) + // ToString() + // + // SplitValue(name, value) + // ToByteArray() + // ParseHeaders(char [], ...) + // ParseHeaders(byte [], ...) + + /// + /// Inserts a header with the specified name and value into the + /// collection. + /// + /// The name of the header that is being added to the + /// collection. + /// The content of the header that is being added + /// (its header-value). If a header with the specified name already + /// exists, this value is concatenated onto the existing header. + /// + /// If a header with the specified name already exists, the header that + /// is being added is concatenated onto the existing header. + /// + /// Throws an exception if the specified header name is the name of a + /// special header. + /// + /// + public void Add(string name, string value) + { + // Special headers are listed in the RestrictedHeaders object. + + name = CheckBadChars(name, false); + ThrowOnRestrictedHeader(name); + value = CheckBadChars(value, true); + + head_val_coll.Add(name, value); + } + + /// + /// Inserts a new header into the collection. + /// + /// A header name/value pair, in the format + /// "myHeaderName:myValue". + /// + /// This method expects a string with the format "myName:myValue", and + /// parses the two parts out. + /// + /// If a header with the specified name already exists, the header that + /// is being added is concatenated onto the existing header. + /// + /// + /// Throws an exception if the specified header name is the name of a + /// special header. + /// + /// + public void Add(string header) + { + // Special headers are listed in the RestrictedHeaders object. + + if (ValidationHelper.IsBlankString(header)) + { + throw new ArgumentNullException(); + } + + int colpos = header.IndexOf(':'); + + // check for badly formed header passed in + if (colpos < 0) + { + throw new ArgumentException(); + } + + string name = header.Substring(0, colpos); + // Handle empty header value + string value; + if (colpos + 1 >= header.Length) + { + value = string.Empty; + } + else + { + value = header.Substring(colpos + 1); + } + + name = CheckBadChars(name, false); + ThrowOnRestrictedHeader(name); + value = CheckBadChars(value, true); + + head_val_coll.Add(name, value); + } + + /// + /// Sets the specified header to the specified value. + /// + /// The header to set. + /// The content of the header to set. + /// + /// Includes validation. + /// Throws an exception if the specified header name is the name of a + /// special header. + /// + public void Set(String name, String value) + { + // Special headers are listed in the RestrictedHeaders object. + + if (ValidationHelper.IsBlankString(name)) + { + throw new ArgumentNullException("name"); + } + + name = CheckBadChars(name, false); + ThrowOnRestrictedHeader(name); + value = CheckBadChars(value, true); + + head_val_coll.Set(name, value); + } + + /// + /// Removes the specified header from the collection. + /// + /// The name of the header to remove. + /// + /// Throws an exception if the specified header name is the name of a + /// special header. + /// + public void Remove(string name) + { + // Special headers are listed in the RestrictedHeaders object. + + if (ValidationHelper.IsBlankString(name)) + { + throw new ArgumentNullException("name"); // netcf.20937 + } + + ThrowOnRestrictedHeader(name); + name = CheckBadChars(name, false); + + head_val_coll.RemoveHeader(name); + } + + /// + /// Returns the values for the specified header name. + /// + /// The name of the header. + /// An array of parsed string objects. + /// + /// Takes a header name and returns a string array representing + /// the individual values for that header. For example, if the headers + /// contain the following line: + /// + /// Accept: text/plain, text/html + /// + /// then GetValues("Accept") returns an array of + /// two strings: "text/plain" and "text/html". + /// + public string[] GetValues(string header) + { + // Get the value pair for the header. + HeaderInfo Info = HInfo[header]; + HeaderValuePair pair = head_val_coll.GetValuePair(header); + + // If header not present or value string not present or empty - + // return null. + if (pair == null || pair.value == null || pair.value.Length == 0) + { + return null; + } + + // Header present. Parse the value string. There is non-empty value + // string. + if (Info == null || !Info.AllowMultiValues) + { + string[] retVal = new string[1]; + retVal[0] = pair.value; + return retVal; + } + + // Multivalue header + return Info.Parser(pair.value); + } + + /// + /// Generates a string representation of the headers, that is ready to + /// be sent except for it being in String format. + /// + /// A string representation of the headers. + /// + /// The format looks like the following: + /// + /// Header-Name: Header-Value\r\n + /// Header-Name2: Header-Value2\r\n + /// ... + /// Header-NameN: Header-ValueN\r\n + /// \r\n + /// + /// + public override string ToString() + { + // Iterates on all headers and add them line by line in form: + // header: value + string retString = ""; + for (int i = 0; i < head_val_coll.Count; i++) + { + // Try to be most efficient by calling Concat. + // There is no Concat with 5 arguments. + retString = String.Concat(retString, ((HeaderValuePair)head_val_coll[i]).headerAsKey, ": ", ((HeaderValuePair)head_val_coll[i]).value); + retString = String.Concat(retString, "\r\n"); + } + + // Adds extra line return at the end of headers. + retString = String.Concat(retString, "\r\n"); + + // Return concatinated headers and + return retString; + } + + /// + /// Generates a byte array representation of the headers, that is ready + /// to be sent. + /// + /// An array of bytes. + /// + /// This method serializes the headers into a byte array that can be + /// sent over the network. The format looks like: + /// + /// Header-Name1: Header-Value1\r\n + /// Header-Name2: Header-Value2\r\n + /// ... + /// Header-NameN: Header-ValueN\r\n + /// \r\n + /// + /// + public byte[] ToByteArray() + { + // Performance Note: We aren't doing a single copy/covert run, + // because (according to Demitry), it's cheaper to copy the headers + // twice than to call the UNICODE-to-ANSI conversion code many + // times. (The code before used to know the size of the output.) + + // Make sure the buffer is big enough. + + string tempStr = ToString(); + + // Use the String of headers, convert to Char Array, then convert to + // Bytes, serializing finally into the buffer, along the way. + byte[] buffer = Encoding.UTF8.GetBytes(tempStr); + + return buffer; + } + + /// + /// Tests whether the specified HTTP header can be set. + /// + /// Name for the header. + /// + /// + /// Throws an exception if the header name is blank, contains illegal + /// characters, or contains characters that are reserved by the HTTP + /// protocol. + /// + public static bool IsRestricted(string headerName) + { + if (ValidationHelper.IsBlankString(headerName)) + { + throw new ArgumentNullException("headerName"); + } + + return HInfo[CheckBadChars(headerName, false)].IsRestricted; + } + + /// + /// Creates an empty collection of WEB headers. + /// + public WebHeaderCollection() + { + } + + /// + /// Private constructor, called internally. + /// + /// Whether this is an HTTP headers + /// object. + internal WebHeaderCollection(bool internalCreate, bool isWebSocket = false) + { + m_IsHttpWebHeaderObject = internalCreate; + _isWebSocket = isWebSocket; + } + + /// + /// Calculates the number of bytes needed to store the headers. + /// + /// + internal int byteLength() + { + int ret = 0; + // Runs for all collection and adds length of header and value + // strings + for (int i = 0; i < head_val_coll.Count; i++) + { + ret += ((HeaderValuePair)head_val_coll[i]).headerAsKey.Length; + ret += 2; //for the ": " + ret += ((HeaderValuePair)head_val_coll[i]).value.Length; + ret += 2; //for the "\r\n" + } + + ret += 2; //for the final "\r\n" + + return ret; + } + + /// + /// Returns the string value for the header. + /// + /// The name of the header. + /// A string containing the value. If no value is present, + /// returns null. + public string this[string header] + { + get + { + HeaderValuePair pair = head_val_coll.GetValuePair(header); + // If header is not present, then pair is null. Return null + // string + if (pair == null) + { + return null; + } + + // Pair was found. Return the value string of it + return pair.value; + } + } + + /// + /// Gets the number of headers in the collection. + /// + /// An Int32 indicating the number of headers in a + /// request. + public int Count + { + get + { + return head_val_coll.Count; + } + } + + /// + /// Gets all header names (keys) in the collection. + /// + /// An array of type String containing all header names in + /// a Web request. + public string[] AllKeys + { + get + { + ArrayList tempCollection = new ArrayList(); + for (int i = 0; i < head_val_coll.Count; i++) + { + tempCollection.Add(((HeaderValuePair)head_val_coll[i]).headerAsKey); + } + + string[] stringArray = new string[tempCollection.Count]; + return (string[])tempCollection.ToArray(typeof(string)); + } + } + + /// + /// Copies the headers into the byte array starting at bytes[offset]. + /// If the byte array is too small to hold the data, an + /// ArgumentException is thrown. + /// + /// The array to copy. + /// The offset to the beginning of the data to be + /// copied into the WEB Headers collection. + /// How many bytes were copied. + internal int copyTo(byte[] bytes, int offset) + { + // Create array representing the headers + byte[] headersBytes = ToByteArray(); + // Copy to destination + headersBytes.CopyTo(bytes, offset); + // Return count of bytes copied. + return headersBytes.Length; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs b/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs new file mode 100644 index 0000000..79f8fb8 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs @@ -0,0 +1,419 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + using System.Collections; + using System.IO; + + /// + /// Makes a request to a Uniform Resource Identifier (URI). This is an + /// abstract class. + /// + /// + /// This is the base class of all Web resource/protocol objects. This class + /// provides common methods, data and proprties for making the top-level + /// request. + /// + public abstract class WebRequest : MarshalByRefObject, IDisposable + { + internal const int DefaultTimeout = 100000; // default timeout is 100 seconds + // (ASP .NET is 90 seconds) + + // Lock to syncronize update of s_PrefixList + private static object g_listLock; + // List of WebRequestPrefixElement that keeps prefix ( string ) and + // IWebRequestCreate + private static ArrayList s_PrefixList; + + private static IWebProxy s_defaultProxy = null; + + /// + /// Initializes a new instance of the + /// class. + /// + protected WebRequest() + { + } + + /// + /// Static constructor to initialize the static variables before the class is used + /// + static WebRequest() + { + Initialize(); + } + + /// + /// Initialize has to be called before the class is used. + /// Normally should be called by the constructor above. + /// + public static void Initialize() + { + if (g_listLock == null) + { + g_listLock = new object(); + s_PrefixList = new ArrayList(); + } + } + + ~WebRequest() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + + + /// + /// When overridden in a descendant class, gets or sets the protocol + /// method to use in this request. + /// + /// + /// This property gets or sets the verb to this request, such as GET or + /// POST for HTTP. + /// + /// The protocol method to use in this request. + public virtual string Method + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, gets the URI of the Internet + /// resource associated with the request. + /// + /// + /// This property is read-only, since the Uri can be specified only on + /// creation. + /// + /// A Uri representing the resource associated + /// with the request. + /// + public virtual Uri RequestUri + { // read-only + get + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, gets or sets the name of the + /// connection group for the request. + /// + /// + /// This property serves as a way of grouping connections. + /// + /// The name of the connection group for the request. + public virtual string ConnectionGroupName + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, gets or sets the collection + /// of header name/value pairs associated with the request. + /// + /// A WebHeaderCollection containing the + /// header name/value pairs associated with this request. + public virtual WebHeaderCollection Headers + { + // read-only + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, gets or sets the content + /// length of the request data being sent. + /// + /// + /// The content length is the length of the message with the verb. + /// It is useful only with verbs that actually support a message, such + /// as POST; it is not used for the GET verb. + /// + /// The number of bytes of request data being sent. + public virtual long ContentLength + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, gets or sets the content type + /// of the request data being sent. + /// + /// + /// The content length is the length of the message with the verb. + /// It is useful only with verbs that actually support a message, such + /// as POST; it is not used for the GET verb. + /// + /// The content type of the request data. + public virtual string ContentType + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// Gets or sets the length of time, in milliseconds, before the request + /// times out. + /// + /// The length of time, in milliseconds, until the request times + /// out, or the value Timeout.Infinite to indicate that the request does + /// not time out. The default value is defined by the descendant + /// class. + public virtual int Timeout + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// Gets or sets the global HTTP proxy. + /// The DefaultWebProxy property determines the default proxy that all WebRequest instances use if the request + /// supports proxies and no proxy is set explicitly using the Proxy property. Proxies are currently supported + /// by HttpWebRequest. + /// + public static IWebProxy DefaultWebProxy + { + get + { + return s_defaultProxy; + } + + set + { + s_defaultProxy = value; + } + } + + /// + /// When overridden in a descendant class, gets or sets the network + /// proxy to use to access this Internet resource. + /// + /// The IWebProxy to use to access the + /// Internet resource. + public virtual IWebProxy Proxy + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, returns a + /// Stream for writing data to the Internet resource. + /// + /// A Stream for writing data to the + /// Internet resource. + public virtual Stream GetRequestStream() + { + // DataStream may need to be extended to URLDataStream or somesuch. + // We might need to be able to get the data available. This should + // be a method of the stream, not of the net classes. Also, we need + // to know whether the stream is seekable. Only streams via cache + // and via socket with Content-Length are seekable. + + throw new NotSupportedException(); + } + + /// + /// When overridden in a descendant class, returns a response to an + /// Internet request. + /// + /// A WebResponse containing the response to + /// the Internet request. + public virtual WebResponse GetResponse() + { + throw new NotSupportedException(); + } + + /// + /// Initializes a new WebRequest instance for the + /// specified URI scheme, such as http://, https://, or file://. + /// + /// The URI that identifies the Internet + /// resource. + /// Newly created WebRequest. + /// A WebRequest descendant for the specific URI scheme. + /// + /// + /// This is the main creation routine. The specified Uri is looked up + /// in the prefix match table, and the appropriate handler is invoked to + /// create the object. + /// + public static WebRequest Create(string requestUriString) + { + return CreateInternal(new Uri(requestUriString)); + } + + /// + /// Creates a WebRequest. + /// + /// A containing the + /// URI of the requested resource. + /// A WebRequest descendant for the specified + /// URI scheme. + /// + /// This is the main creation routine. The specified Uri is looked up + /// in the prefix match table, and the appropriate handler is invoked to + /// create the object. + /// + public static WebRequest Create(Uri requestUri) + { + return CreateInternal(requestUri); + } + + /// + /// Registers a WebRequest descendant for the + /// specified URI. + /// + /// The complete URI or URI prefix that the + /// WebRequest descendant services. + /// The create method that the + /// WebRequest calls to create the + /// WebRequest descendant. + /// true. + public static bool RegisterPrefix(string prefix, + IWebRequestCreate creator) + { + if (prefix == null || creator == null) { throw new ArgumentNullException(); } + + // Changes prefix to lower becuase it is case insensitive. + prefix = prefix.ToLower(); + lock (g_listLock) + { + // Iterate over list of prefixes and checks if this one is + // already present. + for (int i = 0; i < s_PrefixList.Count; i++) + { + if (((WebRequestPrefixElement)s_PrefixList[i]).Prefix == prefix) + { + return false; + } + } + + // This is a new prefix, add it. + s_PrefixList.Add(new WebRequestPrefixElement(prefix, creator)); + } + + return true; + } + + private static int ComparePrefixString(string Url, string prefix, int prefixLen) + { + for (int i = 0; i < prefixLen; i++) + { + if (Url[i] != prefix[i]) + { + return Url[i] < prefix[i] ? -1 : 1; + } + } + + // Actually the URL starts by prefix. + return 0; + } + + private static WebRequest CreateInternal(Uri requestUri) + { + if (requestUri == null) { throw new ArgumentNullException(); } + + // Makes LookupUri lowercase since we need case-insensitive compare + // with prefix + string lookupUri = requestUri.AbsoluteUri.ToLower(); + int lookupUriLent = lookupUri.Length; + + // Walk down the list of prefixes. + int prefixListCount = s_PrefixList.Count; + for (int i = 0; i < prefixListCount; i++) + { + WebRequestPrefixElement Current = (WebRequestPrefixElement)s_PrefixList[i]; + + // See if this prefix is short enough. + int prefixLen = Current.Prefix.Length; + if (lookupUriLent >= prefixLen) + { + // It is. See if these match. + if (ComparePrefixString(lookupUri, Current.Prefix, prefixLen) == 0) + { + return Current.Creator.Create(requestUri); + } + } + } + + throw new NotSupportedException(); + } + + internal static void RegisterPrefix(string prefix) + { + // Creates instance of HttpRequestCreator. HttpRequestCreator creates HttpWebRequest + HttpRequestCreator Creator = new HttpRequestCreator(); + + RegisterPrefix(prefix, Creator); + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs b/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs new file mode 100644 index 0000000..71f0765 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + using System.IO; + + /// + /// Provides a response from a Uniform Resource Identifier (URI). This is + /// an abstract class. + /// + /// + /// This is the abstract base class for all WebResponse + /// objects. + /// + public abstract class WebResponse : MarshalByRefObject, IDisposable + { + /// + /// Initializes a new instance of the WebResponse + /// class. + /// + protected WebResponse() + { + } + + /// + /// When overridden in a descendant class, gets or sets the content + /// length of data being received. + /// + /// The number of bytes returned from the Internet + /// resource. + public virtual long ContentLength + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a derived class, gets or sets the content type of + /// the data being received. + /// + /// A string that contains the content type of the + /// response. + public virtual string ContentType + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a descendant class, returns the data stream from + /// the Internet resource. + /// + /// An instance of the class for + /// reading data from the Internet resource. + public virtual Stream GetResponseStream() + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, gets the URI of the Internet + /// resource that actually responded to the request. + /// + /// An instance of the class that + /// contains the URI of the Internet resource that actually responded to + /// the request. + /// + /// This property gets the final Response URI, that includes any changes + /// that may have transpired from the orginal request. + /// + public virtual Uri ResponseUri + { // read-only + get + { + throw new NotSupportedException(); + } + } + + /// + /// When overridden in a derived class, gets a collection of header + /// name-value pairs associated with this request. + /// + /// An instance of the + /// class that contains + /// header values associated with this response. + public virtual WebHeaderCollection Headers + { + get + { + throw new NotSupportedException(); + } + } + + ~WebResponse() + { + Dispose(false); + } + + /// + /// When overridden by a descendant class, closes the response stream. + /// + public virtual void Close() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose() + { + Close(); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs b/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs new file mode 100644 index 0000000..2321bd6 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System.Net.Sockets; + +namespace System.Net +{ + /// + /// Provides access to the networkStream and socket for creating a websocket + /// HttpListener class. This class cannot be inherited. + /// + internal class WebSocketContext + { + /// + /// Actual network or SSL stream connected to the client. + /// It could be SSL stream, so NetworkStream is not exact type, m_Stream would be derived from NetworkStream + /// + internal NetworkStream NetworkStream; + + /// + /// This is a socket connected to client. + /// OutputNetworkStreamWrapper owns the socket, not NetworkStream. + /// If connection is persistent, then the m_Socket is transferred to the list of + /// + internal Socket Socket; + + /// + /// Gets the collection of header name/value pairs sent in the request. + /// + /// A WebHeaderCollection that contains the + /// HTTP headers included in the request. + internal WebHeaderCollection Headers; + + internal WebSocketContext(Socket socket, NetworkStream networkStream, WebHeaderCollection headers) + { + Socket = socket; + NetworkStream = networkStream; + Headers = headers; + + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs b/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs new file mode 100644 index 0000000..5e27eee --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Defines status codes for the + /// class. + /// + public enum WebExceptionStatus + { + /// No error was encountered. + Success = 0, + /// The name resolver service could not resolve the host name. + /// + NameResolutionFailure = 1, + /// The remote service point could not be contacted at the + /// transport level. + ConnectFailure = 2, + /// A complete response was not received from the remote + /// server. + ReceiveFailure = 3, + /// A complete request could not be sent to the remote + /// server. + SendFailure = 4, + /// The request was a piplined request and the connection was + /// closed before the response was received. + PipelineFailure = 5, + /// The request was canceled or an unclassifiable error + /// occurred. This is the default value for + /// . + RequestCanceled = 6, + /// The response received from the server was complete but + /// indicated a protocol-level error. For example, an HTTP protocol + /// error such as 401 Access Denied would use this status. + ProtocolError = 7, + /// The connection was prematurely closed. + ConnectionClosed = 8, + /// A server certificate could not be validated. + TrustFailure = 9, + /// An error occurred while establishing a connection using + /// SSL. + SecureChannelFailure = 10, + /// The server response was not a valid HTTP + /// response. + ServerProtocolViolation = 11, + /// The connection for a request that specifies the Keep-alive + /// header was closed unexpectedly. + KeepAliveFailure = 12, + /// An internal asynchronous request is pending. + Pending = 13, + /// No response was received during the time-out period for a + /// request. + Timeout = 14, + /// The name resolver service could not resolve the proxy host + /// name. + ProxyNameResolutionFailure = 15 + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs new file mode 100644 index 0000000..2aa80c6 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + internal delegate string[] HeaderParser(string value); + + /// + /// Internal supporting class for validation of HTTP Web Headers. + /// + internal class HeaderInfo + { + + internal bool IsRestricted; + internal HeaderParser Parser; + + /// + /// Note that the HeaderName field is not always valid, and should not + /// be used after initialization. In particular, the HeaderInfo returned + /// for an unknown header will not have the correct header name. + /// + internal string HeaderName; + internal bool AllowMultiValues; + + internal HeaderInfo(string name, bool restricted, bool multi, HeaderParser p) + { + HeaderName = name; + IsRestricted = restricted; + Parser = p; + AllowMultiValues = multi; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs new file mode 100644 index 0000000..eeaf8c5 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System.Collections; + + /// + /// Internal class with utilities to validate HTTP headers. + /// + internal class HeaderInfoTable + { + + private static HeaderInfo[] HeaderTable; + + private static HeaderParser SingleParser = new HeaderParser(ParseSingleValue); + private static HeaderParser MultiParser = new HeaderParser(ParseMultiValue); + + private static string[] ParseSingleValue(string value) + { + return new string[1] { value }; + } + + /// + /// Parses single HTTP header and separates values delimited by comma. + /// Like "Content-Type: text, HTML". The value string "text, HTML" will se parsed into 2 strings. + /// + /// Value string with possible multivalue + /// Array of strings with single value in each. + private static string[] ParseMultiValue(string value) + { + ArrayList tempCollection = new ArrayList(); + + bool inquote = false; + int chIndex = 0; + char[] vp = new char[value.Length]; + string singleValue; + + for (int i = 0; i < value.Length; i++) + { + if (value[i] == '\"') + { + inquote = !inquote; + } + else if ((value[i] == ',') && !inquote) + { + singleValue = new String(vp, 0, chIndex); + tempCollection.Add(singleValue.Trim()); + chIndex = 0; + continue; + } + + vp[chIndex++] = value[i]; + } + + // + // Now add the last of the header values to the stringtable. + // + + if (chIndex != 0) + { + singleValue = new String(vp, 0, chIndex); + tempCollection.Add(singleValue.Trim()); + } + + return (string[])tempCollection.ToArray(typeof(string)); + } + + /// + /// Header info for non-standard headers. + /// + private static HeaderInfo UnknownHeaderInfo = + new HeaderInfo(String.Empty, false, false, SingleParser); + + private static bool m_Initialized = Initialize(); + + /// + /// Initialize table with infomation for HTTP WEB headers. + /// + /// + private static bool Initialize() + { + + HeaderTable = new HeaderInfo[] { + new HeaderInfo(HttpKnownHeaderNames.Age, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Allow, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Accept, true, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Authorization, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.AcceptRanges, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.AcceptCharset, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.AcceptEncoding, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.AcceptLanguage, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Cookie, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Connection, true, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ContentMD5, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.ContentType, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.CacheControl, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ContentRange, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.ContentLength, true, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.ContentEncoding, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ContentLanguage, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ContentLocation, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Date, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.ETag, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Expect, true, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Expires, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.From, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Host, true, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.IfMatch, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.IfRange, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.IfNoneMatch, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.IfModifiedSince, true, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.IfUnmodifiedSince, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Location, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.LastModified, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.MaxForwards, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Pragma, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ProxyAuthenticate, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ProxyAuthorization, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.ProxyConnection, true, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Range, true, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Referer, true, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.RetryAfter, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Server, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.SetCookie, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.SetCookie2, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.TE, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Trailer, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.TransferEncoding, true , true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Upgrade, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.UserAgent, true, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.Via, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Vary, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.Warning, false, true, MultiParser), + new HeaderInfo(HttpKnownHeaderNames.WWWAuthenticate, false, true, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.SecWebSocketAccept, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.SecWebSocketProtocol, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.SecWebSocketVersion, false, false, SingleParser), + new HeaderInfo(HttpKnownHeaderNames.SecWebSocketKey, false, false, SingleParser) + }; + + return true; + } + + /// + /// Return HTTP header information from specified name of HTTP header. + /// + /// Name for HTTP header + /// HTTP header information + internal HeaderInfo this[string name] + { + get + { // Return headerInfo with the same name + string lowerCaseName = name.ToLower(); + for (int i = 0; i < HeaderTable.Length; i++) + { + if (HeaderTable[i].HeaderName.ToLower() == lowerCaseName) + { + return HeaderTable[i]; + } + } + + // Return unknownInfo, instead of NULL + return UnknownHeaderInfo; + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs b/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs new file mode 100644 index 0000000..e13f55d --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs @@ -0,0 +1,489 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Internal class that parses the string with date in HTTP headers to DateTime object. + /// + internal class HttpDateParse + { + /// + /// Bse for decimal numbers + /// + private const int BASE_DEC = 10; + + /// + /// Date indicies used to figure out what each entry is. + /// + private const int DATE_INDEX_DAY_OF_WEEK = 0; + + private const int DATE_1123_INDEX_DAY = 1; + private const int DATE_1123_INDEX_MONTH = 2; + private const int DATE_1123_INDEX_YEAR = 3; + private const int DATE_1123_INDEX_HRS = 4; + private const int DATE_1123_INDEX_MINS = 5; + private const int DATE_1123_INDEX_SECS = 6; + + private const int DATE_ANSI_INDEX_MONTH = 1; + private const int DATE_ANSI_INDEX_DAY = 2; + private const int DATE_ANSI_INDEX_HRS = 3; + private const int DATE_ANSI_INDEX_MINS = 4; + private const int DATE_ANSI_INDEX_SECS = 5; + private const int DATE_ANSI_INDEX_YEAR = 6; + + private const int DATE_INDEX_TZ = 7; + + private const int DATE_INDEX_LAST = DATE_INDEX_TZ; + private const int MAX_FIELD_DATE_ENTRIES = (DATE_INDEX_LAST + 1); + + // + // DATE_TOKEN's DWORD values used to determine what day/month we're on + // + + private const int DATE_TOKEN_JANUARY = 1; + private const int DATE_TOKEN_FEBRUARY = 2; + private const int DATE_TOKEN_MARCH = 3; + private const int DATE_TOKEN_APRIL = 4; + private const int DATE_TOKEN_MAY = 5; + private const int DATE_TOKEN_JUNE = 6; + private const int DATE_TOKEN_JULY = 7; + private const int DATE_TOKEN_AUGUST = 8; + private const int DATE_TOKEN_SEPTEMBER = 9; + private const int DATE_TOKEN_OCTOBER = 10; + private const int DATE_TOKEN_NOVEMBER = 11; + private const int DATE_TOKEN_DECEMBER = 12; + + private const int DATE_TOKEN_LAST_MONTH = (DATE_TOKEN_DECEMBER + 1); + + private const int DATE_TOKEN_SUNDAY = 0; + private const int DATE_TOKEN_MONDAY = 1; + private const int DATE_TOKEN_TUESDAY = 2; + private const int DATE_TOKEN_WEDNESDAY = 3; + private const int DATE_TOKEN_THURSDAY = 4; + private const int DATE_TOKEN_FRIDAY = 5; + private const int DATE_TOKEN_SATURDAY = 6; + + private const int DATE_TOKEN_LAST_DAY = (DATE_TOKEN_SATURDAY + 1); + + private const int DATE_TOKEN_GMT = -1000; + + private const int DATE_TOKEN_LAST = DATE_TOKEN_GMT; + + private const int DATE_TOKEN_ERROR = (DATE_TOKEN_LAST + 1); + + // + // MAKE_UPPER - takes an assumed lower character and bit manipulates into a upper. + // (make sure the character is Lower case alpha char to begin, + // otherwise it corrupts) + // + + private + static + char + MAKE_UPPER(char c) + { + + if (c >= 'a' && c <= 'z') + { + c -= (char)('a' - 'A'); + } + + return c; + } + + /*++ + + Routine Description: + + Arguments: + + lpszDay - + + Return Value: + + DWORD + Success - The Correct date token, 0-6 for day of the week, 1-14 for month, etc + + Failure - DATE_TOKEN_ERROR + + --*/ + /// + /// Looks at the first three bytes of string to determine if we're looking + /// at a Day of the Week, or Month, or "GMT" string. Is inlined so that + /// the compiler can optimize this code into the caller FInternalParseHttpDate. + /// + /// Array of characters respresenting of the string in question. + /// Staring index to for the time date in the string + /// + private + static + int + MapDayMonthToDword( + char[] lpszDay, + int index + ) + { + switch (MAKE_UPPER(lpszDay[index])) + { // make uppercase + case 'A': + switch (MAKE_UPPER(lpszDay[index + 1])) + { + case 'P': + return DATE_TOKEN_APRIL; + case 'U': + return DATE_TOKEN_AUGUST; + + } + + return DATE_TOKEN_ERROR; + + case 'D': + return DATE_TOKEN_DECEMBER; + + case 'F': + switch (MAKE_UPPER(lpszDay[index + 1])) + { + case 'R': + return DATE_TOKEN_FRIDAY; + case 'E': + return DATE_TOKEN_FEBRUARY; + } + + return DATE_TOKEN_ERROR; + + case 'G': + return DATE_TOKEN_GMT; + + case 'M': + + switch (MAKE_UPPER(lpszDay[index + 1])) + { + case 'O': + return DATE_TOKEN_MONDAY; + case 'A': + switch (MAKE_UPPER(lpszDay[index + 2])) + { + case 'R': + return DATE_TOKEN_MARCH; + case 'Y': + return DATE_TOKEN_MAY; + } + + // fall through to error + break; + } + + return DATE_TOKEN_ERROR; + + case 'N': + return DATE_TOKEN_NOVEMBER; + + case 'J': + + switch (MAKE_UPPER(lpszDay[index + 1])) + { + case 'A': + return DATE_TOKEN_JANUARY; + + case 'U': + switch (MAKE_UPPER(lpszDay[index + 2])) + { + case 'N': + return DATE_TOKEN_JUNE; + case 'L': + return DATE_TOKEN_JULY; + } + + // fall through to error + break; + } + + return DATE_TOKEN_ERROR; + + case 'O': + return DATE_TOKEN_OCTOBER; + + case 'S': + + switch (MAKE_UPPER(lpszDay[index + 1])) + { + case 'A': + return DATE_TOKEN_SATURDAY; + case 'U': + return DATE_TOKEN_SUNDAY; + case 'E': + return DATE_TOKEN_SEPTEMBER; + } + + return DATE_TOKEN_ERROR; + + case 'T': + switch (MAKE_UPPER(lpszDay[index + 1])) + { + case 'U': + return DATE_TOKEN_TUESDAY; + case 'H': + return DATE_TOKEN_THURSDAY; + } + + return DATE_TOKEN_ERROR; + + case 'U': + return DATE_TOKEN_GMT; + + case 'W': + return DATE_TOKEN_WEDNESDAY; + + } + + return DATE_TOKEN_ERROR; + } + + /// + /// Parses through a ANSI, RFC850, or RFC1123 date format and covents it into + /// a FILETIME/SYSTEMTIME time format. + /// + /// Important this a time-critical function and should only be changed + /// with the intention of optimizing or a critical need work item. + /// + /// + /// String with the date time information + /// + /// Out DateTime object. Used to return Systime if needed. + /// Success - TRUE, Failure - FALSE + public + static + bool + ParseHttpDate( + String DateString, + out DateTime dtOut + ) + { + int index = 0; + int i = 0, iLastLettered = -1; + bool fIsANSIDateFormat = false; + int[] rgdwDateParseResults = new int[MAX_FIELD_DATE_ENTRIES]; + bool fRet = true; + char[] lpInputBuffer = DateString.ToCharArray(); + + dtOut = new DateTime(0); + + // + // Date Parsing v2 (1 more to go), and here is how it works... + // We take a date string and churn through it once, converting + // integers to integers, Month,Day, and GMT strings into integers, + // and all is then placed IN order in a temp array. + // + // At the completetion of the parse stage, we simple look at + // the data, and then map the results into the correct + // places in the SYSTIME structure. Simple, No allocations, and + // No dirting the data. + // + // The end of the function does something munging and pretting + // up of the results to handle the year 2000, and TZ offsets + // Note: do we need to fully handle TZs anymore? + // + + while (index < DateString.Length && i < MAX_FIELD_DATE_ENTRIES) + { + if (lpInputBuffer[index] >= '0' && lpInputBuffer[index] <= '9') + { + // + // we have a numerical entry, scan through it and convent to DWORD + // + + rgdwDateParseResults[i] = 0; + + do + { + rgdwDateParseResults[i] *= BASE_DEC; + rgdwDateParseResults[i] += (lpInputBuffer[index] - '0'); + index++; + } while (index < DateString.Length && + lpInputBuffer[index] >= '0' && + lpInputBuffer[index] <= '9'); + + i++; // next token + } + else if ((lpInputBuffer[index] >= 'A' && lpInputBuffer[index] <= 'Z') || + (lpInputBuffer[index] >= 'a' && lpInputBuffer[index] <= 'z')) + { + // + // we have a string, should be a day, month, or GMT + // lets skim to the end of the string + // + + rgdwDateParseResults[i] = + MapDayMonthToDword(lpInputBuffer, index); + + iLastLettered = i; + + // We want to ignore the possibility of a time zone such as PST or EST in a non-standard + // date format such as "Thu Dec 17 16:01:28 PST 1998" (Notice that the year is _after_ the time zone + if ((rgdwDateParseResults[i] == DATE_TOKEN_ERROR) + && + !(fIsANSIDateFormat && (i == DATE_ANSI_INDEX_YEAR))) + { + fRet = false; + goto quit; + } + + // + // At this point if we have a vaild string + // at this index, we know for sure that we're + // looking at a ANSI type DATE format. + // + + if (i == DATE_ANSI_INDEX_MONTH) + { + fIsANSIDateFormat = true; + } + + // + // Read past the end of the current set of alpha characters, + // as MapDayMonthToDword only peeks at a few characters + // + + do + { + index++; + } while (index < DateString.Length && + ((lpInputBuffer[index] >= 'A' && lpInputBuffer[index] <= 'Z') || + (lpInputBuffer[index] >= 'a' && lpInputBuffer[index] <= 'z'))); + + i++; // next token + } + else + { + // + // For the generic case its either a space, comma, semi-colon, etc. + // the point is we really don't care, nor do we need to waste time + // worring about it (the orginal code did). The point is we + // care about the actual date information, So we just advance to the + // next lexume. + // + + index++; + } + } + + // + // We're finished parsing the string, now take the parsed tokens + // and turn them to the actual structured information we care about. + // So we build lpSysTime from the Array, using a local if none is passed in. + // + + int year; + int month; + int day; + int hour; + int minute; + int second; + int millisecond; + + millisecond = 0; + + if (fIsANSIDateFormat) + { + day = rgdwDateParseResults[DATE_ANSI_INDEX_DAY]; + month = rgdwDateParseResults[DATE_ANSI_INDEX_MONTH]; + hour = rgdwDateParseResults[DATE_ANSI_INDEX_HRS]; + minute = rgdwDateParseResults[DATE_ANSI_INDEX_MINS]; + second = rgdwDateParseResults[DATE_ANSI_INDEX_SECS]; + if (iLastLettered != DATE_ANSI_INDEX_YEAR) + { + year = rgdwDateParseResults[DATE_ANSI_INDEX_YEAR]; + } + else + { + // Warning! This is a hack to get around the toString/toGMTstring fiasco (where the timezone is + // appended at the end. (See above) + year = rgdwDateParseResults[DATE_INDEX_TZ]; + } + } + else + { + day = rgdwDateParseResults[DATE_1123_INDEX_DAY]; + month = rgdwDateParseResults[DATE_1123_INDEX_MONTH]; + year = rgdwDateParseResults[DATE_1123_INDEX_YEAR]; + hour = rgdwDateParseResults[DATE_1123_INDEX_HRS]; + minute = rgdwDateParseResults[DATE_1123_INDEX_MINS]; + second = rgdwDateParseResults[DATE_1123_INDEX_SECS]; + } + + // + // Normalize the year, 90 == 1990, handle the year 2000, 02 == 2002 + // This is Year 2000 handling folks!!! We get this wrong and + // we all look bad. + // + + if (year < 100) + { + year += ((year < 80) ? 2000 : 1900); + } + + // + // if we got misformed time, then plug in the current time + // !lpszHrs || !lpszMins || !lpszSec + // + + if ((i < 4) + || (day > 31) + || (hour > 23) + || (minute > 59) + || (second > 59)) + { + fRet = false; + goto quit; + } + + // + // Now do the DateTime conversion + // + + dtOut = new DateTime(year, month, day, hour, minute, second, millisecond); + + // + // Hack: we want the system time to be accurate. This is _suhlow_ + // The time passed in is in the local time zone; we have to convert this into GMT. + // + + if (iLastLettered == DATE_ANSI_INDEX_YEAR) + { + // this should be an unusual case. + // FIXME dtOut = dtOut.ToUniversalTime(); + } + + // + // If we have an Offset to another Time Zone + // then convert to appropriate GMT time + // + + if ((i > DATE_INDEX_TZ && + rgdwDateParseResults[DATE_INDEX_TZ] != DATE_TOKEN_GMT)) + { + + // + // if we received +/-nnnn as offset (hhmm), modify the output FILETIME + // + double offset; + + offset = (double)rgdwDateParseResults[DATE_INDEX_TZ]; + + dtOut.AddHours(offset); + } + + // In the end, we leave it all in LocalTime + + // FIXME dtOut = dtOut.ToLocalTime(); + + quit: + + return fRet; + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs b/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs new file mode 100644 index 0000000..67840dc --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs @@ -0,0 +1,807 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System.Diagnostics; + using System.IO; + using System.Net.Sockets; + using System.Runtime.CompilerServices; + using System.Threading; + + /// + /// The InputNetworkStreamWrapper is used to re-implement calls to NetworkStream.Read + /// It has internal buffer and during initial read operation it places available data from socket into buffer. + /// Later it releases data to Stream.Read calls. + /// It also provides direct access to buffered data for internal code. + /// It provides possibility to "unread" or probe data - meaning user can read byte of data and then return it back to stream. + /// + internal class InputNetworkStreamWrapper : Stream, IKnowWhenDone + { + static private Text.Decoder UTF8decoder = System.Text.Encoding.UTF8.GetDecoder(); + static private Text.Encoding UTF8Encoding = System.Text.Encoding.UTF8; + + /// + /// Actual network or SSL stream connected to the server. + /// It could be SSL stream, so NetworkStream is not exact type, m_Stream would be derived from NetworkStream + /// + internal NetworkStream m_Stream; + + /// + /// Last time this stream was used ( used to timeout idle connections ). + /// + internal DateTime m_lastUsed; + + /// + /// This is a socket connected to client. + /// InputNetworkStreamWrapper owns the socket, not NetworkStream. + /// If connection is persistent, then the m_Socket is transferred to the list of + /// + internal Socket m_Socket; + + /// + /// Determines is the NetworkStream owns the socket + /// + internal bool m_OwnsSocket; + + /// + /// Address and port used for connection of the socket. It is in form of Address:Port ( like www.microsoft.com:80 ) + /// + internal string m_rmAddrAndPort; + + /// + /// Determines if the stream is currently in use or not. + /// + internal bool m_InUse; + + /// + /// Buffer for one line of HTTP header. + /// + private byte[] m_lineBuf; + + /// + /// Internal buffer size for read caching + /// + private const int read_buffer_size = 256; + + /// + /// Internal buffer for read caching + /// + internal byte[] m_readBuffer; + + /// + /// End of valid data in internal buffer. + /// + internal int m_dataEnd; + + /// + /// Start of valid data in internal buffer. + /// + internal int m_dataStart; + + /// + /// Indicates that the stream has chunking encoding. + /// We remove chunk markers and stop reading after end of last chunk. + /// + internal bool m_EnableChunkedDecoding; + + /// + /// Chunk data that we are currently decoding. + /// + private Chunk m_chunk; + + /// + /// Inidcates the stream wrapper object is a clone and they underlying stream should not be disposed + /// + private bool m_isClone; + + /// + /// Http web responses can contain the Content-Length of the response. In these cases, we would like the stream to return an EOF indication + /// if the caller tries to read past the content length. + /// + internal long m_BytesLeftInResponse; + + /// + /// Refills internal buffer from network. + /// + [MethodImpl(MethodImplOptions.Synchronized)] + private int RefillInternalBuffer() + { +#if DEBUG + if (m_dataStart != m_dataEnd) + { + Debug.WriteLine("Internal ERROR in InputNetworkStreamWrapper"); + m_dataStart = m_dataEnd = 0; + } +#endif + // m_dataStart should be equal to m_dataEnd. Purge buffered data. + m_dataStart = m_dataEnd = 0; + // Read up to read_buffer_size, but less data can be read. + // This function does not try to block, so it reads available data or 1 byte at least. + int readCount = (int)m_Stream.Length; + if (readCount > read_buffer_size) + { + readCount = read_buffer_size; + } + else if (readCount == 0) + { + readCount = 1; + } + + m_dataEnd = m_Stream.Read(m_readBuffer, 0, readCount); + + return m_dataEnd; + } + + /// + /// Resets position in internal buffers to zeroes. + /// + internal void ResetState() + { + m_dataStart = m_dataEnd = 0; + m_EnableChunkedDecoding = false; + m_chunk = null; + } + + /// + /// Passes socket parameter to the base. + /// Base Network stream never owns the stream. + /// Socket is directly closed by class that contains InputNetworkStreamWrapper or transferred to + /// list of idle sockets. + /// + /// TBD + /// TBD + /// TBD + /// TBD + internal InputNetworkStreamWrapper(NetworkStream stream, Socket socket, bool ownsSocket, string rmAddrAndPort) + { + m_Stream = stream; + m_Socket = socket; + m_OwnsSocket = ownsSocket; + m_rmAddrAndPort = rmAddrAndPort; + m_InUse = true; + // negative value indicates no length is set, in which case we will continue to read upon the callers request + m_BytesLeftInResponse = -1; + + // Start with 80 (0x50) byte buffer for string. If string longer, we double it each time. + m_lineBuf = new byte[0x50]; + m_readBuffer = new byte[read_buffer_size]; + } + + /// + /// Re-implements reading of data to network stream. + /// + /// Buffer with data to write to HTTP client + /// Offset at which to use data from buffer + /// Count of bytes to write. + public override int Read(byte[] buffer, int offset, int size) + { + // If chunking decoding is not needed - perform normal read. + if (!m_EnableChunkedDecoding) + { + return ReadInternal(buffer, offset, size); + } + + // With chunking decoding there are 4 cases: + // 1. We are at the beginning of chunk. Then we read chunk header and fill m_chunk. + // 2. We are in the middle of chunk. Then it is kind of normal read, but no more than chunk length. + // 3. We are close to the end of chunk. Then we read maximum of data in the chunk and set m_chunk to null. + // 4. We already read last chunk of zero size. Return with zero bytes read. + if (m_chunk == null) + { + // We are in the beginnig of the chunk. Create new chunk and continue. This is case 1. + m_chunk = GetChunk(); + } + + // First validate that chunk is more than zero in size. The last chunk is zero size and it indicates end of data. + if (m_chunk.m_Size == 0) + { + // Nothing to read and actually it is the end of the message body. It is "case 4". + IsDone = true; + return 0; + } + + // Check if request to read is larger than remaining data in the chunk. + if (size > m_chunk.m_Size - m_chunk.m_OffsetIntoChunk) + { + // We set size to the maximum data remaining the the chunk. This is the case 3. + size = (int)(m_chunk.m_Size - m_chunk.m_OffsetIntoChunk); + } + + // Ok, we know that we are in process of reading chunk data. This is case 2. + // size is already adjusted to the maximum data remaining in the chunk. + int retVal = ReadInternal(buffer, offset, size); + + // Adjust offset into chunk by amount of data read. retVal could be less than size. + m_chunk.m_OffsetIntoChunk += (uint)retVal; + + // If we reached end of chunk, then set m_chunk to null. This indicates that chunk was completed. + if (m_chunk.m_OffsetIntoChunk == m_chunk.m_Size) + { + m_chunk = null; + } + + return retVal; + } + + /// + /// Clears the read buffer and reads from the underlying stream until no more data is available. + /// + public void FlushReadBuffer() + { + byte[] buffer = new byte[1024]; + + int waitTimeUs = m_BytesLeftInResponse == 0 ? 500000 : 1000000; + + try + { + while (m_Socket.Poll(waitTimeUs, SelectMode.SelectRead)) + { + int avail = m_Socket.Available; + + if (avail == 0) break; + + while (avail > 0) + { + int bytes = m_Stream.Read(buffer, 0, avail > buffer.Length ? buffer.Length : avail); + + if (bytes <= 0) break; + + avail -= bytes; + + if (m_BytesLeftInResponse > 0) m_BytesLeftInResponse -= bytes; + } + } + } + catch + { + } + + m_dataEnd = m_dataStart = 0; + m_BytesLeftInResponse = -1; + } + + private void ReleaseThread() + { + FlushReadBuffer(); + + m_lastUsed = DateTime.UtcNow; + ResetState(); + + m_InUse = false; + } + + /// + /// Flushes the read buffer and resets the streams parameters. When in used in conjunction with the HttpWebRequest class + /// this method enables this stream to be re-used. + /// + public void ReleaseStream() + { + Thread th = new Thread(new ThreadStart(ReleaseThread)); + th.Start(); + } + + /// + /// Re-implements reading of data to network stream. + /// + /// Buffer with data to write to HTTP client + /// Offset at which to use data from buffer + /// Count of bytes to write. + public int ReadInternal(byte[] buffer, int offset, int size) + { + // Need to init return value to zero explicitly, otherwise warning generated. + int retVal = 0; + + // As first step we copy the buffered data if present + int dataBuffered = m_dataEnd - m_dataStart; + if (dataBuffered > 0) + { + int dataToCopy = size < dataBuffered ? size : dataBuffered; + for (int i = 0; i < dataToCopy; i++) + { + buffer[offset + i] = m_readBuffer[m_dataStart + i]; + } + m_dataStart += dataToCopy; + offset += dataToCopy; + size -= dataToCopy; + retVal += dataToCopy; + } + + // + // Now we check if more data is needed. + // if m_BytesLeftInResponse == -1 , then we don't known the content length of the response + // if m_BytesLeftInResponse is > retVal, then the data in the internal buffer (above) was less than the + // the total content length of the response stream + // In either case, we need to read more data to fullfill the read request + // + if (size > 0 && (m_BytesLeftInResponse == -1 || m_BytesLeftInResponse > retVal)) + { + // If buffering desired and requested data is less than internal buffer size + // then we read into internal buffer. + if (size < read_buffer_size) + { + if (0 == RefillInternalBuffer()) + { + // Handle the 'HTTP/1.0' case + IsDone = IsHttp1_0Completed(); + return 0; + } + + dataBuffered = m_dataEnd - m_dataStart; + if (dataBuffered > 0) + { + int dataToCopy = size < dataBuffered ? size : dataBuffered; + for (int i = 0; i < dataToCopy; i++) + { + buffer[offset + i] = m_readBuffer[m_dataStart + i]; + } + m_dataStart += dataToCopy; + offset += dataToCopy; + size -= dataToCopy; + retVal += dataToCopy; + } + } + else // Do not replentish internal buffer. Read rest of data directly + { + int bytesRead = m_Stream.Read(buffer, offset, size); + retVal += bytesRead; + + // Handle the 'HTTP/1.0' case + if ((bytesRead == 0) && IsHttp1_0Completed()) + { + IsDone = true; + return retVal; + } + } + } + + // update the bytes left in response + if (m_BytesLeftInResponse > 0) + { + m_BytesLeftInResponse -= retVal; + + // in case there were more bytes in the buffer than we expected make sure the next call returns 0 + if (m_BytesLeftInResponse < 0) m_BytesLeftInResponse = 0; + } + + return retVal; + } + + /// + /// Returns true if we are in 'HTTP/1.0' mode and the connection has been closed, which marks the end of the body. + /// + /// + /// In 'HTTP/1.0' mode, where the content length is not transmitted in the response header and the server closes the connection to mark the end of the body. + /// (see: RFC9112, 6.3, point 8, https://www.rfc-editor.org/rfc/rfc9112#name-message-body-length) + /// + private bool IsHttp1_0Completed() + { + return + (m_BytesLeftInResponse == -1) && !m_EnableChunkedDecoding && // We are in HTTP/1.0 mode + m_Socket.Poll(1, SelectMode.SelectRead) && (m_Socket.Available == 0); // The socket is disconnected + } + + /// + /// Impletments Write for the stream. + /// Since we do not have write buffering, all we do is delegate to the m_Stream. + /// + /// Buffer to write + /// Start offset to write data + /// Count of bytes to write + public override void Write(byte[] buffer, int offset, int count) + { + m_Stream.Write(buffer, offset, count); + } + + /// + /// Since we do not have write buffering, all we do is delegate to the m_Stream. + /// + public override void Flush() + { + m_Stream.Flush(); + } + + /// + /// Return true if stream support reading. + /// + public override bool CanRead { get { return m_Stream.CanRead; } } + + /// + /// Return true if stream supports seeking + /// + public override bool CanSeek { get { return m_Stream.CanSeek; } } + + /// + /// Return true if timeout is applicable to the stream + /// + public override bool CanTimeout { get { return m_Stream.CanTimeout; } } + + /// + /// Return true if stream support writing. + /// + public override bool CanWrite { get { return m_Stream.CanWrite; } } + + /// + /// Gets the length of the data available on the stream. + /// + /// The length of the data available on the stream. + /// Add data cached in the stream buffer to available on socket + public override long Length { get { return m_EnableChunkedDecoding && m_chunk != null ? m_chunk.m_Size : m_Stream.Length + m_dataEnd - m_dataStart; } } + + /// + /// Position is not supported for NetworkStream + /// + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + /// + /// Seekking is not suported on network streams + /// + /// Offset to seek + /// Relative origin of the seek + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Setting of length is not supported + /// + /// Length to set + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Timeout for read operations. + /// + public override int ReadTimeout + { + get { return m_Stream.ReadTimeout; } + set { m_Stream.ReadTimeout = value; } + } + + /// + /// Timeout for write operations. + /// + public override int WriteTimeout + { + get { return m_Stream.WriteTimeout; } + set { m_Stream.WriteTimeout = value; } + } + + public bool IsDone { get; private set; } + + public Stream CloneStream() + { + InputNetworkStreamWrapper clone = this.MemberwiseClone() as InputNetworkStreamWrapper; + clone.m_isClone = true; + + return clone; + } + + /// + /// Overrides the Dispose Behavior + /// + protected override void Dispose(bool disposing) + { + m_InUse = false; + + // Clones share m_Stream/m_Socket with the original (see CloneStream), so only + // the original may close them. HttpResponseMessage disposes the original deterministically. + if (!m_isClone) + { + m_Stream.Close(); + + if (m_OwnsSocket) + { + m_Socket.Close(); + } + } + + base.Dispose(disposing); + } + + /// + /// Reads one line from steam, terminated by \r\n or by \n. + /// + /// Maxinun length of the line. If line is longer than maxLineLength exception is thrown + /// String that represents the line, not including \r\n or by \n + internal string Read_HTTP_Line(int maxLineLength) + { + int curPos = 0; + bool readLineComplete = false; + + while (!readLineComplete) + { + // We need to read character by character. For efficiency we need stream that implements internal bufferting. + // We cannot read more than one character at a time, since this one could be the last. + int maxCurSize = m_lineBuf.Length - 1; + maxCurSize = maxCurSize < maxLineLength ? maxCurSize : maxLineLength; + while (curPos < maxCurSize) + { + // If data available, Reads one byte of data. + if (m_dataEnd - m_dataStart > 0) + { // Very special code for reading of one character. + m_lineBuf[curPos] = m_readBuffer[m_dataStart]; ++curPos; ++m_dataStart; + } + else + { // Refill internal buffer and read one character. + if (0 == RefillInternalBuffer()) + { + readLineComplete = true; + break; + } + m_lineBuf[curPos] = m_readBuffer[m_dataStart]; ++curPos; ++m_dataStart; + } + + // Accoring to HTTP spec HTTP headers lines should be separated by "\r\n" + // Still spec requires for testing of "\n" only. So we test both + if (m_lineBuf[curPos - 1] == '\r' || m_lineBuf[curPos - 1] == '\n') + { + // Next character should be '\n' if previous was '\r' + if (m_lineBuf[curPos - 1] == '\r') + { + // If data available, Reads one byte of data. + if (m_dataEnd - m_dataStart > 0) + { // Very special code for reading of one character. + m_lineBuf[curPos] = m_readBuffer[m_dataStart]; ++curPos; ++m_dataStart; + } + else + { // Refill internal buffer and read one character. + if (0 == RefillInternalBuffer()) + { + readLineComplete = true; + break; + } + m_lineBuf[curPos] = m_readBuffer[m_dataStart]; ++curPos; ++m_dataStart; + } + } + readLineComplete = true; + break; + } + } + + // If we reached limit of the line size, just throw protocol violation exception. + if (curPos == maxLineLength) + { + throw new WebException("Line too long", WebExceptionStatus.ServerProtocolViolation); + } + + // There was no place in the m_lineBuf or end of line reached. + if (!readLineComplete) + { + // Need to allocate larger m_lineBuf and copy existing line there. + byte[] newLineBuf = new byte[m_lineBuf.Length * 2]; + // Copy data to new array + m_lineBuf.CopyTo(newLineBuf, 0); + // Re-assign. Now m_lineBuf is twice as long and keeps the same data. + m_lineBuf = newLineBuf; + } + } + // Now we need to convert from byte array to string. + if (curPos - 2 > 0) + { + int byteUsed, charUsed; + bool completed = false; + char[] charBuf = new char[curPos - 2]; + UTF8decoder.Convert(m_lineBuf, 0, curPos - 2, charBuf, 0, charBuf.Length, true, out byteUsed, out charUsed, out completed); + return new string(charBuf); + } + else if (curPos == 0) + { + throw new SocketException(SocketError.ConnectionAborted); + } + + return ""; + } + + /// + /// Preview the byte in the input stream without removing it. + /// + /// Next byte in the stream. + private byte PeekByte() + { + // Refills internal buffer if there is no more data + if (m_dataEnd == m_dataStart) + { + if (0 == RefillInternalBuffer()) throw new SocketException(SocketError.ConnectionAborted); + } + return m_readBuffer[m_dataStart]; + } + + /// + /// Returns the byte in the input stream and removes it. + /// + /// Next byte in the stream. + public override int ReadByte() + { + // Refills internal buffer if there is no more data + if (m_dataEnd == m_dataStart) + { + if (0 == RefillInternalBuffer()) throw new SocketException(SocketError.ConnectionAborted); + } + // Very similar to Peek, but moves current position to next byte. + return m_readBuffer[m_dataStart++]; + } + + /// + /// Writes single byte to stream + /// + /// + public override void WriteByte(byte value) + { + m_Stream.WriteByte(value); + } + + /// + /// Reads HTTP header from input stream. + /// HTTP header can be wrapped on multiple lines. + /// If next line starts by white space or tab - means it is continuing current header. + /// Thus we need to use PeekByte() to check next byte without removing it from stream. + /// + /// Maximum length of the header + /// + internal string Read_HTTP_Header(int maxLineLength) + { + string strHeader = Read_HTTP_Line(maxLineLength); + int headLineLen = strHeader.Length; + + // If line is empty - means the last one. Just return it. + if (headLineLen == 0) + { + return strHeader; + } + + maxLineLength -= headLineLen; + // Check next byte in the stream. If it is ' ' or '\t' - next line is continuation of existing header. + while (maxLineLength > 0) + { + byte nextByte = PeekByte(); + // If next byte is not white space or tab, then we are done. + if (!(nextByte == ' ' || nextByte == '\t')) + { + return strHeader; + } + // If we got here - means next line starts by white space or tab. Need to read it and append it. + string strLine = Read_HTTP_Line(maxLineLength); + // Decrease by amount of data read + maxLineLength -= strLine.Length; + // Adds it to the header. + strHeader += strLine; + } + // If we come here - means HTTP header exceedes maximum length. + throw new WebException("HTTP header too long", WebExceptionStatus.ServerProtocolViolation); + } + + /// + /// Retrieve information of the chunk. + /// + /// + private Chunk GetChunk() + { + Chunk nextChunk = new Chunk(); + bool parsing = true; + ChunkState state = ChunkState.InitialLF; + byte[] buffer = new byte[1024]; + byte[] data; + int dataByte = 0; + + while (parsing) + { + int readByte = ReadByte(); + switch (readByte) + { + case 13: //CR + if (state == ChunkState.InitialLF) + break; + + data = new byte[dataByte]; + Array.Copy(buffer, data, dataByte); + switch (state) + { + case ChunkState.Size: + nextChunk.m_Size = (uint)Convert.ToInt32(new string(UTF8Encoding.GetChars(data)), 16); + dataByte = 0; + break; + case ChunkState.Value: + dataByte = 0; + break; + default: + throw new ProtocolViolationException("Wrong state for CR"); + } + state = ChunkState.LF; + break; + case 10: //LF + switch (state) + { + + case ChunkState.LF: + parsing = false; + break; + case ChunkState.InitialLF: + state = ChunkState.Size; + break; + default: + throw new ProtocolViolationException("Incorrectly formated Chunk - Unexpected Line Feed"); + } + break; + case 59: // ; + if (state == ChunkState.Size) + { + data = new byte[dataByte]; + Array.Copy(buffer, data, dataByte); + nextChunk.m_Size = (uint)Convert.ToInt32(new string(UTF8Encoding.GetChars(data)), 16); + dataByte = 0; + } + else + throw new ProtocolViolationException("Incorrectly formated Chunk"); + state = ChunkState.Name; + break; + case 61: // = + if (state == ChunkState.Name) + { + dataByte = 0; + } + else + throw new ProtocolViolationException("Incorrectly formated Chunk"); + state = ChunkState.Value; + break; + default: + if (state == ChunkState.InitialLF) + state = ChunkState.Size; + + // buffer is fixed-size: a chunk-size line/extension without a CR before it fills + // up would otherwise write past the end of the array. Fail cleanly instead. + if (dataByte >= buffer.Length) + { + throw new ProtocolViolationException("Chunk header too long"); + } + + buffer[dataByte] = (byte)readByte; + dataByte++; + if (state == ChunkState.LF) + throw new ProtocolViolationException("Unexpected data after Line Feed"); + break; + } + } + + return nextChunk; + } + + public override int Read(SpanByte buffer) + { + throw new NotSupportedException(); + } + + private enum ChunkState + { + Size, + Name, + Value, + LF, + InitialLF + } + + private class Chunk + { + public uint m_Size; + public uint m_OffsetIntoChunk; + } + + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs b/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs new file mode 100644 index 0000000..7f02c83 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs @@ -0,0 +1,295 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System.IO; + using System.Net.Sockets; + using System.Text; + + /// + /// The OutputNetworkStreamWrapper is used to re-implement calls to NetworkStream.Write + /// On first write HttpListenerResponse needs to send HTTP headers to client. + /// + internal class OutputNetworkStreamWrapper : Stream + { + /// + /// This is a socket connected to client. + /// OutputNetworkStreamWrapper owns the socket, not NetworkStream. + /// If connection is persistent, then the m_Socket is transferred to the list of + /// + internal Socket m_Socket; + + /// + /// Actual network or SSL stream connected to the client. + /// It could be SSL stream, so NetworkStream is not exact type, m_Stream would be derived from NetworkStream + /// + internal NetworkStream m_Stream; + + /// + /// If true causes all written data to be encoded as chunks + /// + internal bool m_enableChunkedEncoding = false; + + /// + /// Type definition of delegate for sending of HTTP headers. + /// + internal delegate void SendHeadersDelegate(); + + /// + /// If not null - indicates whether we have sent headers or not. + /// Calling of delegete sends HTTP headers to client - HttpListenerResponse.SendHeaders() + /// + private SendHeadersDelegate m_headersSend; + + /// + /// EOL marker in chunked encoding + /// + private readonly byte[] EOLMarker = { 0xd, 0xa }; + + /// + /// Just passes parameters to the base. + /// Socket is not owned by base NetworkStream + /// + /// + /// + public OutputNetworkStreamWrapper(Socket socket, NetworkStream stream) + { + m_Socket = socket; + m_Stream = stream; + } + + /// + /// Sets the delegate for sending of headers. + /// + internal SendHeadersDelegate HeadersDelegate { set { m_headersSend = value; } } + + /// + /// Return true if stream support reading. + /// + public override bool CanRead { get { return false; } } + + /// + /// Return true if stream supports seeking + /// + public override bool CanSeek { get { return false; } } + + /// + /// Return true if timeout is applicable to the stream + /// + public override bool CanTimeout { get { return m_Stream.CanTimeout; } } + + /// + /// Return true if stream support writing. It should be true, as this is output stream. + /// + public override bool CanWrite { get { return true; } } + + /// + /// Gets the length of the data available on the stream. + /// Since this is output stream reading is not allowed and length does not have meaning. + /// + /// The length of the data available on the stream. + public override long Length { get { throw new NotSupportedException(); } } + + /// + /// Position is not supported for NetworkStream + /// + public override long Position + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// Timeout for read operations. + /// + public override int ReadTimeout + { + get { return m_Stream.ReadTimeout; } + set { m_Stream.ReadTimeout = value; } + } + + /// + /// Timeout for write operations. + /// + public override int WriteTimeout + { + get { return m_Stream.WriteTimeout; } + set { m_Stream.WriteTimeout = value; } + } + + /// + /// Writes to stream size of chunk and marks start of the chunk + /// + private void WriteChunkStart(int size) + { + byte[] chunkLengthBytes = Encoding.UTF8.GetBytes($"{size:X}"); + m_Stream.Write(chunkLengthBytes, 0, chunkLengthBytes.Length); + m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + } + + /// + /// Writes to stream marker - finish of the chunk + /// + private void WriteChunkEnd() + { + m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + } + + /// + /// Writes to stream marker - finish of all chunks + /// + private void WriteChunkFinish() + { + byte[] zero = { 0x30 }; + m_Stream.Write(zero, 0, 1); + m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + } + + /// + /// Closes the stream. Verifies that HTTP response is sent before closing. + /// + public override void Close() + { + if (m_headersSend != null) + { + // Calls HttpListenerResponse.SendHeaders. HttpListenerResponse.SendHeaders sets m_headersSend to null. + m_headersSend(); + } + + if (m_Stream != null) m_Stream.Close(); + m_Stream = null; + m_Socket = null; + } + + /// + /// Flushes the stream. Verifies that HTTP response is sent before flushing. + /// + public override void Flush() + { + if (m_headersSend != null) + { + // Calls HttpListenerResponse.SendHeaders. HttpListenerResponse.SendHeaders sets m_headersSend to null. + m_headersSend(); + } + + if (m_enableChunkedEncoding) + { + WriteChunkFinish(); + } + + // Need to check for null before using here + m_Stream?.Flush(); + } + + /// + /// This putput stream, so read is not supported. + /// + /// + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// This putput stream, so read is not supported. + /// + /// + public override int ReadByte() + { + throw new NotSupportedException(); + } + + /// + /// Seeking is not suported on network streams + /// + /// Offset to seek + /// Relative origin of the seek + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Setting length is not suported on network streams + /// + /// Length to set + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes single byte to the stream. + /// + /// Byte value to write. + public override void WriteByte(byte value) + { + if (m_headersSend != null) + { + // Calls HttpListenerResponse.SendHeaders. HttpListenerResponse.SendHeaders sets m_headersSend to null. + m_headersSend(); + } + + if (m_enableChunkedEncoding) + { + WriteChunkStart(1); + } + + m_Stream.WriteByte(value); + + if (m_enableChunkedEncoding) + { + WriteChunkEnd(); + } + } + + + /// + /// Re-implements writing of data to network stream. + /// The only functionality - on first write it sends HTTP headers. + /// Then calls base + /// + /// Buffer with data to write to HTTP client + /// Offset at which to use data from buffer + /// Count of bytes to write. + public override void Write(byte[] buffer, int offset, int size) + { + if (m_headersSend != null) + { + // Calls HttpListenerResponse.SendHeaders. HttpListenerResponse.SendHeaders sets m_headersSend to null. + m_headersSend(); + } + + if (m_enableChunkedEncoding) + { + WriteChunkStart(size); + } + + m_Stream.Write(buffer, offset, size); + + if (m_enableChunkedEncoding) + { + WriteChunkEnd(); + } + } + + public override int Read(SpanByte buffer) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs b/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs new file mode 100644 index 0000000..60cb205 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs @@ -0,0 +1,133 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + + /// + /// Internal support class for Validation related stuff. + /// + internal class ValidationHelper + { + public static string[] EmptyArray = new string[0]; + + public static string[] MakeEmptyArrayNull(string[] stringArray) + { + if (stringArray == null || stringArray.Length == 0) + { + return null; + } + else + { + return stringArray; + } + } + + public static string MakeStringNull(string stringValue) + { + if (stringValue == null || stringValue.Length == 0) + { + return null; + } + else + { + return stringValue; + } + } + + public static string MakeStringEmpty(string stringValue) + { + if (stringValue == null || stringValue.Length == 0) + { + return String.Empty; + } + else + { + return stringValue; + } + } + + public static bool IsBlankString(string stringValue) + { + if (stringValue == null || stringValue.Length == 0) + { + return true; + } + else + { + return false; + } + } + + public static short ValidatePort(int port) + { + + if ((port < IPEndPoint.MinPort) || + (port > IPEndPoint.MaxPort)) + { // If upper 16b is not zero... + throw new ArgumentOutOfRangeException("port"); // Then this is not valid port + } + + return (short)port; + } + + public static bool ValidateTcpPort(int port) + { + // on false, API should throw new ArgumentOutOfRangeException("port"); + return port >= IPEndPoint.MinPort && port <= IPEndPoint.MaxPort; + } + + public static void ValidateRange(int actual, int fromAllowed, int toAllowed) + { + if (actual > toAllowed || actual < fromAllowed) + { + throw new ArgumentOutOfRangeException("value"); + } + } + + public static string ExceptionMessage(Exception exception) + { + if (exception == null) + { + return string.Empty; + } + + if (exception.InnerException == null) + { + return exception.Message; + } + + return exception.Message + " (" + ExceptionMessage(exception.InnerException) + ")"; + } + + internal static readonly char[] InvalidParamChars = + new char[]{ + '(', + ')', + '<', + '>', + '@', + ',', + ';', + ':', + '\\', + '"', + '\'', + '/', + '[', + ']', + '?', + '=', + '{', + '}', + ' ', + '\t', + '\r', + '\n'}; + + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs b/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs new file mode 100644 index 0000000..305c53a --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + using System; + + /// + /// Provides the base interface for implementing proxy access for the + /// class. + /// + public interface IWebProxy + { + /// + /// Returns the URI of a proxy. + /// + /// The destination URI. + /// A Uri instance that contains the URI of the proxy + /// used to contact . + Uri GetProxy(Uri destination); + + /// + /// Indicates whether the proxy should not be used for the specified + /// host. + /// + /// The host to check, to determine whether the proxy + /// is needed to access it. + /// Whether the proxy should not be used for the specified + /// host. + bool IsBypassed(Uri host); + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs b/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs new file mode 100644 index 0000000..5cae966 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs @@ -0,0 +1,230 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace System.Net +{ + /// + /// Contains HTTP proxy settings for the + /// class. + /// + public class WebProxy : IWebProxy + { + // true means DO NOT use proxy on local connections. + // false means use proxy for local network connections. + + private bool _BypassOnLocal; + private Uri _ProxyAddress; // Uri of proxy itself + + /// + /// Initializes an empty instance of the WebProxy class. + /// + /// + /// The URI of the proxy can be set later, using the + /// property. + /// + public WebProxy() + : this((Uri)null, false) + { + } + + /// + /// Initializes a new instance of the WebProxy class + /// from the specified instance. + /// + /// A Uri instance that + /// contains the address of the proxy server. + public WebProxy(Uri Address) + : this(Address, false) + { + } + + /// + /// Initializes a new instance of the WebProxy class + /// with the specified instance and bypass + /// setting. + /// + /// A Uri instance that + /// contains the address of the proxy server. + /// Indicates whether to bypass the WebProxy + /// on local network addresses. + public WebProxy(Uri Address, bool BypassOnLocal) + { + _ProxyAddress = Address; + _BypassOnLocal = BypassOnLocal; + } + + /// + /// Initializes a new instance of the WebProxy class + /// with the specified host and port number. + /// + /// The name of the proxy host, such as: contoso + /// The port number on the host to use, such as: + /// 80 + /// + /// The WebProxy instance is initialized with the + /// property set + /// to a instance of the form: http://Host:Port + /// + public WebProxy(string Host, int Port) + : this(new Uri("http://" + Host + ":" + Port.ToString()), false) { } + + /// + /// Initializes a new instance of the WebProxy class + /// with the specified URI. + /// + /// The URI address of the proxy server. + /// + /// The WebProxy instance is initialized with the + /// property set to a + /// instance containing the + /// Address string. + /// + /// For the new instance of the WebProxy class, + /// "Bypass on local addresses" is set to false. + /// + /// + public WebProxy(string Address) + : this(CreateProxyUri(Address), false) { } + + /// + /// Initializes a new instance of the WebProxy class + /// with the specified URI and bypass setting. + /// + /// The URI of the proxy server. + /// Indicates whether to bypass the proxy + /// when accessing local addresses. + public WebProxy(string Address, bool BypassOnLocal) + : this(CreateProxyUri(Address), BypassOnLocal) { } + + /// + /// Gets or sets the address of the proxy server. + /// + /// A instance that contains the address + /// of the proxy server. + public Uri Address + { + get + { + return _ProxyAddress; + } + + set + { + _ProxyAddress = value; + } + } + + /// + /// Gets or sets whether to bypass the proxy server for local + /// addresses. + /// true to bypass the proxy server for local + /// addresses; otherwise, false. + public bool BypassProxyOnLocal + { + get + { + return _BypassOnLocal; + } + + set + { + _BypassOnLocal = value; + } + } + + /// + /// Returns the proxied URI for a request. + /// + /// The Uri instance of the + /// requested Internet resource. + /// The Uri instance of the Internet + /// resource, if the resource is on the bypass list; otherwise, the + /// Uri instance of the proxy. + /// + public Uri GetProxy(Uri destination) + { + if (IsBypassed(destination)) + { + return destination; + } + + Uri proxy = _ProxyAddress; + if (proxy != null) + { + return proxy; + } + + return destination; + } + + /// + /// Maps a string to a Uri. + /// + /// The Url for creation of the Uri. + /// The new Uri corresponding to the Url. + private static Uri CreateProxyUri(string Address) + { + if (Address == null) + { + return null; + } + + // Original code was IndexOf("://", StringComparison.Ordinal), + // changed to IndexOf("://") we only support ASCII in .NET MF. + if (Address.IndexOf("://") == -1) + { + Address = "http://" + Address; + } + + return new Uri(Address); + } + + /// + /// Checks whether the supplied Uri represents a local address. + /// + /// The Uri to check. + /// trueif the address is local; otherwise, + /// false. + private bool IsLocal(Uri host) + { + string hostString = host.Host; + int dot = hostString.IndexOf('.'); + + if (dot == -1) + { + return true; + } + + return false; + } + + /// + /// Indicates whether to use the proxy server for the specified host. + /// + /// The Uri instance of the host + /// to check for proxy use. + /// true if the proxy server should not be + /// used for the host; otherwise, false. + public bool IsBypassed(Uri host) + { + + if (host.IsLoopback) + { + return true; // bypass localhost from using a proxy. + } + + if ((_ProxyAddress == null) || + (_BypassOnLocal && IsLocal(host))) + { + return true; // bypass when non .'s and no proxy on local + } + else + { + return false; + } + } + } +} diff --git a/src/Cosmos.Network.Http/Http/System.Uri.cs b/src/Cosmos.Network.Http/Http/System.Uri.cs new file mode 100644 index 0000000..84f88e6 --- /dev/null +++ b/src/Cosmos.Network.Http/Http/System.Uri.cs @@ -0,0 +1,1393 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System +{ + /// + /// Defines the kinds of s for the + /// method and several + /// methods. + /// + public enum UriKind + { + /// + /// The kind of the Uri is indeterminate. + /// + RelativeOrAbsolute = 0, + + /// + /// The Uri is an absolute Uri. + /// + Absolute = 1, + + /// + /// The Uri is a relative Uri. + /// + Relative = 2, + } + + /// + /// Defines host name types for the http and https protocols. + /// method. + /// + public enum UriHostNameType + { + /// + /// The type of the host name is not supplied. + /// + Unknown = 0, + + /// + /// The host is set, but the type cannot be determined. + /// + Basic = 1, + + /// + /// The host name is a domain name system (DNS) style host name. + /// + Dns = 2, + + /// + /// The host name is an Internet Protocol (IP) version 4 host address. + /// + IPv4 = 3, + + /// + /// The host name is an Internet Protocol (IP) version 6 host address. + /// + IPv6 = 4, + } + + /// + /// Provides an object representation of a uniform resource identifier (URI) + /// and easy access to the parts of the URI. + /// + public class Uri + { + /// + /// Specifies that the URI is accessed through the Hypertext Transfer Protocol (HTTP). This field is read-only. + /// + public const string UriSchemeHttp = "http"; + + /// + /// Specifies that the URI is accessed through the Secure Hypertext Transfer Protocol (HTTPS). This field is read-only. + /// + public const string UriSchemeHttps = "https"; + + internal const string UriSchemeWs = "ws"; + internal const string UriSchemeWss = "wss"; + + private int DefaultPort(string scheme) + { + return scheme switch + { + UriSchemeHttp or UriSchemeWs => HttpDefaultPort, + UriSchemeHttps or UriSchemeWss => HttpsDefaultPort, + "ftp" => 21, + "sftp" or "ssh" or "scp" => 22, + "gopher" => 70, + "nntp" => 119, + "telnet" => 23, + "ldap" => 389, + "ldaps" => 636, + "mysql" => 3306, + "mssql" => 1433, + "oracle" => 1521, + "mqtt" => 1883, + "mqtts" => 8883, + "redis" => 6379, + "rtsp" => 554, + "postgresql" => 5432, + "mongodb" => 27017, + "mailto" or "smtp" => 25, + "net.tcp" => 808, + + _ => UnknownPort, + }; + } + + /// + /// Defines flags kept in m_Flags variable. + /// + [Flags] + protected enum Flags + { + /// + /// Flag value for loopback host + /// + LoopbackHost = 0x00400000 + } + + /// + /// Default port for http protocol - 80 + /// + public const int HttpDefaultPort = 80; + + /// + /// Default port for https protocol - 443 + /// + public const int HttpsDefaultPort = 443; + + /// + /// Constant to indicate that port for this protocol is unknown + /// + protected const int UnknownPort = -1; + + /// + /// Type of the host. + /// + protected UriHostNameType _hostNameType; + + /// + /// Member variable that keeps port used by this uri. + /// + protected int _port = UnknownPort; + + /// + /// Member variable that keeps internal flags/ + /// + protected Flags _Flags; + + /// + /// Member variable that keeps absolute path. + /// + protected string _absolutePath = string.Empty; + + /// + /// Member variable that keeps original string passed to Uri constructor. + /// + protected string _originalUriString = null; + + /// + /// Member variable that keeps scheme of Uri. + /// + protected string _scheme = null; + + /// + /// Member variable that keeps host name ( http and https ). + /// + protected string _host = string.Empty; + + /// + /// Member variable that keeps boolean if Uri is absolute. + /// + protected bool _isAbsoluteUri = false; + + /// + /// Member variable that tells if path is UNC ( Universal Naming Convention ) + /// In this class it is always false, but can be changed in derived classes. + /// + protected bool _isUnc = false; + + /// + /// Member variable that keeps absolute uri (generated in method ParseUriString) + /// + protected string _absoluteUri = null; + + /// + /// Member variable that keeps the fragment (if any found) + /// + protected string _fragment = string.Empty; + + /// + /// Member variable that keeps the query (if any found) + /// + protected string _query = string.Empty; + + /// + /// Member variable that keeps the user info (if any found) + /// + protected string _userInfo = string.Empty; + + + /// + /// Gets the type of the host name specified in the URI. + /// + /// A member of the + /// enumeration. + public UriHostNameType HostNameType => _hostNameType; + + /// + /// Gets the port number of this URI. + /// + /// An Int32 value containing the port number + /// for this URI. + /// + /// This instance represents a relative URI, and this property is valid + /// only for absolute URIs. + /// + public int Port + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _port; + } + } + + /// + /// Gets whether the instance is absolute. + /// + /// true if the Uri + /// instance is absolute; otherwise, false. + public bool IsAbsoluteUri => _isAbsoluteUri; + + /// + /// Gets whether the specified is a universal + /// naming convention (UNC) path. + /// + /// true if the is a + /// UNC path; otherwise, false. + /// + /// This instance represents a relative URI, and this property is valid + /// only for absolute URIs. + /// + public bool IsUnc + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _isUnc; + } + } + + /// + /// Gets a value that indicates whether the port value of the URI is the default for this scheme. + /// + public bool IsDefaultPort => DefaultPort(_scheme) == _port; + + /// + /// Gets a value that indicates whether the specified Uri is a file URI. + /// + public bool IsFile => _scheme == "file"; + + /// + /// Gets an array containing the path segments that make up the specified URI. + /// + public string[] Segments => AbsolutePath.Split('/'); + + /// + /// Gets a local operating-system representation of a file name. + /// + /// A String containing the local + /// operating-system representation of a file name. + /// + /// This instance represents a relative URI, and this property is valid + /// only for absolute URIs. + /// + public string AbsolutePath + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _absolutePath; + } + } + + /// + /// Gets the original URI string that was passed to the Uri constructor. + /// + public string OriginalString => _originalUriString; + + /// + /// Gets a string containing the absolute uri or entire uri of this instance. + /// + /// A String containing the entire URI. + /// + public string AbsoluteUri + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _absoluteUri; + } + } + + /// + /// Gets the escaped URI fragment, including the leading '#' character if not empty. + /// + ///A String containing the fragment if present. + public string Fragment => _fragment; + + /// + /// Gets any query information included in the specified URI, including the leading '?' character if not empty. + /// + public string Query => _query; + + /// + /// Gets the AbsolutePath and Query properties separated by a question mark (?). + /// + public string PathAndQuery => AbsolutePath + Query; + + /// + /// Gets the user name, password, or other user-specific information associated with the specified URI. + /// + public string UserInfo => _userInfo; + + /// + /// Gets the scheme name for this URI. + /// + /// A String containing the scheme for this + /// URI, converted to lowercase. + /// + /// This instance represents a relative URI, and this property is valid only + /// for absolute URIs. + /// + public string Scheme + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _scheme; + } + } + + /// + /// Gets the host component of this instance. + /// + /// A String containing the host name. This + /// is usually the DNS host name or IP address of the server. + public string Host + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _host; + } + } + + /// + /// Gets whether the specified refers to the local host. + /// + /// if the host specified in the Uri is the local computer; otherwise, . + public bool IsLoopback + { + get + { + if (_isAbsoluteUri == false) + { + throw new InvalidOperationException(); + } + + return _Flags.HasFlag(Flags.LoopbackHost); + } + } + + /// + /// Initializes a new instance of the class + /// with the specified URI. + /// + /// + /// This constructor parses the URI string, therefore it can be used to + /// validate a URI. + /// + /// A URI. + /// + /// The is null. + /// + /// + ///

The is empty.

+ ///

-or-

The scheme specified in + /// is not correctly formed.

+ ///

-or-

contains too many + /// slashes.

+ ///

-or-

The password specified in + /// is not valid.

+ ///

-or-

The host name specified in + /// is not valid.

+ ///

-or-

The file name specified in + /// is not valid.

+ ///

-or-

The user name specified in + /// is not valid.

+ ///

-or-

The host or authority name specified in + /// cannot be terminated by backslashes. + ///

+ ///

-or-

The port number specified in + /// is not valid or cannot be parsed.

+ ///

-or-

The length of exceeds + /// 65534 characters.

+ ///

-or-

The length of the scheme specified in + /// exceeds 1023 characters.

+ ///

-or-

There is an invalid character sequence in + /// .

+ ///

-or-

The MS-DOS path specified in + /// must start with c:\\.

+ ///
+ public Uri(string uriString) + { + if (string.IsNullOrEmpty(uriString)) + { + throw new ArgumentNullException(); + } + + if (!ConstructAbsoluteUri(uriString)) + { + throw new ArgumentException(); + } + } + + /// + /// Constructs an absolute Uri from a URI string. + /// + /// A URI. + /// + /// See . + /// + protected bool ConstructAbsoluteUri(string uriString) + { + // ParseUriString provides full validation including testing for null. + if (ParseUriString(uriString)) + { + _originalUriString = uriString; + + return true; + } + else + { + return false; + } + } + + /// + /// Constructs Uri from string and enumeration that tell what is the type of Uri. + /// + /// String to construct Uri from + /// Type of Uri to construct + /// Unable to validate uri + public Uri(string uriString, UriKind kind) + { + // ParseUriString provides full validation including testing for null. + switch (kind) + { + case UriKind.Absolute: + { + if (!ConstructAbsoluteUri(uriString)) + { + throw new FormatException(); + } + break; + } + + case UriKind.RelativeOrAbsolute: + { + // try first with a absolute + if (ConstructAbsoluteUri(uriString)) + { + break; + } + else + { + // now try with relative + if (!ValidateUriPart(uriString, 0)) + { + throw new FormatException(); + } + } + break; + } + + // Relative Uri. Store in original string. + case UriKind.Relative: + { + // Validates the relative Uri. + if (!ValidateUriPart(uriString, 0)) + { + throw new FormatException(); + } + break; + } + } + + _originalUriString = uriString; + } + + /// + /// Initializes a new instance of the class based on the specified base URI and relative URI . + /// + /// The base URI. + /// The relative URI to add to the base URI. + /// is . + /// is not an absolute Uri instance. + /// The scheme specified in the URI formed by combining and is not valid. + public Uri( + Uri baseUri, + string relativeUri = null) + { + if (baseUri is null) + { + throw new ArgumentNullException(); + } + + if (!baseUri.IsAbsoluteUri) + { + throw new ArgumentOutOfRangeException(); + } + + if (string.IsNullOrEmpty(relativeUri)) + { + ConstructAbsoluteUri(baseUri.OriginalString); + } + else + { + // try first with a absolute + if (!ConstructAbsoluteUri(relativeUri)) + { + // now try with relative + string baseUrl; + if (relativeUri[0] == '/') + { + baseUrl = baseUri.AbsoluteUri.Substring(0, baseUri.AbsoluteUri.Length - baseUri.AbsolutePath.Length - baseUri.Query.Length - baseUri.Fragment.Length); + } + else + { + baseUrl = baseUri.AbsoluteUri.Substring(0, baseUri.AbsoluteUri.Length - baseUri.Query.Length - baseUri.Fragment.Length); + if (!baseUrl.EndsWith("/")) + { + int idx = baseUrl.LastIndexOf("/"); + if (idx >= 0) + { + baseUrl = baseUrl.Substring(0, idx + 1); + } + else + { + baseUrl += '/'; + } + } + } + + ConstructAbsoluteUri(baseUrl + relativeUri); + } + } + } + + /// + /// Validates that part of Uri after sheme is valid for unknown Uri scheme + /// + /// Uri string + /// Index in the string where Uri part ( after scheme ) starts + protected bool ValidateUriPart( + string uriString, + int startIndex) + { + uriString = ExtractFragment(uriString); + uriString = ExtractQuery(uriString); + + // Check for valid alpha numeric characters + int pathLength = uriString.Length - startIndex; + + // This is unknown scheme. We do validate following rules: + // 1. All character values are less than 128. For characters it means they are more than zero. + // 2. All charaters are >= 32. Lower values are control characters. + // 3. If there is %, then there should be 2 hex digits which are 0-10 and A-F or a-f. + + for (int i = startIndex; i < pathLength; ++i) + { + //if (!(IsAlphaNumeric(uriString[i]) || uriString[i] == '+' || uriString[i] == '-' || uriString[i] == '.')) + // If character is upper ( in signed more than 127, then value is negative ). + char value = uriString[i]; + if (value < 32) + { + Debug.WriteLine($"Invalid char: {value}"); + return false; + } + + // If it is percent, then there should be 2 hex digits after. + if (value == '%') + { + if (pathLength - i < 3) + { + Debug.WriteLine("No data after %"); + return false; + } + + // There are at least 2 characters. Check their values + for (int j = 1; j < 3; j++) + { + char nextVal = uriString[i + j]; + if (!((nextVal >= '0' && nextVal <= '9') + || (nextVal >= 'A' && nextVal <= 'F') + || (nextVal >= 'a' && nextVal <= 'f') + ) + ) + { + Debug.WriteLine($"Invalid char after %: {value}"); + return false; + } + } + + // Moves i by 2 up to bypass verified characters. + i += 2; + } + } + + // got here, so must be OK + return true; + } + + /// + /// Internal method parses a URI string into Uri variables + /// + /// A Uri. + /// + /// The is null. + /// + /// + /// See constructor description. + /// + protected bool ParseUriString(string uriString) + { + // Check for null or empty string. + if (string.IsNullOrEmpty(uriString)) + { + return false; + } + uriString = uriString.Trim(); + + // Check for presence of ':'. Colon always should be present in URI. + if (uriString.IndexOf(':') == -1) + { + return false; + } + + // If this is a urn parse and return + if (uriString.IndexOf("urn:", 0) == 0) + { + return ValidateUrn(uriString); + } + + // If the uri is a relative path parse and return + if (uriString[0] == '/') + { + return ValidateRelativePath(uriString); + } + + // Validate Scheme + int endIndex = uriString.IndexOf(':'); + _scheme = uriString.Substring(0, endIndex).ToLower(); + + if (!IsAlpha(_scheme[0])) + { + return false; + } + + for (int i = 1; i < _scheme.Length; i++) + { + if (!(IsAlphaNumeric(_scheme[i]) || _scheme[i] == '+' || _scheme[i] == '-' || _scheme[i] == '.')) + { + return false; + } + } + + int startIndex; + // Get past the colon + startIndex = endIndex + 1; + if (startIndex >= uriString.Length) + { + return false; + } + + // If present, extract fragment and query and remove from uriString + uriString = ExtractFragment(uriString); + uriString = ExtractQuery(uriString); + + // Get host, port and absolute path + bool bRooted = ParseSchemeSpecificPart(uriString, startIndex); + + if ((_scheme == "file" || _scheme == "mailto") && _host.Length == 0) + { + _hostNameType = UriHostNameType.Basic; + } + else if (_host.Length == 0) + { + _hostNameType = UriHostNameType.Unknown; + } + else if (_host[0] == '[') + { + if (!IsIPv6(_host)) + { + return false; + } + + _hostNameType = UriHostNameType.IPv6; + } + else if (IsIPv4(_host)) + { + _hostNameType = UriHostNameType.IPv4; + } + else + { + _hostNameType = UriHostNameType.Dns; + } + + if (_host != null) + { + if (_host == "localhost" || + _host == "loopback" || + (_scheme == "file" || _scheme == "mailto") && _host.Length == 0) + { + _Flags |= Flags.LoopbackHost; + } + } + + // Set AbsoluteUri by reassembling the Uri from extracted values + _absoluteUri = _scheme + ":" + + (bRooted ? "//" : string.Empty) + + (String.IsNullOrEmpty(_userInfo) ? string.Empty : _userInfo + "@") + _host + + ((DefaultPort(_scheme) == _port) ? string.Empty : ":" + _port.ToString()) + + (_scheme == "file" && _absolutePath.Length >= 2 && IsAlpha(_absolutePath[0]) && _absolutePath[1] == ':' ? "/" : string.Empty) + + _absolutePath + _query + _fragment; + + _isAbsoluteUri = true; + _isUnc = _scheme == "file" && _host.Length > 0; + + // got here, so it must be OK + return true; + } + + /// + /// Extracts the user info portion of the Uri string, but does not modify it + /// + /// Uri string that is being parsed + /// Start index of user credentials + private void ParseUserInfo(string uriString, int startIndex) + { + // Handle user info + var userSplitIndex = uriString.IndexOf("@", startIndex); + if (userSplitIndex >= 0 && userSplitIndex != startIndex) + { + int userStartIndex = startIndex; + int userEndIndex = userSplitIndex - startIndex; + if (uriString.StartsWith(_scheme + "://")) + { + userStartIndex = startIndex + 2; + userEndIndex = userEndIndex - 2; + } + + _userInfo = uriString.Substring(userStartIndex, userEndIndex); + } + } + + /// + /// Extracts the query portion of the Uri string and returns the Uri string without it + /// + /// Uri string that is being parsed + /// Uri string without the query + private string ExtractQuery(string uriString) + { + // Handle query + var queryIndex = uriString.IndexOf('?'); + if (queryIndex >= 0) + { + _query = uriString.Substring(queryIndex); + uriString = uriString.Substring(0, queryIndex); + } + + return uriString; + } + + /// + /// Extracts the Fragment portion of the Uri string and returns the Uri string without it + /// + /// Uri string that is being parsed + /// Uri string without the fragment + private string ExtractFragment(string uriString) + { + // Handle fragment + var fragmentIndex = uriString.IndexOf('#'); + if (fragmentIndex >= 0) + { + _fragment = uriString.Substring(fragmentIndex); + uriString = uriString.Substring(0, fragmentIndex); + } + + return uriString; + } + + /// + /// Parse Scheme-specific part of uri for host, port and absolute path + /// Briefed syntax abstracted from .NET FX: + /// Group 1 - http, https, ftp, file, gopher, nntp, telnet, ldap, net.tcp and net.pipe + /// Must be rooted. The 1st segment is authority. Empty path should be replace as '/' + /// + /// Group 2 - file + /// Reminder: Treat all '\' as '/' + /// If it starts with only one '/', host should be empty + /// Otherwise, all leading '/' should be ignored before searching for 1st segment. The 1st segment is host + /// + /// Group 3 - news and uuid + /// Authority always be empty. Everything goes to path. + /// + /// Group 4 - mailto and all other shemes + /// The 1st segment is authority iff it was not rooted. + /// + /// Group 5 - all other schemes + /// The 1st segment is authority iff it was rooted. Empty path should be replace as '/' + /// + /// Scheme-specific part of uri + /// + protected bool ParseSchemeSpecificPart( + string sUri, + int iStart) + { + bool bRooted = sUri.Length >= iStart + 2 && sUri.Substring(iStart, 2) == "//"; + bool bAbsoluteUriRooted; + + string sAuthority; + switch (_scheme) + { + case UriSchemeHttp: + case UriSchemeHttps: + case UriSchemeWs: + case UriSchemeWss: + case "ftp": + case "gopher": + case "nntp": + case "telnet": + case "ldap": + case "ldaps": + case "net.tcp": + case "net.pipe": + if (!bRooted) + { + throw new ArgumentException(); + } + + // If present, extract user info and store in member variables + ParseUserInfo(sUri, iStart); + + int credentialSeperater = sUri.IndexOf("@", iStart, sUri.IndexOf('/', iStart) - iStart); + if (credentialSeperater > 0) + { + iStart = credentialSeperater; + } + + bAbsoluteUriRooted = bRooted; + Split(sUri, iStart + 2, out sAuthority, out _absolutePath, true); + break; + + case "file": + if (!bRooted) + { + throw new ArgumentException(); + } + + sUri = sUri.Substring(iStart + 2); + if (sUri.Length > 0) + { + var array = sUri.ToCharArray(); + for (int i = 0; i < array.Length; i++) + { + if (array[i] == '\\') + { + array[i] = '/'; + } + } + sUri = new string(array); + } + + string sTrimmed = sUri.TrimStart('/'); + + if (sTrimmed.Length >= 2 && IsAlpha(sTrimmed[0]) && sTrimmed[1] == ':') + { + //Windows style path + if (sTrimmed.Length < 3 || sTrimmed[2] != '/') + { + throw new ArgumentException(); + } + + sAuthority = string.Empty; + _absolutePath = sTrimmed; + } + else + { + //Unix style path + if (sUri.Length - sTrimmed.Length == 1 || sTrimmed.Length == 0) + { + sAuthority = string.Empty; + _absolutePath = sUri.Length > 0 ? sUri : "/"; + } + else + { + Split(sTrimmed, 0, out sAuthority, out _absolutePath, true); + } + } + + bAbsoluteUriRooted = bRooted; + break; + + case "news": + case "uuid": + sAuthority = string.Empty; + _absolutePath = sUri.Substring(iStart); + bAbsoluteUriRooted = false; + break; + + case "mailto": + // If present, extract user info and store in member variables + ParseUserInfo(sUri, iStart); + + if (bRooted) + { + sAuthority = string.Empty; + _absolutePath = sUri.Substring(iStart); + } + else + { + Split(sUri, iStart, out sAuthority, out _absolutePath, false); + } + bAbsoluteUriRooted = false; + break; + + default: + if (bRooted) + { + Split(sUri, iStart + 2, out sAuthority, out _absolutePath, true); + } + else + { + sAuthority = string.Empty; + _absolutePath = sUri.Substring(iStart); + } + bAbsoluteUriRooted = bRooted; + break; + } + + // Remove user credentials before parsing host and port + int userInfoIndex = sAuthority.IndexOf('@'); + if (userInfoIndex > 0) + { + sAuthority = sAuthority.Substring(userInfoIndex + 1); + } + + // Parse host and port into member variables + int iPortSplitter = sAuthority.LastIndexOf(':'); + if (iPortSplitter < 0 || sAuthority.LastIndexOf(']') > iPortSplitter) + { + _host = sAuthority; + _port = DefaultPort(_scheme); + } + else + { + _host = sAuthority.Substring(0, iPortSplitter); + _port = Convert.ToInt32(sAuthority.Substring(iPortSplitter + 1)); + } + + return bAbsoluteUriRooted; + } + + /// + /// Splits a URI string into its authority and path components, optionally replacing an empty path with a default value. + /// + /// The URI string to be split. + /// The starting index in the URI string from which to begin parsing. + /// + /// The output parameter that will contain the authority part of the URI, extracted from the specified starting index. + /// + /// + /// The output parameter that will contain the path part of the URI. If is true and no path is found, this will be set to "/". + /// + /// + /// A boolean value indicating whether to replace an empty path with a default value of "/". + /// + protected void Split(string sUri, int iStart, out string sAuthority, out string sPath, bool bReplaceEmptyPath) + { + int iSplitter = sUri.IndexOf('/', iStart); + if (iSplitter < 0) + { + sAuthority = sUri.Substring(iStart); + sPath = string.Empty; + } + else + { + sAuthority = sUri.Substring(iStart, iSplitter - iStart); + sPath = sUri.Substring(iSplitter); + } + + if (bReplaceEmptyPath && sPath.Length == 0) + { + sPath = "/"; + } + } + + /// + /// Returns if host name is IP adress 4 bytes. Like 192.1.1.1 + /// + /// string with host name + /// True if name is string with IPv4 address + protected bool IsIPv4(string host) + { + int dots = 0; + int number = 0; + bool haveNumber = false; + int length = host.Length; + + for (int i = 0; i < length; i++) + { + char ch = host[i]; + + if (ch <= '9' && ch >= '0') + { + haveNumber = true; + number = number * 10 + (host[i] - '0'); + if (number > 255) + { + return false; + } + } + else if (ch == '.') + { + if (!haveNumber) + { + return false; + } + + ++dots; + haveNumber = false; + number = 0; + } + else + { + return false; + } + } + + return (dots == 3) && haveNumber; + } + + private bool IsIPv6(string host) + { + return host[0] == '[' && host[host.Length - 1] == ']'; + } + + /// + /// Parses urn string into Uri variables. + /// Parsing is restricted to basic urn:NamespaceID, urn:uuid formats only. + /// + /// A Uri. + /// + /// The is null. + /// + /// + /// See the constructor description. + /// + private bool ValidateUrn(string uri) + { + bool invalidUrn = false; + + // If this is a urn:uuid validate the uuid + if (uri.ToLower().IndexOf("urn:uuid:", 0) == 0) + { + char[] tempUUID = uri.Substring(9).ToLower().ToCharArray(); + int length = tempUUID.Length; + int uuidSegmentCount = 0; + int[] delimiterIndexes = { 8, 13, 18, 23 }; + for (int i = 0; i < length; ++i) + { + // Make sure these are valid hex numbers numbers + if (!IsHex(tempUUID[i]) && tempUUID[i] != '-') + { + invalidUrn = true; + break; + } + else + { + // Check each segment length + if (tempUUID[i] == '-') + { + if (uuidSegmentCount > 3) + { + invalidUrn = true; + break; + } + + if (i != delimiterIndexes[uuidSegmentCount]) + { + invalidUrn = true; + break; + } + + ++uuidSegmentCount; + } + } + } + + _absolutePath = uri.Substring(4); + } + + // Else validate against RFC2141 + else + { + string lowerUrn = uri.Substring(4).ToLower(); + char[] tempUrn = lowerUrn.ToCharArray(); + + // Validate the NamespaceID (NID) + int index = lowerUrn.IndexOf(':'); + if (index == -1) + { + throw new ArgumentException(); + } + + int i; + for (i = 0; i < index; ++i) + { + // Make sure these are valid hex numbers numbers + if (!IsAlphaNumeric(tempUrn[i]) && tempUrn[i] != '-') + { + invalidUrn = true; + break; + } + } + + // Validate the Namespace String + tempUrn = lowerUrn.Substring(index + 1).ToCharArray(); + int urnLength = tempUrn.Length; + if (!invalidUrn && urnLength != 0) + { + string otherChars = "()+,-.:=@;$_!*'"; + for (i = 0; i < urnLength; ++i) + { + if (!IsAlphaNumeric(tempUrn[i]) && !IsHex(tempUrn[i]) && tempUrn[i] != '%' && otherChars.IndexOf(tempUrn[i]) == -1) + { + invalidUrn = true; + break; + } + } + + _absolutePath = uri.Substring(4); + } + } + + if (invalidUrn) + { + return false; + } + + // Set Uri properties + _host = ""; + _isAbsoluteUri = true; + _isUnc = false; + _hostNameType = UriHostNameType.Unknown; + _port = UnknownPort; + _scheme = "urn"; + _absoluteUri = uri; + + return true; + } + + /// + /// Parses relative Uri into variables. + /// + /// A Uri. + private bool ValidateRelativePath(string uri) + { + // Check for null + if (uri == null || uri.Length == 0) + { + return false; + } + + // Check for "//" + if (uri[1] == '/') + { + return false; + } + + // Check for alphnumeric and special characters + for (int i = 1; i < uri.Length; ++i) + { + if (!IsAlphaNumeric(uri[i]) && ("()+,-.:=@;$_!*'").IndexOf(uri[i]) == -1) + { + return false; + } + } + + _absolutePath = uri.Substring(1); + _host = ""; + _isAbsoluteUri = false; + _isUnc = false; + _hostNameType = UriHostNameType.Unknown; + _port = UnknownPort; + + return true; + } + + /// + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + public override bool Equals(object o) + { + return this == (Uri)o; + } + + /// + public static bool operator ==( + Uri lhs, + Uri rhs) + { + object l = lhs, r = rhs; + + if (l == null) + { + return (r == null); + } + else if (r == null) + { + return false; + } + else + { + if (lhs._isAbsoluteUri && rhs._isAbsoluteUri) + { + return lhs._absolutePath.ToLower() == rhs._absolutePath.ToLower(); + } + else + { + return lhs._originalUriString.ToLower() == rhs._originalUriString.ToLower(); + } + } + } + + /// + public static bool operator !=( + Uri lhs, + Uri rhs) + { + object l = lhs, r = rhs; + + if (l == null) + { + return (r != null); + } + else if (r == null) + { + return true; + } + else + { + if (lhs._isAbsoluteUri && rhs._isAbsoluteUri) + { + return lhs._absolutePath.ToLower() != rhs._absolutePath.ToLower(); + } + else + { + return lhs._originalUriString.ToLower() != rhs._originalUriString.ToLower(); + } + } + } + + /// + /// Checks to see if the character value is an alpha character. + /// + /// The character to evaluate. + /// true if the character is Alpha; + /// otherwise, false. + private bool IsAlpha(char testChar) + { + return (testChar >= 'A' && testChar <= 'Z') || (testChar >= 'a' && testChar <= 'z'); + } + + /// + /// Checks to see if the character value is an alpha or numeric. + /// + /// The character to evaluate. + /// true if the character is Alpha or + /// numeric; otherwise, false. + private bool IsAlphaNumeric(char testChar) + { + return (testChar >= 'A' && testChar <= 'Z') || (testChar >= 'a' && testChar <= 'z') || (testChar >= '0' && testChar <= '9'); + } + + /// + /// Checks to see if the character value is Hex. + /// + /// The character to evaluate. + /// true if the character is a valid Hex + /// character; otherwise, false. + private bool IsHex(char testChar) + { + return (testChar >= 'A' && testChar <= 'F') || (testChar >= 'a' && testChar <= 'f') || (testChar >= '0' && testChar <= '9'); + } + + /// + /// Indicates whether the string is well-formed by attempting to + /// construct a URI with the string. + /// + /// A URI. + /// The type of the URI in + /// . + /// + /// true if the string was well-formed in accordance + /// with RFC 2396 and RFC 2732; otherwise false. + /// + public static bool IsWellFormedUriString( + string uriString, + UriKind uriKind) + { + try + { // If absolute Uri was passed - create Uri object. + switch (uriKind) + { + case UriKind.Absolute: + { + Uri testUri = new Uri(uriString); + + if (testUri.IsAbsoluteUri) + { + return true; + } + + return false; + } + + case UriKind.Relative: + { + Uri testUri = new Uri(uriString, UriKind.Relative); + if (!testUri.IsAbsoluteUri) + { + return true; + } + + return false; + } + + default: + return false; + } + } + catch + { + return false; + } + } + } +} diff --git a/src/Cosmos.Network.Http/HttpConnection.cs b/src/Cosmos.Network.Http/HttpConnection.cs deleted file mode 100644 index 7957da5..0000000 --- a/src/Cosmos.Network.Http/HttpConnection.cs +++ /dev/null @@ -1,177 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; - -namespace Cosmos.Network.Http; - -/// -/// One request and its response, over a connection of their own: requests -/// ask the server to close it once it has answered. -/// -/// -/// Written for the Cosmos socket plugs, in a way a desktop runs as well: -/// -/// Host names are resolved with before connecting: -/// the plugged host name overloads of Connect do not resolve. -/// The socket is read and waited on as -/// explains, TLS included: never touches it -/// itself. -/// The connection is closed after the try, not in a -/// finally: a Cosmos kernel skips finally blocks while an -/// exception unwinds. -/// -/// -internal static class HttpConnection -{ - /// Sends to and reads the response. - /// Where to connect. - /// The request, head and body. - /// Whether the request is a HEAD one, whose response has no body. - /// How long the server may stay silent, in milliseconds. - /// The roots an https:// server's certificate must lead to; Mozilla's when . - /// Decides whether to go on with an https:// server, when set. - /// The host could not be resolved or reached, the TLS handshake failed, or the response did not come whole. - public static HttpResponse Exchange(HttpUrl url, byte[] request, bool isHead, int timeout, - TrustedRoots? roots, Func? validation) - { - IPAddress address = Resolve(url.Host); - TrustedRoots? trusted = url.IsSecure ? roots ?? LoadMozillaRoots() : null; - - Socket socket = new(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - ITransport? transport = null; - HttpResponse? response = null; - Exception? failure = null; - try - { - socket.Connect(address, url.Port); - SocketTransport plain = new(socket); - transport = plain; - - if (trusted is not null) - { - TlsTransport secure = new(plain, url, trusted, validation); - transport = secure; - secure.Handshake(timeout); - } - - transport.Send(request); - response = Receive(transport, url, isHead, timeout); - } - catch (Exception exception) - { - failure = exception; - } - - if (transport is not null) - { - transport.Close(); - } - else - { - try - { - socket.Close(); - } - catch (Exception) - { - // Nothing left to do with a connection that would not close. - } - } - - if (failure is HttpException) - { - throw failure; - } - - if (failure is not null) - { - // Not narrowed to SocketException: the Cosmos socket plugs report - // a refused or dropped connection as a bare Exception. - string message = transport is not null - ? $"The connection to {url.Authority} failed: {failure.Message}" - : $"Could not connect to {url.Authority}: {failure.Message}"; - throw new HttpException(message, failure); - } - - return response!; - } - - private static TrustedRoots LoadMozillaRoots() - { - try - { - return TrustedRoots.Mozilla; - } - catch (Exception exception) - { - // Only a package built without its roots, or with a damaged copy. - throw new HttpException($"Could not load the trusted root certificates: {exception.Message}", exception); - } - } - - private static IPAddress Resolve(string host) - { - if (IPAddress.TryParse(host, out IPAddress? literal)) - { - return literal; - } - - IPAddress[] addresses; - try - { - addresses = Dns.GetHostAddresses(host); - } - catch (Exception exception) - { - throw new HttpException($"Could not resolve {host}: {exception.Message}", exception); - } - - // An IPv4 address first: a Cosmos kernel has nothing else to connect to. - foreach (IPAddress address in addresses) - { - if (address.AddressFamily == AddressFamily.InterNetwork) - { - return address; - } - } - - if (addresses.Length == 0) - { - throw new HttpException($"Could not resolve {host}: it has no address."); - } - - return addresses[0]; - } - - private static HttpResponse Receive(ITransport transport, HttpUrl url, bool isHead, int timeout) - { - ResponseReader reader = new(isHead); - byte[] buffer = new byte[SocketTransport.ChunkSize]; - - while (!reader.IsComplete) - { - int read = transport.Receive(buffer); - if (read > 0) - { - reader.Append(buffer, read); - continue; - } - - // The server closed its end. - if (read < 0) - { - break; - } - - if (Stopwatch.GetElapsedTime(transport.LastActivity).TotalMilliseconds > timeout) - { - throw new HttpException($"{url.Authority} sent nothing for {timeout} ms."); - } - } - - return reader.ToResponse(url.ToString()); - } -} diff --git a/src/Cosmos.Network.Http/HttpException.cs b/src/Cosmos.Network.Http/HttpException.cs deleted file mode 100644 index fe2ff4f..0000000 --- a/src/Cosmos.Network.Http/HttpException.cs +++ /dev/null @@ -1,29 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; - -namespace Cosmos.Network.Http; - -/// -/// A request that did not get a usable response: the host could not be -/// resolved or reached, the TLS handshake failed or the server's certificate -/// is not trusted, the server went silent or sent something that is not -/// HTTP, a redirect led nowhere, or -/// found an error status. -/// -public sealed class HttpException : Exception -{ - /// The status of the response that failed, or 0 when there was no response. - public int StatusCode { get; } - - /// Creates an exception, with the status of the response that failed if there was one. - public HttpException(string message, int statusCode = 0) : base(message) - { - StatusCode = statusCode; - } - - /// Creates an exception for a failure underneath HTTP, a socket error for instance. - public HttpException(string message, Exception innerException) : base(message, innerException) - { - } -} diff --git a/src/Cosmos.Network.Http/HttpRequest.cs b/src/Cosmos.Network.Http/HttpRequest.cs deleted file mode 100644 index 20bd2c8..0000000 --- a/src/Cosmos.Network.Http/HttpRequest.cs +++ /dev/null @@ -1,338 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Cosmos.Network.Http; - -/// -/// An HTTP/1.1 request to an http:// or https:// URL. runs -/// it on the calling thread, follows redirects, and returns the response whole. -/// -/// -/// -/// Each request opens a connection of its own and asks the server to close -/// it once it has answered. Responses are asked for without content coding -/// (Accept-Encoding: identity): the body comes as the server stores it. -/// -/// -/// An https:// request runs TLS 1.3 or 1.2 (BouncyCastle's), and goes on only -/// with a server whose certificate leads to one of Mozilla's roots and names -/// the host, unless decides -/// otherwise. There is no revocation check. -/// -/// -public sealed class HttpRequest -{ - /// How long the server may stay silent by default, in milliseconds. - public const int DefaultTimeout = 15_000; - - /// How many redirects are followed by default. - public const int DefaultMaxRedirects = 5; - - /// The User-Agent sent unless names one. - public const string DefaultUserAgent = "Cosmos.Network.Http/2.1"; - - /// - /// Headers the request sets itself: Connection tells where a response - /// without a length ends, Content-Length frames the body. - /// - private static readonly string[] s_reservedHeaders = ["Connection", "Content-Length", "Transfer-Encoding"]; - - /// - /// Headers meant for the URL's own server, left out once a redirect - /// leads to another one: credentials, and a Host set by hand. - /// - private static readonly string[] s_originHeaders = ["Authorization", "Proxy-Authorization", "Cookie", "Host"]; - - private readonly HttpUrl _url; - private readonly string _method = "GET"; - private readonly int _timeout = DefaultTimeout; - private readonly int _maxRedirects = DefaultMaxRedirects; - - /// Creates a request; sends it. - /// An http:// or https:// URL. One without a scheme is taken as an http:// one, as wget takes it. - /// is empty. - /// is neither an http:// nor an https:// URL, or it carries credentials. - /// names no host, or a port that is not a TCP port. - public HttpRequest(string url) - { - ArgumentException.ThrowIfNullOrWhiteSpace(url); - _url = HttpUrl.Parse(url); - } - - /// The URL the request goes to, written out in full. - public string Url => _url.ToString(); - - /// The method, GET unless set: HEAD, POST, PUT, DELETE and so on. - public string Method - { - get => _method; - init - { - ArgumentException.ThrowIfNullOrEmpty(value); - if (!IsToken(value)) - { - throw new ArgumentException($"'{value}' is not an HTTP method.", nameof(value)); - } - - _method = value; - } - } - - /// The body sent after the head, if any; a POST, PUT or PATCH without one sends an empty body. - public byte[]? Body { get; init; } - - /// - /// Headers sent with the request, looked up case-insensitively. They - /// replace the ones the request sends by default: Host, User-Agent, - /// Accept and Accept-Encoding. Connection, Content-Length and - /// Transfer-Encoding are the request's own, and cannot be set. - /// - public Dictionary Headers { get; } = new(StringComparer.OrdinalIgnoreCase); - - /// How long the server may stay silent while it answers before the request fails, in milliseconds. - public int Timeout - { - get => _timeout; - init - { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); - _timeout = value; - } - } - - /// - /// How many redirects (301, 302, 303, 307, 308) are followed. With 0, - /// returns the redirect itself. - /// - public int MaxRedirects - { - get => _maxRedirects; - init - { - ArgumentOutOfRangeException.ThrowIfNegative(value); - _maxRedirects = value; - } - } - - /// Receives a line for every response and redirect, when set. - public Action? Log { get; init; } - - /// - /// Decides whether to go on with an https:// server, given the - /// certificates it presented and what the built-in check made of them - /// (). When not set, the request goes - /// on exactly when the built-in check trusts the server. It runs during - /// the TLS handshake, on the thread that sends the request. - /// - /// - /// Trusting one self-signed server besides the ones Mozilla's roots vouch for: - /// - /// ServerCertificateValidation = certificate => certificate.Error is null - /// || certificate.Fingerprint == "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08" - /// - /// - public Func? ServerCertificateValidation { get; init; } - - /// The roots an https:// server's certificate must lead to: Mozilla's unless a test sets others. - internal TrustedRoots? TrustedRoots { get; init; } - - /// - /// Sends the request on the calling thread, follows redirects, and - /// returns the response once it has arrived whole. Any status is - /// returned, error ones included. - /// - /// - /// - /// A 303, and a 301 or 302 to a POST, turn the request into a GET without - /// a body, as browsers do. The other redirects send it again as it was. - /// - /// - /// A redirect from https:// to http:// is not followed: it would send the - /// request, and take the response, unencrypted. Once a redirect leads to - /// another server (another scheme, host or port), the Authorization, - /// Proxy-Authorization, Cookie and Host set in are - /// no longer sent: they were meant for the first one. - /// - /// - /// sets a header the request sets itself, or holds a name or a value that cannot be sent. - /// The host could not be resolved or reached, the TLS handshake failed or the server's certificate is not trusted, the server went silent for longer than , the response did not come whole or is not HTTP, or the redirects went on for longer than or to a URL that cannot be followed. When throws, its exception is the . - public HttpResponse Send() - { - CheckHeaders(); - - HttpUrl url = _url; - string method = _method; - byte[]? body = Body; - bool sameOrigin = true; - - for (int redirects = 0; ; redirects++) - { - HttpResponse response = HttpConnection.Exchange(url, BuildRequest(url, method, body, sameOrigin), method == "HEAD", _timeout, - TrustedRoots, ServerCertificateValidation); - Log?.Invoke($"{method} {url} {response.StatusCode} {response.ReasonPhrase} ({response.Content.Length} bytes)"); - - string? location = response.GetHeader("Location"); - if (!IsRedirect(response.StatusCode) || location is null || _maxRedirects == 0) - { - return response; - } - - if (redirects == _maxRedirects) - { - throw new HttpException($"{_url} redirected more than {_maxRedirects} times.", response.StatusCode); - } - - HttpUrl next; - try - { - next = url.Resolve(location); - } - catch (Exception exception) - { - // NotSupportedException (neither http:// nor https://) or FormatException. - throw new HttpException($"{url} redirected to a URL that cannot be followed: {exception.Message}", response.StatusCode); - } - - if (url.IsSecure && !next.IsSecure) - { - throw new HttpException($"{url} redirected to {next}, which is not followed: it would leave TLS.", response.StatusCode); - } - - sameOrigin &= next.IsSecure == url.IsSecure && next.Port == url.Port - && next.Host.Equals(url.Host, StringComparison.OrdinalIgnoreCase); - - if ((response.StatusCode == 303 && method != "HEAD") || (response.StatusCode is 301 or 302 && method == "POST")) - { - method = "GET"; - body = null; - } - - Log?.Invoke($"Redirected to {next}"); - url = next; - } - } - - private void CheckHeaders() - { - foreach (KeyValuePair header in Headers) - { - foreach (string reserved in s_reservedHeaders) - { - if (header.Key.Equals(reserved, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"The {reserved} header is set by the request itself."); - } - } - - if (!IsToken(header.Key)) - { - throw new InvalidOperationException($"'{header.Key}' is not a header name."); - } - - if (header.Value is null || header.Value.AsSpan().IndexOfAny('\r', '\n', '\0') >= 0) - { - throw new InvalidOperationException($"The value of the {header.Key} header cannot be sent: it is null or breaks the line."); - } - } - } - - /// Where the request goes. - /// Its method. - /// Its body, if any. - /// Whether it still goes to the server of the request's own URL, which are meant for. - private byte[] BuildRequest(HttpUrl url, string method, byte[]? body, bool sameOrigin) - { - StringBuilder head = new(); - head.Append(method).Append(' ').Append(url.Target).Append(" HTTP/1.1\r\n"); - if (sameOrigin) - { - AppendHeader(head, "Host", url.Authority); - } - else - { - head.Append("Host: ").Append(url.Authority).Append("\r\n"); - } - - AppendHeader(head, "User-Agent", DefaultUserAgent); - AppendHeader(head, "Accept", "*/*"); - // Nothing gzip or deflate would have to undo: a Cosmos kernel has no System.IO.Compression. - AppendHeader(head, "Accept-Encoding", "identity"); - head.Append("Connection: close\r\n"); - - if (body is not null || method is "POST" or "PUT" or "PATCH") - { - head.Append("Content-Length: ").Append(body is null ? 0 : body.Length).Append("\r\n"); - } - - foreach (KeyValuePair header in Headers) - { - if (!IsDefaultHeader(header.Key) && (sameOrigin || !IsOriginHeader(header.Key))) - { - head.Append(header.Key).Append(": ").Append(header.Value).Append("\r\n"); - } - } - - head.Append("\r\n"); - - byte[] headBytes = Encoding.UTF8.GetBytes(head.ToString()); - if (body is null || body.Length == 0) - { - return headBytes; - } - - byte[] request = new byte[headBytes.Length + body.Length]; - Buffer.BlockCopy(headBytes, 0, request, 0, headBytes.Length); - Buffer.BlockCopy(body, 0, request, headBytes.Length, body.Length); - return request; - } - - /// Appends a header the request sends by default, with the value gives it if any. - private void AppendHeader(StringBuilder head, string name, string defaultValue) - { - string value = Headers.TryGetValue(name, out string? set) ? set : defaultValue; - head.Append(name).Append(": ").Append(value).Append("\r\n"); - } - - private static bool IsDefaultHeader(string name) => - name.Equals("Host", StringComparison.OrdinalIgnoreCase) - || name.Equals("User-Agent", StringComparison.OrdinalIgnoreCase) - || name.Equals("Accept", StringComparison.OrdinalIgnoreCase) - || name.Equals("Accept-Encoding", StringComparison.OrdinalIgnoreCase); - - private static bool IsOriginHeader(string name) - { - foreach (string origin in s_originHeaders) - { - if (name.Equals(origin, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - private static bool IsRedirect(int statusCode) => statusCode is 301 or 302 or 303 or 307 or 308; - - /// Whether is a token (RFC 9110, section 5.6.2), as methods and header names are. - private static bool IsToken(string value) - { - if (value.Length == 0) - { - return false; - } - - foreach (char c in value) - { - if (!char.IsAsciiLetterOrDigit(c) && "!#$%&'*+-.^_`|~".IndexOf(c) < 0) - { - return false; - } - } - - return true; - } -} diff --git a/src/Cosmos.Network.Http/HttpResponse.cs b/src/Cosmos.Network.Http/HttpResponse.cs deleted file mode 100644 index 849f742..0000000 --- a/src/Cosmos.Network.Http/HttpResponse.cs +++ /dev/null @@ -1,142 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Text; - -namespace Cosmos.Network.Http; - -/// -/// What a server answered: the status, the headers and the whole body. -/// returns any status, error ones included; -/// turns those into an exception. -/// -public sealed class HttpResponse -{ - /// The URL that answered: the one requested, or where its redirects led. - public string Url { get; } - - /// The HTTP version the server answered with, such as HTTP/1.1. - public string Version { get; } - - /// The status code, such as 200 or 404. - public int StatusCode { get; } - - /// The text after the status code, such as OK; it may be empty. - public string ReasonPhrase { get; } - - /// - /// The headers, looked up case-insensitively. A header the server sent - /// more than once holds its values joined by ", ". - /// - public IReadOnlyDictionary Headers { get; } - - /// - /// The body, with any chunked transfer coding removed. It is empty for a - /// HEAD request and for a 204 or 304. - /// - public byte[] Content { get; } - - /// Whether the status is a 2xx one. - public bool IsSuccessStatusCode => StatusCode is >= 200 and <= 299; - - /// The Content-Type header, or null when the server sent none. - public string? ContentType => GetHeader("Content-Type"); - - internal HttpResponse(string url, string version, int statusCode, string reasonPhrase, Dictionary headers, byte[] content) - { - Url = url; - Version = version; - StatusCode = statusCode; - ReasonPhrase = reasonPhrase; - Headers = headers; - Content = content; - } - - /// A header's value, or null when the server did not send it. - public string? GetHeader(string name) => Headers.TryGetValue(name, out string? value) ? value : null; - - /// - /// The body as text, decoded with the charset of the Content-Type header: - /// UTF-8, US-ASCII, ISO-8859-1 or UTF-16. Any other charset, or none, is - /// read as UTF-8. A byte order mark is not part of the text. - /// - public string GetString() - { - byte[] content = Content; - if (content.Length >= 3 && content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF) - { - return Encoding.UTF8.GetString(content, 3, content.Length - 3); - } - - return GetEncoding(GetCharset(ContentType)).GetString(content); - } - - /// Throws unless the status is a 2xx one. - /// This response, so the call can be chained. - /// The status is not a 2xx one; the exception carries it. - public HttpResponse EnsureSuccessStatusCode() - { - if (!IsSuccessStatusCode) - { - string status = ReasonPhrase.Length == 0 ? $"{StatusCode}" : $"{StatusCode} {ReasonPhrase}"; - throw new HttpException($"{Url} answered {status}.", StatusCode); - } - - return this; - } - - private static string? GetCharset(string? contentType) - { - if (contentType is null) - { - return null; - } - - foreach (string parameter in contentType.Split(';')) - { - string trimmed = parameter.Trim(); - if (trimmed.StartsWith("charset=", StringComparison.OrdinalIgnoreCase)) - { - return trimmed.Substring("charset=".Length).Trim('"', ' '); - } - } - - return null; - } - - /// - /// Only the encodings every .NET runtime has built in: a Cosmos kernel has - /// no code page provider for to - /// find the others in. - /// - private static Encoding GetEncoding(string? charset) - { - if (charset is null) - { - return Encoding.UTF8; - } - - if (charset.Equals("us-ascii", StringComparison.OrdinalIgnoreCase) || charset.Equals("ascii", StringComparison.OrdinalIgnoreCase)) - { - return Encoding.ASCII; - } - - if (charset.Equals("iso-8859-1", StringComparison.OrdinalIgnoreCase) || charset.Equals("latin1", StringComparison.OrdinalIgnoreCase)) - { - return Encoding.Latin1; - } - - if (charset.Equals("utf-16", StringComparison.OrdinalIgnoreCase) || charset.Equals("utf-16le", StringComparison.OrdinalIgnoreCase)) - { - return Encoding.Unicode; - } - - if (charset.Equals("utf-16be", StringComparison.OrdinalIgnoreCase)) - { - return Encoding.BigEndianUnicode; - } - - return Encoding.UTF8; - } -} diff --git a/src/Cosmos.Network.Http/HttpUrl.cs b/src/Cosmos.Network.Http/HttpUrl.cs deleted file mode 100644 index 43028f3..0000000 --- a/src/Cosmos.Network.Http/HttpUrl.cs +++ /dev/null @@ -1,257 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Globalization; -using System.Text; - -namespace Cosmos.Network.Http; - -/// -/// An http:// or https:// URL taken apart into what a request needs: whether -/// to talk TLS, the host and port to connect to, and the target its request -/// line names. The fragment is dropped, as it never leaves the client. -/// -internal sealed class HttpUrl -{ - /// The port of an http:// URL that names none. - public const int DefaultPort = 80; - - /// The port of an https:// URL that names none. - public const int DefaultSecurePort = 443; - - private const string HexDigits = "0123456789ABCDEF"; - - /// Whether this is an https:// URL, whose connection runs TLS. - public bool IsSecure { get; } - - /// The host name or address, an IPv6 literal without its brackets. - public string Host { get; } - - /// The TCP port to connect to. - public int Port { get; } - - /// The path and query the request line names: at least /, percent-encoded where it has to be. - public string Target { get; } - - /// The host, and the port when it is not the scheme's default one: what the Host header names. - public string Authority { get; } - - private HttpUrl(bool isSecure, string host, int port, string target) - { - IsSecure = isSecure; - Host = host; - Port = port; - Target = target; - - string name = host.Contains(':') ? "[" + host + "]" : host; - Authority = port == (isSecure ? DefaultSecurePort : DefaultPort) ? name : $"{name}:{port}"; - } - - /// http or https. - public string Scheme => IsSecure ? "https" : "http"; - - public override string ToString() => Scheme + "://" + Authority + Target; - - /// - /// Takes apart an absolute URL. One without a scheme is an http:// URL, as - /// wget takes it. - /// - /// The URL is neither an http:// nor an https:// one, or it carries credentials. - /// The URL names no host, or a port that is not a TCP port. - public static HttpUrl Parse(string url) - { - string rest = url.Trim(); - - int fragment = rest.IndexOf('#'); - if (fragment >= 0) - { - rest = rest.Substring(0, fragment); - } - - bool isSecure = false; - int schemeLength = SchemeLength(rest); - if (schemeLength > 0) - { - string scheme = rest.Substring(0, schemeLength); - isSecure = scheme.Equals("https", StringComparison.OrdinalIgnoreCase); - if (!isSecure && !scheme.Equals("http", StringComparison.OrdinalIgnoreCase)) - { - throw new NotSupportedException($"{url} is neither an http:// nor an https:// URL."); - } - - rest = rest.Substring(schemeLength + 3); - } - - int authorityEnd = rest.IndexOfAny(['/', '?']); - string authority = authorityEnd < 0 ? rest : rest.Substring(0, authorityEnd); - string target = authorityEnd < 0 ? "/" : rest.Substring(authorityEnd); - - if (authority.Contains('@')) - { - throw new NotSupportedException($"{url} carries credentials, which are not supported."); - } - - string host; - string? portText = null; - if (authority.StartsWith('[')) - { - int close = authority.IndexOf(']'); - if (close < 0) - { - throw new FormatException($"{url} has an IPv6 address with no closing bracket."); - } - - host = authority.Substring(1, close - 1); - string afterHost = authority.Substring(close + 1); - if (afterHost.Length > 0) - { - if (afterHost[0] != ':') - { - throw new FormatException($"{url} has something other than a port after its IPv6 address."); - } - - portText = afterHost.Substring(1); - } - } - else - { - int colon = authority.IndexOf(':'); - host = colon < 0 ? authority : authority.Substring(0, colon); - portText = colon < 0 ? null : authority.Substring(colon + 1); - } - - if (host.Length == 0) - { - throw new FormatException($"{url} names no host."); - } - - // An empty port ("host:/") is the default one (RFC 3986, section 3.2.3). - int port = isSecure ? DefaultSecurePort : DefaultPort; - if (!string.IsNullOrEmpty(portText) - && (!int.TryParse(portText, NumberStyles.None, CultureInfo.InvariantCulture, out port) || port < 1 || port > 65535)) - { - throw new FormatException($"{url} names port {portText}, which is not a TCP port."); - } - - if (target.StartsWith('?')) - { - target = "/" + target; - } - - return new HttpUrl(isSecure, host, port, EncodeTarget(target)); - } - - /// - /// Resolves the Location of a redirect against this URL: an absolute URL, - /// a URL without a scheme (//host/path, which keeps this URL's - /// scheme), an absolute path, a query, or a path relative to this URL's - /// directory. - /// - /// The location is an absolute URL does not support. - /// The location is an absolute URL cannot take apart. - public HttpUrl Resolve(string location) - { - string reference = location.Trim(); - - if (SchemeLength(reference) > 0) - { - return Parse(reference); - } - - if (reference.StartsWith("//", StringComparison.Ordinal)) - { - return Parse(Scheme + ":" + reference); - } - - int fragment = reference.IndexOf('#'); - if (fragment >= 0) - { - reference = reference.Substring(0, fragment); - } - - if (reference.Length == 0) - { - return this; - } - - string path = Target; - int query = path.IndexOf('?'); - if (query >= 0) - { - path = path.Substring(0, query); - } - - string target = reference[0] switch - { - '/' => reference, - '?' => path + reference, - // The target always starts with '/', so there is a directory to resolve against. - _ => path.Substring(0, path.LastIndexOf('/') + 1) + reference, - }; - - return new HttpUrl(IsSecure, Host, Port, EncodeTarget(target)); - } - - /// - /// The length of the scheme starts with, when it - /// starts with scheme:// (RFC 3986, section 3.1); otherwise 0. - /// - private static int SchemeLength(string url) - { - int end = url.IndexOf("://", StringComparison.Ordinal); - if (end <= 0) - { - return 0; - } - - for (int i = 0; i < end; i++) - { - char c = url[i]; - bool valid = char.IsAsciiLetter(c) || (i > 0 && (char.IsAsciiDigit(c) || c is '+' or '-' or '.')); - if (!valid) - { - return 0; - } - } - - return end; - } - - /// - /// Percent-encodes what a request line cannot carry as is: spaces, control - /// characters and anything beyond ASCII, as UTF-8. What is already - /// percent-encoded is left alone. - /// - private static string EncodeTarget(string target) - { - bool clean = true; - foreach (char c in target) - { - if (c <= ' ' || c > '~') - { - clean = false; - break; - } - } - - if (clean) - { - return target; - } - - StringBuilder encoded = new(target.Length + 16); - foreach (byte b in Encoding.UTF8.GetBytes(target)) - { - if (b <= ' ' || b > '~') - { - encoded.Append('%').Append(HexDigits[b >> 4]).Append(HexDigits[b & 0xF]); - } - else - { - encoded.Append((char)b); - } - } - - return encoded.ToString(); - } -} diff --git a/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs b/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3e3c039 --- /dev/null +++ b/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Cosmos.Network.Http")] +[assembly: AssemblyCompany("nanoFramework Contributors")] +[assembly: AssemblyProduct("Cosmos.Network.Http")] +[assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")] + +////////////////////////////////////////////////////// +// this assembly does NOT have a native counterpart // +////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +[assembly: AssemblyNativeVersion("0.0.0.0")] +//////////////////////////////////////////////////////////////// + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] diff --git a/src/Cosmos.Network.Http/ResponseReader.cs b/src/Cosmos.Network.Http/ResponseReader.cs deleted file mode 100644 index ef6f566..0000000 --- a/src/Cosmos.Network.Http/ResponseReader.cs +++ /dev/null @@ -1,298 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; - -namespace Cosmos.Network.Http; - -/// -/// Takes a response apart as its bytes arrive, and tells when it has arrived -/// whole (RFC 9112, section 6.3), so the connection need not be read to its -/// end: a response with a Content-Length or a chunked body is complete -/// before the server closes. One without either ends where the connection -/// does. -/// -internal sealed class ResponseReader -{ - /// The longest response head taken, in bytes. - internal const int MaxHeadLength = 64 * 1024; - - /// The largest Content-Length the receive buffer is sized for up front, in bytes. - private const int MaxPreallocatedLength = 64 * 1024 * 1024; - - private enum Framing - { - /// No body: a response to HEAD, a 1xx, a 204 or a 304. - None, - - /// As many bytes as Content-Length says. - Length, - - /// The chunked transfer coding. - Chunked, - - /// Everything up to the end of the connection. - UntilClose, - } - - private readonly MemoryStream _received = new(); - private readonly bool _isHead; - - private Head? _head; - private Framing _framing; - private long _contentLength; - - /// Where the head being looked for starts: past any 1xx interim response. - private int _headStart; - - /// Where the search for the end of that head resumes. - private int _scanFrom; - - private int _bodyStart; - - /// Where the search for the end of a chunked body resumes. - private int _chunkScan; - - /// Whether the request was a HEAD one, whose response has no body whatever its headers say. - public ResponseReader(bool isHead) - { - _isHead = isHead; - } - - /// Whether the whole response has arrived. - public bool IsComplete { get; private set; } - - /// Takes the next bytes of the response. - /// The bytes are not an HTTP response. - public void Append(byte[] buffer, int count) - { - _received.Write(buffer, 0, count); - - if (_head is null && !TryReadHead()) - { - return; - } - - IsComplete = _framing switch - { - Framing.None => true, - Framing.Length => _received.Length - _bodyStart >= _contentLength, - Framing.Chunked => ChunkedBody.FindEnd(_received.GetBuffer(), (int)_received.Length, ref _chunkScan), - _ => false, - }; - } - - /// - /// The response, once it is complete or the server has closed the - /// connection, which completes a response that runs until it does. - /// - /// The URL that answered. - /// The connection closed before the response was whole. - public HttpResponse ToResponse(string url) - { - if (_head is null) - { - throw new HttpException(_received.Length == _headStart - ? "The server closed the connection without answering." - : "The server closed the connection in the middle of the response head."); - } - - byte[] data = _received.GetBuffer(); - int received = (int)_received.Length - _bodyStart; - byte[] content; - switch (_framing) - { - case Framing.None: - content = []; - break; - - case Framing.Length: - if (received < _contentLength) - { - throw new HttpException($"The server closed the connection after {received} of {_contentLength} bytes.", _head.StatusCode); - } - - content = Slice(data, _bodyStart, (int)_contentLength); - break; - - case Framing.Chunked: - content = ChunkedBody.Decode(data, _bodyStart, (int)_received.Length); - break; - - default: - content = Slice(data, _bodyStart, received); - break; - } - - return new HttpResponse(url, _head.Version, _head.StatusCode, _head.ReasonPhrase, _head.Headers, content); - } - - /// Looks for the end of the head, and takes the head apart once it arrived. - /// Whether the head of the final response arrived. - private bool TryReadHead() - { - byte[] data = _received.GetBuffer(); - int length = (int)_received.Length; - - while (true) - { - int end = IndexOfHeadEnd(data, _scanFrom, length); - if (end < 0) - { - if (length - _headStart > MaxHeadLength) - { - throw new HttpException($"The response head is longer than {MaxHeadLength} bytes."); - } - - // The blank line may straddle what arrives next. - _scanFrom = Math.Max(_headStart, length - 3); - return false; - } - - Head head = ParseHead(data, _headStart, end - _headStart); - if (head.StatusCode is >= 100 and <= 199 && head.StatusCode != 101) - { - // An interim response, such as 100 Continue: the final one follows. - _headStart = _scanFrom = end; - continue; - } - - _head = head; - _bodyStart = _chunkScan = end; - ChooseFraming(head); - return true; - } - } - - private void ChooseFraming(Head head) - { - if (_isHead || head.StatusCode is 204 or 304 or (>= 100 and <= 199)) - { - _framing = Framing.None; - return; - } - - // Transfer-Encoding wins over Content-Length. A coding other than - // chunked last leaves nothing to tell the end of the body by but the - // end of the connection. - if (head.Headers.TryGetValue("Transfer-Encoding", out string? transferEncoding)) - { - _framing = transferEncoding.Trim().EndsWith("chunked", StringComparison.OrdinalIgnoreCase) ? Framing.Chunked : Framing.UntilClose; - return; - } - - if (head.Headers.TryGetValue("Content-Length", out string? contentLength)) - { - _framing = Framing.Length; - _contentLength = ParseContentLength(contentLength); - if (_contentLength <= MaxPreallocatedLength) - { - _received.Capacity = Math.Max(_received.Capacity, _bodyStart + (int)_contentLength); - } - - return; - } - - _framing = Framing.UntilClose; - } - - /// - /// A Content-Length, which a server that sent it more than once joined - /// with commas: taken if every copy agrees (RFC 9110, section 8.6). - /// - private static long ParseContentLength(string value) - { - long length = -1; - foreach (string part in value.Split(',')) - { - if (!long.TryParse(part.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out long parsed) - || parsed > int.MaxValue - || (length >= 0 && parsed != length)) - { - throw new HttpException($"The response has an invalid Content-Length: {value}."); - } - - length = parsed; - } - - return length; - } - - /// Takes apart a status line and the header lines after it (RFC 9112, sections 4 and 5). - private static Head ParseHead(byte[] data, int start, int length) - { - // Headers are ASCII but for the odd value, which UTF-8 reads as well as anything. - string[] lines = Encoding.UTF8.GetString(data, start, length).Split("\r\n"); - string statusLine = lines[0]; - - // HTTP-version SP 3DIGIT [SP reason-phrase] - int space = statusLine.IndexOf(' '); - int statusCode = 0; - if (!statusLine.StartsWith("HTTP/", StringComparison.Ordinal) - || space < 0 - || statusLine.Length < space + 4 - || (statusLine.Length > space + 4 && statusLine[space + 4] != ' ') - || !int.TryParse(statusLine.Substring(space + 1, 3), NumberStyles.None, CultureInfo.InvariantCulture, out statusCode)) - { - string shown = statusLine.Length > 64 ? statusLine.Substring(0, 64) + "..." : statusLine; - throw new HttpException($"The server answered with something that is not HTTP: {shown}"); - } - - string version = statusLine.Substring(0, space); - string reasonPhrase = statusLine.Length > space + 5 ? statusLine.Substring(space + 5) : string.Empty; - - Dictionary headers = new(StringComparer.OrdinalIgnoreCase); - for (int i = 1; i < lines.Length; i++) - { - string line = lines[i]; - int colon = line.IndexOf(':'); - - // Lines with no name, and the obsolete continuations that start with whitespace, hold nothing to keep. - if (colon <= 0 || line[0] is ' ' or '\t') - { - continue; - } - - string name = line.Substring(0, colon).Trim(); - string value = line.Substring(colon + 1).Trim(); - headers[name] = headers.TryGetValue(name, out string? previous) ? previous + ", " + value : value; - } - - return new Head(version, statusCode, reasonPhrase, headers); - } - - /// The index just past the blank line that ends a head, or -1. - private static int IndexOfHeadEnd(byte[] data, int start, int length) - { - for (int i = start; i + 3 < length; i++) - { - if (data[i] == '\r' && data[i + 1] == '\n' && data[i + 2] == '\r' && data[i + 3] == '\n') - { - return i + 4; - } - } - - return -1; - } - - private static byte[] Slice(byte[] data, int start, int length) - { - byte[] slice = new byte[length]; - Buffer.BlockCopy(data, start, slice, 0, length); - return slice; - } - - private sealed class Head(string version, int statusCode, string reasonPhrase, Dictionary headers) - { - public string Version { get; } = version; - - public int StatusCode { get; } = statusCode; - - public string ReasonPhrase { get; } = reasonPhrase; - - public Dictionary Headers { get; } = headers; - } -} diff --git a/src/Cosmos.Network.Http/ServerCertificate.cs b/src/Cosmos.Network.Http/ServerCertificate.cs deleted file mode 100644 index bc64ce2..0000000 --- a/src/Cosmos.Network.Http/ServerCertificate.cs +++ /dev/null @@ -1,94 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using Org.BouncyCastle.Crypto.Digests; -using Org.BouncyCastle.X509; - -namespace Cosmos.Network.Http; - -/// -/// The certificates an https:// server presented, and what the built-in check -/// made of them. -/// receives one to decide whether to go on. -/// -public sealed class ServerCertificate -{ - private const string HexDigits = "0123456789ABCDEF"; - - internal ServerCertificate(string host, IReadOnlyList chain, X509Certificate certificate, string? error) - { - Host = host; - Chain = chain; - Subject = certificate.SubjectDN.ToString(); - Issuer = certificate.IssuerDN.ToString(); - NotBefore = certificate.NotBefore; - NotAfter = certificate.NotAfter; - Fingerprint = Sha256Hex(chain[0]); - Error = error; - } - - /// The host the request connected to, as its URL names it. - public string Host { get; } - - /// - /// The certificates as the server sent them, DER-encoded: its own first, - /// then whatever intermediates it chose to send. - /// - public IReadOnlyList Chain { get; } - - /// - /// The distinguished name of the server's certificate, as BouncyCastle - /// writes it: in the certificate's own order, without spaces, e.g. - /// C=US,O=Example Inc.,CN=example.com. - /// - public string Subject { get; } - - /// The distinguished name of who issued the server's certificate, written as is. - public string Issuer { get; } - - /// When the server's certificate starts being valid, in UTC. - public DateTime NotBefore { get; } - - /// When the server's certificate stops being valid, in UTC. - public DateTime NotAfter { get; } - - /// - /// The SHA-256 of the server's certificate, in uppercase hex without - /// separators: what to compare to trust one certificate in particular, - /// a self-signed one for instance. openssl x509 -noout -fingerprint - /// -sha256 -in cert.pem | tr -d : prints the same, after - /// sha256 Fingerprint=. - /// - public string Fingerprint { get; } - - /// - /// Why the built-in check does not trust the server, or - /// when it does: the chain leads to one of Mozilla's roots, every - /// certificate on it is valid now, and the server's certificate names - /// . - /// - /// - /// It names the first problem found, and the dates are checked last: when - /// a date is the problem, nothing else is wrong, so accepting such an - /// error forgives a wrong clock and nothing more. - /// - public string? Error { get; } - - private static string Sha256Hex(byte[] data) - { - Sha256Digest digest = new(); - digest.BlockUpdate(data, 0, data.Length); - byte[] hash = new byte[digest.GetDigestSize()]; - digest.DoFinal(hash, 0); - - char[] hex = new char[hash.Length * 2]; - for (int i = 0; i < hash.Length; i++) - { - hex[2 * i] = HexDigits[hash[i] >> 4]; - hex[(2 * i) + 1] = HexDigits[hash[i] & 0xF]; - } - - return new string(hex); - } -} diff --git a/src/Cosmos.Network.Http/SocketTransport.cs b/src/Cosmos.Network.Http/SocketTransport.cs deleted file mode 100644 index 17eca02..0000000 --- a/src/Cosmos.Network.Http/SocketTransport.cs +++ /dev/null @@ -1,114 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Diagnostics; -using System.Net.Sockets; - -namespace Cosmos.Network.Http; - -/// The bytes of a request and its response, as they cross the connection: plain, or through TLS. -internal interface ITransport -{ - /// When bytes last went through the connection, either way, as a timestamp. - long LastActivity { get; } - - /// Sends all of . - void Send(byte[] data); - - /// - /// Takes what has arrived into , waiting a - /// little where the socket can wait. - /// - /// How many bytes were taken; 0 when none have arrived yet; -1 once the server has closed the connection and everything it sent was taken. - int Receive(byte[] buffer); - - /// Closes the connection, without failing: the server may be gone already. - void Close(); -} - -/// -/// A TCP connection, used as the Cosmos socket plugs allow: -/// -/// Bytes come in through the byte[] Receive overload, for what -/// reports: the Span overloads lose what they -/// receive, and a Receive with nothing waiting returns 0 on an open -/// connection. -/// The waiting is done by , -/// never by Thread.Sleep. Poll blocks for its timeout where sockets -/// block, and returns at once on a Cosmos kernel: a request may run on any -/// thread there, the kernel's main loop included, which must never -/// block. -/// -/// -internal sealed class SocketTransport : ITransport -{ - /// The most taken from the socket at a time, in bytes. - public const int ChunkSize = 16 * 1024; - - /// How long a Poll waits for data where it can wait, in microseconds. - private const int PollIntervalUs = 10_000; - - private readonly Socket _socket; - - /// Wraps a socket that is connected. - public SocketTransport(Socket socket) - { - _socket = socket; - LastActivity = Stopwatch.GetTimestamp(); - } - - public long LastActivity { get; private set; } - - public void Send(byte[] data) => Send(data, data.Length); - - /// Sends the first bytes of . - public void Send(byte[] data, int count) - { - int sent = 0; - while (sent < count) - { - int written = _socket.Send(data, sent, count - sent, SocketFlags.None); - if (written <= 0) - { - throw new HttpException("The server closed the connection before the request was sent."); - } - - sent += written; - LastActivity = Stopwatch.GetTimestamp(); - } - } - - public int Receive(byte[] buffer) - { - int available = _socket.Available; - if (available > 0) - { - int read = _socket.Receive(buffer, 0, Math.Min(available, buffer.Length), SocketFlags.None); - if (read > 0) - { - LastActivity = Stopwatch.GetTimestamp(); - return read; - } - } - - // Readable with nothing to read: the server closed its end. - if (_socket.Poll(PollIntervalUs, SelectMode.SelectRead) && _socket.Available == 0) - { - return -1; - } - - return 0; - } - - public void Close() - { - try - { - _socket.Close(); - } - catch (Exception) - { - // Nothing left to do with a connection that would not close. - } - } -} diff --git a/src/Cosmos.Network.Http/System.Net.Http.nfproj b/src/Cosmos.Network.Http/System.Net.Http.nfproj new file mode 100644 index 0000000..9d04dff --- /dev/null +++ b/src/Cosmos.Network.Http/System.Net.Http.nfproj @@ -0,0 +1,148 @@ + + + + + $(MSBuildExtensionsPath)\nanoFramework\v1.0\ + + + + Debug + AnyCPU + {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + c3aba656-9831-4006-a3b3-ea842a3c508e + Library + Properties + 512 + System.Net.Http + System.Net.Http + v1.0 + True + bin\$(Configuration)\System.Net.Http.xml + true + true + + + true + + + key.snk + + + false + + + + bin\$(Configuration)\Stubs + System.Net.Http + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll + + + ..\packages\nanoFramework.Runtime.Events.1.11.39\lib\nanoFramework.Runtime.Events.dll + + + ..\packages\nanoFramework.System.Collections.1.5.75\lib\nanoFramework.System.Collections.dll + + + ..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll + + + ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + + ..\packages\nanoFramework.System.Net.1.11.64\lib\System.Net.dll + + + ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/Cosmos.Network.Http/TlsTransport.cs b/src/Cosmos.Network.Http/TlsTransport.cs deleted file mode 100644 index c460d50..0000000 --- a/src/Cosmos.Network.Http/TlsTransport.cs +++ /dev/null @@ -1,483 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using Org.BouncyCastle.Tls; -using Org.BouncyCastle.Tls.Crypto; -using Org.BouncyCastle.Tls.Crypto.Impl.BC; -using Org.BouncyCastle.Utilities; -using Org.BouncyCastle.X509; -using X86 = System.Runtime.Intrinsics.X86; - -namespace Cosmos.Network.Http; - -/// -/// TLS 1.2 or 1.3 over a TCP connection, run by BouncyCastle in its -/// non-blocking mode: the protocol never touches the socket, it is handed -/// what arrives and asked for what to send, so the socket keeps being used -/// the way uses it. -/// -/// -/// -/// The client offers ECDHE key exchange only (X25519, P-256, P-384) with AEAD -/// ciphers, and AES-CBC for TLS 1.2 servers that have nothing better: no -/// RSA key exchange, no finite-field Diffie-Hellman, which is slow in managed -/// code. ChaCha20-Poly1305 comes first unless the CPU has AES instructions, -/// which BouncyCastle only uses on x86 and which a Cosmos kernel does not -/// enable: its AES runs from tables there. -/// -/// -/// A server may close the connection without sending close_notify first, -/// as many do: the response's framing (Content-Length or chunked) tells -/// whether it came whole, as for http://. -/// -/// -internal sealed class TlsTransport : ITransport -{ - private static TlsCrypto? s_crypto; - - private readonly SocketTransport _socket; - private readonly HttpUrl _url; - private readonly Client _client; - private readonly TlsClientProtocol _protocol = new(); - private readonly byte[] _input = new byte[SocketTransport.ChunkSize]; - private readonly byte[] _output = new byte[SocketTransport.ChunkSize]; - - public TlsTransport(SocketTransport socket, HttpUrl url, TrustedRoots roots, Func? validation) - { - _socket = socket; - _url = url; - _client = new Client(Crypto, url.Host, roots, validation); - } - - public long LastActivity => _socket.LastActivity; - - /// - /// The BouncyCastle crypto every connection shares: it holds the random - /// generator, which seeds itself from RandomNumberGenerator once. - /// - private static TlsCrypto Crypto - { - get - { - TlsCrypto? crypto = Volatile.Read(ref s_crypto); - if (crypto is null) - { - Interlocked.CompareExchange(ref s_crypto, new BcTlsCrypto(), null); - crypto = Volatile.Read(ref s_crypto)!; - } - - return crypto; - } - } - - /// Runs the handshake, which checks the server's certificate. - /// How long the handshake may take, in milliseconds. - /// The handshake failed, or the server's certificate is not trusted. - public void Handshake(int timeout) - { - // BouncyCastle runs Miller-Rabin rounds on every RSA modulus it meets, - // to refuse a prime one (whose private key anyone could work out): a - // few modular exponentiations per key, a tenth of a second for a - // 4096-bit root on a desktop and much more on a kernel. The keys here - // are the roots' or certified by a CA, which checks them, and none - // encrypts anything (there is no RSA key exchange), so this thread - // skips the rounds for the handshake's length; the cheaper checks - // (odd modulus, no small factor) still run. - string? maxTests = Properties.GetThreadProperty(Properties.RsaMaxMRTests); - Properties.SetThreadInt32(Properties.RsaMaxMRTests, 0); - - Exception? failure = null; - try - { - RunHandshake(timeout); - } - catch (Exception exception) - { - failure = exception; - } - - // Not in a finally: a Cosmos kernel skips them while an exception unwinds. - if (maxTests is null) - { - Properties.RemoveThreadProperty(Properties.RsaMaxMRTests); - } - else - { - Properties.SetThreadProperty(Properties.RsaMaxMRTests, maxTests); - } - - if (failure is not null) - { - throw Describe(failure); - } - } - - public void Send(byte[] data) - { - _protocol.WriteApplicationData(data, 0, data.Length); - Flush(); - } - - public int Receive(byte[] buffer) - { - while (true) - { - int count = _protocol.ReadInput(buffer, 0, buffer.Length); - if (count > 0) - { - return count; - } - - // Everything received is taken, and the server sent close_notify, - // or closed the connection and CloseInput below took that as one. - if (_protocol.IsClosed) - { - return -1; - } - - int read = _socket.Receive(_input); - if (read == 0) - { - return 0; - } - - Exception? failure = null; - try - { - if (read > 0) - { - _protocol.OfferInput(_input, 0, read); - } - else - { - // Closed without close_notify: what was decrypted stands, - // and the framing tells whether the response is whole. - _protocol.CloseInput(); - } - } - catch (Exception exception) - { - failure = exception; - } - - // Nothing the server waits for: our close_notify answering its - // own, a warning, or the alert that tells it why we gave up. The - // connection may be closing, and a Cosmos socket refuses to send - // once the server has closed. - TryFlush(); - - if (failure is TlsFatalAlertReceived received) - { - throw new HttpException($"{_url.Authority} sent the {AlertDescription.GetText(received.AlertDescription)} alert.", received); - } - - if (failure is EndOfStreamException) - { - throw new HttpException($"{_url.Authority} closed the connection in the middle of a TLS record: the response is cut short.", failure); - } - - if (failure is not null) - { - throw new HttpException($"The TLS connection to {_url.Authority} failed: {failure.Message}", failure); - } - } - } - - public void Close() - { - try - { - if (!_protocol.IsClosed) - { - _protocol.Close(); - } - } - catch (Exception) - { - // Closing a connection that failed has nothing to send. - } - - // close_notify, or the alert that tells the server why the handshake failed. - TryFlush(); - _socket.Close(); - } - - private void RunHandshake(int timeout) - { - long start = Stopwatch.GetTimestamp(); - _protocol.Connect(_client); - Flush(); - - while (_protocol.IsHandshaking) - { - // A whole deadline, not only a silence one: a server may keep - // sending a byte now and then, or records that carry nothing. - if (Stopwatch.GetElapsedTime(start).TotalMilliseconds > timeout) - { - throw new HttpException($"{_url.Authority} did not finish the TLS handshake in {timeout} ms."); - } - - int read = _socket.Receive(_input); - if (read > 0) - { - _protocol.OfferInput(_input, 0, read); - Flush(); - continue; - } - - if (read < 0) - { - throw new HttpException($"{_url.Authority} closed the connection during the TLS handshake."); - } - - } - - if (!_protocol.IsConnected) - { - throw new HttpException($"{_url.Authority} closed the connection during the TLS handshake."); - } - } - - /// Sends what the protocol has to send. - private void Flush() - { - int pending; - while ((pending = _protocol.GetAvailableOutputBytes()) > 0) - { - int count = _protocol.ReadOutput(_output, 0, Math.Min(pending, _output.Length)); - _socket.Send(_output, count); - } - } - - private void TryFlush() - { - try - { - Flush(); - } - catch (Exception) - { - // What could not be sent is lost, which leaves the response as it is. - } - } - - /// Turns a failed handshake into an exception that tells why. - private HttpException Describe(Exception exception) - { - if (exception is HttpException http) - { - return http; - } - - if (_client.ValidationFailure is Exception callback) - { - return new HttpException($"Checking the certificate of {_url.Host} failed: {callback.Message}", callback); - } - - if (_client.CertificateError is string error) - { - return new HttpException($"The certificate of {_url.Host} is not trusted: {error}.", exception); - } - - if (_client.ClosedDuringHandshake) - { - return new HttpException($"{_url.Authority} closed the connection during the TLS handshake.", exception); - } - - string reason = exception switch - { - TlsFatalAlertReceived received => $"it sent the {AlertDescription.GetText(received.AlertDescription)} alert", - TlsFatalAlert { AlertDescription: AlertDescription.protocol_version } => "it only speaks TLS versions older than 1.2", - _ => exception.Message, - }; - return new HttpException($"The TLS handshake with {_url.Authority} failed: {reason}", exception); - } - - /// What the client offers, and how it checks the server. - private sealed class Client : DefaultTlsClient - { - private static readonly int[] s_tls13Suites = - [ - CipherSuite.TLS_AES_128_GCM_SHA256, - CipherSuite.TLS_AES_256_GCM_SHA384, - CipherSuite.TLS_CHACHA20_POLY1305_SHA256, - ]; - - private static readonly int[] s_tls12AeadSuites = - [ - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - ]; - - private static readonly int[] s_tls12CbcSuites = - [ - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - ]; - - private readonly string _host; - private readonly TrustedRoots _roots; - private readonly Func? _validation; - - public Client(TlsCrypto crypto, string host, TrustedRoots roots, Func? validation) - : base(crypto) - { - _host = host; - _roots = roots; - _validation = validation; - } - - /// Why the server's certificate was refused, once it was. - public string? CertificateError { get; private set; } - - /// What threw, if it did. - public Exception? ValidationFailure { get; private set; } - - /// Whether the server sent close_notify before the handshake was over: it gave up. - public bool ClosedDuringHandshake { get; private set; } - - private bool _handshakeComplete; - - public override void NotifyHandshakeComplete() - { - base.NotifyHandshakeComplete(); - _handshakeComplete = true; - } - - public override void NotifyAlertReceived(short alertLevel, short alertDescription) - { - if (alertDescription == AlertDescription.close_notify && !_handshakeComplete) - { - ClosedDuringHandshake = true; - } - } - - public override TlsAuthentication GetAuthentication() => new Authentication(this); - - // Many servers close without close_notify; the response's framing - // tells a cut-short response from a whole one. - public override bool RequiresCloseNotify() => false; - - protected override int[] GetSupportedCipherSuites() - { - bool aesInstructions = X86.Aes.IsSupported && X86.Pclmulqdq.IsSupported; - List suites = new(s_tls13Suites.Length + s_tls12AeadSuites.Length + s_tls12CbcSuites.Length); - AddPreferred(suites, s_tls13Suites, aesInstructions); - AddPreferred(suites, s_tls12AeadSuites, aesInstructions); - suites.AddRange(s_tls12CbcSuites); - return TlsUtilities.GetSupportedCipherSuites(Crypto, suites.ToArray()); - } - - protected override IList GetSupportedGroups(IList namedGroupRoles) => - new List { NamedGroup.x25519, NamedGroup.secp256r1, NamedGroup.secp384r1 }; - - // No server name for an address: RFC 6066 forbids it. - protected override IList? GetSniServerNames() => IPAddress.TryParse(_host, out _) - ? null - : new List { new(NameType.host_name, Encoding.ASCII.GetBytes(_host.TrimEnd('.'))) }; - - protected override IList GetProtocolNames() => new List { ProtocolName.Http_1_1 }; - - // No OCSP stapling: there is no revocation check to use it. - protected override CertificateStatusRequest? GetCertificateStatusRequest() => null; - - /// - /// Adds (AES-GCM ones, then ChaCha20 - /// ones) with ChaCha20 first unless the CPU has AES instructions. - /// - private static void AddPreferred(List suites, int[] candidates, bool aesInstructions) - { - foreach (int suite in candidates) - { - if (IsChaCha(suite) != aesInstructions) - { - suites.Add(suite); - } - } - - foreach (int suite in candidates) - { - if (IsChaCha(suite) == aesInstructions) - { - suites.Add(suite); - } - } - } - - private static bool IsChaCha(int suite) => suite - is CipherSuite.TLS_CHACHA20_POLY1305_SHA256 - or CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 - or CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256; - - private void CheckServer(TlsServerCertificate serverCertificate) - { - TlsCertificate[] presented = serverCertificate.Certificate.GetCertificateList(); - if (presented.Length == 0) - { - CertificateError = "the server sent no certificate"; - throw new TlsFatalAlert(AlertDescription.bad_certificate, CertificateError); - } - - List chain = new(presented.Length); - List certificates = new(presented.Length); - try - { - foreach (TlsCertificate certificate in presented) - { - byte[] encoded = certificate.GetEncoded(); - chain.Add(encoded); - certificates.Add(new X509Certificate(encoded)); - } - } - catch (Exception exception) - { - CertificateError = $"the certificates could not be read: {exception.Message}"; - throw new TlsFatalAlert(AlertDescription.bad_certificate, CertificateError); - } - - string? error = CertificateValidator.Validate(certificates, _host, _roots, DateTime.UtcNow, out short alert); - - bool trusted = error is null; - if (_validation is not null) - { - try - { - trusted = _validation(new ServerCertificate(_host, chain, certificates[0], error)); - } - catch (Exception exception) - { - ValidationFailure = exception; - throw new TlsFatalAlert(AlertDescription.internal_error, exception.Message); - } - } - - if (!trusted) - { - CertificateError = error ?? "ServerCertificateValidation refused it"; - throw new TlsFatalAlert(error is null ? AlertDescription.bad_certificate : alert, CertificateError); - } - } - - private sealed class Authentication(Client client) : TlsAuthentication - { - public void NotifyServerCertificate(TlsServerCertificate serverCertificate) => client.CheckServer(serverCertificate); - - // No client certificate. - public TlsCredentials? GetClientCredentials(CertificateRequest certificateRequest) => null; - } - } -} diff --git a/src/Cosmos.Network.Http/TrustedRoots.cs b/src/Cosmos.Network.Http/TrustedRoots.cs deleted file mode 100644 index 85b3e89..0000000 --- a/src/Cosmos.Network.Http/TrustedRoots.cs +++ /dev/null @@ -1,158 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using Org.BouncyCastle.Asn1.X509; -using Org.BouncyCastle.X509; - -namespace Cosmos.Network.Http; - -/// -/// The root certificates a server's chain has to lead to, looked up by -/// subject. holds the ones the package embeds. -/// -/// -/// Each root is taken apart only as far as its subject until a chain names -/// it: a full certificate builds its public key, and BouncyCastle tests every -/// RSA modulus it meets, which a kernel takes its time over. -/// -internal sealed class TrustedRoots -{ - private const string ResourceName = "Cosmos.Network.Http.cacert.pem"; - private const string BeginMarker = "-----BEGIN CERTIFICATE-----"; - private const string EndMarker = "-----END CERTIFICATE-----"; - - private static TrustedRoots? s_mozilla; - - private readonly Dictionary> _bySubject = new(StringComparer.Ordinal); - private readonly List _all = []; - - private TrustedRoots(IEnumerable certificates) - { - foreach (byte[] der in certificates) - { - Root root = new(X509CertificateStructure.GetInstance(der)); - string key = SubjectKey(root.Structure.Subject); - if (!_bySubject.TryGetValue(key, out List? roots)) - { - roots = []; - _bySubject[key] = roots; - } - - roots.Add(root); - _all.Add(root); - } - } - - /// - /// Mozilla's roots, as curl extracts them, read from the package the - /// first time an https:// request needs them. - /// - public static TrustedRoots Mozilla - { - get - { - // Two threads may both load them the first time; one copy wins. - // No lock: a lock is released in a finally, which a Cosmos kernel - // skips while an exception unwinds. - TrustedRoots? roots = Volatile.Read(ref s_mozilla); - if (roots is null) - { - Interlocked.CompareExchange(ref s_mozilla, LoadEmbedded(), null); - roots = Volatile.Read(ref s_mozilla)!; - } - - return roots; - } - } - - /// How many roots there are. - public int Count => _all.Count; - - /// Roots from PEM text: every BEGIN CERTIFICATE block in it, the rest ignored. - /// A block is not base64, or not a certificate. - public static TrustedRoots FromPem(string pem) => new(ReadPem(pem)); - - /// Roots from DER-encoded certificates. - public static TrustedRoots FromCertificates(IEnumerable certificates) => new(certificates); - - /// - /// The roots whose subject is : those encoded - /// alike first, then any that only compare equal once normalized. - /// - public IEnumerable FindBySubject(X509Name name) - { - if (_bySubject.TryGetValue(SubjectKey(name), out List? roots)) - { - foreach (Root root in roots) - { - yield return root.Certificate; - } - - yield break; - } - - foreach (Root root in _all) - { - if (root.Structure.Subject.Equivalent(name, true)) - { - yield return root.Certificate; - } - } - } - - private static string SubjectKey(X509Name name) => Convert.ToBase64String(name.GetEncoded()); - - private static TrustedRoots LoadEmbedded() - { - Stream? stream = typeof(TrustedRoots).Assembly.GetManifestResourceStream(ResourceName); - if (stream is null) - { - throw new InvalidOperationException($"The {ResourceName} resource is missing from the package."); - } - - MemoryStream text = new(); - stream.CopyTo(text); - stream.Dispose(); - return FromPem(Encoding.ASCII.GetString(text.GetBuffer(), 0, (int)text.Length)); - } - - private static List ReadPem(string pem) - { - List certificates = []; - int position = 0; - while (true) - { - int begin = pem.IndexOf(BeginMarker, position, StringComparison.Ordinal); - if (begin < 0) - { - return certificates; - } - - begin += BeginMarker.Length; - int end = pem.IndexOf(EndMarker, begin, StringComparison.Ordinal); - if (end < 0) - { - throw new FormatException("A certificate in the PEM text has no END line."); - } - - // Convert skips the line breaks and spaces between the base64 lines. - certificates.Add(Convert.FromBase64String(pem.Substring(begin, end - begin))); - position = end + EndMarker.Length; - } - } - - /// A root, kept as parsed structure until a chain needs its key. - private sealed class Root(X509CertificateStructure structure) - { - private X509Certificate? _certificate; - - public X509CertificateStructure Structure { get; } = structure; - - /// The full certificate, built once: it caches its public key. - public X509Certificate Certificate => _certificate ??= new X509Certificate(Structure); - } -} diff --git a/src/Cosmos.Network.Http/key.snk b/src/Cosmos.Network.Http/key.snk new file mode 100644 index 0000000000000000000000000000000000000000..67c9bb0ad77fd9cfb31a5fe1f8e4f6537f8883f8 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096IAgVrqn?0oVUK<}}z@wqRV>QE=V9G(P zv&4>n;N)r`28$iZ}__~(k83t)%SuJd!((DT{8XK)X~rK64E`*F3l z_5^K&AM;wi;^44^3SyC?622Htvk|)gU+)k0iCdaqB2%u@FR*KYku>(Zol_KusD7d= zA40qu=-~L94PXsGJ#eO@FlCr&O~0PO!6o-*GQq`zy7)g4B)IFgLhI0vfH=+L6i;`a zn(iWy{`8{{oj}vGf9S9M=48~NC!$coEAg@m^H3NXd_JrnT}HOd0AR%m^g zLg4+2jC7g-iYgo9N=!v@nv=I99O}eXu|AyULz5*Z*LDuQt4pP~UX|Zf`1Y#v4}kU8;^ac8l5!?oeMbz%wA_dHw*Ud81*FK}w1r zw3t~`4#bY+anQY5XU!i9#lc>IiLAxX~!c5@{0(|Buf%3XVh2FE%G?X~1 z7e5uWO?|rx0b=vcr3gpq3{-d$I8fk&Qrx<8j#tO3HOsxjGMJn4IT0*|11Oqu{gBd3 z&+6?KA|)R~alAn@8a!Y9Eq$TwFM@mu$U=dJ?PzCQY8jWLBlv^YEFIb8Fvhr*vMTOc zG+a5pRFEs9BT_>6MCF{WdN&DTsp^F!N=e1<>cRNkafH_Fr3`v9f%5G2(}LQ$j`vCM iwz?1qR;-^j3D_^H<{zl%^r6ivhR`zlIvhi1+jkY8izavg literal 0 HcmV?d00001 diff --git a/src/Cosmos.Network.Http/packages.config b/src/Cosmos.Network.Http/packages.config new file mode 100644 index 0000000..211bede --- /dev/null +++ b/src/Cosmos.Network.Http/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cosmos.Network.Http/packages.lock.json b/src/Cosmos.Network.Http/packages.lock.json new file mode 100644 index 0000000..6b18a4c --- /dev/null +++ b/src/Cosmos.Network.Http/packages.lock.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "dependencies": { + ".NETnanoFramework,Version=v1.0": { + "nanoFramework.CoreLibrary": { + "type": "Direct", + "requested": "[1.17.11, 1.17.11]", + "resolved": "1.17.11", + "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.39, 1.11.39]", + "resolved": "1.11.39", + "contentHash": "qNcgsk1QU5lTj2MgMYyF4iTx85YjFcAfJP9sYYydUlEXL1JaO2MnKGI3oBG4p20dJ1Fqi1ClIgKXJfAp9z80tA==" + }, + "nanoFramework.System.Collections": { + "type": "Direct", + "requested": "[1.5.75, 1.5.75]", + "resolved": "1.5.75", + "contentHash": "OiMpKo8RWbjZq1594YjdRnOGNN2aT4nowa+ET7scK0frGCux0a8LGaPkG56mmSfBCKzLlVs13LIlla3XLhXaMA==" + }, + "nanoFramework.System.IO.Streams": { + "type": "Direct", + "requested": "[1.1.96, 1.1.96]", + "resolved": "1.1.96", + "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" + }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.64, 1.11.64]", + "resolved": "1.11.64", + "contentHash": "jyC28lSWbBs1aajgp12HlHn6U0o00OMGzFP95SAeJ1CJ6BdmK+bq+peTvsfzpqyT0Babuaqq/x9G94XLaGapGw==" + }, + "nanoFramework.System.Text": { + "type": "Direct", + "requested": "[1.3.42, 1.3.42]", + "resolved": "1.3.42", + "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" + }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.10.94, 3.10.94]", + "resolved": "3.10.94", + "contentHash": "sn6gFIx6bwFj3Me0XGmek3gLZi/MZC8C4tRLwslXGeBcXcVxvFpZyDrQA93G8TiYgxTS7J7triKB7hj5wXzSxA==" + } + } + } +} \ No newline at end of file From c9aa4848a75c396a2b6bf39b25cd23d62f546d3d Mon Sep 17 00:00:00 2001 From: valentinbreiz Date: Sun, 4 Oct 2026 14:10:09 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20Port=20nanoFramework's=20System?= =?UTF-8?q?.Net.Http=20to=20Cosmos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cosmos.Network.Http 3.0.0: nanoFramework's HttpClient, HttpWebRequest and HttpListener, with the NetworkStream, SslStream and X.509 parts of its System.Net, on Cosmos's BCL sockets. nano's tree and file names are kept, every port edit carries a "Cosmos:" comment, and one csproj builds it all. - TLS 1.3 and 1.2, client and server, on BouncyCastle driven without blocking (Security/SslNative.cs, in place of nano's native mbedTLS layer), with the certificate check mbedTLS ran (Security/CertificateVerifier.cs) against the Mozilla roots or a given CA. - HttpListener serves every connection on the thread calling GetContext: accepts, advances TLS handshakes, watches kept-alive connections. Stop, Close and Abort from another thread leave the sockets to that one. - No finally, using or lock on a throwing path, no null dereference: a Cosmos kernel skips the first while unwinding and panics on the second. - nano bugs fixed along the way: chunked responses never ended, keep-alive never reused a connection, Host without its port, User-Agent refused, no way to read a response header, truncated bodies taken as complete. Tests: nanoFramework's HttpUnitTests on MSTest (but its own Uri's), plus TLS, certificate, key and HttpClient-HttpListener loopback tests. --- README.md | 118 ++- THIRD-PARTY-NOTICES.txt | 30 + src/Cosmos.Network.Http.Client/Http/.gitkeep | 0 .../Http/Headers/.gitkeep | 0 .../Properties/.gitkeep | 0 .../System.Net.Http.Client.nfproj | 248 ----- .../packages.config | 11 - .../packages.lock.json | 55 - src/Cosmos.Network.Http.Server/Http/.gitkeep | 0 .../Properties/.gitkeep | 0 .../System.Net.Http.Server.nfproj | 174 ---- .../packages.config | 11 - .../packages.lock.json | 55 - .../Cosmos.Network.Http.csproj | 35 +- .../Http/ByteArrayContent.cs | 2 +- .../Http/ClientCertificateOption.cs | 2 +- .../Http/Headers/HeaderDescriptor.cs | 2 +- .../Http/Headers/HttpContentHeaders.cs | 21 +- .../Http/Headers/HttpGeneralHeaders.cs | 2 +- .../Http/Headers/HttpHeaderType.cs | 2 +- .../Http/Headers/HttpHeaders.cs | 82 +- .../Http/Headers/HttpRequestHeaders.cs | 14 +- .../Http/Headers/HttpResponseHeaders.cs | 2 +- .../Http/Headers/KnownHeader.cs | 2 +- .../Http/Headers/MediaTypeHeaderValue.cs | 24 +- src/Cosmos.Network.Http/Http/HttpClient.cs | 90 +- .../Http/HttpClientHandler.cs | 33 +- .../Http/HttpCompletionOption.cs | 2 +- src/Cosmos.Network.Http/Http/HttpContent.cs | 6 +- .../Http/HttpMessageHandler.cs | 2 +- .../Http/HttpMessageInvoker.cs | 4 +- src/Cosmos.Network.Http/Http/HttpMethod.cs | 2 +- .../Http/HttpRequestException.cs | 2 +- .../Http/HttpRequestMessage.cs | 6 +- .../Http/HttpResponseMessage.cs | 6 +- .../Http/ReadOnlyStream.cs | 4 +- src/Cosmos.Network.Http/Http/StreamContent.cs | 9 +- src/Cosmos.Network.Http/Http/StringContent.cs | 4 +- .../Http/System.Net.AuthenticationType.cs | 2 +- .../Http/System.Net.Http.Constants.cs | 2 +- .../Http/System.Net.HttpListener.cs | 692 ++++++++----- .../Http/System.Net.HttpListenerContext.cs | 104 +- .../Http/System.Net.HttpListenerRequest.cs | 42 +- .../Http/System.Net.HttpListenerResponse.cs | 122 ++- .../Http/System.Net.HttpStatusCode.cs | 2 +- .../Http/System.Net.HttpUtility.cs | 2 +- .../Http/System.Net.HttpVersion.cs | 2 +- .../Http/System.Net.HttpWebRequest.cs | 273 +++-- .../Http/System.Net.HttpWebResponse.cs | 20 +- .../Http/System.Net.IKnowWhenDone.cs | 2 +- .../Http/System.Net.IWebRequestCreate.cs | 2 +- ...em.Net.Internal.WebRequestPrefixElement.cs | 2 +- .../Http/System.Net.Internal.cs | 2 +- .../Http/System.Net.NetworkCredential.cs | 2 +- .../System.Net.ProtocolViolationException.cs | 2 +- .../Http/System.Net.WebException.cs | 2 +- .../Http/System.Net.WebHeaders.cs | 6 +- .../Http/System.Net.WebRequest.cs | 8 +- .../Http/System.Net.WebResponse.cs | 2 +- .../Http/System.Net.WebSocketContext.cs | 2 +- .../Http/System.Net.WebStatus.cs | 2 +- .../Http/System.Net._HeaderInfo.cs | 2 +- .../Http/System.Net._HeaderInfoTable.cs | 7 +- .../Http/System.Net._HttpDateParse.cs | 2 +- .../System.Net._InputNetworkStreamWrapper.cs | 191 +++- .../System.Net._OutputNetworkStreamWrapper.cs | 123 ++- .../Http/System.Net._ValidationHelper.cs | 2 +- .../Http/System.Net.iwebproxy.cs | 2 +- .../Http/System.Net.webproxy.cs | 2 +- .../Properties/AssemblyInfo.cs | 7 +- .../Security/CertificateManager.cs | 173 ++++ .../Security/CertificateVerifier.cs | 406 ++++++++ .../Security/NetworkSecurity.cs | 37 + src/Cosmos.Network.Http/Security/SslNative.cs | 959 ++++++++++++++++++ src/Cosmos.Network.Http/Security/SslStream.cs | 456 +++++++++ .../Sockets/NetworkStream.cs | 422 ++++++++ .../X509Certificates/X509Certificate.cs | 267 +++++ .../X509Certificates/X509Certificate2.cs | 394 +++++++ src/Cosmos.Network.Http/packages.lock.json | 54 +- .../ByteArrayContentTest.cs | 182 ++++ .../CertificateValidatorTests.cs | 389 ------- .../CertificateVerifierTests.cs | 287 ++++++ .../Cosmos.Network.Http.Tests.csproj | 20 +- .../Cosmos.Network.Http.Tests/GlobalUsings.cs | 6 + .../Cosmos.Network.Http.Tests/HeadersTests.cs | 56 + .../HttpClientTest.cs | 140 +++ .../HttpContentTest.cs | 125 +++ .../HttpListenerRequestTests.cs | 36 + .../HttpLoopbackTests.cs | 852 ++++++++++++++++ .../HttpRequestTests.cs | 390 ------- .../Cosmos.Network.Http.Tests/HttpUrlTests.cs | 124 --- .../HttpUtilityTest.cs | 141 +++ tests/Cosmos.Network.Http.Tests/HttpsTests.cs | 379 ------- .../LoopbackServer.cs | 332 ++++++ .../MediaTypeHeaderValueTest.cs | 177 ++++ .../Cosmos.Network.Http.Tests/MockContent.cs | 157 +++ .../Properties/AssemblyInfo.cs | 31 + .../ResponseReaderTests.cs | 243 ----- .../SslStreamTests.cs | 341 +++++++ .../StreamContentTest.cs | 432 ++++++++ .../StringContentTest.cs | 55 + .../TestCertificates.cs | 237 +++++ .../TestFramework.cs | 82 ++ tests/Cosmos.Network.Http.Tests/TestPki.cs | 169 --- tests/Cosmos.Network.Http.Tests/TestServer.cs | 258 ----- .../Cosmos.Network.Http.Tests/UriUnitTests.cs | 484 +++++++++ .../WebHeaderCollectionTests.cs | 101 ++ .../X509Certificate2Tests.cs | 211 ++++ 108 files changed, 9111 insertions(+), 3191 deletions(-) delete mode 100644 src/Cosmos.Network.Http.Client/Http/.gitkeep delete mode 100644 src/Cosmos.Network.Http.Client/Http/Headers/.gitkeep delete mode 100644 src/Cosmos.Network.Http.Client/Properties/.gitkeep delete mode 100644 src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj delete mode 100644 src/Cosmos.Network.Http.Client/packages.config delete mode 100644 src/Cosmos.Network.Http.Client/packages.lock.json delete mode 100644 src/Cosmos.Network.Http.Server/Http/.gitkeep delete mode 100644 src/Cosmos.Network.Http.Server/Properties/.gitkeep delete mode 100644 src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj delete mode 100644 src/Cosmos.Network.Http.Server/packages.config delete mode 100644 src/Cosmos.Network.Http.Server/packages.lock.json create mode 100644 src/Cosmos.Network.Http/Security/CertificateManager.cs create mode 100644 src/Cosmos.Network.Http/Security/CertificateVerifier.cs create mode 100644 src/Cosmos.Network.Http/Security/NetworkSecurity.cs create mode 100644 src/Cosmos.Network.Http/Security/SslNative.cs create mode 100644 src/Cosmos.Network.Http/Security/SslStream.cs create mode 100644 src/Cosmos.Network.Http/Sockets/NetworkStream.cs create mode 100644 src/Cosmos.Network.Http/X509Certificates/X509Certificate.cs create mode 100644 src/Cosmos.Network.Http/X509Certificates/X509Certificate2.cs create mode 100644 tests/Cosmos.Network.Http.Tests/ByteArrayContentTest.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/CertificateValidatorTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/CertificateVerifierTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/GlobalUsings.cs create mode 100644 tests/Cosmos.Network.Http.Tests/HeadersTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/HttpClientTest.cs create mode 100644 tests/Cosmos.Network.Http.Tests/HttpContentTest.cs create mode 100644 tests/Cosmos.Network.Http.Tests/HttpListenerRequestTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/HttpLoopbackTests.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/HttpRequestTests.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/HttpUrlTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/HttpUtilityTest.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/HttpsTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/LoopbackServer.cs create mode 100644 tests/Cosmos.Network.Http.Tests/MediaTypeHeaderValueTest.cs create mode 100644 tests/Cosmos.Network.Http.Tests/MockContent.cs create mode 100644 tests/Cosmos.Network.Http.Tests/Properties/AssemblyInfo.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/ResponseReaderTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/SslStreamTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/StreamContentTest.cs create mode 100644 tests/Cosmos.Network.Http.Tests/StringContentTest.cs create mode 100644 tests/Cosmos.Network.Http.Tests/TestCertificates.cs create mode 100644 tests/Cosmos.Network.Http.Tests/TestFramework.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/TestPki.cs delete mode 100644 tests/Cosmos.Network.Http.Tests/TestServer.cs create mode 100644 tests/Cosmos.Network.Http.Tests/UriUnitTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/WebHeaderCollectionTests.cs create mode 100644 tests/Cosmos.Network.Http.Tests/X509Certificate2Tests.cs diff --git a/README.md b/README.md index af02ecd..e2720d0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

CosmosHTTP Client 🚀

+

CosmosHTTP 🚀

Version @@ -8,7 +8,9 @@

-> CosmosHTTP is an HTTP/1.1 client, `https://` included, made in C# for the Cosmos operating system construction kit. +> CosmosHTTP is an HTTP/1.1 client and server, `https://` included, for the Cosmos operating system construction kit: [.NET nanoFramework's System.Net.Http](https://github.com/nanoframework/System.Net.Http) ported to Cosmos Gen3. + +The sources keep nanoFramework's folder tree and file names (`nanoFramework.System.Net.Http` became `src/Cosmos.Network.Http`), in the `Cosmos.Network.Http` namespace, and its API: `HttpClient`, `HttpListener`, `HttpWebRequest`. What the port changed is marked `Cosmos:` in the code. The library uses the BCL's `System.Net.Sockets`, which a Cosmos kernel plugs onto its network stack, so it runs on a desktop as is, which is how the tests drive it. ## Usage @@ -16,96 +18,114 @@ Add the package to your kernel .csproj: ```xml - + ``` -The kernel needs networking (`CosmosEnableNetwork`, on by default), an IP configuration (DHCP or static) and, for host names, a DNS server. It also needs a Cosmos that plugs `RandomNumberGenerator` with a real random generator, whether it asks for `https://` or not: the TLS code is part of the package, and a kernel without that plug does not link (see [HTTPS](#https)). `Send()` runs the request on the calling thread and returns the response once it has arrived whole: +The kernel needs networking (`CosmosEnableNetwork`, on by default), an IP configuration (DHCP or static) and, for host names, a DNS server. Keep `ImplicitUsings` off, or remove `System.Net.Http` from them: its `HttpClient` would clash with this one. For the same reason, don't import `System.Net` next to `Cosmos.Network.Http`. + +### Client ```csharp using System; -using System.IO; +using System.Text; using Cosmos.Network.Http; -HttpResponse response = new HttpRequest("http://httpforever.com/").Send(); +using HttpClient client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + +string text = client.GetString("http://example.com/"); +byte[] data = client.GetByteArray("https://example.com/"); -Console.WriteLine($"{response.StatusCode} {response.ReasonPhrase}, {response.Content.Length} bytes"); -File.WriteAllBytes("/0/index.html", response.Content); +using HttpResponseMessage response = client.Post("http://10.0.2.2:8000/api", new StringContent("{\"name\":\"cosmos\"}", Encoding.UTF8, "application/json")); +Console.WriteLine((int)response.StatusCode + " " + response.Content.ReadAsString()); ``` -`Send()` returns error statuses too; `EnsureSuccessStatusCode()` turns them into an `HttpException`, and `GetString()` decodes the body with the charset of its Content-Type: +`GetString`, `GetByteArray` and `GetStream` throw an `HttpRequestException` for an error status; `Get`, `Post`, `Put`, `Patch`, `Delete` and `Send` return it. They read the whole body, and every failure (connection, DNS, TLS, a response cut short or malformed) is an `HttpRequestException` whose innermost exception says why. With `HttpCompletionOption.ResponseHeadersRead` the body is read from the content's stream later, whose failures are `IOException`s. Dispose the responses you get: their connection goes back to the pool or is closed then. -```csharp -string json = new HttpRequest("https://example.com/data.json").Send().EnsureSuccessStatusCode().GetString(); -``` +`DefaultRequestHeaders` (and a request's `Headers`) take `Accept`, `User-Agent`, `Referer`, `Range` and `If-Modified-Since`, but not the headers the request sets itself (`Host`, `Connection`, `Content-Length`, `Transfer-Encoding`). A response's headers are read with `TryGetValues`, `GetValues` and `Contains`, its body's type with `Content.Headers.ContentType` (null without one). Redirects are not followed: a 3xx comes back as the response, its `Location` in its headers. -A request can set its method, body, headers, timeout and how many redirects it follows: +`Timeout` is infinite by default, as on nanoFramework: set it, or a server that stops answering holds the thread for the 5 minutes each read may wait. It bounds each wait for the response's head and for its body. + +`HttpClient` asks the server to close the connection after each response (`DefaultRequestHeaders.ConnectionClose`, as nanoFramework does). Set it to `false` to keep connections alive: the next request to the same server, with the same TLS settings, reuses one. + +### Server ```csharp -HttpResponse response = new HttpRequest("http://example.com/api") +using System.Text; +using Cosmos.Network.Http; + +HttpListener listener = new HttpListener("http", 8080); +listener.Start(); + +while (listener.IsListening) { - Method = "POST", - Body = Encoding.UTF8.GetBytes("{\"name\":\"cosmos\"}"), - Headers = { ["Content-Type"] = "application/json" }, - Timeout = 10_000, // how long the server may stay silent, in milliseconds (15 s by default) - MaxRedirects = 0, // return redirects instead of following them (5 by default) - // Optional: one line per response and redirect. - Log = message => Cosmos.Kernel.System.Diagnostics.Log.WriteString(message + "\n"), -}.Send(); + HttpListenerContext context = listener.GetContext(); + if (context == null) + { + break; // stopped + } + + HttpListenerResponse response = context.Response; + byte[] body = Encoding.UTF8.GetBytes("hello from cosmos " + context.Request.RawUrl); + response.ContentType = "text/plain"; + response.ContentLength64 = body.Length; + response.OutputStream.Write(body, 0, body.Length); + response.Close(); +} ``` -The Host header comes from the URL. Setting it in `Headers` reaches a virtual host by IP address: +One thread serves every connection: `GetContext` accepts them, runs their TLS handshakes (without waiting on any client) and watches those waiting for a request, and returns the next request that has arrived. It sleeps 50 ms between rounds when nothing is pending, so call it from a thread of its own, never from the kernel's main loop, and handle each request on that thread. `Stop`, `Close` and `Abort` may come from another thread, which leaves the sockets to the serving one: waiting in `GetContext`, it returns `null` within a round; handling a request, it answers it, then closes them once the response is closed, or in its next `GetContext` call (which throws `InvalidOperationException`), or in its own `Close`. -```csharp -new HttpRequest("http://34.223.124.45/") { Headers = { ["Host"] = "neverssl.com" } }.Send(); -``` +A response closes its connection unless `KeepAlive` is set (`response.KeepAlive = context.Request.KeepAlive` keeps what the client asked for), and even then when it doesn't turn out whole (its `ContentLength64` not all written, or no length and no `SendChunked`), or when the handler left the request's body unread. `SendChunked` streams a body of unknown length. ### HTTPS -`https://` URLs run TLS 1.3 or 1.2 through [BouncyCastle](https://github.com/bcgit/bc-csharp), all managed code: the BCL's `SslStream` and cryptography call OpenSSL, which a kernel does not have. The client offers ECDHE key exchange (X25519, P-256, P-384) with AES-GCM, ChaCha20-Poly1305 and, for old TLS 1.2 servers, AES-CBC, and asks for `http/1.1` through ALPN. +`https://` runs TLS 1.3 or 1.2 through [BouncyCastle](https://github.com/bcgit/bc-csharp), all managed code, in place of nanoFramework's native mbedTLS: the BCL's `SslStream` and cryptography call OpenSSL, which a kernel does not have. Key exchanges are ECDHE (X25519, P-256, P-384), ciphers AES-GCM and ChaCha20-Poly1305 (preferred on a CPU without AES-NI, as a Cosmos kernel runs), plus AES-CBC for old TLS 1.2 servers. -A request goes on only with a server whose certificate chain leads to one of Mozilla's roots (embedded in the package, from [curl's extract](https://curl.se/docs/caextract.html) of 2026-09-25), every certificate on it valid now, signed with SHA-2 or EdDSA and allowed to issue what it issued, and whose certificate names the host. Otherwise `Send()` throws an `HttpException` that tells why: +A client goes on only with a server whose certificate chain leads to a trusted certificate, each one valid now and allowed to issue what it issued, and whose certificate names the host. The trusted certificates are Mozilla's roots (embedded in the package, from [curl's extract](https://curl.se/docs/caextract.html)), or those given: -``` -The certificate of expired.badssl.com is not trusted: the certificate expired on 2015-04-12 23:59:59 UTC. +```csharp +// For this client: one CA, PEM or DER. +HttpClient client = new HttpClient { HttpsAuthentCert = new X509Certificate(caPem), SslProtocols = SslProtocols.None }; + +// For every client given none: replaces the embedded roots. +CertificateManager.AddCaCertificateBundle(bundlePem); ``` -`ServerCertificateValidation` decides instead, given what the server presented and what the built-in check made of it (`Error` is `null` when it trusts the server). To trust one self-signed server too, compare its `Fingerprint`, the SHA-256 of its certificate in uppercase hex (`openssl x509 -noout -fingerprint -sha256 -in cert.pem | tr -d :`): +`SslProtocols` is TLS 1.2 by default, as on nanoFramework; `SslProtocols.None` offers TLS 1.3 and 1.2. `SslVerification.NoVerification` skips the check. + +A server needs a certificate with its private key, PEM or DER (PKCS#8, encrypted or not, PKCS#1 RSA or SEC1 EC): ```csharp -new HttpRequest("https://10.0.2.2:8443/") +HttpListener listener = new HttpListener("https", 443) { - ServerCertificateValidation = certificate => certificate.Error is null - || certificate.Fingerprint == "9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08", -}.Send(); + HttpsCert = new X509Certificate2(certificatePem, privateKeyPem, null), + SslProtocols = SslProtocols.None, +}; ``` -Redirects from `https://` to `http://` are not followed, and a redirect to another server no longer sends the `Authorization`, `Proxy-Authorization`, `Cookie` and `Host` headers set by hand. - On a Cosmos kernel: -- TLS keys come from `RandomNumberGenerator`, which a kernel without a plug for it cannot even link: use a Cosmos that has one. +- TLS keys come from `RandomNumberGenerator`, which Cosmos plugs with a kernel generator. - Certificate dates are checked against `DateTime.UtcNow`: the kernel's clock has to be right, which it is in QEMU, whose RTC holds UTC. -- The first handshake also loads the roots and warms BouncyCastle up, so it takes longer than the next ones. +- The first handshake loads the roots and warms BouncyCastle up, so it takes longer than the next ones. ### Limits -- No revocation check (OCSP or CRLs), no client certificates, no session resumption, and no fetching of an intermediate certificate a server leaves out. -- A response with neither Content-Length nor chunked encoding ends when the server closes the connection, close_notify or not, as many servers skip it: over `https://`, someone in the middle could cut such a response short unnoticed. -- Host names are matched as given: internationalized names have to be written in their `xn--` form. -- Each request opens a connection of its own, and the server closes it once it has answered. -- Responses come without content coding (`Accept-Encoding: identity`): a Cosmos kernel has no gzip to undo. -- The whole body is held in memory. +- HTTP/1.1 only, IPv4 only, no proxy authentication, no content coding (a Cosmos kernel has no gzip to undo). +- No revocation check (OCSP or CRLs), no session resumption, no fetching of an intermediate certificate a server leaves out, no name constraints (a certificate with critical ones is refused, as mbedTLS refuses it). +- The server asks no client certificate and offers no ALPN. Certificates are read with RSA, EC (named curves), Ed25519 or Ed448 keys only. +- Cosmos's network stack takes no lock: a listener serving on one thread while requests run on another may run into each other. Keep the sockets to one thread at a time. -### Threads +## Tests -`Send()` never waits in `Thread.Sleep`: it waits in `Socket.Poll`, which returns at once on a Cosmos kernel. So it runs on the kernel's main loop, which must never block, as well as on a thread of its own. TLS runs on the same thread: BouncyCastle is driven without blocking, handed what the socket received and asked for what to send. +`dotnet test` runs nanoFramework's unit tests (`HttpUnitTests`, on MSTest, but for those of its own `System.Uri`, which the port leaves out for .NET's) and the port's: TLS client and server over loopback, certificate checks and key formats, `HttpClient` against `HttpListener` over http and https. ## Authors 👤 **[@valentinbreiz](https://github.com/valentinbreiz)** -👤 **[@2881099](https://github.com/2881099)** (the first version was inspired by [TcpClientHttpRequest](https://github.com/2881099/TcpClientHttpRequest)) +The HTTP code is .NET nanoFramework's, by the .NET Foundation and contributors. ## 🤝 Contributing @@ -117,4 +137,4 @@ Feel free to check [issues page](https://github.com/CosmosOS/Cosmos.Network.Http Copyright © 2023-2026 [CosmosOS](https://github.com/CosmosOS). -This project is [BSD Clause 3](https://github.com/CosmosOS/Cosmos.Network.Http/blob/main/LICENSE.txt) licensed. It depends on [BouncyCastle.Cryptography](https://www.nuget.org/packages/BouncyCastle.Cryptography/) (MIT), and embeds Mozilla's root certificates, [`resources/cacert.pem`](https://github.com/CosmosOS/Cosmos.Network.Http/blob/main/resources/cacert.pem), under the [Mozilla Public License 2.0](https://www.mozilla.org/MPL/2.0/) (see [THIRD-PARTY-NOTICES.txt](https://github.com/CosmosOS/Cosmos.Network.Http/blob/main/THIRD-PARTY-NOTICES.txt)). To refresh them, replace that file with the latest `https://curl.se/ca/cacert.pem`. +This project is [BSD Clause 3](https://github.com/CosmosOS/Cosmos.Network.Http/blob/main/LICENSE.txt) licensed. Its HTTP code is .NET nanoFramework's System.Net.Http and System.Net, under the MIT license. It depends on [BouncyCastle.Cryptography](https://www.nuget.org/packages/BouncyCastle.Cryptography/) (MIT), and embeds Mozilla's root certificates, [`resources/cacert.pem`](https://github.com/CosmosOS/Cosmos.Network.Http/blob/main/resources/cacert.pem), under the [Mozilla Public License 2.0](https://www.mozilla.org/MPL/2.0/) (see [THIRD-PARTY-NOTICES.txt](https://github.com/CosmosOS/Cosmos.Network.Http/blob/main/THIRD-PARTY-NOTICES.txt)). To refresh them, replace that file with the latest `https://curl.se/ca/cacert.pem`. diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 9f5001d..94f9baa 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -1,5 +1,35 @@ Cosmos.Network.Http embeds third-party material: +src/Cosmos.Network.Http (but for Security/SslNative.cs and Security/CertificateVerifier.cs) +tests/Cosmos.Network.Http.Tests (nanoFramework's HttpUnitTests, but for the port's own test files) + .NET nanoFramework's System.Net.Http and, for NetworkStream, SslStream and the + X.509 certificates, System.Net, ported to Cosmos. + https://github.com/nanoframework/System.Net.Http + https://github.com/nanoframework/System.Net + License: MIT + + MIT License + + Copyright (c) .NET Foundation and Contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + resources/cacert.pem Mozilla's root certificates (CA bundle), as extracted by the curl project from Mozilla's certdata.txt: https://curl.se/docs/caextract.html diff --git a/src/Cosmos.Network.Http.Client/Http/.gitkeep b/src/Cosmos.Network.Http.Client/Http/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Cosmos.Network.Http.Client/Http/Headers/.gitkeep b/src/Cosmos.Network.Http.Client/Http/Headers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Cosmos.Network.Http.Client/Properties/.gitkeep b/src/Cosmos.Network.Http.Client/Properties/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj b/src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj deleted file mode 100644 index 28370c4..0000000 --- a/src/Cosmos.Network.Http.Client/System.Net.Http.Client.nfproj +++ /dev/null @@ -1,248 +0,0 @@ - - - - - $(MSBuildExtensionsPath)\nanoFramework\v1.0\ - - - - Debug - AnyCPU - {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 6c6ee1b2-20f0-4f0e-8085-7ecd1e692567 - Library - Properties - 512 - System.Net.Http - System.Net.Http - v1.0 - True - bin\$(Configuration)\System.Net.Http.xml - true - true - - - true - - - ..\Cosmos.Network.Http\key.snk - - - false - - - - bin\$(Configuration)\Stubs - System.Net.Http - - - - false - - - - - - Http\ByteArrayContent.cs - - - Http\ClientCertificateOption.cs - - - Http\Headers\HeaderDescriptor.cs - - - Http\Headers\HttpContentHeaders.cs - - - Http\Headers\HttpGeneralHeaders.cs - - - Http\Headers\HttpHeaders.cs - - - Http\Headers\HttpHeaderType.cs - - - Http\Headers\HttpRequestHeaders.cs - - - Http\Headers\HttpResponseHeaders.cs - - - Http\Headers\KnownHeader.cs - - - Http\Headers\MediaTypeHeaderValue.cs - - - Http\HttpClient.cs - - - Http\HttpClientHandler.cs - - - Http\HttpCompletionOption.cs - - - Http\HttpContent.cs - - - Http\HttpMessageHandler.cs - - - Http\HttpMessageInvoker.cs - - - Http\HttpMethod.cs - - - Http\HttpRequestException.cs - - - Http\HttpRequestMessage.cs - - - Http\HttpResponseMessage.cs - - - - Http\StreamContent.cs - - - Http\StringContent.cs - - - Http\System.Net.Http.Constants.cs - - - Http\System.Net.IKnowWhenDone.cs - - - Http\System.Net.Internal.WebRequestPrefixElement.cs - - - Http\System.Net.AuthenticationType.cs - - - Http\System.Net.HttpStatusCode.cs - - - Http\System.Net.HttpVersion.cs - - - Http\System.Net.HttpWebRequest.cs - - - Http\System.Net.HttpWebResponse.cs - - - Http\System.Net.Internal.cs - - - Http\System.Net.iwebproxy.cs - - - Http\System.Net.IWebRequestCreate.cs - - - Http\System.Net.NetworkCredential.cs - - - Http\System.Net.ProtocolViolationException.cs - - - Http\System.Net.WebException.cs - - - Http\System.Net.WebHeaders.cs - - - Http\System.Net.webproxy.cs - - - Http\System.Net.WebRequest.cs - - - Http\System.Net.WebResponse.cs - - - Http\System.Net.WebStatus.cs - - - Http\System.Net._HeaderInfo.cs - - - Http\System.Net._HeaderInfoTable.cs - - - Http\System.Net._HttpDateParse.cs - - - Http\System.Net._InputNetworkStreamWrapper.cs - - - Http\System.Net._OutputNetworkStreamWrapper.c - - - Http\System.Net._ValidationHelper.cs - - - Http\System.Uri.cs - - - Properties\AssemblyInfo.cs - - - Http\System.Net.HttpUtility.cs - - - - - - - - - - - - - - - - ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll - - - ..\packages\nanoFramework.Runtime.Events.1.11.39\lib\nanoFramework.Runtime.Events.dll - - - ..\packages\nanoFramework.System.Collections.1.5.75\lib\nanoFramework.System.Collections.dll - - - ..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll - - - ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll - - - ..\packages\nanoFramework.System.Net.1.11.64\lib\System.Net.dll - - - ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. - - - - - - \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Client/packages.config b/src/Cosmos.Network.Http.Client/packages.config deleted file mode 100644 index 211bede..0000000 --- a/src/Cosmos.Network.Http.Client/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Client/packages.lock.json b/src/Cosmos.Network.Http.Client/packages.lock.json deleted file mode 100644 index 6b18a4c..0000000 --- a/src/Cosmos.Network.Http.Client/packages.lock.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETnanoFramework,Version=v1.0": { - "nanoFramework.CoreLibrary": { - "type": "Direct", - "requested": "[1.17.11, 1.17.11]", - "resolved": "1.17.11", - "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" - }, - "nanoFramework.Runtime.Events": { - "type": "Direct", - "requested": "[1.11.39, 1.11.39]", - "resolved": "1.11.39", - "contentHash": "qNcgsk1QU5lTj2MgMYyF4iTx85YjFcAfJP9sYYydUlEXL1JaO2MnKGI3oBG4p20dJ1Fqi1ClIgKXJfAp9z80tA==" - }, - "nanoFramework.System.Collections": { - "type": "Direct", - "requested": "[1.5.75, 1.5.75]", - "resolved": "1.5.75", - "contentHash": "OiMpKo8RWbjZq1594YjdRnOGNN2aT4nowa+ET7scK0frGCux0a8LGaPkG56mmSfBCKzLlVs13LIlla3XLhXaMA==" - }, - "nanoFramework.System.IO.Streams": { - "type": "Direct", - "requested": "[1.1.96, 1.1.96]", - "resolved": "1.1.96", - "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" - }, - "nanoFramework.System.Net": { - "type": "Direct", - "requested": "[1.11.64, 1.11.64]", - "resolved": "1.11.64", - "contentHash": "jyC28lSWbBs1aajgp12HlHn6U0o00OMGzFP95SAeJ1CJ6BdmK+bq+peTvsfzpqyT0Babuaqq/x9G94XLaGapGw==" - }, - "nanoFramework.System.Text": { - "type": "Direct", - "requested": "[1.3.42, 1.3.42]", - "resolved": "1.3.42", - "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" - }, - "nanoFramework.System.Threading": { - "type": "Direct", - "requested": "[1.1.52, 1.1.52]", - "resolved": "1.1.52", - "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" - }, - "Nerdbank.GitVersioning": { - "type": "Direct", - "requested": "[3.10.94, 3.10.94]", - "resolved": "3.10.94", - "contentHash": "sn6gFIx6bwFj3Me0XGmek3gLZi/MZC8C4tRLwslXGeBcXcVxvFpZyDrQA93G8TiYgxTS7J7triKB7hj5wXzSxA==" - } - } - } -} \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Server/Http/.gitkeep b/src/Cosmos.Network.Http.Server/Http/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Cosmos.Network.Http.Server/Properties/.gitkeep b/src/Cosmos.Network.Http.Server/Properties/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj b/src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj deleted file mode 100644 index 62700ab..0000000 --- a/src/Cosmos.Network.Http.Server/System.Net.Http.Server.nfproj +++ /dev/null @@ -1,174 +0,0 @@ - - - - - $(MSBuildExtensionsPath)\nanoFramework\v1.0\ - - - - Debug - AnyCPU - {11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 3b91a15a-40b1-4d57-93f2-eb96af0e7949 - Library - Properties - 512 - System.Net.Http - System.Net.Http - v1.0 - True - bin\$(Configuration)\System.Net.Http.xml - true - true - - - true - - - ..\Cosmos.Network.Http\key.snk - - - false - - - - bin\$(Configuration)\Stubs - System.Net.Http - - - - false - - - - - - Http\System.Net.AuthenticationType.cs - - - Http\System.Net.Http.Constants.cs - - - Http\System.Net.HttpListener.cs - - - Http\System.Net.HttpListenerContext.cs - - - Http\System.Net.HttpListenerRequest.cs - - - Http\System.Net.HttpListenerResponse.cs - - - Http\System.Net.HttpStatusCode.cs - - - Http\System.Net.HttpVersion.cs - - - Http\System.Net.IKnowWhenDone.cs - - - Http\System.Net.Internal.cs - - - Http\System.Net.iwebproxy.cs - - - Http\System.Net.NetworkCredential.cs - - - Http\System.Net.ProtocolViolationException.cs - - - Http\System.Net.WebException.cs - - - Http\System.Net.WebHeaders.cs - - - \Http\System.Net.WebResponse.cs - - - Http\System.Net.WebStatus.cs - - - Http\System.Net._HeaderInfo.cs - - - Http\System.Net._HeaderInfoTable.cs - - - Http\System.Net._HttpDateParse.cs - - - Http\System.Net._InputNetworkStreamWrapper.cs - - - Http\System.Net._OutputNetworkStreamWrapper.cs - - - Http\System.Net._ValidationHelper.cs - - - Http\System.Uri.cs - - - Properties\AssemblyInfo.cs - - - Http\System.Net.HttpUtility.cs - - - Http\System.Net.WebSocketContext.cs - - - - - - - - - - - - - - - ..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll - - - ..\packages\nanoFramework.Runtime.Events.1.11.39\lib\nanoFramework.Runtime.Events.dll - - - ..\packages\nanoFramework.System.Collections.1.5.75\lib\nanoFramework.System.Collections.dll - - - ..\packages\nanoFramework.System.Text.1.3.42\lib\nanoFramework.System.Text.dll - - - ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll - - - ..\packages\nanoFramework.System.Net.1.11.64\lib\System.Net.dll - - - ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. - - - - - - \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Server/packages.config b/src/Cosmos.Network.Http.Server/packages.config deleted file mode 100644 index 211bede..0000000 --- a/src/Cosmos.Network.Http.Server/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Cosmos.Network.Http.Server/packages.lock.json b/src/Cosmos.Network.Http.Server/packages.lock.json deleted file mode 100644 index 6b18a4c..0000000 --- a/src/Cosmos.Network.Http.Server/packages.lock.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETnanoFramework,Version=v1.0": { - "nanoFramework.CoreLibrary": { - "type": "Direct", - "requested": "[1.17.11, 1.17.11]", - "resolved": "1.17.11", - "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" - }, - "nanoFramework.Runtime.Events": { - "type": "Direct", - "requested": "[1.11.39, 1.11.39]", - "resolved": "1.11.39", - "contentHash": "qNcgsk1QU5lTj2MgMYyF4iTx85YjFcAfJP9sYYydUlEXL1JaO2MnKGI3oBG4p20dJ1Fqi1ClIgKXJfAp9z80tA==" - }, - "nanoFramework.System.Collections": { - "type": "Direct", - "requested": "[1.5.75, 1.5.75]", - "resolved": "1.5.75", - "contentHash": "OiMpKo8RWbjZq1594YjdRnOGNN2aT4nowa+ET7scK0frGCux0a8LGaPkG56mmSfBCKzLlVs13LIlla3XLhXaMA==" - }, - "nanoFramework.System.IO.Streams": { - "type": "Direct", - "requested": "[1.1.96, 1.1.96]", - "resolved": "1.1.96", - "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" - }, - "nanoFramework.System.Net": { - "type": "Direct", - "requested": "[1.11.64, 1.11.64]", - "resolved": "1.11.64", - "contentHash": "jyC28lSWbBs1aajgp12HlHn6U0o00OMGzFP95SAeJ1CJ6BdmK+bq+peTvsfzpqyT0Babuaqq/x9G94XLaGapGw==" - }, - "nanoFramework.System.Text": { - "type": "Direct", - "requested": "[1.3.42, 1.3.42]", - "resolved": "1.3.42", - "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" - }, - "nanoFramework.System.Threading": { - "type": "Direct", - "requested": "[1.1.52, 1.1.52]", - "resolved": "1.1.52", - "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" - }, - "Nerdbank.GitVersioning": { - "type": "Direct", - "requested": "[3.10.94, 3.10.94]", - "resolved": "3.10.94", - "contentHash": "sn6gFIx6bwFj3Me0XGmek3gLZi/MZC8C4tRLwslXGeBcXcVxvFpZyDrQA93G8TiYgxTS7J7triKB7hj5wXzSxA==" - } - } - } -} \ No newline at end of file diff --git a/src/Cosmos.Network.Http/Cosmos.Network.Http.csproj b/src/Cosmos.Network.Http/Cosmos.Network.Http.csproj index f0a7666..15ca3af 100644 --- a/src/Cosmos.Network.Http/Cosmos.Network.Http.csproj +++ b/src/Cosmos.Network.Http/Cosmos.Network.Http.csproj @@ -1,32 +1,35 @@ net10.0 latest - enable + disable disable true true Cosmos.Network.Http + + false + false + false Cosmos.Network.Http - 2.1.0 + 3.0.0 Cosmos.Network.Http Cosmos Cosmos - HTTP client for Cosmos Gen3 kernels. - cosmos;http;https;tls;client;kernel + HTTP client and server for Cosmos Gen3 kernels, https included: .NET nanoFramework's System.Net.Http (HttpClient, HttpListener, HttpWebRequest) on Cosmos's sockets, with TLS 1.3 and 1.2 by BouncyCastle. + cosmos;http;https;tls;client;server;httpclient;httplistener;nanoframework;kernel icon.png LICENSE.txt README.md @@ -41,6 +44,24 @@ + + + + + + + + + + + + + + diff --git a/src/Cosmos.Network.Http/Http/ByteArrayContent.cs b/src/Cosmos.Network.Http/Http/ByteArrayContent.cs index 592b709..4b9e459 100644 --- a/src/Cosmos.Network.Http/Http/ByteArrayContent.cs +++ b/src/Cosmos.Network.Http/Http/ByteArrayContent.cs @@ -6,7 +6,7 @@ using System.IO; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Provides HTTP content based on a byte array. diff --git a/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs b/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs index ca32713..6699237 100644 --- a/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs +++ b/src/Cosmos.Network.Http/Http/ClientCertificateOption.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Specifies how client certificates are provided. diff --git a/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs b/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs index dd7dd2b..2acb440 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HeaderDescriptor.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { // This struct represents a particular named header -- // if the header is one of our known headers, then it contains a reference to the KnownHeader object; diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs index 99d0a0c..29e0ffd 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HttpContentHeaders.cs @@ -4,9 +4,9 @@ // See LICENSE file in the project root for full license information. // -using System.Net.Http.Http.Headers; +using Cosmos.Network.Http.Headers; -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { /// /// Represents the collection of Content Headers as defined in RFC 2616. @@ -61,7 +61,22 @@ public MediaTypeHeaderValue ContentType { get { - return MediaTypeHeaderValue.Parse(_headerStore[HttpKnownHeaderNames.ContentType]); + // Cosmos: null without the header, or with one that isn't a media type, as .NET's; nanoFramework threw + // ArgumentNullException or FormatException. + string value = _headerStore[HttpKnownHeaderNames.ContentType]; + if (value == null) + { + return null; + } + + try + { + return MediaTypeHeaderValue.Parse(value); + } + catch (FormatException) + { + return null; + } } set diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs index c68a965..e564fbe 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HttpGeneralHeaders.cs @@ -6,7 +6,7 @@ using System.Diagnostics; -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { // The purpose of this type is to extract the handling of general headers in one place rather than duplicating // functionality in both HttpRequestHeaders and HttpResponseHeaders. diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs b/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs index 76cb40f..68126dd 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HttpHeaderType.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { [Flags] internal enum HttpHeaderType : byte diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs index 5bc064e..ead7151 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HttpHeaders.cs @@ -4,9 +4,10 @@ // See LICENSE file in the project root for full license information. // +using System.Collections.Generic; using System.Diagnostics; -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { /// /// Key/value pairs of headers. The value is either a raw or a . @@ -67,9 +68,88 @@ public void Add( string name, string value) { + // Cosmos: the headers HttpWebRequest restricts but .NET's HttpClient takes (Accept, User-Agent, Referer, + // Range, If-Modified-Since), which nanoFramework refused here too: a request had no way to name its user + // agent. Those the request sets itself (Host, Connection, Content-Length, Transfer-Encoding, Expect) stay + // refused. + if (IsCallerHeader(name)) + { + _headerStore.AddWithoutValidate(name, value); + return; + } + _headerStore.Add(name, value); } + /// + /// Returns whether a specific header exists in the collection. + /// + /// The specific header. + /// if the specified header exists in the collection; otherwise . + /// Cosmos: .NET's, which nanoFramework lacks: the headers of a response could be added, not read. + public bool Contains(string name) + { + return _headerStore.GetValues(name) != null; + } + + /// + /// Returns all header values for a specified header stored in the collection. + /// + /// The specified header to return values for. + /// An array of header strings. + /// The header cannot be found. + /// Cosmos: .NET's, which nanoFramework lacks. + public IEnumerable GetValues(string name) + { + if (!TryGetValues(name, out IEnumerable values)) + { + throw new InvalidOperationException("The header " + name + " is not there."); + } + + return values; + } + + /// + /// Returns whether a specified header and specified values are stored in the collection. + /// + /// The specified header. + /// The specified header values. + /// if the specified header name and values are stored in the collection; otherwise . + /// Cosmos: .NET's, which nanoFramework lacks. + public bool TryGetValues(string name, out IEnumerable values) + { + string[] found = _headerStore.GetValues(name); + values = found; + return found != null; + } + + /// + /// Removes the specified header from the collection. + /// + /// The name of the header to remove from the collection. + /// if the header was removed; otherwise . + /// Cosmos: .NET's, which nanoFramework lacks. + public bool Remove(string name) + { + if (!Contains(name)) + { + return false; + } + + _headerStore.RemoveInternal(name); + return true; + } + + private static bool IsCallerHeader(string name) + { + return name != null + && (string.Equals(name, HttpKnownHeaderNames.Accept, StringComparison.OrdinalIgnoreCase) + || string.Equals(name, HttpKnownHeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase) + || string.Equals(name, HttpKnownHeaderNames.Referer, StringComparison.OrdinalIgnoreCase) + || string.Equals(name, HttpKnownHeaderNames.Range, StringComparison.OrdinalIgnoreCase) + || string.Equals(name, HttpKnownHeaderNames.IfModifiedSince, StringComparison.OrdinalIgnoreCase)); + } + internal virtual void AddHeaders(HttpHeaders sourceHeaders) { foreach (var headerKey in sourceHeaders._headerStore.AllKeys) diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs index 78bc9e5..bca5afe 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HttpRequestHeaders.cs @@ -6,7 +6,7 @@ using System.Diagnostics; -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { /// /// Represents the collection of Request Headers as defined in RFC 2616. @@ -48,15 +48,9 @@ public bool ConnectionClose set { - var connectionHeader = _headerStore.GetValues(HttpKnownHeaderNames.Connection); - if (connectionHeader is not null) - { - connectionHeader[0] = value ? "close" : "keep-alive"; - } - else - { - _headerStore.AddInternal(HttpKnownHeaderNames.Connection, value ? "close" : "keep-alive"); - } + // Cosmos: replaces the header. nanoFramework sets the first element of the array GetValues returns, a + // copy, so a Connection header set once (HttpClient's constructor sets close) could never change. + _headerStore.ChangeInternal(HttpKnownHeaderNames.Connection, value ? "close" : "keep-alive"); } } diff --git a/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs b/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs index 746ab29..734323b 100644 --- a/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs +++ b/src/Cosmos.Network.Http/Http/Headers/HttpResponseHeaders.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { /// /// diff --git a/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs b/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs index 1cdc292..8594bc2 100644 --- a/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs +++ b/src/Cosmos.Network.Http/Http/Headers/KnownHeader.cs @@ -6,7 +6,7 @@ using System.Diagnostics; -namespace System.Net.Http.Headers +namespace Cosmos.Network.Http.Headers { internal sealed partial class KnownHeader { diff --git a/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs b/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs index 4cd14d0..c269ef0 100644 --- a/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs +++ b/src/Cosmos.Network.Http/Http/Headers/MediaTypeHeaderValue.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http.Http.Headers +namespace Cosmos.Network.Http.Headers { /// /// Represents a media type used in a Content-Type header as defined in the RFC 2616. @@ -90,26 +90,18 @@ public static MediaTypeHeaderValue Parse(string input) // assume it's MediaType value = new MediaTypeHeaderValue(tempValue); - if (values.Length > 1 && values.Length <= 2) + // Cosmos: the charset among any parameters, its name in any case, as .NET reads them; the others (boundary, + // name...) and empty ones (a trailing ';') skipped, where nanoFramework threw FormatException for anything + // but one lowercase charset: a server's "text/html; Charset=utf-8" could not be read. + for (int i = 1; i < values.Length; i++) { // remove leading and trailing spaces - tempValue = values[1].Trim(' '); + tempValue = values[i].Trim(' '); - // assume 2nd part, if present is charset - int indexOfCharSet = tempValue.IndexOf(_CharSetLabel); - - // check for misplaced start of charset - if (indexOfCharSet != 0) + if (tempValue.StartsWith(_CharSetLabel, StringComparison.OrdinalIgnoreCase)) { - throw new FormatException(); + value.CharSet = tempValue.Substring(_CharSetLabelLenght).Trim(' ', '"'); } - - value.CharSet = tempValue.Substring(indexOfCharSet + _CharSetLabelLenght); - } - - if (values.Length > 2) - { - throw new FormatException(); } return value; diff --git a/src/Cosmos.Network.Http/Http/HttpClient.cs b/src/Cosmos.Network.Http/Http/HttpClient.cs index 3f26189..d76758c 100644 --- a/src/Cosmos.Network.Http/Http/HttpClient.cs +++ b/src/Cosmos.Network.Http/Http/HttpClient.cs @@ -5,11 +5,11 @@ // using System.IO; -using System.Net.Http.Headers; +using Cosmos.Network.Http.Headers; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Initializes a new instance of the HttpClient class. @@ -17,8 +17,8 @@ namespace System.Net.Http /// /// The HttpClient class instance acts as a session to send HTTP requests. /// An HttpClient instance is a collection of settings applied to all requests executed by that instance. - /// In addition, every HttpClient instance uses its own connection pool, - /// isolating its requests from requests executed by other HttpClient instances. + /// Cosmos: the connections kept alive are pooled for every instance (HttpWebRequest's), each for a server and the + /// TLS settings it was authenticated with. /// /// HttpClient is intended to be instantiated once and reused throughout the life of an application. /// @@ -80,8 +80,9 @@ public Uri BaseAddress /// The timespan to wait before the request times out. /// /// - /// The default value is 100,000 milliseconds (100 seconds). - /// To set an infinite timeout, set the property value to . + /// Cosmos: the default value is infinite, as nanoFramework's constructor sets it, so each read may wait for the + /// 5 minutes of HttpWebRequest.ReadWriteTimeout. A value bounds each wait for the response's head and for its body. + /// To set an infinite timeout, set the property value to . /// /// A Domain Name System (DNS) query may take up to 15 seconds to return or time out. If your request contains a host name that requires resolution and you set to a value less than 15 seconds, it may take 15 seconds or more before a is thrown to indicate a timeout on your request. /// @@ -98,7 +99,7 @@ public TimeSpan Timeout set { - if (value != Threading.Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value.TotalMilliseconds > int.MaxValue)) + if (value != System.Threading.Timeout.InfiniteTimeSpan && (value <= TimeSpan.Zero || value.TotalMilliseconds > int.MaxValue)) { throw new ArgumentOutOfRangeException(); } @@ -146,7 +147,7 @@ public HttpClient() : base( new HttpClientHandler(), true) { - _timeout = Threading.Timeout.InfiniteTimeSpan; + _timeout = System.Threading.Timeout.InfiniteTimeSpan; // The default in REST API is to close the connection after each request. DefaultRequestHeaders.ConnectionClose = true; @@ -290,10 +291,7 @@ public HttpClient() : base( /// public byte[] GetByteArray(string requestUri) { - using var resp = Get(requestUri, HttpCompletionOption.ResponseContentRead); - resp.EnsureSuccessStatusCode(); - - return resp.Content.ReadAsByteArray(); + return ReadContent(Get(requestUri, HttpCompletionOption.ResponseContentRead)); } /// @@ -319,10 +317,9 @@ public byte[] GetByteArray(string requestUri) /// public Stream GetStream(string requestUri) { - var resp = Get(requestUri, HttpCompletionOption.ResponseHeadersRead); - resp.EnsureSuccessStatusCode(); - - return resp.Content.ReadAsStream(); + // Cosmos: the body read into memory, as ReadAsStream reads it, and the response disposed, which nanoFramework + // never did: its connection stayed open, and a Cosmos kernel runs no finalizer to close it. + return new MemoryStream(ReadContent(Get(requestUri, HttpCompletionOption.ResponseContentRead)), false); } /// @@ -346,10 +343,22 @@ public Stream GetStream(string requestUri) /// public string GetString(string requestUri) { - using HttpResponseMessage resp = Get(requestUri, HttpCompletionOption.ResponseContentRead); - resp.EnsureSuccessStatusCode(); + HttpResponseMessage resp = Get(requestUri, HttpCompletionOption.ResponseContentRead); + string content; + try + { + resp.EnsureSuccessStatusCode(); + content = resp.Content.ReadAsString(); + } + catch + { + // Cosmos: see ReadContent. + resp.Dispose(); + throw; + } - return resp.Content.ReadAsString(); + resp.Dispose(); + return content; } #region Advanced Send Overloads @@ -438,12 +447,51 @@ private HttpResponseMessage SendWorker(HttpRequestMessage request, HttpCompletio // Read the content when default HttpCompletionOption.ResponseContentRead is set if (response.Content != null && completionOption == HttpCompletionOption.ResponseContentRead) { - response.Content.LoadIntoBuffer(); + try + { + response.Content.LoadIntoBuffer(); + } + catch (Exception e) + { + // Cosmos: the response disposed, as nobody gets it to dispose, and the failure reported as Send's + // others are. + response.Dispose(); + + if (e is HttpRequestException) + { + throw; + } + + throw new HttpRequestException("An error occurred while reading the response", e); + } } return response; } + /// + /// Cosmos: the body of a successful response, which is then disposed. nanoFramework's using statement disposes it + /// in a finally block, which a Cosmos kernel skips while an exception unwinds: a failed status would leave the + /// connection open. + /// + private static byte[] ReadContent(HttpResponseMessage resp) + { + byte[] content; + try + { + resp.EnsureSuccessStatusCode(); + content = resp.Content.ReadAsByteArray(); + } + catch + { + resp.Dispose(); + throw; + } + + resp.Dispose(); + return content; + } + #endregion #region helper methods @@ -477,7 +525,7 @@ private void CheckDisposed() { if (_disposed) { - throw new ObjectDisposedException(); + throw new ObjectDisposedException(nameof(HttpClient)); } } diff --git a/src/Cosmos.Network.Http/Http/HttpClientHandler.cs b/src/Cosmos.Network.Http/Http/HttpClientHandler.cs index e4ca4ce..cd84562 100644 --- a/src/Cosmos.Network.Http/Http/HttpClientHandler.cs +++ b/src/Cosmos.Network.Http/Http/HttpClientHandler.cs @@ -4,11 +4,11 @@ // See LICENSE file in the project root for full license information. // -using System.Net.Http.Headers; +using Cosmos.Network.Http.Headers; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// The default message handler used by HttpClient in .NET nanoFramework. @@ -204,7 +204,7 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request) { if (_disposed) { - throw new ObjectDisposedException(); + throw new ObjectDisposedException(nameof(HttpClientHandler)); } var webRequest = CreateWebRequest(request); @@ -256,16 +256,26 @@ protected internal override HttpResponseMessage Send(HttpRequestMessage request) wresponse = (HttpWebResponse)webRequest.GetResponse(); } - catch (WebException we) + catch (Exception ex) { - if (we.Status != WebExceptionStatus.RequestCanceled) + // Cosmos: any failure, as Cosmos's sockets throw plain exceptions, with the request's connection closed + // here, as nothing else will. nanoFramework let other exceptions through, and returned a response + // without one for a canceled request (a null dereference, a kernel panic on Cosmos). + try { - throw new HttpRequestException("An error occurred while sending the request", we); + webRequest.Dispose(); } + catch + { + } + + throw new HttpRequestException("An error occurred while sending the request", ex); } - catch (IO.IOException ex) + + if (wresponse == null) { - throw new HttpRequestException("An error occurred while sending the request", ex); + webRequest.Dispose(); + throw new HttpRequestException("The request got no response."); } try @@ -321,6 +331,13 @@ private HttpWebRequest CreateWebRequest(HttpRequestMessage request) if (_timeout != TimeSpan.Zero) { wr.Timeout = (int)_timeout.TotalMilliseconds; + + // Cosmos: the body's reads too, which wait ReadWriteTimeout (5 minutes) otherwise, on a Cosmos kernel's + // UI thread as often as not. + if (wr.Timeout != System.Threading.Timeout.Infinite && wr.Timeout < wr.ReadWriteTimeout) + { + wr.ReadWriteTimeout = wr.Timeout; + } } wr.SslProtocols = _sslProtocols; diff --git a/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs b/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs index 2d1f68a..f6b343c 100644 --- a/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs +++ b/src/Cosmos.Network.Http/Http/HttpCompletionOption.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Indicates if operations should be considered completed either as soon as a response is available, or after reading the entire response message including the content. diff --git a/src/Cosmos.Network.Http/Http/HttpContent.cs b/src/Cosmos.Network.Http/Http/HttpContent.cs index 2a074bd..4c8568b 100644 --- a/src/Cosmos.Network.Http/Http/HttpContent.cs +++ b/src/Cosmos.Network.Http/Http/HttpContent.cs @@ -5,10 +5,10 @@ // using System.IO; -using System.Net.Http.Headers; +using Cosmos.Network.Http.Headers; using System.Text; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// A base class representing an HTTP entity body and content headers. @@ -93,7 +93,7 @@ public void LoadIntoBuffer() { if (_disposed) { - throw new ObjectDisposedException(); + throw new ObjectDisposedException(nameof(HttpContent)); } if (_buffer != null) diff --git a/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs b/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs index 55b8ea6..d852400 100644 --- a/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs +++ b/src/Cosmos.Network.Http/Http/HttpMessageHandler.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// A base type for HTTP message handlers. diff --git a/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs b/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs index 877139a..b1903ca 100644 --- a/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs +++ b/src/Cosmos.Network.Http/Http/HttpMessageInvoker.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// A specialty class that allows applications to call the (HttpRequestMessage) method on an HTTP handler chain. @@ -86,7 +86,7 @@ private void CheckDisposed() { if (_disposed) { - throw new ObjectDisposedException(); + throw new ObjectDisposedException(nameof(HttpMessageInvoker)); } } } diff --git a/src/Cosmos.Network.Http/Http/HttpMethod.cs b/src/Cosmos.Network.Http/Http/HttpMethod.cs index 1a6cc67..8e9c73a 100644 --- a/src/Cosmos.Network.Http/Http/HttpMethod.cs +++ b/src/Cosmos.Network.Http/Http/HttpMethod.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// A helper class for retrieving and comparing standard HTTP methods and for creating new HTTP methods. diff --git a/src/Cosmos.Network.Http/Http/HttpRequestException.cs b/src/Cosmos.Network.Http/Http/HttpRequestException.cs index 04a7a9d..b5cc994 100644 --- a/src/Cosmos.Network.Http/Http/HttpRequestException.cs +++ b/src/Cosmos.Network.Http/Http/HttpRequestException.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// A base class for exceptions thrown by the and HttpMessageHandler classes. diff --git a/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs b/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs index 12dd663..9c10f28 100644 --- a/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs +++ b/src/Cosmos.Network.Http/Http/HttpRequestMessage.cs @@ -4,9 +4,9 @@ // See LICENSE file in the project root for full license information. // -using System.Net.Http.Headers; +using Cosmos.Network.Http.Headers; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Represents a HTTP request message. @@ -153,7 +153,7 @@ private void CheckDisposed() { if (_disposed) { - throw new ObjectDisposedException(); + throw new ObjectDisposedException(nameof(HttpRequestMessage)); } } diff --git a/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs b/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs index 0e3fa40..494c683 100644 --- a/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs +++ b/src/Cosmos.Network.Http/Http/HttpResponseMessage.cs @@ -4,9 +4,9 @@ // See LICENSE file in the project root for full license information. // -using System.Net.Http.Headers; +using Cosmos.Network.Http.Headers; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Represents a HTTP response message including the status code and data. @@ -194,7 +194,7 @@ private void CheckDisposed() { if (_disposed) { - throw new ObjectDisposedException(); + throw new ObjectDisposedException(nameof(HttpResponseMessage)); } } } diff --git a/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs b/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs index 0fc9187..002eaba 100644 --- a/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs +++ b/src/Cosmos.Network.Http/Http/ReadOnlyStream.cs @@ -1,6 +1,6 @@ using System.IO; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// A wrapper stream that provides read-only access to an underlying stream. @@ -86,7 +86,7 @@ protected override void Dispose(bool disposing) public override void Flush() => throw new NotSupportedException(); /// - public override int Read(SpanByte buffer) + public override int Read(Span buffer) { return _innerStream.Read(buffer); } diff --git a/src/Cosmos.Network.Http/Http/StreamContent.cs b/src/Cosmos.Network.Http/Http/StreamContent.cs index 8b18c1b..dd7bb91 100644 --- a/src/Cosmos.Network.Http/Http/StreamContent.cs +++ b/src/Cosmos.Network.Http/Http/StreamContent.cs @@ -7,7 +7,7 @@ using System.IO; using System.Threading; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Provides HTTP content based on a stream. @@ -97,8 +97,11 @@ protected override void SerializeToStream(Stream stream) if (read == 0 && !isDone) { - // need to let the native layer get more data - Thread.Sleep(10); + // Cosmos: no more. The Cosmos NetworkStream's Read waits for data itself and returns 0 only once + // the connection is closed, which nanoFramework's sleep here would then wait on forever (on a + // Cosmos kernel's main thread, which must never sleep). A body cut short throws in Read + // (InputNetworkStreamWrapper.ThrowIfCutShort). + break; } else if (read > 0) { diff --git a/src/Cosmos.Network.Http/Http/StringContent.cs b/src/Cosmos.Network.Http/Http/StringContent.cs index dce12bb..1a885a6 100644 --- a/src/Cosmos.Network.Http/Http/StringContent.cs +++ b/src/Cosmos.Network.Http/Http/StringContent.cs @@ -4,10 +4,10 @@ // See LICENSE file in the project root for full license information. // -using System.Net.Http.Http.Headers; +using Cosmos.Network.Http.Headers; using System.Text; -namespace System.Net.Http +namespace Cosmos.Network.Http { /// /// Provides HTTP content based on a string. diff --git a/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs b/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs index 1bcebbd..3a3f74e 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.AuthenticationType.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Network authentication type. diff --git a/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs b/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs index c63926c..82475de 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.Http.Constants.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { internal class HttpConstants { diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs index ffca5d9..ad4d14e 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListener.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System.Collections; using System.Diagnostics; @@ -59,7 +59,29 @@ public class HttpListener /// /// Event that indicates arrival of new event from client. /// - private AutoResetEvent m_RequestArrived; + // Cosmos: the connections waiting for a request, which GetContext watches where nanoFramework starts a thread + // for each (WaitingConnection). + private ArrayList m_WaitingConnections; + + // Cosmos: the listener the current thread serves (calls GetContext for, and handles the requests of), and + // whether one does: the sockets are that thread's alone, as Cosmos's network stack takes no lock. Stop, Close + // and Abort on another thread only ask; the serving thread closes them on its way into or out of GetContext, + // when a response hands a connection back, or in its own Stop or Close. + [ThreadStatic] + private static HttpListener t_served; + private volatile bool m_HasServingThread; + + private bool OnServingThread => !m_HasServingThread || t_served == this; + + // Cosmos: the https connections whose TLS handshake is under way (Handshaking), which GetContext advances. + private ArrayList m_Handshaking; + + // Cosmos: whether Abort, called while a thread waited in GetContext, left that thread to close the connections. + internal volatile bool m_AbortPending; + + // Cosmos: whether the listening socket was closed (1), claimed with Interlocked so that one thread closes it: + // Stop's, or the one in GetContext. + private int m_ListenerClosed = 1; /// /// The queue of connected networks streams with pending client data. @@ -80,7 +102,7 @@ public class HttpListener /// Indicates whether the listener is started and is currently accepting /// connections. /// - private bool m_ServiceRunning; + private volatile bool m_ServiceRunning; /// /// Indicates whether the listener has been closed @@ -92,11 +114,6 @@ public class HttpListener /// private ArrayList m_ClientStreams; - /// - /// Http Thread for accepting new connections. - /// - private Thread m_thAccept; - /// /// SslProtocol which shall be used. /// @@ -162,23 +179,21 @@ public HttpListener(string prefix, int port, IPAddress localEndpointIP = null) /// private void InitListener(string prefix, int port, IPAddress localEndpointIp = null) { - switch (prefix.ToLower()) + // Cosmos: .NET's Uri schemes are not constants, and its Uri has no default port fields. + string scheme = prefix.ToLower(); + if (scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeWs) { - case Uri.UriSchemeHttp: - case Uri.UriSchemeWs: - { - m_IsHttpsConnection = false; - m_Port = Uri.HttpDefaultPort; - break; - } - case Uri.UriSchemeHttps: - case Uri.UriSchemeWss: - { - m_IsHttpsConnection = true; - m_Port = Uri.HttpsDefaultPort; - break; - } - default: throw new ArgumentException("Prefix should be http or https"); + m_IsHttpsConnection = false; + m_Port = 80; + } + else if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeWss) + { + m_IsHttpsConnection = true; + m_Port = 443; + } + else + { + throw new ArgumentException("Prefix should be http or https"); } if (port != -1) @@ -192,7 +207,8 @@ private void InitListener(string prefix, int port, IPAddress localEndpointIp = n } // Default members initialization m_maxResponseHeadersLen = 4; - m_RequestArrived = new AutoResetEvent(false); + m_WaitingConnections = new ArrayList(); + m_Handshaking = new ArrayList(); m_InputStreamsQueue = new Queue(); m_ClientStreams = new ArrayList(); } @@ -231,77 +247,181 @@ internal void RemoveClientStream(OutputNetworkStreamWrapper clientStream) } } + // Cosmos: nanoFramework accepts on a thread of its own, waits for a kept-alive connection's next request on a + // thread per connection blocked in Socket.Poll, and wakes GetContext with an AutoResetEvent. On a Cosmos kernel + // Socket.Poll doesn't wait, a thread's stack is never freed, the network stack has no locks, and two threads + // throwing at once halt the kernel (a failed TLS handshake throws). So the thread in GetContext does it all: it + // accepts, runs the TLS handshakes and watches the connections waiting for a request, and no other thread + // touches the sockets. + + // How long GetContext sleeps between two rounds with nothing to do: each Thread.Sleep writes a line to a Cosmos + // kernel's serial port (its LowLevelMonitor plug logs), which shorter sleeps would keep busy. + private const int IdleSleepMilliseconds = 50; + + // How long a client may take to send what a request still lacks: one thread serves every connection. + private const int ClientReadTimeout = 10000; + /// - /// Packages together an HttpListener and a socket. + /// A connection waiting for its next request, and since when. /// - /// This class is used to package together an HttpListener and a socket. - /// We need to start new thread and pass 2 parameters - instance of listener and socket. - /// For that purpose we create class that keeps references to both listerner and socket and - /// start thread using member function of this class as delegate. - /// Internal class not visible to user. - private class HttpListernerAndStream + private class WaitingConnection { - internal HttpListernerAndStream(HttpListener listener, OutputNetworkStreamWrapper outputStream) + internal WaitingConnection(OutputNetworkStreamWrapper stream) { - m_listener = listener; - m_outStream = outputStream; + m_stream = stream; + m_since = Stopwatch.GetTimestamp(); } - internal HttpListener m_listener; - internal OutputNetworkStreamWrapper m_outStream; + internal readonly OutputNetworkStreamWrapper m_stream; + internal readonly long m_since; + } + + // How long GetContext sleeps between two rounds while TLS handshakes are under way, which take a few round trips. + private const int HandshakeSleepMilliseconds = 10; - // Forwards to waiting function of the listener. - internal void AddToWaitingConnections() + /// + /// An https connection whose TLS handshake is under way, and since when. + /// + private class Handshaking + { + internal Handshaking(Socket socket, SslStream stream) { - m_listener.WaitingConnectionThreadFunc(m_outStream); + m_socket = socket; + m_stream = stream; + m_since = Stopwatch.GetTimestamp(); } + + internal readonly Socket m_socket; + internal readonly SslStream m_stream; + internal readonly long m_since; } - internal void AddToWaitingConnections(OutputNetworkStreamWrapper outputStream) + /// + /// Advances the TLS handshakes under way with what their clients sent, without waiting: those complete wait for + /// a request, those failed or too slow are closed. + /// + /// Whether a handshake completed or was closed. + /// Cosmos: nanoFramework's accept thread runs each handshake to its end, which GetContext's thread, the + /// one serving every connection, can't wait for. + private bool CheckHandshakes() { - // Create a thread that blocks onsocket.Poll - basically waits for new data from client. - HttpListernerAndStream listAndSock = new HttpListernerAndStream(this, outputStream); + bool changed = false; - // Creates new thread to wait on data - Thread thWaitData = new Thread(listAndSock.AddToWaitingConnections); - thWaitData.Start(); + for (int i = m_Handshaking.Count - 1; i >= 0; i--) + { + Handshaking handshaking = (Handshaking)m_Handshaking[i]; + + bool complete = false; + bool failed; + try + { + complete = handshaking.m_stream.AdvanceAuthentication(); + failed = !complete && Stopwatch.GetElapsedTime(handshaking.m_since).TotalMilliseconds > ClientReadTimeout; + } + catch + { + // An alert, a certificate, or a client gone. + failed = true; + } + + if (complete) + { + m_Handshaking.RemoveAt(i); + AddToWaitingConnections(new OutputNetworkStreamWrapper(handshaking.m_socket, handshaking.m_stream)); + changed = true; + } + else if (failed) + { + m_Handshaking.RemoveAt(i); + DisposeQuietly(handshaking.m_stream); + changed = true; + } + } + + return changed; } /// - /// Waits for new data from the client. + /// Adds a connection to those waiting for a request: a new one, or one kept alive after a response. /// - private void WaitingConnectionThreadFunc(OutputNetworkStreamWrapper outputStream) + internal void AddToWaitingConnections(OutputNetworkStreamWrapper outputStream) { - try + // Cosmos: GetContext watches it, where nanoFramework starts a thread to wait for its data. Not once stopped: + // nothing would. + if (!m_ServiceRunning) { - // This is a blocking call waiting for more data. - outputStream.m_Socket.Poll(HttpConstants.DefaultKeepAliveMilliseconds * 1000, SelectMode.SelectRead); - } - catch (Exception ex) - { - // Poll failed (e.g. connection reset) - outputStream isn't queued anywhere else, so it - // must be disposed here or its socket leaks. - Debug.WriteLine(ex.Message); - outputStream.Dispose(); + DisposeQuietly(outputStream); + + // Cosmos: on the serving thread, after a Stop from another: the listening socket and the other waiting + // connections close with this one. + if (OnServingThread) + { + CloseListenerSocket(); + } + return; } - if (outputStream.m_Socket.Available > 0) + m_WaitingConnections.Add(new WaitingConnection(outputStream)); + } + + /// + /// Queues the waiting connections whose request has arrived, and closes those the client closed or left idle. + /// + /// Whether a connection was queued or closed. + private bool CheckWaitingConnections() + { + bool changed = false; + + for (int i = m_WaitingConnections.Count - 1; i >= 0; i--) { + WaitingConnection waiting = (WaitingConnection)m_WaitingConnections[i]; + OutputNetworkStreamWrapper outputStream = waiting.m_stream; - // Add this connected stream to the list. - lock (m_InputStreamsQueue) + bool arrived = false; + bool closed; + try { - m_InputStreamsQueue.Enqueue(outputStream); + // Cosmos: closed by HttpListenerContext.Close or its OutputStream's Close since its response, which + // leaves no stream: nanoFramework's thread then fails on it, a null dereference here (a kernel + // panic on Cosmos, not an exception). + NetworkStream stream = outputStream.m_Stream; + Socket socket = outputStream.m_Socket; + if (stream == null || socket == null) + { + closed = true; + } + else + { + // The stream's DataAvailable: decrypted data, for https. Cosmos: or what the connection's + // reader already took from it, after the previous request. + arrived = outputStream.HasBufferedInput || stream.DataAvailable; + closed = !arrived + && ((socket.Poll(0, SelectMode.SelectRead) && socket.Available == 0) + || Stopwatch.GetElapsedTime(waiting.m_since).TotalMilliseconds > HttpConstants.DefaultKeepAliveMilliseconds); + } + } + catch + { + // A reset, or a TLS alert. + closed = true; } - // Set event that client stream or exception is added to the queue. - m_RequestArrived.Set(); - } - else // If no data available - means connection was close on other side or timed out. - { - outputStream.Dispose(); + if (arrived) + { + m_WaitingConnections.RemoveAt(i); + m_InputStreamsQueue.Enqueue(outputStream); + changed = true; + } + else if (closed) + { + m_WaitingConnections.RemoveAt(i); + DisposeQuietly(outputStream); + changed = true; + } } + + return changed; } /// @@ -321,148 +441,177 @@ private void WaitingConnectionThreadFunc(OutputNetworkStreamWrapper outputStream /// public void Abort() { - lock (lockObj) + // First we shut down the service. + Close(); + + // Cosmos: closed by the serving thread, when another calls this (see t_served). + m_AbortPending = true; + if (OnServingThread) { - // First we shut down the service. - Close(); + CloseClientStreams(); + } + } - // Now we need to go through list of all client sockets and close all of them. - // This will cause exceptions on read/write operations on these sockets. - foreach (OutputNetworkStreamWrapper netStream in m_ClientStreams) - { - netStream.Close(); - } + /// + /// Cosmos: what a Stop or an Abort from another thread left to the serving thread. + /// + private void CloseOnServingThread() + { + CloseListenerSocket(); + + if (m_AbortPending) + { + CloseClientStreams(); + } + } + + /// + /// Closes the connections whose response is under way. + /// + private void CloseClientStreams() + { + m_AbortPending = false; + + // Now we need to go through list of all client sockets and close all of them. + // This will cause exceptions on read/write operations on these sockets. + // Cosmos: no accept thread to join, and no lock, which an exception would leave held on a Cosmos kernel. + // A stream closed so throws ObjectDisposedException on its next use, where nanoFramework's dereferences null. + object[] streams; + lock (m_ClientStreams) + { + streams = m_ClientStreams.ToArray(); m_ClientStreams.Clear(); } - if (m_thAccept != null) + foreach (OutputNetworkStreamWrapper netStream in streams) { - m_thAccept.Join(); + DisposeQuietly(netStream); } } /// - /// Waits for new connections from the client. + /// Accepts the connection waiting on the listening socket, if any. /// - /// On new connections, this method enques the new input - /// network stream and sets an event that a new connection is available. - /// - private void AcceptThreadFunc() + /// Whether a connection was accepted. + /// Cosmos: what nanoFramework's accept thread does for each connection, called by GetContext. The + /// connection then waits for its request with the kept-alive ones, so a client that connects and sends + /// nothing, as a browser does ahead of time, doesn't hold the thread. + private bool AcceptPendingConnection() { - Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; - // If there was no exception up to this point, means we succeded to start listening. - m_ServiceRunning = true; - int retry = 0; - - // The Start function is waiting on this event. We set it to indicate that - // thread that waits for connections is already started. - m_RequestArrived.Set(); + Socket listener = m_listener; + if (listener == null || !listener.Poll(0, SelectMode.SelectRead)) + { + return false; + } - // The value of m_serviceStarted normally is changed from other thread by calling Stop. - while (m_ServiceRunning) + Socket clientSock; + try { - Socket clientSock; - // Need to create NetworkStream or SSL stream depending on protocol used. - NetworkStream netStream = null; + clientSock = listener.Accept(); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + return false; + } - try - { - // It is important that multithread access to m_listener.Accept(); is not locked. - // If it was locked - then Close or Stop would be blocked potnetially for ever while waiting for connection. - // This is a blocking call waiting for connection. - clientSock = m_listener.Accept(); + // Cosmos: no NoDelay option, which faults on Cosmos's sockets (see Start). - retry = 0; - try - { - // set NoDelay to increase HTTP(s) response times - clientSock.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - } - catch - { - // empty on purpose - } + // Need to create NetworkStream or SSL stream depending on protocol used. + NetworkStream netStream = null; + + try + { + if (!m_IsHttpsConnection) + { + // This is case of normal HTTP. Create network stream. + netStream = new NetworkStream(clientSock, true); } - catch (SocketException) + else { - if (++retry > 5) - { - // If request to stop listener flag is set or locking call is interupted return - // On exception we stop the service and record the exception. - if (m_ServiceRunning && !m_Closed) - { - Stop(); - } - - // Set event to unblock thread waiting for accept. - m_RequestArrived.Set(); + // This is the case of https. + // Once connection established need to create secure stream and authenticate server. + SslStream sslStream = new SslStream(clientSock); + netStream = sslStream; + netStream.ReadTimeout = ClientReadTimeout; + + // Throws exception if this fails + // pass the server certificate + // do not require client certificate + // Cosmos: started only; CheckHandshakes goes on with it as the client answers. + sslStream.BeginAuthenticateAsServer(m_httpsCert, m_sslProtocols); + m_Handshaking.Add(new Handshaking(clientSock, sslStream)); + return true; + } - break; - } + // Cosmos: nanoFramework sets it for https only. + netStream.ReadTimeout = ClientReadTimeout; + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); - continue; + if (netStream != null) + { + DisposeQuietly(netStream); } - catch + else { - // If request to stop listener flag is set or locking call is interupted return - // On exception we stop the service and record the exception. - if (m_ServiceRunning && !m_Closed) - { - Stop(); - } + SslNative.CloseSocket(clientSock); + } - // Set event to unblock thread waiting for accept. - m_RequestArrived.Set(); + return true; + } - break; - } + AddToWaitingConnections(new OutputNetworkStreamWrapper(clientSock, netStream)); + return true; + } - try - { - if (!m_IsHttpsConnection) - { - // This is case of normal HTTP. Create network stream. - netStream = new NetworkStream(clientSock, true); - } - else - { - // This is the case of https. - // Once connection established need to create secure stream and authenticate server. - netStream = new SslStream(clientSock); + private static void DisposeQuietly(IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch + { + // Closed already, or reset by the client. + } + } - // Throws exception if this fails - // pass the server certificate - // do not require client certificate - ((SslStream)netStream).AuthenticateAsServer(m_httpsCert, false, m_sslProtocols); + /// + /// Closes the listening socket and the connections waiting for a request. + /// + private void CloseListenerSocket() + { + if (Interlocked.Exchange(ref m_ListenerClosed, 1) != 0) + { + return; + } - netStream.ReadTimeout = 10000; - } - } - catch - { - if (netStream != null) - { - netStream.Dispose(); - } - else - { - clientSock.Close(); - } + Socket listener = m_listener; + m_listener = null; - m_RequestArrived.Set(); + if (listener != null) + { + SslNative.CloseSocket(listener); + } - // try again - continue; - } + for (int i = 0; i < m_WaitingConnections.Count; i++) + { + DisposeQuietly(((WaitingConnection)m_WaitingConnections[i]).m_stream); + } + m_WaitingConnections.Clear(); - // Add this connected stream to the list. - lock (m_InputStreamsQueue) - { - m_InputStreamsQueue.Enqueue(new OutputNetworkStreamWrapper(clientSock, netStream)); - } + for (int i = 0; i < m_Handshaking.Count; i++) + { + DisposeQuietly(((Handshaking)m_Handshaking[i]).m_stream); + } + m_Handshaking.Clear(); - // Set event that client stream or exception is added to the queue. - m_RequestArrived.Set(); + while (m_InputStreamsQueue.Count > 0) + { + DisposeQuietly((OutputNetworkStreamWrapper)m_InputStreamsQueue.Dequeue()); } } @@ -477,79 +626,73 @@ private void AcceptThreadFunc() /// public void Start() { - lock (lockObj) + // Cosmos: no lock (an exception would leave it held on a Cosmos kernel), and no accept thread. + if (m_Closed) throw new ObjectDisposedException(nameof(HttpListener)); + + // If service was already started, the call has no effect. + if (m_ServiceRunning) { - if (m_Closed) throw new ObjectDisposedException(); + return; + } - // If service was already started, the call has no effect. - if (m_ServiceRunning) - { - return; - } + // Cosmos: a stop the serving thread hasn't carried out yet is undone, its listening socket kept: another + // bound to the port would leave that one open for good. + if (Volatile.Read(ref m_ListenerClosed) == 0) + { + m_ServiceRunning = true; + return; + } - m_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - try - { - // set NoDelay to increase HTTP(s) response times - m_listener.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - } - catch - { - // empty on purpose - } + // Cosmos: no NoDelay nor ReuseAddress option. Cosmos's sockets have no native handle for SetSocketOption + // to reach, and it faults on the null one (a kernel panic, which no catch stops). Cosmos's TCP sends every + // write at once and keeps no TIME_WAIT, which leaves those options nothing to do. - try - { - // Start server socket to accept incoming connections. - m_listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch - { - // empty on purpose - } + // Cosmos: nanoFramework's GetDefaultLocalAddress (the device's address) is .NET's Any. + IPAddress addr = m_localEndpointIP ?? IPAddress.Any; - IPAddress addr = m_localEndpointIP ?? IPAddress.GetDefaultLocalAddress(); + IPEndPoint endPoint = new IPEndPoint(addr, m_Port); - IPEndPoint endPoint = new IPEndPoint(addr, m_Port); - m_listener.Bind(endPoint); + try + { + listener.Bind(endPoint); // Starts to listen to maximum of 10 connections. - m_listener.Listen(MaxCountOfPendingConnections); - - // Create a thread that blocks on m_listener.Accept() - basically waits for connection from client. - m_thAccept = new Thread(AcceptThreadFunc); - m_thAccept.Start(); - - // Waits for thread that calls Accept to start. - m_RequestArrived.WaitOne(); + listener.Listen(MaxCountOfPendingConnections); + } + catch + { + SslNative.CloseSocket(listener); + throw; } + + m_listener = listener; + Interlocked.Exchange(ref m_ListenerClosed, 0); + m_ServiceRunning = true; } /// - /// Shuts down the HttpListener after processing all - /// currently queued requests. + /// Shuts down the HttpListener. /// /// After calling this method, you can no longer use the /// HttpListener object. To temporarily pause an /// HttpListener object, use the - /// method. + /// method. + /// + /// Cosmos: as , the requests that arrived but weren't returned by GetContext yet are closed + /// unanswered; the one being handled is answered. + /// + /// public void Close() { - lock (lockObj) + // close does not throw + if (!m_Closed) { - // close does not throw - try - { - Stop(); - } - catch - { - // empty on purpose to catch any exceptions thrown when calling the Stop above - } - - m_Closed = true; + Stop(); } + + m_Closed = true; } /// @@ -562,27 +705,25 @@ public void Close() /// you can use the method /// to restart it. /// + /// + /// Cosmos: called from another thread than the one serving (calling ), it leaves the + /// sockets to that one: waiting in GetContext, it closes them and returns null within a round (50 ms); handling a + /// request, it closes them once the response is closed, or in its next GetContext call, which throws + /// InvalidOperationException, or in its own Close. + /// /// public void Stop() { - // Need to lock access to object, because Stop can be called from a - // different thread. - lock (lockObj) - { - if (m_Closed) throw new ObjectDisposedException(); + if (m_Closed) throw new ObjectDisposedException(nameof(HttpListener)); - m_ServiceRunning = false; + m_ServiceRunning = false; - // We close the server socket that listen for incoming connection. - // Connections that already accepted are processed. - // Connections that has been in queue for server socket, but not accepted, are lost. - if (m_listener != null) - { - m_listener.Close(); - m_listener = null; - - m_RequestArrived.Set(); - } + // We close the server socket that listen for incoming connection. + // Cosmos: and the connections waiting for a request, on the serving thread (see t_served); the request + // being handled is answered. + if (OnServingThread) + { + CloseListenerSocket(); } } @@ -591,7 +732,7 @@ public void Stop() /// /// /// An object that - /// represents a client request. + /// represents a client request, or once the listener is stopped. /// /// A socket call failed. Check the /// exception's ErrorCode property to determine the cause of the exception. @@ -599,6 +740,11 @@ public void Stop() /// currently stopped or The HttpListener does not have any Uniform Resource Identifier /// (URI) prefixes to respond to. /// This object is closed. + /// + /// Cosmos: the connections are accepted, and the TLS handshakes run, on the thread that calls this method, + /// which must be the one using the contexts it returns. It sleeps when nothing is pending, so it must not be + /// a Cosmos kernel's main loop. + /// /// This example shows how to call the /// GetContext method. /// @@ -615,35 +761,53 @@ public void Stop() /// public HttpListenerContext GetContext() { - // Protects access for simultaneous call for GetContext and Close or Stop. - lock (lockObj) - { - if (m_Closed) throw new ObjectDisposedException(); + if (m_Closed) throw new ObjectDisposedException(nameof(HttpListener)); - if (!m_ServiceRunning) throw new InvalidOperationException(); + // Cosmos: this thread serves the listener (see t_served). + t_served = this; + m_HasServingThread = true; + + if (!m_ServiceRunning) + { + // Cosmos: stopped from another thread while this one served a request: the sockets are its to close. + CloseOnServingThread(); + throw new InvalidOperationException("The listener is not started."); } // Try to get context until service is running. - while (m_ServiceRunning) + HttpListenerContext context = null; + while (m_ServiceRunning && context == null) { // Before waiting for event we need to look for pending connections. - lock (m_InputStreamsQueue) + if (m_InputStreamsQueue.Count > 0) { - if (m_InputStreamsQueue.Count > 0) + OutputNetworkStreamWrapper outputStreamWrap = m_InputStreamsQueue.Dequeue() as OutputNetworkStreamWrapper; + if (outputStreamWrap != null) { - OutputNetworkStreamWrapper outputStreamWrap = m_InputStreamsQueue.Dequeue() as OutputNetworkStreamWrapper; - if (outputStreamWrap != null) - { - return new HttpListenerContext(outputStreamWrap, this); - } + context = new HttpListenerContext(outputStreamWrap, this); } + + continue; } - // Waits for new connection to arrive on new or existing socket. - m_RequestArrived.WaitOne(); + // Cosmos: what the accept thread and the waiting threads do on nanoFramework. + bool busy = AcceptPendingConnection(); + busy |= CheckHandshakes(); + busy |= CheckWaitingConnections(); + + if (!busy && m_InputStreamsQueue.Count == 0) + { + Thread.Sleep(m_Handshaking.Count > 0 ? HandshakeSleepMilliseconds : IdleSleepMilliseconds); + } + } + + // Stopped from another thread: the sockets are this thread's to close. + if (!m_ServiceRunning) + { + CloseOnServingThread(); } - return null; + return context; } /// diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs index 925a26e..636d84e 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerContext.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System.Net.Sockets; @@ -66,7 +66,17 @@ internal HttpListenerContext(OutputNetworkStreamWrapper clientStream, HttpListen m_clientOutputStream = clientStream; // Input stream does not own socket. - m_clientInputStream = new InputNetworkStreamWrapper(clientStream.m_Stream, clientStream.m_Socket, false, null); + // Cosmos: the connection's, kept across its requests (OutputNetworkStreamWrapper.m_Input). + if (clientStream.m_Input == null) + { + clientStream.m_Input = new InputNetworkStreamWrapper(clientStream.m_Stream, clientStream.m_Socket, false, null); + } + else + { + clientStream.m_Input.StartRequest(); + } + + m_clientInputStream = clientStream.m_Input; // Constructs request and response classes. m_ClientRequest = new HttpListenerRequest(m_clientInputStream, httpListener.m_maxResponseHeadersLen); @@ -102,16 +112,35 @@ public HttpListenerRequest Request { if (!m_IsHTTPRequestParsed) { + // Cosmos: parsed once, even when that throws: nanoFramework parsed again on every access after a + // failure, from the middle of the request, and the response could never be sent. + m_IsHTTPRequestParsed = true; + m_ClientRequest.ParseHTTPRequest(); // After request parsed check for "transfer-ecoding" header. If it is chunked, change stream property. // If m_EnableChunkedDecoding is set to true, then readig from stream automatically processing chunks. + // Cosmos: not after Close, which drops the streams: a null dereference is a kernel panic on Cosmos. string chunkedVal = m_ClientRequest.Headers[HttpKnownHeaderNames.TransferEncoding]; - if (chunkedVal != null && chunkedVal.ToLower() == "chunked") + if (m_clientInputStream == null) + { + } + else if (chunkedVal != null && chunkedVal.ToLower() == "chunked") { m_clientInputStream.m_EnableChunkedDecoding = true; } + else + { + // Cosmos: the body's length, which bounds reading it, so the next request isn't read as its + // end and its Read returns 0 at its end rather than waiting; none without Content-Length, as a + // request has no body to end with the connection (RFC 9112, 6.3). + m_clientInputStream.m_BytesLeftInResponse = m_ClientRequest.ContentLength64 > 0 ? m_ClientRequest.ContentLength64 : 0; + } - m_IsHTTPRequestParsed = true; + // Cosmos: for the response, which has no body for a HEAD request. + if (m_ResponseToClient != null) + { + m_ResponseToClient.m_RequestMethod = m_ClientRequest.HttpMethod; + } } return m_ClientRequest; @@ -130,8 +159,8 @@ public HttpListenerResponse Response { if (!m_IsHTTPRequestParsed) { - m_ClientRequest.ParseHTTPRequest(); - m_IsHTTPRequestParsed = true; + // Cosmos: see Request. Its chunked body decoded, which nanoFramework skipped when Response came first. + _ = Request; } return m_ResponseToClient; @@ -162,41 +191,60 @@ internal WebSocketContext GetWebsocketContext() /// public void Close() { - try + // Cosmos: no DontLinger option, which faults on Cosmos's sockets (see HttpListener.Start); Cosmos's + // Close doesn't linger past the peer's acknowledgment anyway. + // Cosmos: each step guarded on its own, where nanoFramework's single try skipped the closing of the + // connection when the response's Close threw (a client gone). + + bool keptAlive = false; + if (m_ResponseToClient != null) { - if (m_clientOutputStream != null) + try { - try - { - if (m_clientOutputStream.m_Socket != null) - { - m_clientOutputStream.m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); - } - } - catch { } + m_ResponseToClient.Close(); } - - if (m_ResponseToClient != null) + catch { - m_ResponseToClient.Close(); - m_ResponseToClient = null; } - // Close the underlying stream - if (m_clientOutputStream != null) + // Cosmos: a connection kept alive is the listener's now, waiting for its next request: nanoFramework + // closed it here, after the response had put it on the waiting list. + keptAlive = m_ResponseToClient.m_KeptAlive; + m_ResponseToClient = null; + } + + if (keptAlive) + { + m_clientOutputStream = null; + m_clientInputStream = null; + return; + } + + // Close the underlying stream + if (m_clientOutputStream != null) + { + try { m_clientOutputStream.Dispose(); - m_clientOutputStream = null; } - - if (m_clientInputStream != null) + catch { - m_clientInputStream.Dispose(); - m_clientInputStream = null; } + + m_clientOutputStream = null; } - catch + + if (m_clientInputStream != null) { + try + { + m_clientInputStream.Dispose(); + } + catch + { + } + + m_clientInputStream = null; } } } diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs index 54d9c6d..a6e1d4d 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerRequest.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; using System.IO; @@ -100,6 +100,18 @@ internal HttpListenerRequest(InputNetworkStreamWrapper clientStream, int maxHead { m_clientStream = clientStream; + // Cosmos: the endpoints while the connection is open: Cosmos's socket forgets them once closed (when the + // response is), where nanoFramework's throws ObjectDisposedException. + try + { + m_localEndPoint = clientStream.m_Socket.LocalEndPoint as IPEndPoint; + m_remoteEndPoint = clientStream.m_Socket.RemoteEndPoint as IPEndPoint; + } + catch + { + // Closed already. + } + // maxHeaderLen is in kilobytes (Desktop designer decided so). If -1 just maximum integer value m_maxResponseHeadersLen = maxHeaderLen == -1 ? 0x7FFFFFFF : maxHeaderLen * 1024; // If not set, default for content length is -1 @@ -166,6 +178,10 @@ internal void ParseHTTPRequest() throw new ProtocolViolationException("Unsupported HTTP version: " + requestStr[2]); } + // Cosmos: an HTTP/1.1 connection persists unless the client says close (RFC 9112, 9.3), where nanoFramework + // kept only one whose client says keep-alive, which HTTP/1.1 clients don't. + m_KeepAlive = m_requestHttpVer == HttpVersion.Version11; + // Now it is list of HTTP headers: string line; int headersLen = m_maxResponseHeadersLen; @@ -199,7 +215,15 @@ internal void ParseHTTPRequest() { // If value is "Keep-Alive" ( lower case now ), set m_KeepAlive to true; headerValue = headerValue.ToLower(); - m_KeepAlive = headerValue == "keep-alive"; + // Cosmos: close or keep-alive among the header's options, else the version's default. + if (headerValue.IndexOf("close") >= 0) + { + m_KeepAlive = false; + } + else if (headerValue.IndexOf("keep-alive") >= 0) + { + m_KeepAlive = true; + } } // If user supplied user name and password - parse it and store in m_NetworkCredentials @@ -364,9 +388,13 @@ public bool KeepAlive /// public IPEndPoint LocalEndPoint { - get { return (IPEndPoint)m_clientStream.m_Socket.LocalEndPoint; } + get { return m_localEndPoint; } } + // Cosmos: see the constructor. + private readonly IPEndPoint m_localEndPoint; + private readonly IPEndPoint m_remoteEndPoint; + /// /// Gets the HTTP version used by the requesting client. /// @@ -389,7 +417,7 @@ public Version ProtocolVersion /// address and port number from which the request originated. public IPEndPoint RemoteEndPoint { - get { return (IPEndPoint)m_clientStream.m_Socket.RemoteEndPoint; } + get { return m_remoteEndPoint; } } /// @@ -418,7 +446,8 @@ public string UserAgent /// information. public string UserHostAddress { - get { return ((IPEndPoint)m_clientStream.m_Socket.LocalEndPoint).Address.ToString(); } + // Cosmos: null when unknown, where nanoFramework dereferences it (a kernel panic on Cosmos). + get { return m_localEndPoint?.Address?.ToString(); } } /// @@ -428,7 +457,8 @@ public string UserHostAddress /// A String value that contains the text of the request's Host header. public string UserHostName { - get { return m_httpRequestHeaders[HttpKnownHeaderNames.UserAgent]; } + // Cosmos: the Host header, as documented, where nanoFramework returns the User-Agent one. + get { return m_httpRequestHeaders[HttpKnownHeaderNames.Host]; } } /// diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs index 3856ee2..f0899dd 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpListenerResponse.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; using System.IO; @@ -49,6 +49,26 @@ public sealed class HttpListenerResponse : IDisposable /// private bool m_KeepAlive = false; + /// + /// Cosmos: whether Close handed the connection to the listener to wait for its next request, which + /// HttpListenerContext.Close then leaves open. + /// + internal bool m_KeptAlive; + + /// + /// Cosmos: the request's method, set once it is parsed: the response to a HEAD request has no body. + /// + internal string m_RequestMethod; + + /// + /// Cosmos: whether the response has no body whatever its headers say (RFC 9112, 6.3): one to a HEAD request, + /// or a 1xx, 204 or 304 one. + /// + private bool IsBodiless => m_RequestMethod == "HEAD" + || (m_ResponseStatusCode >= 100 && m_ResponseStatusCode < 200) + || m_ResponseStatusCode == (int)HttpStatusCode.NoContent + || m_ResponseStatusCode == (int)HttpStatusCode.NotModified; + /// /// Encoding for this response's OutputStream. /// @@ -110,7 +130,8 @@ private void ThrowIfResponseSent() internal HttpListenerResponse(OutputNetworkStreamWrapper clientStream, HttpListener httpListener) { // Sets the delegate, so SendHeaders will be called on first write. - clientStream.HeadersDelegate = new OutputNetworkStreamWrapper.SendHeadersDelegate(SendHeaders); + // Cosmos: and forgets the chunked encoding of a previous response on a kept-alive connection. + clientStream.StartResponse(new OutputNetworkStreamWrapper.SendHeadersDelegate(SendHeaders)); // Saves network stream as member. m_clientStream = clientStream; // Saves list of client streams. m_clientStream is removed from clientStreamsList during Close(). @@ -124,6 +145,26 @@ internal HttpListenerResponse(OutputNetworkStreamWrapper clientStream, HttpListe /// private void PrepareHeaders() { + if (IsBodiless) + { + // Cosmos: no chunk, not even the last one, which the next response on the connection would start with. + m_sendChunked = false; + m_clientStream.m_enableChunkedEncoding = false; + } + else if (!m_sendChunked && m_ContentLength == -1) + { + // Cosmos: a body neither sized nor chunked ends with the connection: closed after it, where + // nanoFramework said Keep-Alive and the client waited for an end that never came. + m_KeepAlive = false; + } + + // Cosmos: a request whose body the handler left unread closes its connection (see Dispose), which the + // client is told now rather than finding it closed. + if (m_clientStream.m_Input != null && !m_clientStream.m_Input.IsBodyRead) + { + m_KeepAlive = false; + } + // Adds content length if it was present. if (m_ContentLength != -1) { @@ -188,11 +229,19 @@ private void SendHeaders() // SendHeaders() again. m_clientStream.HeadersDelegate = null; + // Cosmos: closed by its OutputStream's Close, or by an Abort: an exception, where nanoFramework dereferences + // null, a kernel panic on Cosmos. + NetworkStream stream = m_clientStream.m_Stream; + if (stream == null) + { + throw new ObjectDisposedException(nameof(HttpListenerResponse)); + } + // Creates encoder, generates headers and sends the data. Encoding encoder = Encoding.UTF8; byte[] statusLine = encoder.GetBytes(ComposeHTTPResponse()); - m_clientStream.m_Stream.Write(statusLine, 0, statusLine.Length); + stream.Write(statusLine, 0, statusLine.Length); // Prepares/Updates WEB header collection. PrepareHeaders(); @@ -201,7 +250,7 @@ private void SendHeaders() byte[] pHeaders = m_httpResponseHeaders.ToByteArray(); // Sends the headers - m_clientStream.m_Stream.Write(pHeaders, 0, pHeaders.Length); + stream.Write(pHeaders, 0, pHeaders.Length); m_WasResponseSent = true; } @@ -268,7 +317,11 @@ public WebHeaderCollection Headers /// /// true if the server requests a persistent /// connection; otherwise, false. The default is - /// true. + /// false (nanoFramework's). + /// + /// Cosmos: kept only if the response turns out whole (its Content-Length written, its last chunk sent, or no + /// body) and the request's body was read; a body neither sized nor chunked ends with the connection. + /// public bool KeepAlive { get { return m_KeepAlive; } @@ -433,11 +486,17 @@ public void Close() SendHeaders(); } } - finally + catch { - // Removes from the list of streams and closes the socket. + // Cosmos: in a catch, as a finally block doesn't run while an exception unwinds through it on a + // Cosmos kernel: a client gone before its response would leave its socket open. Not kept alive. + m_KeepAlive = false; ((IDisposable)this).Dispose(); + throw; } + + // Removes from the list of streams and closes the socket. + ((IDisposable)this).Dispose(); } } @@ -470,24 +529,49 @@ private void Dispose(bool disposing) if (disposing) { - try - { - // Iterates over list of client connections and remove its stream from it. - m_Listener.RemoveClientStream(m_clientStream); + // Iterates over list of client connections and remove its stream from it. + m_Listener.RemoveClientStream(m_clientStream); - m_clientStream.m_Stream.Flush(); - - // If KeepAlive is true, - if (m_KeepAlive) - { // Then socket is tramsferred to the list of waiting for new data. - m_Listener.AddToWaitingConnections(m_clientStream); + // Cosmos: the stream is gone once the OutputStream was closed, which closes the connection; nanoFramework + // flushed it anyway, a null dereference (a kernel panic on Cosmos, not an exception). + bool open = m_clientStream.m_Stream != null; + if (open) + { + try + { + // Cosmos: ends a chunked response. + m_clientStream.FinishChunks(); + m_clientStream.m_Stream?.Flush(); + } + catch + { + // The client is gone. + open = false; } - else // If not KeepAlive then close + } + + // Cosmos: kept alive only once both messages are whole: the response sent as its headers said (its + // Content-Length written, its last chunk sent, or no body), and the request's body read, so the next + // request starts where its bytes do. Otherwise the client would wait for the rest, or the server read + // the body as a request; and not once Abort was called. + bool responseWhole = IsBodiless + || (m_sendChunked ? m_clientStream.ChunksFinished : m_ContentLength >= 0 && m_clientStream.m_BodyWritten == m_ContentLength); + bool requestRead = m_clientStream.m_Input == null || m_clientStream.m_Input.IsBodyRead; + + // If KeepAlive is true, + if (open && m_KeepAlive && responseWhole && requestRead && !m_Listener.m_AbortPending) + { // Then socket is tramsferred to the list of waiting for new data. + m_Listener.AddToWaitingConnections(m_clientStream); + m_KeptAlive = true; + } + else // If not KeepAlive then close + { + try { m_clientStream.Dispose(); } + catch { } } - catch { } } else { diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs index e9c71a4..2df4f0e 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpStatusCode.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { // Any int can be cast to a HttpStatusCode to allow checking for non http1.1 // codes. diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs index 79f44d7..1ed07a7 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpUtility.cs @@ -6,7 +6,7 @@ using System.Text; -namespace System.Web +namespace Cosmos.Network.Http { /// /// Utilities to encode and decode URLs. diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs index 229e5b8..96f32a9 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpVersion.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Defines the HTTP version numbers that are supported by the diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs index afc775a..dcefa79 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpWebRequest.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; using System.Collections; @@ -56,11 +56,9 @@ public class HttpWebRequest : WebRequest /// internal static ArrayList m_ConnectedStreams; - /// - /// Timer that checks on open connections and closes them if they are - /// idle for a long time. - /// - static Timer m_DropOldConnectionsTimer; + // Cosmos: no timer to drop the connections left idle (nanoFramework's m_DropOldConnectionsTimer): each new + // connection drops them first. A Timer runs its callback on thread pool threads, whose stacks a Cosmos kernel + // never frees, and against sockets the request thread uses, which Cosmos's network stack doesn't lock. /// /// If a response was created then Dispose on the Request will not dispose the underlying stream. @@ -81,6 +79,7 @@ static private void CheckPersistentConnections(object unused) if (count > 0) { DateTime curTime = DateTime.UtcNow; + ArrayList expired = null; lock (m_ConnectedStreams) { @@ -90,6 +89,12 @@ static private void CheckPersistentConnections(object unused) { InputNetworkStreamWrapper streamWrapper = (InputNetworkStreamWrapper)m_ConnectedStreams[i]; + // Cosmos: not one in use, whose m_lastUsed tells when it was lent, not when it was last used. + if (streamWrapper.m_InUse) + { + continue; + } + TimeSpan timePassed = curTime - streamWrapper.m_lastUsed; // If the socket is old, then close and remove from the list. @@ -97,22 +102,35 @@ static private void CheckPersistentConnections(object unused) { m_ConnectedStreams.RemoveAt(i); - // Closes the socket to release resources. - streamWrapper.Dispose(); + (expired ??= new ArrayList()).Add(streamWrapper); } } - // Keep the timer going for another DefaultKeepAliveMilliseconds if we still have persistent connections in m_ConnectedStreams. - // Otherwise, do nothing. The timer won't be fired again. - if (m_ConnectedStreams.Count > 0) - { - m_DropOldConnectionsTimer.Change(HttpConstants.DefaultKeepAliveMilliseconds, System.Threading.Timeout.Infinite); - } + } + // Cosmos: closed out of the lock, which a Cosmos kernel doesn't release while an exception unwinds. + for (int i = 0; expired != null && i < expired.Count; i++) + { + // Closes the socket to release resources. + DisposeQuietly((InputNetworkStreamWrapper)expired[i]); } } } + /// + /// Cosmos: disposes a pooled connection, which may be closed or reset already. + /// + private static void DisposeQuietly(InputNetworkStreamWrapper stream) + { + try + { + stream.Dispose(); + } + catch + { + } + } + /// /// Registers HttpRequestCreator as the creator for the "http" prefix. /// @@ -133,7 +151,6 @@ static HttpWebRequest() { // Creates new list for connected sockets. m_ConnectedStreams = new ArrayList(); - m_DropOldConnectionsTimer = new Timer(CheckPersistentConnections, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); } } @@ -399,7 +416,7 @@ public SslVerification SslVerification /// One of the values defined in the enumeration. /// /// - /// Setting this property is mandatory when performing HTTPS requests, otherwise the authentication will fail. + /// Cosmos: SslProtocols.None (the default) offers TLS 1.3 and 1.2; nanoFramework required a value for HTTPS. /// /// This property is specific to nanoFramework. There is no equivalent in the .NET API. /// @@ -1284,7 +1301,9 @@ private void PrepareHeaders() } } - m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.Host, m_originalUrl.Host); + // Cosmos: the authority, with a port other than the scheme's (RFC 9112, 3.2), where nanoFramework sends the host + // alone: a server on port 8080 would read port 80. + m_httpRequestHeaders.ChangeInternal(HttpKnownHeaderNames.Host, m_originalUrl.Authority); // Adds user name and password for basic Http authentication. if (m_NetworkCredentials != null && m_NetworkCredentials.AuthenticationType == AuthenticationType.Basic) @@ -1337,26 +1356,33 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe { InputNetworkStreamWrapper retStream = null; + // Cosmos: what nanoFramework's m_DropOldConnectionsTimer does. + CheckPersistentConnections(null); + // Create a socket and set reuse true. // But before creating new socket we look in the list of existing sockets. If socket for this host already // exist - use it. No need to create new socket. string remoteServer = targetServer.Host + ":" + targetServer.Port; + string connectionKey = ConnectionKey(targetServer); + ArrayList removeStreamList = new ArrayList(); lock (m_ConnectedStreams) { - ArrayList removeStreamList = new ArrayList(); for (int i = 0; i < m_ConnectedStreams.Count; i++) { InputNetworkStreamWrapper inputStream = (InputNetworkStreamWrapper)m_ConnectedStreams[i]; - if (inputStream.m_rmAddrAndPort == remoteServer && !inputStream.m_InUse) + if (inputStream.m_rmAddrAndPort == connectionKey && !inputStream.m_InUse) { // Re-use the connected socket. // But first we need to know that socket is not closed. try { // Non-blocking liveness check: Available == 0 after a ready SelectRead poll means the peer closed the connection. - bool peerClosed = inputStream.m_Socket.Poll(0, SelectMode.SelectRead) && inputStream.m_Socket.Available == 0; + // Cosmos: and one no longer connected is dead too: Cosmos's Poll says nothing of a socket + // closed on this side, and one the peer closed is not connected even with data left. + bool peerClosed = !inputStream.m_Socket.Connected + || (inputStream.m_Socket.Poll(0, SelectMode.SelectRead) && inputStream.m_Socket.Available == 0); if (!peerClosed) { // No exception, good we can condtinue and re-use connected stream. @@ -1386,11 +1412,15 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe // Means socket was closed. Remove it from the list. m_ConnectedStreams.Remove(removeStream); - - removeStream.Dispose(); } } + // Cosmos: closed out of the lock, which a Cosmos kernel doesn't release while an exception unwinds. + for (int i = 0; i < removeStreamList.Count; i++) + { + DisposeQuietly((InputNetworkStreamWrapper)removeStreamList[i]); + } + if (retStream == null) { // No persistent connection found. Need to establish new one. @@ -1398,7 +1428,11 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe UriHostNameType hostNameType = proxyServer.HostNameType; if (hostNameType == UriHostNameType.IPv4) { - address = IPAddress.Parse(proxyServer.Host); + // Cosmos: TryParse, as Cosmos's IPAddress.Parse returns null rather than throwing. + if (!IPAddress.TryParse(proxyServer.Host, out address)) + { + throw new WebException("Invalid IPv4 address: " + proxyServer.Host, WebExceptionStatus.NameResolutionFailure); + } } else if (hostNameType == UriHostNameType.Dns) { @@ -1413,11 +1447,15 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe throw new WebException("host not available", se, WebExceptionStatus.ConnectFailure, null); } + // Cosmos: the first IPv4 address, as nanoFramework's resolver returns no other; .NET's may list IPv6 + // ones first, which an IPv4 socket can't reach. int addressListSize = hostEntry.AddressList.Length; for (int i = 0; i < addressListSize; i++) { - if ((address = hostEntry.AddressList[i]) != null) + IPAddress candidate = hostEntry.AddressList[i]; + if (candidate != null && candidate.AddressFamily == AddressFamily.InterNetwork) { + address = candidate; break; } } @@ -1436,33 +1474,9 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe Socket socket = null; socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - try - { - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch (Exception) - { - // We can safely ignore exceptions - } - - try - { - socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); - } - catch (Exception) - { - // We can safely ignore exceptions - } - - try - { - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, m_keepAlive); - } - catch (Exception) - { - // We can safely ignore exceptions - } - + // Cosmos: no ReuseAddress, NoDelay nor KeepAlive option. Cosmos's sockets have no native handle for + // SetSocketOption to reach, and it faults on the null one (a kernel panic, which no catch stops). + // Cosmos's TCP sends every write at once and keeps no TIME_WAIT, which leaves those options nothing to do. // Connect to remote endpoint try @@ -1470,8 +1484,10 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe IPEndPoint remoteEP = new IPEndPoint(address, proxyServer.Port); socket.Connect((EndPoint)remoteEP); } - catch (SocketException e) + catch (Exception e) { + // Cosmos: any exception, as Cosmos's Connect throws a plain Exception where .NET throws a + // SocketException. // need to close socket, otherwise this will cause an out of memory exception socket.Close(); @@ -1522,14 +1538,24 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe retStream.m_Stream = sslStream; // Changes the address. Originally socket was connected to proxy, now as if it connected to m_originalUrl.Host on m_originalUrl.Port - retStream.m_rmAddrAndPort = m_originalUrl.Host + ":" + m_originalUrl.Port; + // Cosmos: with the TLS settings it was authenticated with (see ConnectionKey). + retStream.m_rmAddrAndPort = ConnectionKey(m_originalUrl); } - catch + catch (Exception e) { // Proxy CONNECT failure or SSL handshake failure: retStream is not usable and // isn't referenced anywhere else yet, so it must be closed here or the socket leaks. retStream.Dispose(); - throw; + + // Cosmos: a failed handshake as a WebException, which HttpClient reports as an + // HttpRequestException, where nanoFramework let SslStream's exception through. + if (e is WebException) + { + throw; + } + + throw new WebException("The TLS connection to " + m_originalUrl.Host + " failed: " + e.Message, e, + e is AuthenticationException ? WebExceptionStatus.TrustFailure : WebExceptionStatus.SecureChannelFailure, null); } } @@ -1539,12 +1565,6 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe lock (m_ConnectedStreams) { m_ConnectedStreams.Add(retStream); - - // if the current stream list was empty then start the timer that drops unused connections. - if (m_ConnectedStreams.Count == 1) - { - m_DropOldConnectionsTimer.Change(HttpConstants.DefaultKeepAliveMilliseconds, System.Threading.Timeout.Infinite); - } } } } @@ -1552,6 +1572,53 @@ private InputNetworkStreamWrapper EstablishConnection(Uri proxyServer, Uri targe return retStream; } + /// + /// Cosmos: throws for a response ParseHTTPResponse couldn't read, which has no headers: nanoFramework went on with + /// it, to a null dereference (a kernel panic on Cosmos, not an exception). + /// + private static void ThrowIfMalformed(CoreResponseData respData) + { + if (respData.m_status != WebExceptionStatus.Success || respData.m_headers == null) + { + throw new WebException(respData.m_exceptionMessage ?? "The response couldn't be read.", respData.m_innerException, WebExceptionStatus.ServerProtocolViolation, null); + } + } + + /// + /// Cosmos: what a pooled connection serves: a host and port, and for https the TLS settings it was authenticated + /// with, so that a request doesn't reuse a connection another request trusted under other certificates or + /// verification. nanoFramework's pool tells them by host and port only. + /// + private string ConnectionKey(Uri server) + { + string key = server.Host + ":" + server.Port; + + if (server.Scheme == Uri.UriSchemeHttps || server.Scheme == Uri.UriSchemeWss) + { + key += " tls " + (int)m_sslProtocols + " " + (int)_sslVerification + " " + CertificateKey(m_caCert); + } + + return key; + } + + private static string CertificateKey(X509Certificate certificate) + { + byte[] raw = certificate?.GetRawCertData(); + if (raw == null) + { + return "store"; + } + + // FNV-1a: the certificates' content, as each request may come with its own instance of the same one. + uint hash = 2166136261; + for (int i = 0; i < raw.Length; i++) + { + hash = (hash ^ raw[i]) * 16777619; + } + + return raw.Length + "/" + hash.ToString("x8"); + } + /// /// Submits request to the WEB server. /// @@ -1690,7 +1757,8 @@ private CoreResponseData ParseHTTPResponse(InputNetworkStreamWrapper inStream, b ret.m_statusCode = statusCode; // Advance to the status message. The message is optional - for (; currentOffset < line.Length && ' ' != line[currentOffset]; ++currentOffset) ; + // Cosmos: past the space, where nanoFramework's loop stopped on it: the reason phrase began with it. + for (; currentOffset < line.Length && ' ' == line[currentOffset]; ++currentOffset) ; ret.m_statusDescription = line.Substring(currentOffset); ret.m_headers = new WebHeaderCollection(true); @@ -1813,27 +1881,53 @@ public override WebResponse GetResponse() // reset the total response bytes for the new request. m_requestStream.m_BytesLeftInResponse = -1; - // create the request timeout timer. This will kill the operation if it takes longer than specified by the Timeout property. - // The underlying socket will be closed to end the web request - using (Timer tmr = new Timer(new TimerCallback(OnRequestTimeout), null, m_timeout, System.Threading.Timeout.Infinite)) + // Cosmos: the Timeout bounds each wait for the response's head, in place of nanoFramework's timer that closes + // the socket from another thread once it elapses (a Timer runs on thread pool threads, whose stacks a + // Cosmos kernel never frees, and Cosmos's network stack has no locks). The head comes in time or the + // read throws. + int readTimeout = m_requestStream.ReadTimeout; + if (m_timeout != System.Threading.Timeout.Infinite && (readTimeout == System.Threading.Timeout.Infinite || m_timeout < readTimeout)) + { + m_requestStream.ReadTimeout = m_timeout; + } + { // Processes response from server. Request stream should already be there. + // Cosmos: a new response on the connection, so a clone made before (GetRequestStream's) gives back + // nothing over it when disposed (InputNetworkStreamWrapper.TakeReadState). + m_requestStream.StartResponse(); + respData = ParseHTTPResponse(m_requestStream, m_keepAlive); + ThrowIfMalformed(respData); - if (respData.m_statusCode == (int)HttpStatusCode.Continue) + // Cosmos: every interim response (1xx but 101) handed to the continue delegate, if any, and skipped, + // where nanoFramework skipped one 100 Continue, and none with a delegate: a 103 Early Hints would + // have been the response, and the real one read by the next request on the connection. + while (respData.m_statusCode >= 100 && respData.m_statusCode < 200 && respData.m_statusCode != (int)HttpStatusCode.SwitchingProtocols) { if (m_continueDelegate != null) { m_continueDelegate(respData.m_statusCode, respData.m_headers); } - else - { - respData = ParseHTTPResponse(m_requestStream, m_keepAlive); - } + + respData = ParseHTTPResponse(m_requestStream, m_keepAlive); + ThrowIfMalformed(respData); } } + m_requestStream.ReadTimeout = readTimeout; + + // Cosmos: a response that has no body whatever its headers say (RFC 9112, 6.3): reading one would wait + // for bytes that never come on a kept-alive connection. + // 101 excepted: the connection is the new protocol's (a WebSocket's) after it. + int status = respData.m_statusCode; + if (m_method == "HEAD" || status == (int)HttpStatusCode.NoContent || status == (int)HttpStatusCode.NotModified) + { + m_requestStream.m_BytesLeftInResponse = 0; + respData.m_chunked = false; + } + response = new HttpWebResponse(m_method, m_originalUrl, respData, this); // Now we look if response has chunked encoding. If it is chunked, we need to set flag in m_requestStream we return. @@ -1845,35 +1939,36 @@ public override WebResponse GetResponse() m_responseStatus = response.StatusCode; m_responseCreated = true; - m_requestStream.m_InUse = false; // Persistent connections are not yet supported, but they wouldn't work without this. - } - catch (SocketException se) - { - if (m_requestStream != null) - { - m_requestStream.m_InUse = false; - if (m_requestStream.m_Socket != null) - { - this.m_requestStream.m_Socket.Close(); - } - } - throw new WebException("GetResponse() failed", se); + // Cosmos: the connection stays in use until the response is disposed, which releases it once its body is + // read (InputNetworkStreamWrapper.ReleaseStream). nanoFramework lent it to the next request now, which + // would then read what is left of this response's body. } catch (Exception e) { - // Same cleanup as above: without it, any failure between the connection being - // established and the response being fully constructed (bad status line, malformed - // headers, a throwing continue-delegate, etc.) leaks the connection/socket. + // Without it, any failure between the connection being established and the response being fully + // constructed (bad status line, malformed headers, a throwing continue-delegate, etc.) leaks the + // connection/socket. + // Cosmos: the connection out of the pool too, as nanoFramework closed its socket only: Cosmos's Poll + // says a closed socket has nothing to read, so the pool would lend it to every later request. if (m_requestStream != null) { - m_requestStream.m_InUse = false; + RemoveStreamFromPool(m_requestStream); - if (m_requestStream.m_Socket != null) + try { - this.m_requestStream.m_Socket.Close(); + m_requestStream.Dispose(); } + catch + { + } + } + + if (e is WebException) + { + throw; } + throw new WebException("GetResponse() failed", e); } diff --git a/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs b/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs index 45790af..ca9dbb0 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.HttpWebResponse.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; using System.IO; @@ -297,7 +297,11 @@ protected override void Dispose(bool disposing) if (m_httpResponseHeaders != null) connValue = m_httpResponseHeaders[HttpKnownHeaderNames.Connection]; // If server had not send this header or value is not "close", then we keep connection. - closeConnection = connValue == null || connValue.ToLower() == HttpKnownHeaderValues.close; + // Cosmos: as this says, where nanoFramework closed a connection without the header too. An HTTP/1.0 + // server keeps it only when it says so. + closeConnection = connValue != null + ? connValue.ToLower().IndexOf(HttpKnownHeaderValues.close) >= 0 + : m_version == null || m_version.Major < 1 || (m_version.Major == 1 && m_version.Minor < 1); } // If it is not in the list - Add it @@ -330,14 +334,10 @@ protected override void Dispose(bool disposing) /// public override void Close() { - if (m_responseStream != null) - { - // Closing connection socket - m_responseStream.Dispose(); - - // Set flag that we already completed work on this stream. - m_responseStream = null; - } + // Cosmos: releases the connection for reuse, as said above, through Dispose. nanoFramework closed its socket + // and left it in the pool of persistent connections, and WebResponse.Dispose calls Close. + Dispose(true); + GC.SuppressFinalize(this); } } } diff --git a/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs b/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs index bbcab69..73740c5 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.IKnowWhenDone.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// The interface to let its consumer know work is done diff --git a/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs b/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs index 1bae096..7c47b57 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.IWebRequestCreate.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// The interface for creating class diff --git a/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs b/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs index 5a26c99..eb7e55b 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.Internal.WebRequestPrefixElement.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { internal class WebRequestPrefixElement { diff --git a/src/Cosmos.Network.Http/Http/System.Net.Internal.cs b/src/Cosmos.Network.Http/Http/System.Net.Internal.cs index bb53448..7eaa3c2 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.Internal.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.Internal.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Used during parsing to capture all the information contained in the http diff --git a/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs b/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs index 03563c9..5c8d4c4 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.NetworkCredential.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Class that keeps user name and password. diff --git a/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs b/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs index d2eb103..6e58f30 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.ProtocolViolationException.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// The exception that is thrown when an error is made while using a network diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebException.cs b/src/Cosmos.Network.Http/Http/System.Net.WebException.cs index 9408a10..5aeda4c 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.WebException.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.WebException.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs b/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs index 9587186..498eb63 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.WebHeaders.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System.Collections; using System.Text; @@ -716,7 +716,9 @@ public string[] AllKeys } string[] stringArray = new string[tempCollection.Count]; - return (string[])tempCollection.ToArray(typeof(string)); + // Cosmos: ArrayList.ToArray(Type) makes the array by reflection, which NativeAOT may not have compiled. + tempCollection.CopyTo(stringArray); + return stringArray; } } diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs b/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs index 79f8fb8..5a92af5 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.WebRequest.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; using System.Collections; @@ -58,6 +58,12 @@ public static void Initialize() { g_listLock = new object(); s_PrefixList = new ArrayList(); + + // Cosmos: http and https, which HttpWebRequest's static constructor registers on nanoFramework, where + // every static constructor runs at startup. .NET runs it on HttpWebRequest's first use, so Create would + // throw NotSupportedException before any. + RegisterPrefix("http:"); + RegisterPrefix("https:"); } } diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs b/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs index 71f0765..abc0829 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.WebResponse.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; using System.IO; diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs b/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs index 2321bd6..018e927 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.WebSocketContext.cs @@ -6,7 +6,7 @@ using System.Net.Sockets; -namespace System.Net +namespace Cosmos.Network.Http { /// /// Provides access to the networkStream and socket for creating a websocket diff --git a/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs b/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs index 5e27eee..0363730 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.WebStatus.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Defines status codes for the diff --git a/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs index 2aa80c6..26fc870 100644 --- a/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs +++ b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfo.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { internal delegate string[] HeaderParser(string value); diff --git a/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs index eeaf8c5..b13f682 100644 --- a/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs +++ b/src/Cosmos.Network.Http/Http/System.Net._HeaderInfoTable.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System.Collections; @@ -66,7 +66,10 @@ private static string[] ParseMultiValue(string value) tempCollection.Add(singleValue.Trim()); } - return (string[])tempCollection.ToArray(typeof(string)); + // Cosmos: ArrayList.ToArray(Type) makes the array by reflection, which NativeAOT may not have compiled. + string[] values = new string[tempCollection.Count]; + tempCollection.CopyTo(values); + return values; } /// diff --git a/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs b/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs index e13f55d..ef80323 100644 --- a/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs +++ b/src/Cosmos.Network.Http/Http/System.Net._HttpDateParse.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Internal class that parses the string with date in HTTP headers to DateTime object. diff --git a/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs b/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs index 67840dc..f717d2f 100644 --- a/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs +++ b/src/Cosmos.Network.Http/Http/System.Net._InputNetworkStreamWrapper.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System.Diagnostics; using System.IO; @@ -21,8 +21,8 @@ namespace System.Net /// internal class InputNetworkStreamWrapper : Stream, IKnowWhenDone { - static private Text.Decoder UTF8decoder = System.Text.Encoding.UTF8.GetDecoder(); - static private Text.Encoding UTF8Encoding = System.Text.Encoding.UTF8; + static private System.Text.Decoder UTF8decoder = System.Text.Encoding.UTF8.GetDecoder(); + static private System.Text.Encoding UTF8Encoding = System.Text.Encoding.UTF8; /// /// Actual network or SSL stream connected to the server. @@ -98,6 +98,16 @@ internal class InputNetworkStreamWrapper : Stream, IKnowWhenDone /// private bool m_isClone; + /// + /// Cosmos: the stream this one is a clone of, which gets back what the clone read when it is disposed. + /// + private InputNetworkStreamWrapper m_original; + + /// + /// Cosmos: counts the responses the connection carried, so a clone of an earlier one gives nothing back. + /// + private int m_generation; + /// /// Http web responses can contain the Content-Length of the response. In these cases, we would like the stream to return an EOF indication /// if the caller tries to read past the content length. @@ -107,7 +117,10 @@ internal class InputNetworkStreamWrapper : Stream, IKnowWhenDone /// /// Refills internal buffer from network. /// - [MethodImpl(MethodImplOptions.Synchronized)] + /// + /// Cosmos: not synchronized, as nanoFramework's is: a Cosmos kernel doesn't release the lock while an exception + /// unwinds, and a read that times out throws. One thread reads a response at a time. + /// private int RefillInternalBuffer() { #if DEBUG @@ -144,6 +157,10 @@ internal void ResetState() m_dataStart = m_dataEnd = 0; m_EnableChunkedDecoding = false; m_chunk = null; + + // Cosmos: the end of the previous response, which a clone gives back (see TakeReadState). + IsDone = false; + m_generation++; } /// @@ -163,6 +180,8 @@ internal InputNetworkStreamWrapper(NetworkStream stream, Socket socket, bool own m_OwnsSocket = ownsSocket; m_rmAddrAndPort = rmAddrAndPort; m_InUse = true; + // Cosmos: when it was lent, as CheckPersistentConnections reads it. + m_lastUsed = DateTime.UtcNow; // negative value indicates no length is set, in which case we will continue to read upon the callers request m_BytesLeftInResponse = -1; @@ -234,40 +253,54 @@ public void FlushReadBuffer() { byte[] buffer = new byte[1024]; - int waitTimeUs = m_BytesLeftInResponse == 0 ? 500000 : 1000000; + // Cosmos: drops what can be read without waiting. nanoFramework waits up to a second in Socket.Poll for + // more, which Cosmos's Poll doesn't do, and reads the socket's Available, which counts encrypted bytes for + // https and would leave Read waiting for a record that never comes; the stream's Length counts what it + // can return. + // What was read ahead belongs to the response. + if (m_BytesLeftInResponse > 0) m_BytesLeftInResponse -= Math.Min(m_dataEnd - m_dataStart, m_BytesLeftInResponse); try { - while (m_Socket.Poll(waitTimeUs, SelectMode.SelectRead)) - { - int avail = m_Socket.Available; - - if (avail == 0) break; + int avail = (int)m_Stream.Length; - while (avail > 0) - { - int bytes = m_Stream.Read(buffer, 0, avail > buffer.Length ? buffer.Length : avail); + while (avail > 0) + { + int bytes = m_Stream.Read(buffer, 0, avail > buffer.Length ? buffer.Length : avail); - if (bytes <= 0) break; + if (bytes <= 0) break; - avail -= bytes; + if (m_BytesLeftInResponse > 0) m_BytesLeftInResponse -= bytes; - if (m_BytesLeftInResponse > 0) m_BytesLeftInResponse -= bytes; - } + avail = (int)m_Stream.Length; } } catch { } + // Cosmos: whether the whole response was read, which nanoFramework's wait for the rest of it makes likely. + m_ResponseComplete = m_BytesLeftInResponse == 0 || (m_EnableChunkedDecoding && IsDone); + m_dataEnd = m_dataStart = 0; m_BytesLeftInResponse = -1; } + // Cosmos: set by FlushReadBuffer. + private bool m_ResponseComplete; + private void ReleaseThread() { FlushReadBuffer(); + // Cosmos: a connection with part of a response still to come can't carry the next request. + if (!m_ResponseComplete) + { + HttpWebRequest.RemoveStreamFromPool(this); + Dispose(); + return; + } + m_lastUsed = DateTime.UtcNow; ResetState(); @@ -280,8 +313,9 @@ private void ReleaseThread() /// public void ReleaseStream() { - Thread th = new Thread(new ThreadStart(ReleaseThread)); - th.Start(); + // Cosmos: on the calling thread, which FlushReadBuffer no longer holds; nanoFramework starts a thread for + // each response, and a Cosmos kernel never frees a thread's stack. + ReleaseThread(); } /// @@ -295,6 +329,19 @@ public int ReadInternal(byte[] buffer, int offset, int size) // Need to init return value to zero explicitly, otherwise warning generated. int retVal = 0; + // Cosmos: the end of a body of known length, or of a response without one (HEAD, 1xx, 204, 304), where + // nanoFramework would go on to return what follows it: the next response on a kept-alive connection. + if (m_BytesLeftInResponse == 0) + { + IsDone = true; + return 0; + } + + if (m_BytesLeftInResponse > 0 && size > m_BytesLeftInResponse) + { + size = (int)m_BytesLeftInResponse; + } + // As first step we copy the buffered data if present int dataBuffered = m_dataEnd - m_dataStart; if (dataBuffered > 0) @@ -325,9 +372,16 @@ public int ReadInternal(byte[] buffer, int offset, int size) { if (0 == RefillInternalBuffer()) { + // Cosmos: what was copied from the buffer above is returned, where nanoFramework returned 0; + // and a body cut short is an error, where it was the end. + if (retVal == 0) + { + ThrowIfCutShort(); + } + // Handle the 'HTTP/1.0' case IsDone = IsHttp1_0Completed(); - return 0; + return retVal; } dataBuffered = m_dataEnd - m_dataStart; @@ -347,6 +401,13 @@ public int ReadInternal(byte[] buffer, int offset, int size) else // Do not replentish internal buffer. Read rest of data directly { int bytesRead = m_Stream.Read(buffer, offset, size); + + // Cosmos: see above. + if (bytesRead == 0 && retVal == 0) + { + ThrowIfCutShort(); + } + retVal += bytesRead; // Handle the 'HTTP/1.0' case @@ -365,11 +426,32 @@ public int ReadInternal(byte[] buffer, int offset, int size) // in case there were more bytes in the buffer than we expected make sure the next call returns 0 if (m_BytesLeftInResponse < 0) m_BytesLeftInResponse = 0; + + // Cosmos: the end of the body. + if (m_BytesLeftInResponse == 0) IsDone = true; } return retVal; } + /// + /// Cosmos: throws when the connection ended before the body did: the rest of a length it gave, or of a chunk. + /// nanoFramework's StreamContent slept and read again, forever, and a Cosmos one would return the body cut + /// short as if complete. + /// + private void ThrowIfCutShort() + { + if (m_BytesLeftInResponse > 0) + { + throw new IOException("The connection was closed " + m_BytesLeftInResponse + " bytes before the end of the body."); + } + + if (m_EnableChunkedDecoding) + { + throw new IOException("The connection was closed before the last chunk of the body."); + } + } + /// /// Returns true if we are in 'HTTP/1.0' mode and the connection has been closed, which marks the end of the body. /// @@ -490,10 +572,57 @@ public Stream CloneStream() { InputNetworkStreamWrapper clone = this.MemberwiseClone() as InputNetworkStreamWrapper; clone.m_isClone = true; + clone.m_original = this; return clone; } + /// + /// Cosmos: the next request on a kept-alive connection: its own framing, after what was read ahead of it. + /// + internal void StartRequest() + { + m_EnableChunkedDecoding = false; + m_chunk = null; + IsDone = false; + m_BytesLeftInResponse = -1; + m_generation++; + } + + /// + /// Cosmos: whether the whole body of the message was read: its length, or its last chunk. + /// + internal bool IsBodyRead => m_BytesLeftInResponse == 0 || (m_EnableChunkedDecoding && IsDone); + + /// + /// Cosmos: a response begins on the connection: what a clone made before gives back is no longer its state. + /// + internal void StartResponse() + { + m_generation++; + } + + /// + /// Cosmos: takes what a clone of this stream read of the response, so ReleaseStream knows whether all of it + /// was read and the connection can carry another request. nanoFramework reads whatever comes for a second + /// after a response and reuses the connection anyway. + /// + private void TakeReadState(InputNetworkStreamWrapper clone) + { + // Disposed after its response was released: the connection carries another now. + if (clone.m_generation != m_generation) + { + return; + } + + m_readBuffer = clone.m_readBuffer; + m_dataStart = clone.m_dataStart; + m_dataEnd = clone.m_dataEnd; + m_BytesLeftInResponse = clone.m_BytesLeftInResponse; + m_chunk = clone.m_chunk; + IsDone = clone.IsDone; + } + /// /// Overrides the Dispose Behavior /// @@ -503,7 +632,16 @@ protected override void Dispose(bool disposing) // Clones share m_Stream/m_Socket with the original (see CloneStream), so only // the original may close them. HttpResponseMessage disposes the original deterministically. - if (!m_isClone) + if (m_isClone) + { + // Cosmos: see TakeReadState. + if (m_original != null) + { + m_original.TakeReadState(this); + m_original = null; + } + } + else { m_Stream.Close(); @@ -604,7 +742,7 @@ internal string Read_HTTP_Line(int maxLineLength) } else if (curPos == 0) { - throw new SocketException(SocketError.ConnectionAborted); + throw new SocketException((int)SocketError.ConnectionAborted); } return ""; @@ -619,7 +757,7 @@ private byte PeekByte() // Refills internal buffer if there is no more data if (m_dataEnd == m_dataStart) { - if (0 == RefillInternalBuffer()) throw new SocketException(SocketError.ConnectionAborted); + if (0 == RefillInternalBuffer()) throw new SocketException((int)SocketError.ConnectionAborted); } return m_readBuffer[m_dataStart]; } @@ -633,7 +771,7 @@ public override int ReadByte() // Refills internal buffer if there is no more data if (m_dataEnd == m_dataStart) { - if (0 == RefillInternalBuffer()) throw new SocketException(SocketError.ConnectionAborted); + if (0 == RefillInternalBuffer()) throw new SocketException((int)SocketError.ConnectionAborted); } // Very similar to Peek, but moves current position to next byte. return m_readBuffer[m_dataStart++]; @@ -783,9 +921,10 @@ private Chunk GetChunk() return nextChunk; } - public override int Read(SpanByte buffer) + public override int Read(Span buffer) { - throw new NotSupportedException(); + // Cosmos: nanoFramework's SpanByte overload threw; .NET's reads through Read(byte[], int, int), as Stream's does. + return base.Read(buffer); } private enum ChunkState diff --git a/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs b/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs index 7f02c83..03776eb 100644 --- a/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs +++ b/src/Cosmos.Network.Http/Http/System.Net._OutputNetworkStreamWrapper.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System.IO; using System.Net.Sockets; @@ -34,6 +34,32 @@ internal class OutputNetworkStreamWrapper : Stream /// internal bool m_enableChunkedEncoding = false; + /// + /// Cosmos: whether the last chunk was sent, so it is sent once: by Flush, else by HttpListenerResponse.Close. + /// + private bool m_chunksFinished = false; + + /// + /// Cosmos: the connection's reader, kept across its requests: what it read ahead of one request is the next's. + /// nanoFramework made one per request, which dropped it. + /// + internal InputNetworkStreamWrapper m_Input; + + /// + /// Cosmos: the body bytes the response wrote, so its Close knows whether it sent what its Content-Length said. + /// + internal long m_BodyWritten; + + /// + /// Cosmos: whether the last chunk of a chunked response was sent. + /// + internal bool ChunksFinished => m_chunksFinished; + + /// + /// Cosmos: whether bytes of a request wait in the connection's reader, already read from the socket. + /// + internal bool HasBufferedInput => m_Input != null && m_Input.m_dataEnd > m_Input.m_dataStart; + /// /// Type definition of delegate for sending of HTTP headers. /// @@ -67,6 +93,25 @@ public OutputNetworkStreamWrapper(Socket socket, NetworkStream stream) /// internal SendHeadersDelegate HeadersDelegate { set { m_headersSend = value; } } + /// + /// Cosmos: the stream to write to, which is gone once this one is closed (by the handler, or by an Abort on + /// another thread): an ObjectDisposedException then, where nanoFramework dereferences null, a kernel panic on + /// Cosmos. + /// + private NetworkStream Target + { + get + { + NetworkStream stream = m_Stream; + if (stream == null) + { + throw new ObjectDisposedException(nameof(OutputNetworkStreamWrapper)); + } + + return stream; + } + } + /// /// Return true if stream support reading. /// @@ -80,7 +125,7 @@ public OutputNetworkStreamWrapper(Socket socket, NetworkStream stream) /// /// Return true if timeout is applicable to the stream /// - public override bool CanTimeout { get { return m_Stream.CanTimeout; } } + public override bool CanTimeout { get { return Target.CanTimeout; } } /// /// Return true if stream support writing. It should be true, as this is output stream. @@ -115,8 +160,8 @@ public override long Position /// public override int ReadTimeout { - get { return m_Stream.ReadTimeout; } - set { m_Stream.ReadTimeout = value; } + get { return Target.ReadTimeout; } + set { Target.ReadTimeout = value; } } /// @@ -124,8 +169,8 @@ public override int ReadTimeout /// public override int WriteTimeout { - get { return m_Stream.WriteTimeout; } - set { m_Stream.WriteTimeout = value; } + get { return Target.WriteTimeout; } + set { Target.WriteTimeout = value; } } /// @@ -134,8 +179,8 @@ public override int WriteTimeout private void WriteChunkStart(int size) { byte[] chunkLengthBytes = Encoding.UTF8.GetBytes($"{size:X}"); - m_Stream.Write(chunkLengthBytes, 0, chunkLengthBytes.Length); - m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + Target.Write(chunkLengthBytes, 0, chunkLengthBytes.Length); + Target.Write(EOLMarker, 0, EOLMarker.Length); } /// @@ -143,7 +188,32 @@ private void WriteChunkStart(int size) /// private void WriteChunkEnd() { - m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + Target.Write(EOLMarker, 0, EOLMarker.Length); + } + + /// + /// Cosmos: starts a response on the connection, which may be kept alive after another: not chunked yet. + /// + internal void StartResponse(SendHeadersDelegate headersSend) + { + m_headersSend = headersSend; + m_enableChunkedEncoding = false; + m_chunksFinished = false; + m_BodyWritten = 0; + } + + /// + /// Cosmos: sends the last chunk of a chunked response whose headers were sent, unless it was. nanoFramework's + /// HttpListenerResponse.Close flushes the network stream, not this one, so a chunked response never ended + /// unless its OutputStream was flushed. + /// + internal void FinishChunks() + { + if (m_enableChunkedEncoding && !m_chunksFinished && m_headersSend == null && m_Stream != null) + { + m_chunksFinished = true; + WriteChunkFinish(); + } } /// @@ -152,9 +222,9 @@ private void WriteChunkEnd() private void WriteChunkFinish() { byte[] zero = { 0x30 }; - m_Stream.Write(zero, 0, 1); - m_Stream.Write(EOLMarker, 0, EOLMarker.Length); - m_Stream.Write(EOLMarker, 0, EOLMarker.Length); + Target.Write(zero, 0, 1); + Target.Write(EOLMarker, 0, EOLMarker.Length); + Target.Write(EOLMarker, 0, EOLMarker.Length); } /// @@ -165,7 +235,15 @@ public override void Close() if (m_headersSend != null) { // Calls HttpListenerResponse.SendHeaders. HttpListenerResponse.SendHeaders sets m_headersSend to null. - m_headersSend(); + // Cosmos: the connection closed even when the client is gone, which nanoFramework left open. + try + { + m_headersSend(); + } + catch + { + m_headersSend = null; + } } if (m_Stream != null) m_Stream.Close(); @@ -184,10 +262,7 @@ public override void Flush() m_headersSend(); } - if (m_enableChunkedEncoding) - { - WriteChunkFinish(); - } + FinishChunks(); // Need to check for null before using here m_Stream?.Flush(); @@ -249,7 +324,8 @@ public override void WriteByte(byte value) WriteChunkStart(1); } - m_Stream.WriteByte(value); + m_BodyWritten++; + Target.WriteByte(value); if (m_enableChunkedEncoding) { @@ -268,6 +344,12 @@ public override void WriteByte(byte value) /// Count of bytes to write. public override void Write(byte[] buffer, int offset, int size) { + // Cosmos: a chunk of 0 bytes is the last one. + if (size == 0 && m_enableChunkedEncoding) + { + return; + } + if (m_headersSend != null) { // Calls HttpListenerResponse.SendHeaders. HttpListenerResponse.SendHeaders sets m_headersSend to null. @@ -279,7 +361,8 @@ public override void Write(byte[] buffer, int offset, int size) WriteChunkStart(size); } - m_Stream.Write(buffer, offset, size); + m_BodyWritten += size; + Target.Write(buffer, offset, size); if (m_enableChunkedEncoding) { @@ -287,7 +370,7 @@ public override void Write(byte[] buffer, int offset, int size) } } - public override int Read(SpanByte buffer) + public override int Read(Span buffer) { throw new NotSupportedException(); } diff --git a/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs b/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs index 60cb205..24e4072 100644 --- a/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs +++ b/src/Cosmos.Network.Http/Http/System.Net._ValidationHelper.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; diff --git a/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs b/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs index 305c53a..55bc453 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.iwebproxy.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { using System; diff --git a/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs b/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs index 5cae966..e44b15c 100644 --- a/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs +++ b/src/Cosmos.Network.Http/Http/System.Net.webproxy.cs @@ -4,7 +4,7 @@ // See LICENSE file in the project root for full license information. // -namespace System.Net +namespace Cosmos.Network.Http { /// /// Contains HTTP proxy settings for the diff --git a/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs b/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs index 3e3c039..d411f6a 100644 --- a/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs +++ b/src/Cosmos.Network.Http/Properties/AssemblyInfo.cs @@ -9,12 +9,7 @@ [assembly: AssemblyProduct("Cosmos.Network.Http")] [assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")] -////////////////////////////////////////////////////// -// this assembly does NOT have a native counterpart // -////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -[assembly: AssemblyNativeVersion("0.0.0.0")] -//////////////////////////////////////////////////////////////// +// Cosmos: nanoFramework's AssemblyNativeVersion (the version of the native counterpart) has no .NET equivalent. // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/src/Cosmos.Network.Http/Security/CertificateManager.cs b/src/Cosmos.Network.Http/Security/CertificateManager.cs new file mode 100644 index 0000000..800a996 --- /dev/null +++ b/src/Cosmos.Network.Http/Security/CertificateManager.cs @@ -0,0 +1,173 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +// nanoFramework's System.Net CertificateManager (nanoFramework.System.Net/Security/CertificateManager.cs). Cosmos: a +// kernel has no device certificate store, so the store is in memory and starts with the Mozilla roots the package +// embeds (resources/cacert.pem): an https:// server is checked against them unless a CA certificate is given, or a +// bundle added here replaces them, as one replaces the device's bundle on nanoFramework. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; + +namespace Cosmos.Network.Http +{ + /// + /// Provides an interface to the device certificate store to manage . + /// + /// + /// Cosmos: the store starts with the Mozilla root certificates, as curl extracts them. + /// + public static class CertificateManager + { + private const string RootsResource = "Cosmos.Network.Http.cacert.pem"; + + // The bundle added, which replaces the embedded roots once s_added is 1, and those roots, parsed on first use. + // Each replaced whole, never changed in place, so a handshake on another thread reads a complete one without a + // lock; two fields, so the roots parsed on one thread can't replace a bundle another adds meanwhile. An int and + // Interlocked for s_added, as Interlocked on references isn't atomic on a Cosmos kernel. + private static volatile Org.BouncyCastle.X509.X509Certificate[] s_bundle; + private static volatile Org.BouncyCastle.X509.X509Certificate[] s_roots; + private static int s_added; + + /// + /// Adds a Certificate Authority Root bundle to the store. + /// If there is already a CA Root bundle it will be replaced with this one. + /// + /// The Certificate Authority certificate bundle to be added store. + /// + /// True if the certificate bundle was correctly added to the device certificate store. + /// + /// + /// This method is exclusive of nanoFramework. There is no equivalent in .NET framework. + /// + public static bool AddCaCertificateBundle(X509Certificate[] ca) + { + // Cosmos: each certificate's own parse, where nanoFramework concatenates their PEM text for mbedTLS (which + // loses a byte of a DER one). + if (ca == null) + { + return false; + } + + List bundle = new List(); + try + { + foreach (X509Certificate cert in ca) + { + bundle.AddRange(cert.GetCertificates()); + } + } + catch + { + return false; + } + + s_bundle = bundle.ToArray(); + Interlocked.Exchange(ref s_added, 1); + return true; + } + + /// + /// Adds a Certificate Authority Root bundle to the store. + /// If there is already a CA Root bundle it will be replaced with this one. + /// + /// The Certificate Authority certificate bundle to be added store. + /// + /// True if the certificate bundle was correctly added to the device certificate store. + /// + /// + /// This method is exclusive of nanoFramework. There is no equivalent in .NET framework. + /// + public static bool AddCaCertificateBundle(string ca) + { + return AddCaCertificateBundle(Encoding.UTF8.GetBytes(ca)); + } + + /// + /// Adds a Certificate Authority Root bundle to the store. + /// If there is already a CA Root bundle it will be replaced with this one. + /// + /// The Certificate Authority certificate bundle to be added store. + /// + /// True if the certificate bundle was correctly added to the device certificate store. + /// + /// + /// This method is exclusive of nanoFramework. There is no equivalent in .NET framework. + /// + public static bool AddCaCertificateBundle(byte[] ca) + { + // Cosmos: what nanoFramework stores natively. + if (ca == null) + { + return false; + } + + Org.BouncyCastle.X509.X509Certificate[] bundle; + try + { + bundle = X509Certificate.ParseCertificates(ca); + } + catch + { + return false; + } + + s_bundle = bundle; + Interlocked.Exchange(ref s_added, 1); + return true; + } + + /// + /// Cosmos: the store's certificates, which a TLS client given no CA certificate trusts. + /// + internal static Org.BouncyCastle.X509.X509Certificate[] GetTrustedCertificates() + { + // Parsed on first use: about 150 roots. Two threads may both parse them; either result will do. + if (Volatile.Read(ref s_added) != 0) + { + return s_bundle; + } + + Org.BouncyCastle.X509.X509Certificate[] roots = s_roots; + if (roots == null) + { + roots = LoadEmbeddedRoots(); + s_roots = roots; + } + + // Unless a bundle was added meanwhile, which wins. + return Volatile.Read(ref s_added) != 0 ? s_bundle : roots; + } + + private static Org.BouncyCastle.X509.X509Certificate[] LoadEmbeddedRoots() + { + Stream stream = typeof(CertificateManager).Assembly.GetManifestResourceStream(RootsResource); + if (stream == null) + { + throw new InvalidOperationException("The embedded root certificates (" + RootsResource + ") are missing."); + } + + byte[] pem = new byte[stream.Length]; + int read = 0; + while (read < pem.Length) + { + int count = stream.Read(pem, read, pem.Length - read); + if (count <= 0) + { + break; + } + + read += count; + } + + stream.Dispose(); + + return X509Certificate.ParseCertificates(pem); + } + } +} diff --git a/src/Cosmos.Network.Http/Security/CertificateVerifier.cs b/src/Cosmos.Network.Http/Security/CertificateVerifier.cs new file mode 100644 index 0000000..cb12e7c --- /dev/null +++ b/src/Cosmos.Network.Http/Security/CertificateVerifier.cs @@ -0,0 +1,406 @@ +// Cosmos: the certificate check mbedTLS runs natively for nanoFramework (mbedtls_x509_crt_verify): the chain the peer +// sent must lead to a trusted certificate through valid signatures, every certificate must be within its validity +// period, and a server's must name the host. BouncyCastle's TLS leaves this to its caller. + +using System.Collections.Generic; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.EdEC; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.X509.Extension; +using BcCertificate = Org.BouncyCastle.X509.X509Certificate; + +namespace Cosmos.Network.Http +{ + /// + /// Checks a peer's certificate chain. + /// + internal static class CertificateVerifier + { + // Longer chains than a leaf, a few intermediates and a root are refused. + private const int MaxDepth = 8; + + private const int KeyCertSign = 5; + + /// + /// Checks a certificate chain. + /// + /// The certificates the peer sent, its own first. + /// The host the leaf certificate must name, or null not to check it (a client certificate). + /// The trusted certificates. + /// The time the certificates must be valid at. + /// null if the chain is valid, else why it is not. + internal static string Verify(BcCertificate[] chain, string host, IList trusted, DateTime now) + { + if (chain == null || chain.Length == 0) + { + return "The peer sent no certificate."; + } + + BcCertificate current = chain[0]; + + string error = CheckValidity(current, now); + if (error != null) + { + return error; + } + + if (host != null && !NamesHost(current, host)) + { + return "The certificate of " + Name(current) + " doesn't name " + host + "."; + } + + // A server's certificate must be one for servers, a client's one for clients, when it says what it is for. + KeyPurposeID purpose = host != null ? KeyPurposeID.id_kp_serverAuth : KeyPurposeID.id_kp_clientAuth; + if (!AllowsPurpose(current, purpose)) + { + return "The certificate of " + Name(current) + " isn't meant for TLS " + (host != null ? "servers." : "clients."); + } + + for (int depth = 0; depth < MaxDepth; depth++) + { + // Refused as mbedTLS refuses it: an extension it can't honour that says it must be. + error = CheckCriticalExtensions(current); + if (error != null) + { + return error; + } + + // A certificate trusted as such: the end of the chain. + if (Contains(trusted, current)) + { + return null; + } + + // Signed by a trusted certificate. Above the leaf, depth certificate authorities stand below it. + BcCertificate anchor = FindIssuer(current, trusted, 0, now, depth, out error); + if (anchor != null) + { + return null; + } + + // Signed by an intermediate the peer sent. + BcCertificate issuer = FindIssuer(current, chain, 1, now, depth, out string chainError); + if (issuer == null || issuer.Equals(current)) + { + // No issuer, or a self-signed root the peer sent that isn't trusted. + return error ?? chainError ?? "The certificate of " + Name(current) + " was issued by " + current.IssuerDN + ", which isn't trusted."; + } + + current = issuer; + } + + return "The certificate chain is longer than " + MaxDepth + " certificates."; + } + + /// + /// Whether a server certificate names a host, by its subjectAltName (dNSName or iPAddress), else its common name. + /// + internal static bool NamesHost(BcCertificate certificate, string host) + { + host = host.TrimEnd('.'); + + byte[] address = ParseIPv4(host); + + bool hasDnsName = false; + + Asn1OctetString extension = certificate.GetExtensionValue(X509Extensions.SubjectAlternativeName); + if (extension != null) + { + // A malformed extension names nothing, the common name included, as mbedTLS refuses the certificate. + // Null for an empty one, which a peer may send: a null dereference is a kernel panic on Cosmos. + GeneralName[] all; + try + { + all = GeneralNames.GetInstance(X509ExtensionUtilities.FromExtensionValue(extension))?.GetNames(); + } + catch + { + return false; + } + + if (all == null) + { + return false; + } + + foreach (GeneralName name in all) + { + if (name.TagNo == GeneralName.DnsName) + { + hasDnsName = true; + if (address == null && MatchesDnsName(DerIA5String.GetInstance(name.Name).GetString(), host)) + { + return true; + } + } + else if (name.TagNo == GeneralName.IPAddress && address != null) + { + if (SameBytes(Asn1OctetString.GetInstance(name.Name).GetOctets(), address)) + { + return true; + } + } + } + } + + // The common name counts only for a certificate without DNS names (RFC 6125, 6.4.4). + if (hasDnsName || address != null) + { + return false; + } + + foreach (string commonName in certificate.SubjectDN.GetValueList(X509Name.CN)) + { + if (MatchesDnsName(commonName, host)) + { + return true; + } + } + + return false; + } + + /// + /// The four bytes of an IPv4 address literal, or null for a host name. + /// + /// + /// Parsed here, as Cosmos's IPAddress.TryParse throws and catches a FormatException within for a name of four + /// labels (www.example.co.uk), on every handshake: an exception is slow on a Cosmos kernel, and two threads + /// throwing at once halt it. + /// + internal static byte[] ParseIPv4(string host) + { + byte[] address = new byte[4]; + int part = 0; + int value = -1; + + for (int i = 0; i <= host.Length; i++) + { + char c = i < host.Length ? host[i] : '.'; + if (c >= '0' && c <= '9') + { + value = (value < 0 ? 0 : value * 10) + (c - '0'); + if (value > 255) + { + return null; + } + } + else if (c == '.' && value >= 0 && part < 4) + { + address[part++] = (byte)value; + value = -1; + } + else + { + return null; + } + } + + return part == 4 ? address : null; + } + + /// + /// Whether a certificate's DNS name matches a host: equal ignoring case, or a wildcard for its first label only. + /// + internal static bool MatchesDnsName(string pattern, string host) + { + pattern = pattern.TrimEnd('.'); + + if (pattern.StartsWith("*.", StringComparison.Ordinal)) + { + // "*.example.com" names "www.example.com", not "example.com" nor "a.b.example.com". + int dot = host.IndexOf('.'); + if (dot <= 0) + { + return false; + } + + return string.Equals(pattern.Substring(1), host.Substring(dot), StringComparison.OrdinalIgnoreCase); + } + + return string.Equals(pattern, host, StringComparison.OrdinalIgnoreCase); + } + + private static string CheckValidity(BcCertificate certificate, DateTime now) + { + // The clock is in the message: a kernel's comes from the RTC, which may well be wrong. + if (now < certificate.NotBefore) + { + return "The certificate of " + Name(certificate) + " isn't valid before " + certificate.NotBefore.ToString("u") + " (the clock says " + now.ToString("u") + ")."; + } + + if (now > certificate.NotAfter) + { + return "The certificate of " + Name(certificate) + " expired on " + certificate.NotAfter.ToString("u") + " (the clock says " + now.ToString("u") + ")."; + } + + return null; + } + + // The extensions that may be critical: those the checks here honour, and the certificate policies, which + // mbedTLS accepts too. + private static readonly string[] s_handledExtensions = + { + X509Extensions.BasicConstraints.Id, + X509Extensions.KeyUsage.Id, + X509Extensions.ExtendedKeyUsage.Id, + X509Extensions.SubjectAlternativeName.Id, + X509Extensions.CertificatePolicies.Id, + }; + + private static string CheckCriticalExtensions(BcCertificate certificate) + { + ISet critical = certificate.GetCriticalExtensionOids(); + if (critical == null) + { + return null; + } + + foreach (string oid in critical) + { + if (Array.IndexOf(s_handledExtensions, oid) < 0) + { + // nameConstraints (2.5.29.30), for one. + return "The certificate of " + Name(certificate) + " has a critical extension that isn't supported (" + oid + ")."; + } + } + + return null; + } + + private static bool AllowsPurpose(BcCertificate certificate, KeyPurposeID purpose) + { + IList purposes = certificate.GetExtendedKeyUsage(); + if (purposes == null) + { + return true; + } + + foreach (DerObjectIdentifier allowed in purposes) + { + if (allowed.Equals(purpose) || allowed.Equals(KeyPurposeID.AnyExtendedKeyUsage)) + { + return true; + } + } + + return false; + } + + /// + /// The certificate among (from on) that issued and signed + /// and may sign certificates, or null. + /// + private static BcCertificate FindIssuer(BcCertificate certificate, IList candidates, int first, DateTime now, int authoritiesBelow, out string error) + { + error = null; + + for (int i = first; i < candidates.Count; i++) + { + BcCertificate candidate = candidates[i]; + if (!candidate.SubjectDN.Equivalent(certificate.IssuerDN) || !IsSignedBy(certificate, candidate)) + { + continue; + } + + // A certificate authority's: basicConstraints cA, and keyCertSign when it has a key usage. + bool[] keyUsage = candidate.GetKeyUsage(); + if (candidate.GetBasicConstraints() < 0 || (keyUsage != null && !keyUsage[KeyCertSign])) + { + error = "The certificate of " + Name(certificate) + " was signed by " + Name(candidate) + ", which isn't a certificate authority."; + continue; + } + + // Its basicConstraints' pathLenConstraint: how many certificate authorities may stand below it. + if (candidate.GetBasicConstraints() < authoritiesBelow) + { + error = "The certificate of " + Name(candidate) + " may not issue certificates through " + authoritiesBelow + " intermediate authorities."; + continue; + } + + string validity = CheckValidity(candidate, now); + if (validity != null) + { + error = validity; + continue; + } + + return candidate; + } + + return null; + } + + /// + /// Whether BouncyCastle reads the certificate's public key without dereferencing null: an RSA, EC (on a named + /// curve), Ed25519 or Ed448 one. It does for an EC key without its curve, or a DH, ElGamal or GOST key without + /// parameters, which a peer may send: a kernel panic on Cosmos, which no catch stops. mbedTLS reads these + /// kinds only, too. + /// + internal static bool HasReadableKey(BcCertificate certificate) + { + AlgorithmIdentifier algorithm = certificate.SubjectPublicKeyInfo?.Algorithm; + if (algorithm == null) + { + return false; + } + + DerObjectIdentifier oid = algorithm.Algorithm; + if (oid.Equals(PkcsObjectIdentifiers.RsaEncryption) || oid.Equals(PkcsObjectIdentifiers.IdRsassaPss) + || oid.Equals(EdECObjectIdentifiers.id_Ed25519) || oid.Equals(EdECObjectIdentifiers.id_Ed448)) + { + return true; + } + + // A named curve: an OID, as parsed, or as wrapped in X962Parameters in a certificate BouncyCastle made. + return oid.Equals(X9ObjectIdentifiers.IdECPublicKey) && algorithm.Parameters?.ToAsn1Object() is DerObjectIdentifier; + } + + private static bool IsSignedBy(BcCertificate certificate, BcCertificate issuer) + { + if (!HasReadableKey(issuer)) + { + return false; + } + + try + { + certificate.Verify(issuer.GetPublicKey()); + return true; + } + catch + { + // A different key, or a signature algorithm BouncyCastle doesn't know. + return false; + } + } + + private static bool Contains(IList certificates, BcCertificate certificate) + { + // BouncyCastle compares the encodings, which it keeps. + for (int i = 0; i < certificates.Count; i++) + { + if (certificates[i].Equals(certificate)) + { + return true; + } + } + + return false; + } + + private static bool SameBytes(byte[] a, byte[] b) + { + return a.AsSpan().SequenceEqual(b); + } + + private static string Name(BcCertificate certificate) + { + IList commonNames = certificate.SubjectDN.GetValueList(X509Name.CN); + return commonNames.Count > 0 ? commonNames[0] : certificate.SubjectDN.ToString(); + } + } +} diff --git a/src/Cosmos.Network.Http/Security/NetworkSecurity.cs b/src/Cosmos.Network.Http/Security/NetworkSecurity.cs new file mode 100644 index 0000000..18e309e --- /dev/null +++ b/src/Cosmos.Network.Http/Security/NetworkSecurity.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +// nanoFramework's System.Net NetworkSecurity.cs. Cosmos: SslProtocols is .NET's +// (System.Security.Authentication.SslProtocols, the same values), and SslNative, nanoFramework's native TLS +// (mbedTLS), is managed code over BouncyCastle in SslNative.cs. + +namespace Cosmos.Network.Http +{ + /// + /// The verification scheme to use for authentication. + /// + public enum SslVerification + { + /// + /// No verification of certificates is required for authentication. + /// + NoVerification = 1, + /// + /// If authenticating as a client, verifies the peer certificate and fails if no certificate is sent. If authenticating as a server, + /// it verifies the peer certificate only if a certificate is sent. + /// + VerifyPeer = 2, + /// + /// A certificate is required for authentication. If authenticating as a client, the server certificate is required. + /// If authenticating as a server, the client certificate is required. + /// + CertificateRequired = 4, + /// + /// Verify the client certificate only once. Applies only to authenticating as a server. + /// + VerifyClientOnce = 8, + } +} diff --git a/src/Cosmos.Network.Http/Security/SslNative.cs b/src/Cosmos.Network.Http/Security/SslNative.cs new file mode 100644 index 0000000..33b916b --- /dev/null +++ b/src/Cosmos.Network.Http/Security/SslNative.cs @@ -0,0 +1,959 @@ +// Cosmos: nanoFramework's SslNative (the externs at the end of nanoFramework.System.Net/Security/NetworkSecurity.cs) +// is native code over mbedTLS. This is its managed counterpart over BouncyCastle's TLS 1.2 and 1.3, which runs on a +// Cosmos kernel: its SecureRandom draws from .NET's RandomNumberGenerator, which Cosmos plugs with a kernel CSPRNG. +// One instance is one SSL context and its session; SslStream holds it where nanoFramework holds a context handle. +// BouncyCastle runs non-blocking here: what the socket holds is offered to it and what it produces is sent, so every +// wait is NetworkStream's (Cosmos's sockets don't wait themselves). + +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.Utilities; + +namespace Cosmos.Network.Http +{ + /// + /// The TLS context and session of an . + /// + internal sealed class SslNative + { + // How long a handshake waits for the peer when the stream has no read timeout: a native socket waits as long as + // mbedTLS lets it, which would hang a kernel on a peer that never answers. + private const int DefaultHandshakeTimeout = 30_000; + + // A TLS record: 16 KiB of data, plus the header and what encryption adds. + private const int ReceiveBufferSize = 17 * 1024; + + // The key exchange groups: elliptic curves only, as finite-field Diffie-Hellman is slow in managed code. + private static readonly int[] Groups = { NamedGroup.x25519, NamedGroup.secp256r1, NamedGroup.secp384r1 }; + + // Cosmos: AES runs from tables without AES-NI and PCLMULQDQ (a Cosmos kernel enables neither), where + // ChaCha20-Poly1305 is fast, so a client without them offers ChaCha20 first. + private static readonly int[] ClientCipherSuites = System.Runtime.Intrinsics.X86.Aes.IsSupported && System.Runtime.Intrinsics.X86.Pclmulqdq.IsSupported + ? new int[] + { + CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + } + : new int[] + { + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + }; + + // A server's: TLS 1.3, and TLS 1.2's ephemeral elliptic-curve AEAD suites for its key's kind. + private static readonly int[] ServerCipherSuitesRsa = + { + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + }; + + private static readonly int[] ServerCipherSuitesEcdsa = + { + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }; + + private readonly bool _isServer; + private readonly SslProtocols _protocols; + private readonly SslVerification _verification; + private readonly X509Certificate _certificate; + private readonly AsymmetricKeyParameter _key; + private readonly Org.BouncyCastle.X509.X509Certificate[] _trusted; + private readonly BcTlsCrypto _crypto; + + private TlsProtocol _protocol; + private NetworkStream _transport; + private byte[] _receiveBuffer; + private bool _inputClosed; + + private SslNative(bool isServer, SslProtocols protocols, SslVerification verification, X509Certificate certificate, X509Certificate ca) + { + if (protocols != SslProtocols.None && (protocols & (SslProtocols.Tls12 | SslProtocols.Tls13)) == 0) + { + throw new NotSupportedException("Only TLS 1.2 and TLS 1.3 are supported, which " + protocols + " doesn't include."); + } + + _isServer = isServer; + _protocols = protocols; + _verification = verification; + _certificate = certificate; + + if (certificate != null) + { + _key = (certificate as X509Certificate2)?.Key; + if (_key == null) + { + throw new AuthenticationException("The " + (isServer ? "server" : "client") + " certificate has no private key: give an X509Certificate2 made with its key."); + } + + // Parsed now, so a certificate that doesn't parse fails here rather than in the handshake. + certificate.GetCertificates(); + } + + // The CA given to the stream, else the CertificateManager's, as nanoFramework falls back on the device's store. + _trusted = ca != null ? ca.GetCertificates() : null; + + _crypto = new BcTlsCrypto(new SecureRandom()); + } + + /// + /// Creates a server context (nanoFramework: SecureServerInit). + /// + /// + /// : a Cosmos kernel has no device certificate store. + /// + internal static SslNative SecureServerInit( + SslProtocols sslProtocols, + SslVerification sslCertVerify, + X509Certificate certificate, + X509Certificate ca, + bool useDeviceCertificate) + { + if (certificate == null) + { + throw new AuthenticationException("A TLS server needs a certificate."); + } + + return new SslNative(true, sslProtocols, sslCertVerify, certificate, ca); + } + + /// + /// Creates a client context (nanoFramework: SecureClientInit). + /// + /// + /// : a Cosmos kernel has no device certificate store. + /// + internal static SslNative SecureClientInit( + SslProtocols sslProtocols, + SslVerification sslCertVerify, + X509Certificate certificate, + X509Certificate ca, + bool useDeviceCertificate) + { + return new SslNative(false, sslProtocols, sslCertVerify, certificate, ca); + } + + /// + /// Runs the handshake as the server (nanoFramework: SecureAccept). + /// + internal void SecureAccept(NetworkStream transport) + { + _transport = transport; + + TlsServerProtocol protocol = new TlsServerProtocol(); + _protocol = protocol; + + try + { + protocol.Accept(new Server(this)); + } + catch (Exception e) + { + throw Failure(null, e); + } + + Handshake(null, transport.ReadTimeout); + } + + /// + /// Starts the handshake as the server without waiting for the client: goes on + /// with it as its messages arrive. Cosmos: for HttpListener, whose one thread serves every connection. + /// + internal void BeginAccept(NetworkStream transport) + { + _transport = transport; + + TlsServerProtocol protocol = new TlsServerProtocol(); + _protocol = protocol; + + try + { + protocol.Accept(new Server(this)); + } + catch (Exception e) + { + throw Failure(null, e); + } + } + + /// + /// Goes on with a handshake started, with what has arrived, without waiting. + /// + /// Whether the handshake is complete. + internal bool AdvanceHandshake() + { + // As in Handshake. + Properties.SetThreadInt32(Properties.RsaMaxMRTests, 0); + + bool complete = false; + Exception error = null; + try + { + complete = StepHandshake(); + } + catch (Exception e) + { + error = e; + } + + Properties.RemoveThreadProperty(Properties.RsaMaxMRTests); + + if (error != null) + { + throw error; + } + + return complete; + } + + private bool StepHandshake() + { + Socket socket = _transport._socket; + + while (!_protocol.IsConnected) + { + if (_protocol.IsClosed) + { + throw new AuthenticationException(Describe(null) + " failed: the TLS session was closed."); + } + + SendOutput(); + + int available = socket.Available; + if (available == 0) + { + // Read before Poll, as in NetworkStream.WaitForData. + bool connected = socket.Connected; + if (socket.Poll(0, SelectMode.SelectRead)) + { + if (socket.Available > 0) + { + continue; + } + + throw new AuthenticationException(Describe(null) + " failed: the peer closed the connection."); + } + + if (!connected) + { + throw new IOException("The connection was closed."); + } + + return false; + } + + int read = ReceiveAvailable(socket, available); + try + { + _protocol.OfferInput(ReceiveBuffer, 0, read); + } + catch (Exception e) + { + // The alert BouncyCastle queued tells the peer why. + SendOutputQuietly(); + throw Failure(null, e); + } + } + + // The client's Finished in TLS 1.3, the server's in a resumed TLS 1.2 handshake. + SendOutput(); + return true; + } + + /// + /// Runs the handshake as the client (nanoFramework: SecureConnect). + /// + internal void SecureConnect(string targetHost, NetworkStream transport) + { + _transport = transport; + + TlsClientProtocol protocol = new TlsClientProtocol(); + _protocol = protocol; + + try + { + protocol.Connect(new Client(this, targetHost)); + } + catch (Exception e) + { + throw Failure(targetHost, e); + } + + Handshake(targetHost, transport.ReadTimeout); + } + + /// + /// Reads decrypted data, waiting up to for some (nanoFramework: SecureRead). + /// + /// The number of bytes read, 0 once the peer has closed the session or the connection. + internal int SecureRead(byte[] buffer, int offset, int size, int timeout_ms) + { + while (_protocol.ApplicationDataAvailable == 0) + { + if (_protocol.IsClosed || _inputClosed) + { + return 0; + } + + int read = Receive(timeout_ms); + if (read == 0) + { + // Closed without a close_notify alert: the end of the data, as the HTTP code knows its length. + _inputClosed = true; + return 0; + } + + Offer(read); + } + + return _protocol.ReadInput(buffer, offset, size); + } + + /// + /// Encrypts and sends data (nanoFramework: SecureWrite). + /// + /// The number of bytes written. + internal int SecureWrite(byte[] buffer, int offset, int size, int timeout_ms) + { + try + { + _protocol.WriteApplicationData(buffer, offset, size); + } + catch (Exception e) + { + SendOutputQuietly(); + throw new IOException("TLS: " + e.Message, e); + } + + SendOutput(); + return size; + } + + /// + /// The number of decrypted bytes that can be read without waiting (nanoFramework: DataAvailable). + /// + internal int DataAvailable() + { + // What has arrived is decrypted first: nanoFramework's native layer does it as data comes in. + Socket socket = _transport._socket; + + int available; + while (!_protocol.IsClosed && !_inputClosed && (available = socket.Available) > 0) + { + int read = ReceiveAvailable(socket, available); + if (read <= 0) + { + break; + } + + Offer(read); + } + + return _protocol.ApplicationDataAvailable; + } + + /// + /// Sends the close_notify alert and closes the socket (nanoFramework: SecureCloseSocket). + /// + internal void SecureCloseSocket() + { + if (_transport == null) + { + return; + } + + try + { + // Not to a peer that has closed the connection: Cosmos's Send would throw. + if (_protocol != null && !_protocol.IsClosed && _transport._socket.Connected) + { + _protocol.Close(); + SendOutput(); + } + } + catch + { + // The peer may be gone already; the socket is closed all the same. + } + + CloseSocket(_transport._socket); + } + + /// + /// Releases the context (nanoFramework: ExitSecureContext). + /// + internal void ExitSecureContext() + { + // Nothing to free: the stream drops this context, and the garbage collector takes it. Its fields stay set, + // so a read or write on another thread meanwhile fails on the closed socket, with an exception, rather + // than on null (a kernel panic on Cosmos). + } + + /// + /// Closes a socket, which may have been closed already. + /// + internal static void CloseSocket(Socket socket) + { + try + { + socket.Close(); + } + catch + { + // Closed already. + } + } + + private byte[] ReceiveBuffer => _receiveBuffer ??= new byte[ReceiveBufferSize]; + + private void Handshake(string peer, int timeout) + { + if (timeout == Timeout.Infinite) + { + timeout = DefaultHandshakeTimeout; + } + + // BouncyCastle tests every RSA modulus it meets for primality (Miller-Rabin), certificates' included, + // which is slow in managed code and checks nothing a signature check needs. Restored on both paths below, + // not in a finally: a Cosmos kernel skips them while an exception unwinds. + Properties.SetThreadInt32(Properties.RsaMaxMRTests, 0); + + Exception error = null; + try + { + RunHandshake(peer, timeout); + } + catch (Exception e) + { + error = e; + } + + Properties.RemoveThreadProperty(Properties.RsaMaxMRTests); + + if (error != null) + { + throw error; + } + } + + private void RunHandshake(string peer, int timeout) + { + // The whole handshake within the timeout, which a peer trickling its messages would otherwise stretch: + // a server handshakes on the thread serving every connection. + long start = Stopwatch.GetTimestamp(); + + while (!_protocol.IsConnected) + { + if (_protocol.IsClosed) + { + throw new AuthenticationException(Describe(peer) + " failed: the TLS session was closed."); + } + + SendOutput(); + + int left = timeout - (int)Stopwatch.GetElapsedTime(start).TotalMilliseconds; + if (left <= 0) + { + throw new IOException(Describe(peer) + " took longer than " + timeout + " ms.", new SocketException((int)SocketError.TimedOut)); + } + + int read = Receive(left); + if (read == 0) + { + throw new AuthenticationException(Describe(peer) + " failed: the peer closed the connection."); + } + + try + { + _protocol.OfferInput(ReceiveBuffer, 0, read); + } + catch (Exception e) + { + // The alert BouncyCastle queued tells the peer why. + SendOutputQuietly(); + throw Failure(peer, e); + } + } + + // The client's Finished in TLS 1.3, the server's in a resumed TLS 1.2 handshake. + SendOutput(); + } + + private int Receive(int timeout) + { + int available = _transport.WaitForData(timeout); + if (available == 0) + { + return 0; + } + + return ReceiveAvailable(_transport._socket, available); + } + + private int ReceiveAvailable(Socket socket, int available) + { + try + { + return socket.Receive(ReceiveBuffer, 0, Math.Min(available, ReceiveBufferSize), SocketFlags.None); + } + catch (Exception e) + { + // Cosmos's Receive throws InvalidOperationException for a socket closed meanwhile. + throw new IOException("Receiving failed: " + e.Message, e); + } + } + + private void Offer(int count) + { + try + { + _protocol.OfferInput(ReceiveBuffer, 0, count); + } + catch (Exception e) + { + SendOutputQuietly(); + throw new IOException("TLS: " + e.Message, e); + } + + // An alert, a key update or the answer to a close_notify. That answer is a courtesy, to a peer that may + // have closed the connection already (Cosmos's Send then throws), and what came with its close_notify is + // still to be read. + if (_protocol.IsClosed) + { + SendOutputQuietly(); + } + else + { + SendOutput(); + } + } + + private void SendOutput() + { + int count = _protocol.GetAvailableOutputBytes(); + if (count == 0) + { + return; + } + + byte[] output = new byte[count]; + count = _protocol.ReadOutput(output, 0, count); + + Socket socket = _transport._socket; + int sent = 0; + while (sent < count) + { + int bytes; + try + { + bytes = socket.Send(output, sent, count - sent, SocketFlags.None); + } + catch (Exception e) + { + // Cosmos's Send throws a plain Exception once the peer has closed the connection. + throw new IOException("Sending failed: " + e.Message, e); + } + + if (bytes <= 0) + { + throw new IOException("The connection was closed while sending."); + } + + sent += bytes; + } + } + + private void SendOutputQuietly() + { + try + { + SendOutput(); + } + catch + { + // The handshake has failed already; the alert is a courtesy. + } + } + + private static string Describe(string peer) + { + return string.IsNullOrEmpty(peer) ? "The TLS handshake" : "The TLS handshake with " + peer; + } + + private static AuthenticationException Failure(string peer, Exception e) + { + // BouncyCastle wraps what fails in a callback (the certificate check, for one) in an internal_error alert: + // the innermost exception says why. + string reason = e.Message; + for (Exception inner = e.InnerException; inner != null; inner = inner.InnerException) + { + if (!string.IsNullOrEmpty(inner.Message)) + { + reason = inner.Message; + } + } + + return new AuthenticationException(Describe(peer) + " failed: " + reason, e); + } + + private ProtocolVersion[] SupportedVersions(ProtocolVersion[] defaults) + { + // SslProtocols.None lets BouncyCastle choose (TLS 1.3 and 1.2), as it lets the system choose in .NET. + if (_protocols == SslProtocols.None) + { + return defaults; + } + + List versions = new List(); + if ((_protocols & SslProtocols.Tls13) != 0) + { + versions.Add(ProtocolVersion.TLSv13); + } + + if ((_protocols & SslProtocols.Tls12) != 0) + { + versions.Add(ProtocolVersion.TLSv12); + } + + return versions.ToArray(); + } + + /// + /// Checks the certificate the peer sent, as mbedTLS's verify mode does. + /// + /// The peer's certificate chain, leaf first. + /// The host the certificate must name, or null for a client certificate. + private void VerifyPeer(Certificate chain, string host) + { + // Cosmos: certificates whose key BouncyCastle would dereference null on are refused first, verification + // or not: it reads the peer's key after this, and the verifier its issuers' (CertificateVerifier.HasReadableKey). + for (int i = 0; chain != null && i < chain.Length; i++) + { + var certificate = new Org.BouncyCastle.X509.X509Certificate(chain.GetCertificateAt(i).GetEncoded()); + if (!CertificateVerifier.HasReadableKey(certificate)) + { + throw new TlsFatalAlert(AlertDescription.unsupported_certificate, "The certificate of " + certificate.SubjectDN + " has a key of a kind that isn't supported."); + } + } + + if (_verification == SslVerification.NoVerification) + { + return; + } + + if (chain == null || chain.IsEmpty) + { + // A server that only asked (VerifyPeer) goes on without one. + if (_isServer && _verification == SslVerification.VerifyPeer) + { + return; + } + + throw new TlsFatalAlert(_isServer ? AlertDescription.certificate_required : AlertDescription.bad_certificate, "The peer sent no certificate."); + } + + Org.BouncyCastle.X509.X509Certificate[] certificates = new Org.BouncyCastle.X509.X509Certificate[chain.Length]; + for (int i = 0; i < certificates.Length; i++) + { + certificates[i] = new Org.BouncyCastle.X509.X509Certificate(chain.GetCertificateAt(i).GetEncoded()); + } + + string error = CertificateVerifier.Verify(certificates, host, _trusted ?? CertificateManager.GetTrustedCertificates(), DateTime.UtcNow); + if (error != null) + { + throw new TlsFatalAlert(AlertDescription.bad_certificate, error); + } + } + + /// + /// The signer for this side's certificate, or null when there is none or no signature algorithm of the peer fits its key. + /// + private TlsCredentialedSigner Credentials(TlsContext context, IList peerAlgorithms) + { + if (_key == null) + { + return null; + } + + bool tls13 = TlsUtilities.IsTlsV13(context); + + SignatureAndHashAlgorithm algorithm = ChooseSignatureAlgorithm(_key, peerAlgorithms, tls13); + if (algorithm == null) + { + return null; + } + + Org.BouncyCastle.X509.X509Certificate[] certificates = _certificate.GetCertificates(); + + Certificate chain; + if (tls13) + { + CertificateEntry[] entries = new CertificateEntry[certificates.Length]; + for (int i = 0; i < entries.Length; i++) + { + entries[i] = new CertificateEntry(_crypto.CreateCertificate(certificates[i].GetEncoded()), null); + } + + chain = new Certificate(TlsUtilities.EmptyBytes, entries); + } + else + { + TlsCertificate[] list = new TlsCertificate[certificates.Length]; + for (int i = 0; i < list.Length; i++) + { + list[i] = _crypto.CreateCertificate(certificates[i].GetEncoded()); + } + + chain = new Certificate(list); + } + + return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(context), _crypto, _key, chain, algorithm); + } + + private static SignatureAndHashAlgorithm ChooseSignatureAlgorithm(AsymmetricKeyParameter key, IList offered, bool tls13) + { + if (offered == null) + { + if (tls13) + { + return null; + } + + // A TLS 1.2 peer that sent no signature_algorithms takes SHA-1 (RFC 5246, 7.4.1.4.1). + if (key is RsaKeyParameters) + { + return new SignatureAndHashAlgorithm(HashAlgorithm.sha1, SignatureAlgorithm.rsa); + } + + if (key is ECPrivateKeyParameters) + { + return new SignatureAndHashAlgorithm(HashAlgorithm.sha1, SignatureAlgorithm.ecdsa); + } + + return null; + } + + foreach (SignatureAndHashAlgorithm algorithm in offered) + { + if (Fits(key, algorithm, tls13)) + { + return algorithm; + } + } + + return null; + } + + private static bool Fits(AsymmetricKeyParameter key, SignatureAndHashAlgorithm algorithm, bool tls13) + { + short signature = algorithm.Signature; + + if (key is RsaKeyParameters) + { + // TLS 1.3 signs the handshake with RSA-PSS only (RFC 8446, 4.2.3). + return signature == SignatureAlgorithm.rsa_pss_rsae_sha256 + || signature == SignatureAlgorithm.rsa_pss_rsae_sha384 + || signature == SignatureAlgorithm.rsa_pss_rsae_sha512 + || (!tls13 && signature == SignatureAlgorithm.rsa); + } + + if (key is ECPrivateKeyParameters ec) + { + if (signature != SignatureAlgorithm.ecdsa) + { + return false; + } + + if (!tls13) + { + return true; + } + + // TLS 1.3 ties the hash to the curve (ecdsa_secp256r1_sha256...). + int bits = ec.Parameters.Curve.FieldSize; + short hash = algorithm.Hash; + return (bits == 256 && hash == HashAlgorithm.sha256) + || (bits == 384 && hash == HashAlgorithm.sha384) + || (bits == 521 && hash == HashAlgorithm.sha512); + } + + if (key is Ed25519PrivateKeyParameters) + { + return signature == SignatureAlgorithm.ed25519; + } + + if (key is Ed448PrivateKeyParameters) + { + return signature == SignatureAlgorithm.ed448; + } + + return false; + } + + private static bool IsAddress(string host) + { + // An IPv4 literal (see CertificateVerifier.ParseIPv4), or an IPv6 one. + return CertificateVerifier.ParseIPv4(host) != null || host.IndexOf(':') >= 0; + } + + /// + /// BouncyCastle's TLS client, configured as nanoFramework's SecureClientInit configures mbedTLS. + /// + private sealed class Client : DefaultTlsClient + { + private readonly SslNative _context; + private readonly string _targetHost; + + internal Client(SslNative context, string targetHost) + : base(context._crypto) + { + _context = context; + _targetHost = targetHost; + } + + protected override ProtocolVersion[] GetSupportedVersions() + { + return _context.SupportedVersions(base.GetSupportedVersions()); + } + + protected override int[] GetSupportedCipherSuites() + { + return TlsUtilities.GetSupportedCipherSuites(Crypto, ClientCipherSuites); + } + + protected override IList GetSupportedGroups(IList namedGroupRoles) + { + return new List(Groups); + } + + protected override IList GetProtocolNames() + { + return new List { ProtocolName.Http_1_1 }; + } + + public override bool RequiresCloseNotify() + { + // Many servers close without one; the HTTP code knows where the data ends. + return false; + } + + protected override IList GetSniServerNames() + { + // Server Name Indication names a host, never an address (RFC 6066, 3). + if (string.IsNullOrEmpty(_targetHost) || IsAddress(_targetHost)) + { + return null; + } + + return new List { new ServerName(NameType.host_name, Encoding.ASCII.GetBytes(_targetHost)) }; + } + + public override TlsAuthentication GetAuthentication() + { + return new ClientAuthentication(this); + } + + private sealed class ClientAuthentication : TlsAuthentication + { + private readonly Client _client; + + internal ClientAuthentication(Client client) + { + _client = client; + } + + public void NotifyServerCertificate(TlsServerCertificate serverCertificate) + { + _client._context.VerifyPeer(serverCertificate.Certificate, _client._targetHost); + } + + public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest) + { + // The client certificate given to AuthenticateAsClient, if any. + return _client._context.Credentials(_client.m_context, certificateRequest.SupportedSignatureAlgorithms); + } + } + } + + /// + /// BouncyCastle's TLS server, configured as nanoFramework's SecureServerInit configures mbedTLS. + /// + private sealed class Server : DefaultTlsServer + { + private readonly SslNative _context; + + internal Server(SslNative context) + : base(context._crypto) + { + _context = context; + } + + protected override ProtocolVersion[] GetSupportedVersions() + { + return _context.SupportedVersions(base.GetSupportedVersions()); + } + + protected override int[] GetSupportedCipherSuites() + { + // The TLS 1.2 suites whose signature the server's key can make; TLS 1.3's don't depend on the key. + return TlsUtilities.GetSupportedCipherSuites(Crypto, _context._key is RsaKeyParameters ? ServerCipherSuitesRsa : ServerCipherSuitesEcdsa); + } + + public override int[] GetSupportedGroups() + { + return Groups; + } + + public override TlsCredentials GetCredentials() + { + TlsCredentialedSigner signer = _context.Credentials(m_context, m_context.SecurityParameters.ClientSigAlgs); + if (signer == null) + { + throw new TlsFatalAlert(AlertDescription.handshake_failure, "None of the client's signature algorithms fits the server's key."); + } + + return signer; + } + + public override CertificateRequest GetCertificateRequest() + { + if (_context._verification == SslVerification.NoVerification) + { + return null; + } + + IList algorithms = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); + + if (TlsUtilities.IsTlsV13(m_context)) + { + return new CertificateRequest(TlsUtilities.EmptyBytes, algorithms, null, null); + } + + return new CertificateRequest(new short[] { ClientCertificateType.rsa_sign, ClientCertificateType.ecdsa_sign }, algorithms, null); + } + + public override void NotifyClientCertificate(Certificate clientCertificate) + { + _context.VerifyPeer(clientCertificate, null); + } + } + } +} diff --git a/src/Cosmos.Network.Http/Security/SslStream.cs b/src/Cosmos.Network.Http/Security/SslStream.cs new file mode 100644 index 0000000..cf0c3be --- /dev/null +++ b/src/Cosmos.Network.Http/Security/SslStream.cs @@ -0,0 +1,456 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +// nanoFramework's System.Net SslStream (nanoFramework.System.Net/Security/SslStream.cs). Cosmos: the TLS that +// nanoFramework runs natively (SslNative, mbedTLS) is BouncyCastle's managed TLS, in SslNative.cs: .NET's SslStream +// needs OpenSSL, which a Cosmos kernel doesn't have. Each native call becomes a call to the SslNative object the +// stream holds in place of nanoFramework's SSL context handle. + +using System.IO; +using System.Net.Sockets; +using System.Runtime.CompilerServices; + +namespace Cosmos.Network.Http +{ + /// + /// Provides a stream used for client-server communication that uses the Secure Socket Layer (SSL) security + /// protocol to authenticate the server and optionally the client. + /// + /// + /// Cosmos: internal, so it doesn't clash with System.Net.Security.SslStream. + /// + internal class SslStream : NetworkStream + { + private SslVerification _sslVerification; + private bool _useStoredDeviceCertificate = false; + + // Internal flags + // Cosmos: the TLS session, null where nanoFramework's native context handle is -1. + private SslNative _sslContext; + private bool _isServer; + + /// + /// Option for SSL verification. + /// The default behaviour is . + /// + public SslVerification SslVerification { get => _sslVerification; set => _sslVerification = value; } + + /// + /// Option to use the certificate stored in the device as client or server certificate. + /// The default option is . + /// + /// + /// This property is exclusive of .NET nanoFramework. + /// In case there is no device certificate stored, the authentication will use whatever is provided (or not) in the parameter of the method being called. + /// Cosmos: a kernel has no device certificate store, so this changes nothing. + /// + public bool UseStoredDeviceCertificate { get => _useStoredDeviceCertificate; set => _useStoredDeviceCertificate = value; } + + //--// + + /// + /// Initializes a new instance of the SslStream class using the specified Socket. + /// + /// A valid socket that currently has a TCP connection. + /// is . + /// is not connected. -or- The property of is not . + /// + /// The SslStream maintains the lifetime of the socket. When the SslStream object is disposed, + /// the underlying TCP socket will be closed. + /// + public SslStream(Socket socket) + : base(socket, false) + { + _sslContext = null; + _isServer = false; + + _sslVerification = SslVerification.CertificateRequired; + } + + /// + /// Called by clients to authenticate the server and optionally the client in a client-server connection. + /// The authentication process uses the specified SSL protocols. + /// + /// The name of the server that will share this SslStream. + /// The value that represents the protocol used for authentication. + /// Authentication has already been performed on this stream. + /// The TLS handshake failed: no common protocol or cipher suite, a certificate that doesn't verify, or an alert from the peer. + /// The connection was closed, or nothing was received for , during the handshake. + public void AuthenticateAsClient( + string targetHost, + SslProtocols enabledSslProtocols) + { + Authenticate(false, + targetHost, + null, + null, + enabledSslProtocols); + } + + /// + /// Called by clients to authenticate the server and optionally the client in a client-server connection. + /// The authentication process uses the specified certificate collections and SSL protocols. + /// + /// The name of the server that will share this SslStream. + /// The client certificate. + /// The value that represents the protocol used for authentication. + /// + /// Instead of providing the client certificate in the parameter the property can be used to use the certificate stored in the device. + /// + /// Authentication has already been performed on this stream. + /// The TLS handshake failed: no common protocol or cipher suite, a certificate that doesn't verify, or an alert from the peer. + /// The connection was closed, or nothing was received for , during the handshake. + public void AuthenticateAsClient( + string targetHost, + X509Certificate clientCertificate, + SslProtocols enabledSslProtocols) + { + Authenticate(false, + targetHost, + clientCertificate, + null, + enabledSslProtocols); + } + + /// + /// Called by clients to authenticate the server and optionally the client in a client-server connection. + /// The authentication process uses the specified certificate collections and SSL protocols. + /// + /// The name of the server that will share this SslStream. + /// The client certificate. + /// Certificate Authority certificate to use for authentication with the server. + /// The value that represents the protocol used for authentication. + /// + /// Instead of providing the client certificate in the parameter the property can be used to use the certificate stored in the device. + /// + /// Authentication has already been performed on this stream. + /// The TLS handshake failed: no common protocol or cipher suite, a certificate that doesn't verify, or an alert from the peer. + /// The connection was closed, or nothing was received for , during the handshake. + public void AuthenticateAsClient( + string targetHost, + X509Certificate clientCertificate, + X509Certificate ca, + SslProtocols enabledSslProtocols) + { + Authenticate(false, + targetHost, + clientCertificate, + ca, + enabledSslProtocols); + } + + /// + /// Called by servers to authenticate the server and optionally the client in a client-server connection using the specified certificate, + /// verification requirements and security protocol. + /// + /// The certificate used to authenticate the server. + /// The protocols that may be used for authentication. + /// + /// Instead of providing the server certificate in the parameter the property can be used to use the certificate stored in the device. + /// + /// Authentication has already been performed on this stream. + /// The TLS handshake failed: no common protocol or cipher suite, a certificate that doesn't verify, or an alert from the peer. + /// The connection was closed, or nothing was received for , during the handshake. + public void AuthenticateAsServer( + X509Certificate serverCertificate, + SslProtocols enabledSslProtocols) + { + Authenticate(true, + "", + serverCertificate, + null, + enabledSslProtocols); + } + + /// + /// Called by servers to authenticate the server and optionally the client in a client-server connection using the specified certificates, requirements and security protocol. + /// + /// The X509Certificate used to authenticate the server. + /// A value that specifies whether the client is asked for a certificate for authentication. Note that this is only a request — if no certificate is provided, the server still accepts the connection request. + /// The protocols that may be used for authentication. + /// + /// Instead of providing the server certificate in the parameter the property can be used to use the certificate stored in the device. + /// + /// Authentication has already been performed on this stream. + /// The TLS handshake failed: no common protocol or cipher suite, a certificate that doesn't verify, or an alert from the peer. + /// The connection was closed, or nothing was received for , during the handshake. + public void AuthenticateAsServer( + X509Certificate serverCertificate, + bool clientCertificateRequired, + SslProtocols enabledSslProtocols) + { + SslVerification = clientCertificateRequired ? SslVerification.VerifyClientOnce : SslVerification.NoVerification; + + Authenticate(true, + "", + serverCertificate, + null, + enabledSslProtocols); + } + + internal void Authenticate(bool isServer, string targetHost, X509Certificate certificate, X509Certificate ca, SslProtocols enabledSslProtocols) + { + if (null != _sslContext) throw new InvalidOperationException("The stream is already authenticated."); + + _isServer = isServer; + + try + { + if (isServer) + { + _sslContext = SslNative.SecureServerInit( + enabledSslProtocols, + _sslVerification, + certificate, + ca, + _useStoredDeviceCertificate); + + _sslContext.SecureAccept(this); + } + else + { + _sslContext = SslNative.SecureClientInit( + enabledSslProtocols, + _sslVerification, + certificate, + ca, + _useStoredDeviceCertificate); + + _sslContext.SecureConnect(targetHost, this); + } + } + catch + { + if (_sslContext != null) + { + _sslContext.ExitSecureContext(); + _sslContext = null; + } + + throw; + } + } + + /// + /// Cosmos: starts authenticating as the server without waiting for the client, for HttpListener, whose one + /// thread serves every connection: goes on with it. + /// + /// The certificate used to authenticate the server. + /// The protocols that may be used for authentication. + /// Authentication has already been performed on this stream. + /// The certificate can't be used. + internal void BeginAuthenticateAsServer(X509Certificate serverCertificate, SslProtocols enabledSslProtocols) + { + if (null != _sslContext) throw new InvalidOperationException("The stream is already authenticated."); + + _isServer = true; + _sslVerification = SslVerification.NoVerification; + + try + { + _sslContext = SslNative.SecureServerInit(enabledSslProtocols, _sslVerification, serverCertificate, null, _useStoredDeviceCertificate); + _sslContext.BeginAccept(this); + } + catch + { + _sslContext = null; + throw; + } + } + + /// + /// Cosmos: goes on authenticating as the server with what the client has sent, without waiting. + /// + /// Whether the stream is authenticated. + /// The TLS handshake failed. + /// The connection was closed. + internal bool AdvanceAuthentication() + { + if (_disposed) throw new ObjectDisposedException(nameof(SslStream)); + + return Context.AdvanceHandshake(); + } + + /// + /// Gets a value that indicates whether the local side of the connection used by this SslStream was authenticated as the server. + /// + public bool IsServer { get { return _isServer; } } + + /// + /// Cosmos: the session, read once, as a Dispose on another thread may clear it: an exception rather than a null + /// dereference (a kernel panic on Cosmos). + /// + private SslNative Context + { + get + { + SslNative context = _sslContext; + if (context == null) + { + if (_disposed) throw new ObjectDisposedException(nameof(SslStream)); + throw new IOException("The stream is not authenticated."); + } + + return context; + } + } + + /// + /// Gets the number of bytes of decrypted data available to be read from the stream. + /// + public override long Length + { + get + { + return Context.DataAvailable(); + } + } + + /// + /// Gets a value that indicates whether decrypted data is available on the stream to be read. + /// + public override bool DataAvailable + { + get + { + return Context.DataAvailable() > 0; + } + } + + /// + /// + /// + ~SslStream() + { + // Do not re-create Dispose clean-up code here. + // Calling Dispose(false) is optimal in terms of + // readability and maintainability. + Dispose(false); + } + + /// + /// Releases the unmanaged resources used by the SslStream and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + /// + /// Cosmos: not synchronized, as nanoFramework's is: a Cosmos kernel doesn't release the lock while an exception + /// unwinds. + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + + SslNative context = _sslContext; + _sslContext = null; + + // Cosmos: nanoFramework's native SecureCloseSocket sends the close_notify alert and closes the socket, + // which has no native handle to mark here. + if (context != null) + { + context.SecureCloseSocket(); + context.ExitSecureContext(); + } + else + { + SslNative.CloseSocket(_socket); + } + } + } + + /// + /// Reads data from this stream and stores it in the specified array. + /// + /// An array that receives the bytes read from this stream. + /// An integer that contains the zero-based location in buffer at which to begin storing the data read from this stream. + /// The maximum number of bytes to read from this stream. + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(SslStream)); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(); + } + + if (count < 0 || count > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(); + } + + return Context.SecureRead(buffer, offset, count, ReadTimeout); + } + + /// + /// Write the specified number of bytes to the underlying stream using the specified buffer and offset. + /// + /// An array that supplies the bytes written to the stream. + /// The zero-based location in buffer at which to begin reading bytes to be written to the stream. + /// The number of bytes to read from buffer. + /// is . + /// + /// + /// or is less than zero + /// + /// + /// -or- + /// + /// + /// is greater than the length of . + /// + /// + /// -or- + /// + /// + /// + is greater than the length of . + /// + /// + /// The stream has been disposed. + /// The write operation failed. + public override void Write( + byte[] buffer, + int offset, + int count) + { + if (buffer == null) + { + throw new ArgumentNullException(); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(SslStream)); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(); + } + + if (count < 0 || count > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(); + } + + int written = Context.SecureWrite(buffer, offset, count, WriteTimeout); + + if (written <= 0 && count > 0) + { + throw new IOException("The TLS write failed."); + } + } + } +} diff --git a/src/Cosmos.Network.Http/Sockets/NetworkStream.cs b/src/Cosmos.Network.Http/Sockets/NetworkStream.cs new file mode 100644 index 0000000..277efd2 --- /dev/null +++ b/src/Cosmos.Network.Http/Sockets/NetworkStream.cs @@ -0,0 +1,422 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +// nanoFramework's System.Net NetworkStream (nanoFramework.System.Net/Sockets/NetworkStream.cs), which the HTTP +// code reads as it reads it on nanoFramework: Length is what can be read without waiting, and Read waits for at +// least one byte, returning 0 only once the peer has closed. Cosmos: .NET's NetworkStream does neither (its Length +// throws, and Cosmos's Socket.Receive returns 0 while the connection is still open), so the Cosmos port waits +// itself, in Socket.Poll and Available, and keeps the timeouts, which Cosmos's sockets don't have. + +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; + +namespace Cosmos.Network.Http +{ + /// + /// Provides the underlying stream of data for network access. + /// + /// + /// Cosmos: internal, so it doesn't clash with System.Net.Sockets.NetworkStream. + /// + internal class NetworkStream : Stream + { + // Cosmos: how long one Poll waits for data before the wait loop checks the timeout. Cosmos's Poll returns at + // once, which makes the loop spin; a desktop's waits this long. + private const int PollIntervalUs = 10_000; + + // Summary: + // Internal members + + // Internal Socket object + internal Socket _socket; + + /// + /// Internal property used to store the socket type + /// + protected int _socketType; + + /// + /// Internal endpoint ref of the remote peer + /// + protected EndPoint _remoteEndPoint; + + // Internal flags + private bool _ownsSocket; + + /// + /// Internal disposed flag + /// + protected bool _disposed; + + // Cosmos: the read and write timeouts, which nanoFramework keeps on the socket (Socket.ReceiveTimeout and + // SendTimeout throw on Cosmos). + private int _readTimeout = System.Threading.Timeout.Infinite; + private int _writeTimeout = System.Threading.Timeout.Infinite; + + /// + /// Creates a new instance of the class for the specified . + /// + /// The that the will use to send and receive data. + /// is . + /// is not connected. -or- The property of is not . + public NetworkStream(Socket socket) + : this(socket, false) + { + } + + /// + /// Initializes a new instance of the class for the specified + /// with the specified ownership. + /// + /// The that the will + /// use to send and receive data. + /// to indicate that the will take ownership of the ; + /// otherwise, . + /// is . + /// is not connected. -or- The property of is not . + public NetworkStream(Socket socket, bool ownsSocket) + { + if (socket == null) throw new ArgumentNullException(nameof(socket)); + + // Cosmos: Socket.RemoteEndPoint doesn't throw for a socket that isn't connected, and Socket.SocketType is + // never set (the socket constructor is a plug), so Connected is checked instead of both. A connection + // the peer has already closed counts, as Cosmos's sockets keep what it sent: a client's request and its + // FIN may come before Accept returns. + if (!socket.Connected && socket.Available == 0 && !socket.Poll(0, SelectMode.SelectRead)) + { + throw new IOException("The socket is not connected."); + } + + _remoteEndPoint = socket.RemoteEndPoint; + + // Set the internal socket + _socket = socket; + + // set the socket type + _socketType = (int)SocketType.Stream; + + _ownsSocket = ownsSocket; + } + + /// + /// Gets a value that indicates whether the System.Net.Sockets.NetworkStream supports reading. + /// + /// true if data can be read from the stream; otherwise, false. The default value is true. + /// + /// If CanRead is true, allows calls to the method. Provide the appropriate FileAccess enumerated value in the constructor to set + /// the readability and write-ability of the . The CanRead property is set when the is initialized. + /// + public override bool CanRead { get { return true; } } + + /// + /// Gets a value that indicates whether the stream supports seeking. This property is not currently supported.This property always returns false. + /// + /// false in all cases to indicate that System.Net.Sockets.NetworkStream cannot seek a specific location in the stream. + public override bool CanSeek { get { return false; } } + + /// + /// Indicates whether timeout properties are usable for System.Net.Sockets.NetworkStream. + /// + /// true in all cases. + public override bool CanTimeout { get { return true; } } + + /// + /// Gets a value that indicates whether the System.Net.Sockets.NetworkStream supports writing. + /// + /// true if data can be written to the System.Net.Sockets.NetworkStream; otherwise, false. The default value is true. + public override bool CanWrite { get { return true; } } + + /// + /// Gets or sets the amount of time that a read operation blocks waiting for data. + /// + /// A Int32 that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, Infinite, specifies that the read operation does not time out. + public override int ReadTimeout + { + get { return _readTimeout; } + set + { + if (value == 0 || value < System.Threading.Timeout.Infinite) throw new ArgumentOutOfRangeException(nameof(value)); + + _readTimeout = value; + } + } + + /// + /// Gets or sets the amount of time that a write operation blocks waiting for data. + /// + /// A Int32 that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, Infinite, specifies that the write operation does not time out. + public override int WriteTimeout + { + get { return _writeTimeout; } + set + { + if (value == 0 || value < System.Threading.Timeout.Infinite) throw new ArgumentOutOfRangeException(nameof(value)); + + _writeTimeout = value; + } + } + + /// + /// Gets the length of the data available on the stream. + /// This property is not currently supported and always throws a NotSupportedException. + /// + /// The length of the data available on the stream. + public override long Length + { + get + { + if (_disposed) throw new ObjectDisposedException(nameof(NetworkStream)); + + return _socket.Available; + } + } + + /// + /// Gets or sets the current position in the stream. This property is not currently supported and always throws a NotSupportedException. + /// + /// The current position in the stream. + public override long Position + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + /// Gets a value that indicates whether data is available on the to be read. + /// + /// true if data is available on the stream to be read; otherwise, false. + public virtual bool DataAvailable + { + get + { + if (_disposed) throw new ObjectDisposedException(nameof(NetworkStream)); + + return (_socket.Available > 0); + } + } + + /// + /// Closes the after waiting the specified time to allow data to be sent. + /// + /// A 32-bit signed integer that specifies the number of milliseconds to wait to send any remaining data before closing. + /// is less than -1. + /// + /// The method frees both unmanaged and managed resources associated with the . If the owns the underlying Socket, it is closed as well. + /// + public void Close(int timeout) + { + if (timeout < -1) + throw new ArgumentOutOfRangeException(nameof(timeout)); + + System.Threading.Thread.Sleep(timeout); + + Close(); + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + /// + /// This method is called by the public Dispose method and the Finalize method. Dispose invokes the protected Dispose(Boolean) method with the disposing parameter set to true. Finalize invokes Dispose with disposing set to false. + /// When the disposing parameter is true, this method releases all resources held by any managed objects that this NetworkStream references. This method invokes the Dispose method of each referenced object. + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + // Cosmos: set first, as a finally block doesn't run while an exception unwinds on a Cosmos kernel. + _disposed = true; + + if (disposing) + { + if (_ownsSocket == true) + _socket.Close(); + } + } + } + + /// + /// Flushes data from the stream. This method is reserved for future use. + /// + public override void Flush() + { + } + + /// + /// Reads data from the NetworkStream. + /// + /// An array of type that is the location in memory to store data read from the . + /// The location in to begin storing the data to. + /// The number of bytes to read from the . + /// The total number of bytes read into the buffer between zero (0) and the requested count. The method returns zero (0) only if zero bytes were requested or if no more bytes are available because the peer socket performed a graceful shutdown. + /// The underlying is closed. -or- There was a failure while reading from the network. + /// is . + /// The is closed. + /// is less than 0 or greater than the length of . -or- is less than 0 or greater than the length of minus the value of the parameter. + /// + /// This method reads data into the parameter and returns the number of bytes successfully read. The operation reads as much data as is available, up to the number of bytes specified by the parameter. If the remote host shuts down the connection, and all available data has been received, the Read method completes immediately and return zero bytes. + /// + /// Check to see if the is readable by calling the property. If you attempt to read from a that is not readable, you will get an . + /// + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (_disposed) throw new ObjectDisposedException(nameof(NetworkStream)); + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || offset > buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || count > buffer.Length - offset) throw new ArgumentOutOfRangeException(nameof(count)); + + // Cosmos: waits here for what nanoFramework's socket waits for, then receives no more than is there, as + // Cosmos's Receive would otherwise give up and return 0 with the connection still open. + int available = WaitForData(_readTimeout); + + if (available == 0 || count == 0) + { + return 0; + } + + if (count > available) + { + count = available; + } + + try + { + return _socket.Receive(buffer, offset, count, SocketFlags.None); + } + catch (Exception e) + { + // Cosmos: Receive throws InvalidOperationException for a socket closed meanwhile. + throw new IOException("Receiving failed: " + e.Message, e); + } + } + + /// + /// Sets the current position of the stream to the given value. This method is + /// not currently supported and always throws a System.NotSupportedException. + /// + /// This parameter is not used. + /// This parameter is not used. + /// The position in the stream. + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Sets the length of the stream. This method always throws a System.NotSupportedException. + /// + /// This parameter is not used. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes data to the . + /// + /// An array of type that contains the data to write to the . + /// The location in from which to start writing data. + /// The number of bytes to write to the . + /// is less than 0 or greater than the length of . -or- is less than 0 or greater than the length of minus the value of the parameter. + /// The is closed. + /// There was a failure while writing to the network. -or- An error occurred when accessing the socket. See the Remarks section for more information. + /// is . + /// + /// The Write method starts at the specified offset and sends count bytes from the contents of buffer to the network. + /// The Write method blocks until the requested number of bytes is sent or a is thrown. + /// If you receive a , use the property to obtain + /// the specific error code, and refer to the Windows Sockets version 2 API error code documentation in MSDN for a detailed description of the error. + /// + public override void Write(byte[] buffer, int offset, int count) + { + if (_disposed) throw new ObjectDisposedException(nameof(NetworkStream)); + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || offset > buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || count > buffer.Length - offset) throw new ArgumentOutOfRangeException(nameof(count)); + + int bytesSent; + try + { + bytesSent = _socket.Send(buffer, offset, count, SocketFlags.None); + } + catch (Exception e) + { + // Cosmos: Send throws a plain Exception once the peer has closed the connection, where nanoFramework's + // throws a SocketException; callers expect an IOException from a stream. + throw new IOException("Sending failed: " + e.Message, e); + } + + if (bytesSent != count) throw new IOException("Only " + bytesSent + " of " + count + " bytes were sent."); + } + + /// + /// Waits until data can be read, the peer has closed the connection or the timeout has elapsed. + /// + /// The timeout in milliseconds, or . + /// The number of bytes that can be read, 0 once the peer has closed the connection. + /// Nothing arrived within , or the socket was closed. + /// + /// Cosmos: nanoFramework's sockets wait in Receive, Cosmos's don't, nor in Poll, so this polls. + /// + internal int WaitForData(int timeout) + { + long start = Stopwatch.GetTimestamp(); + + while (true) + { + int available = _socket.Available; + if (available > 0) + { + return available; + } + + // Cosmos: read before Poll, as Cosmos's network stack takes in packets meanwhile: a response and the + // peer's FIN arriving after Poll leave the socket not connected, with the response still to read. + bool connected = _socket.Connected; + + if (_socket.Poll(PollIntervalUs, SelectMode.SelectRead)) + { + // Readable: data arrived meanwhile, or nothing will as the peer closed the connection (0). + return _socket.Available; + } + + // Cosmos: a socket closed on this side (by a request timeout, say) is neither readable nor connected. + if (!connected) + { + throw new IOException("The connection was closed."); + } + + if (timeout != System.Threading.Timeout.Infinite && Stopwatch.GetElapsedTime(start).TotalMilliseconds >= timeout) + { + throw new IOException("Nothing was received for " + timeout + " ms.", new SocketException((int)SocketError.TimedOut)); + } + } + } + + /// + /// + /// Cosmos: nanoFramework's SpanByte overload throws NotImplementedException; .NET's Span overload reads + /// through . + /// + public override int Read(Span buffer) + { + byte[] array = new byte[buffer.Length]; + int read = Read(array, 0, array.Length); + array.AsSpan(0, read).CopyTo(buffer); + return read; + } + } +} diff --git a/src/Cosmos.Network.Http/X509Certificates/X509Certificate.cs b/src/Cosmos.Network.Http/X509Certificates/X509Certificate.cs new file mode 100644 index 0000000..c41d30c --- /dev/null +++ b/src/Cosmos.Network.Http/X509Certificates/X509Certificate.cs @@ -0,0 +1,267 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +// nanoFramework's System.Net X509Certificate (nanoFramework.System.Net/X509Certificates/X509Certificate.cs). +// Cosmos: in the Cosmos.Network.Http namespace, as .NET's X509Certificate parses through OpenSSL, which a Cosmos +// kernel doesn't have; BouncyCastle parses here what mbedTLS parses natively on nanoFramework. + +namespace Cosmos.Network.Http +{ + using System; + using System.Collections.Generic; + using System.Security.Cryptography; + using System.Text; + using Org.BouncyCastle.X509; + + /// + /// Provides methods that help you use X.509 v.3 certificates. + /// + /// + /// Supported formats: DER and PEM. + /// + public class X509Certificate + { + private readonly byte[] _certificate; + + /// + /// Contains the certificate issuer. + /// + protected string _issuer; + /// + /// Contains the subject. + /// + protected string _subject; + /// + /// Contains the effective date of the certificate. + /// + protected DateTime _effectiveDate; + /// + /// Contains the expiration date of the certificate. + /// + protected DateTime _expirationDate; + /// + /// Contains the handle. + /// + protected byte[] _handle; + /// + /// Contains the session handle. + /// + protected byte[] _sessionHandle; + + /// + /// Initializes a new instance of the X509Certificate class. + /// + public X509Certificate() + { + } + + /// + /// Initializes a new instance of the class defined from a sequence of bytes representing an X.509v3 certificate. + /// + /// A byte array containing data from an X.509 certificate. + /// + /// DER and PEM encoding are the supported formats. + /// + public X509Certificate(byte[] certificate) + { + _certificate = certificate; + + ParseCertificate( + certificate, + ref _issuer, + ref _subject, + ref _effectiveDate, + ref _expirationDate); + } + + /// + /// Initializes a new instance of the class defined from a string with the content of an X.509v3 certificate. + /// + /// A string containing a X.509 certificate. + /// + /// Supported formats: DER and PEM. + /// This methods is exclusive of .NET nanoFramework. The equivalent .NET constructor accepts a file name as the parameter. + /// + public X509Certificate(string certificate) + { + var tempCertificate = Encoding.UTF8.GetBytes(certificate); + + ////////////////////////////////////////////// + // because this is parsing from a string // + // we need to keep the terminator // + ////////////////////////////////////////////// + _certificate = new byte[tempCertificate.Length + 1]; + Array.Copy(tempCertificate, _certificate, tempCertificate.Length); + _certificate[_certificate.Length - 1] = 0; + + ParseCertificate( + _certificate, + ref _issuer, + ref _subject, + ref _effectiveDate, + ref _expirationDate); + } + + /// + /// Gets the name of the certificate authority that issued the X.509v3 certificate. + /// + /// + /// The name of the certificate authority that issued the X.509v3 certificate. + /// + public virtual string Issuer + { + get { return _issuer; } + } + + /// + /// Gets the subject distinguished name from the certificate. + /// + /// + /// The subject distinguished name from the certificate. + /// + public virtual string Subject + { + get { return _subject; } + } + + /// + /// Returns the effective date of this X.509v3 certificate. + /// + /// The effective date for this X.509 certificate. + /// + /// This methods is exclusive of nanoFramework. The equivalent .NET method is GetEffectiveDateString(). + /// + public virtual DateTime GetEffectiveDate() + { + return _effectiveDate; + } + + /// + /// Returns the expiration date of this X.509v3 certificate. + /// + /// The expiration date for this X.509 certificate. + /// + /// This methods is exclusive of nanoFramework. The equivalent .NET method is GetExpirationDateString(). + /// + public virtual DateTime GetExpirationDate() + { + return _expirationDate; + } + + /// + /// Returns the raw data for the entire X.509v3 certificate as an array of bytes. + /// + /// A byte array containing the X.509 certificate data. + public virtual byte[] GetRawCertData() + { + return _certificate; + } + + // Cosmos: what nanoFramework parses natively. A PEM bundle is described by its first certificate. + internal static void ParseCertificate( + byte[] cert, + ref string issuer, + ref string subject, + ref DateTime effectiveDate, + ref DateTime expirationDate) + { + Org.BouncyCastle.X509.X509Certificate first = ParseCertificates(cert)[0]; + + issuer = first.IssuerDN.ToString(); + subject = first.SubjectDN.ToString(); + effectiveDate = first.NotBefore; + expirationDate = first.NotAfter; + } + + /// + /// Cosmos: every certificate of the raw data, for the TLS code: a PEM bundle holds several, as a CA bundle + /// does on nanoFramework. + /// + internal Org.BouncyCastle.X509.X509Certificate[] GetCertificates() + { + if (_certificate == null) + { + throw new CryptographicException("The X509Certificate holds no certificate."); + } + + return ParseCertificates(_certificate); + } + + /// + /// Cosmos: parses DER or PEM (one certificate or a bundle) with BouncyCastle. + /// + /// No certificate could be read from . + internal static Org.BouncyCastle.X509.X509Certificate[] ParseCertificates(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + int length = EncodedLength(data); + + byte[] input = new byte[length]; + Array.Copy(data, input, length); + + IList certificates; + try + { + certificates = new X509CertificateParser().ReadCertificates(input); + } + catch (Exception e) + { + throw new CryptographicException("The certificate could not be parsed: " + e.Message, e); + } + + if (certificates == null || certificates.Count == 0) + { + throw new CryptographicException("No X.509 certificate could be read, neither DER nor PEM."); + } + + Org.BouncyCastle.X509.X509Certificate[] result = new Org.BouncyCastle.X509.X509Certificate[certificates.Count]; + certificates.CopyTo(result, 0); + return result; + } + + /// + /// Cosmos: how many bytes of hold the DER or PEM, without the terminator nanoFramework + /// ends what it gets as a string with, for mbedTLS. DER is measured by its length, as it may end with zeros. + /// + internal static int EncodedLength(byte[] data) + { + if (data.Length >= 2 && data[0] == 0x30) + { + // A DER SEQUENCE: its tag, its length (short or long form), its content. + int lengthOctets = data[1] < 0x80 ? 0 : data[1] & 0x7F; + if (lengthOctets <= 4 && data.Length >= 2 + lengthOctets) + { + long contentLength = data[1] < 0x80 ? data[1] : 0; + for (int i = 0; i < lengthOctets; i++) + { + contentLength = (contentLength << 8) | data[2 + i]; + } + + long total = 2 + lengthOctets + contentLength; + if (total <= data.Length) + { + return (int)total; + } + } + + return data.Length; + } + + int length = data.Length; + while (length > 0 && data[length - 1] == 0) + { + length--; + } + + return length; + } + } +} + diff --git a/src/Cosmos.Network.Http/X509Certificates/X509Certificate2.cs b/src/Cosmos.Network.Http/X509Certificates/X509Certificate2.cs new file mode 100644 index 0000000..a1ac312 --- /dev/null +++ b/src/Cosmos.Network.Http/X509Certificates/X509Certificate2.cs @@ -0,0 +1,394 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +// nanoFramework's System.Net X509Certificate2 (nanoFramework.System.Net/X509Certificates/X509Certificate2.cs). +// Cosmos: BouncyCastle decodes the private key, which mbedTLS decodes natively on nanoFramework, and the decoded key +// is kept for SslStream (a server certificate, or a client one). + +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; + +namespace Cosmos.Network.Http +{ + /// + /// Represents an X.509 certificate. + /// + public class X509Certificate2 : X509Certificate + { +#pragma warning disable S3459 // Unassigned members should be removed + // field required to be accessible by native code + private readonly byte[] _privateKey; + private readonly string _password; + + // Cosmos: the decoded private key. + private readonly AsymmetricKeyParameter _key; +#pragma warning restore S3459 // Unassigned members should be removed + + /// + /// Initializes a new instance of the class. + /// + public X509Certificate2() + : base() + { + } + + /// + /// Initializes a new instance of the class using information from a byte array. + /// + /// A byte array containing data from an X.509 certificate. + public X509Certificate2(byte[] rawData) + : base(rawData) + { + } + + /// + /// Initializes a new instance of the class using a string with the content of an X.509 certificate. + /// + /// A string containing a X.509 certificate. + /// + /// This methods is exclusive of .NET nanoFramework. The equivalent .NET constructor accepts a file name as the parameter. + /// + public X509Certificate2(string certificate) + : base(certificate) + { + } + + /// + /// Initializes a new instance of the class using a string with the content of an X.509 public certificate, the private key and a password used to access the private key. + /// + /// A string containing a X.509 certificate. + /// A string containing a private key in PEM or DER format. + /// The password required to decrypt the private key. Set to if the or are not encrypted and do not require a password. + /// + /// This methods is exclusive of .NET nanoFramework. There is no equivalent in .NET framework. + /// + public X509Certificate2( + string rawData, + string key, + string password) + : base(rawData) + { + var tempKey = Encoding.UTF8.GetBytes(key); + + ////////////////////////////////////////////// + // because this is parsing from a string // + // we need to keep the terminator // + ////////////////////////////////////////////// + var keyBuffer = new byte[tempKey.Length + 1]; + Array.Copy(tempKey, keyBuffer, tempKey.Length); + keyBuffer[keyBuffer.Length - 1] = 0; + + _privateKey = keyBuffer; + _password = password; + + _key = DecodePrivateKey( + keyBuffer, + password); + } + + /// + /// Initializes a new instance of the class using a string with the content of an X.509 public certificate, the private key and a password used to access the certificate. + /// + /// A byte array containing data from an X.509 certificate. + /// A string containing a private key in PEM or DER format. + /// The password required to decrypt the private key. Set to if the or are not encrypted and do not require a password. + /// + /// This methods is exclusive of .NET nanoFramework. There is no equivalent in .NET framework. + /// + public X509Certificate2( + byte[] rawData, + string key, + string password) + : base(rawData) + { + var tempKey = Encoding.UTF8.GetBytes(key); + + ////////////////////////////////////////////// + // because this is parsing from a string // + // we need to keep the terminator // + ////////////////////////////////////////////// + var keyBuffer = new byte[tempKey.Length + 1]; + Array.Copy(tempKey, keyBuffer, tempKey.Length); + keyBuffer[keyBuffer.Length - 1] = 0; + + _privateKey = keyBuffer; + _password = password; + + _key = DecodePrivateKey( + keyBuffer, + password); + } + + /// + /// Initializes a new instance of the class using a string with the content of an X.509 public certificate, the private key and a password used to access the certificate. + /// + /// A byte array containing data from an X.509 certificate. + /// A byte array containing a PEM private key. + /// The password required to decrypt the private key. if the or are not encrypted. + /// + /// This methods is exclusive of nanoFramework. There is no equivalent in .NET framework. + /// + public X509Certificate2( + byte[] rawData, + byte[] key, + string password) + : base(rawData) + { + _privateKey = key; + _password = password; + + _key = DecodePrivateKey( + key, + password); + } + + /// + /// Gets a value that indicates whether an object contains a private key. + /// + /// if the object contains a private key; otherwise, . + public bool HasPrivateKey + { + get + { + return (_privateKey != null); + } + } + + /// + /// Gets the private key, null if there isn't a private key. + /// + /// This will give you access directly to the raw decoded byte array of the private key + public byte[] PrivateKey => _privateKey; + + /// + /// Gets the public key. + /// + /// This will give you access directly to the raw decoded byte array of the public key. + public byte[] PublicKey => RawData; + + /// + /// Gets the date (in UTC time) after which a certificate is no longer valid. + /// + /// A object that represents the expiration date for the certificate. + public DateTime NotAfter + { + get + { + return _expirationDate; + } + } + + /// + /// Gets the date (in UTC time) on which a certificate becomes valid. + /// + /// A object that represents the effective date of the certificate. + public DateTime NotBefore + { + get + { + return _effectiveDate; + } + } + + /// + /// Gets the raw data of a certificate. + /// + /// The raw data of the certificate as a byte array. + public byte[] RawData + { + get + { + return base.GetRawCertData(); + } + } + + /// + /// Cosmos: the decoded private key, for SslStream. + /// + internal AsymmetricKeyParameter Key => _key; + + /// + /// Cosmos: decodes what nanoFramework decodes natively (DecodePrivateKeyNative): PEM (PKCS#8, encrypted PKCS#8, + /// PKCS#1 RSA, SEC1 EC, OpenSSL's encrypted PEM) or DER (PKCS#8, encrypted PKCS#8, PKCS#1 RSA, SEC1 EC). + /// + /// No private key could be decoded from . + internal static AsymmetricKeyParameter DecodePrivateKey( + byte[] keyBuffer, + string password) + { + if (keyBuffer == null) + { + throw new ArgumentNullException(nameof(keyBuffer)); + } + + // Without the terminator nanoFramework adds to a key given as a string. + int length = EncodedLength(keyBuffer); + + AsymmetricKeyParameter key = null; + Exception error = null; + try + { + if (length > 0 && keyBuffer[0] == 0x30) + { + // A DER SEQUENCE. + byte[] der = new byte[length]; + Array.Copy(keyBuffer, der, length); + key = DecodeDerPrivateKey(der, password); + } + else + { + key = DecodePemPrivateKey(Encoding.UTF8.GetString(keyBuffer, 0, length), password); + } + } + catch (Exception e) + { + error = e; + } + + if (key == null) + { + throw new CryptographicException(error == null ? "No private key could be read, neither PEM nor DER." : "The private key could not be decoded: " + error.Message, error); + } + + return key; + } + + /// + /// Cosmos: the PEM blocks read one by one, the keys decoded as DER (DecodeDerPrivateKey checks them first): + /// BouncyCastle's OpenSSL reader builds an EC key without its curve, a null dereference (a kernel panic on Cosmos). + /// It reads only OpenSSL's encrypted PEM (Proc-Type: 4,ENCRYPTED), which can be checked only once decrypted. + /// + private static AsymmetricKeyParameter DecodePemPrivateKey(string pem, string password) + { + var reader = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(new StringReader(pem)); + + Org.BouncyCastle.Utilities.IO.Pem.PemObject block; + while ((block = reader.ReadPemObject()) != null) + { + bool legacyEncrypted = false; + foreach (Org.BouncyCastle.Utilities.IO.Pem.PemHeader header in block.Headers) + { + legacyEncrypted |= header.Name == "Proc-Type" && header.Value.IndexOf("ENCRYPTED", StringComparison.Ordinal) >= 0; + } + + switch (block.Type) + { + case "PRIVATE KEY": + case "RSA PRIVATE KEY": + case "EC PRIVATE KEY": + case "ENCRYPTED PRIVATE KEY": + if (!legacyEncrypted) + { + return DecodeDerPrivateKey(block.Content, block.Type == "ENCRYPTED PRIVATE KEY" ? password ?? "" : null); + } + + return DecodeLegacyEncryptedPem(pem, password); + } + } + + return null; + } + + private static AsymmetricKeyParameter DecodeLegacyEncryptedPem(string pem, string password) + { + PemReader reader = new PemReader(new StringReader(pem), password == null ? null : new PasswordFinder(password)); + + object pemObject; + while ((pemObject = reader.ReadObject()) != null) + { + if (pemObject is AsymmetricCipherKeyPair pair) + { + return pair.Private; + } + + if (pemObject is AsymmetricKeyParameter parameter && parameter.IsPrivate) + { + return parameter; + } + } + + return null; + } + + private static AsymmetricKeyParameter DecodeDerPrivateKey(byte[] der, string password) + { + Asn1Sequence sequence = Asn1Sequence.GetInstance(der); + + if (password != null && sequence.Count == 2) + { + // EncryptedPrivateKeyInfo: algorithm, encrypted data. + // Cosmos: decrypted first, so the key is checked before BouncyCastle builds it (CreateKey). + return CreateKey(PrivateKeyInfoFactory.CreatePrivateKeyInfo(password.ToCharArray(), EncryptedPrivateKeyInfo.GetInstance(sequence))); + } + + if (sequence.Count == 9) + { + // PKCS#1 RSAPrivateKey: version, n, e, d, p, q, dP, dQ, qInv. + RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(sequence); + return new RsaPrivateCrtKeyParameters(rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent, rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient); + } + + if (sequence.Count >= 2 && sequence[1] is Asn1OctetString) + { + // SEC1 ECPrivateKey: version, private key, [0] parameters, [1] public key. + ECPrivateKeyStructure ec = ECPrivateKeyStructure.GetInstance(sequence); + AlgorithmIdentifier algorithm = new AlgorithmIdentifier(X9ObjectIdentifiers.IdECPublicKey, ec.Parameters); + return CreateKey(new PrivateKeyInfo(algorithm, ec.ToAsn1Object())); + } + + // PKCS#8 PrivateKeyInfo. + return CreateKey(PrivateKeyInfo.GetInstance(sequence)); + } + + /// + /// Cosmos: BouncyCastle's CreateKey for the kinds of keys it builds without dereferencing null: RSA, EC with its + /// curve, Ed25519 and Ed448 (those a TLS signer takes). It does for an EC key without its curve, or a DH, + /// ElGamal or GOST key without parameters: a kernel panic on Cosmos, where this throws. + /// + private static AsymmetricKeyParameter CreateKey(PrivateKeyInfo info) + { + AlgorithmIdentifier algorithm = info.PrivateKeyAlgorithm; + DerObjectIdentifier oid = algorithm.Algorithm; + + bool supported = oid.Equals(PkcsObjectIdentifiers.RsaEncryption) || oid.Equals(PkcsObjectIdentifiers.IdRsassaPss) + || oid.Equals(Org.BouncyCastle.Asn1.EdEC.EdECObjectIdentifiers.id_Ed25519) || oid.Equals(Org.BouncyCastle.Asn1.EdEC.EdECObjectIdentifiers.id_Ed448) + || (oid.Equals(X9ObjectIdentifiers.IdECPublicKey) && algorithm.Parameters != null && !(algorithm.Parameters is Asn1Null)); + + if (!supported) + { + throw new CryptographicException("A private key of this kind isn't supported (" + oid.Id + "), or an EC one lacks its curve."); + } + + return PrivateKeyFactory.CreateKey(info); + } + + private sealed class PasswordFinder : IPasswordFinder + { + private readonly string _password; + + internal PasswordFinder(string password) + { + _password = password; + } + + public char[] GetPassword() + { + return _password.ToCharArray(); + } + } + } +} diff --git a/src/Cosmos.Network.Http/packages.lock.json b/src/Cosmos.Network.Http/packages.lock.json index 6b18a4c..8667910 100644 --- a/src/Cosmos.Network.Http/packages.lock.json +++ b/src/Cosmos.Network.Http/packages.lock.json @@ -1,54 +1,18 @@ { "version": 1, "dependencies": { - ".NETnanoFramework,Version=v1.0": { - "nanoFramework.CoreLibrary": { + "net10.0": { + "BouncyCastle.Cryptography": { "type": "Direct", - "requested": "[1.17.11, 1.17.11]", - "resolved": "1.17.11", - "contentHash": "HezzAc0o2XrSGf85xSeD/6xsO6ohF9hX6/iMQ1IZS6Zw6umr4WfAN2Jv0BrPxkaYwzEegJxxZujkHoUIAqtOMw==" + "requested": "[2.7.0, )", + "resolved": "2.7.0", + "contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw==" }, - "nanoFramework.Runtime.Events": { + "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[1.11.39, 1.11.39]", - "resolved": "1.11.39", - "contentHash": "qNcgsk1QU5lTj2MgMYyF4iTx85YjFcAfJP9sYYydUlEXL1JaO2MnKGI3oBG4p20dJ1Fqi1ClIgKXJfAp9z80tA==" - }, - "nanoFramework.System.Collections": { - "type": "Direct", - "requested": "[1.5.75, 1.5.75]", - "resolved": "1.5.75", - "contentHash": "OiMpKo8RWbjZq1594YjdRnOGNN2aT4nowa+ET7scK0frGCux0a8LGaPkG56mmSfBCKzLlVs13LIlla3XLhXaMA==" - }, - "nanoFramework.System.IO.Streams": { - "type": "Direct", - "requested": "[1.1.96, 1.1.96]", - "resolved": "1.1.96", - "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" - }, - "nanoFramework.System.Net": { - "type": "Direct", - "requested": "[1.11.64, 1.11.64]", - "resolved": "1.11.64", - "contentHash": "jyC28lSWbBs1aajgp12HlHn6U0o00OMGzFP95SAeJ1CJ6BdmK+bq+peTvsfzpqyT0Babuaqq/x9G94XLaGapGw==" - }, - "nanoFramework.System.Text": { - "type": "Direct", - "requested": "[1.3.42, 1.3.42]", - "resolved": "1.3.42", - "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" - }, - "nanoFramework.System.Threading": { - "type": "Direct", - "requested": "[1.1.52, 1.1.52]", - "resolved": "1.1.52", - "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" - }, - "Nerdbank.GitVersioning": { - "type": "Direct", - "requested": "[3.10.94, 3.10.94]", - "resolved": "3.10.94", - "contentHash": "sn6gFIx6bwFj3Me0XGmek3gLZi/MZC8C4tRLwslXGeBcXcVxvFpZyDrQA93G8TiYgxTS7J7triKB7hj5wXzSxA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" } } } diff --git a/tests/Cosmos.Network.Http.Tests/ByteArrayContentTest.cs b/tests/Cosmos.Network.Http.Tests/ByteArrayContentTest.cs new file mode 100644 index 0000000..3ba04e7 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/ByteArrayContentTest.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.IO; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class ByteArrayContentTest + { + [TestMethod] + public void Ctor_NullSourceArray_ThrowsArgumentNullException() + { + Assert.ThrowsException(typeof(ArgumentNullException), () => new ByteArrayContent(null)); + } + + [TestMethod] + public void Ctor_NullSourceArrayWithRange_ThrowsArgumentNullException() + { + Assert.ThrowsException(typeof(ArgumentNullException), () => new ByteArrayContent(null, 0, 1)); + } + + [TestMethod] + public void Ctor_EmptySourceArrayWithRange_ThrowsArgumentOutOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), () => new ByteArrayContent(new byte[0], 0, 1)); + } + + [TestMethod] + public void Ctor_StartIndexTooBig_ThrowsArgumentOufOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), () => new ByteArrayContent(new byte[5], 5, 1)); + } + + [TestMethod] + public void Ctor_StartIndexNegative_ThrowsArgumentOutOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), () => new ByteArrayContent(new byte[5], -1, 1)); + } + + [TestMethod] + public void Ctor_LengthTooBig_ThrowsArgumentOutOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), () => new ByteArrayContent(new byte[5], 1, 5)); + } + + [TestMethod] + public void Ctor_LengthPlusOffsetCauseIntOverflow_ThrowsArgumentOutOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), () => new ByteArrayContent(new byte[5], 1, int.MaxValue)); + } + + [TestMethod] + public void Ctor_LengthNegative_ThrowsArgumentOutOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), () => new ByteArrayContent(new byte[5], 0, -1)); + } + + [TestMethod] + public void ContentLength_UseWholeSourceArray_LengthMatchesArrayLength() + { + var contentData = new byte[10]; + var content = new ByteArrayContent(contentData); + + Assert.AreEqual(contentData.Length, content.Headers.ContentLength); + } + + [TestMethod] + public void ContentLength_UsePartialSourceArray_LengthMatchesArrayLength() + { + var contentData = new byte[10]; + var content = new ByteArrayContent(contentData, 5, 3); + + Assert.AreEqual(3, content.Headers.ContentLength); + } + + [TestMethod] + public void ReadAsStreamAsync_EmptySourceArray_Succeed() + { + var content = new ByteArrayContent(new byte[0]); + using Stream stream = content.ReadAsStream(); + Assert.AreEqual(0, stream.Length); + } + + [TestMethod] + public void ReadAsStream_Call_MemoryStreamWrappingByteArrayReturned() + { + var contentData = new byte[10]; + var content = new ByteArrayContent(contentData, 5, 3); + + Stream stream = content.ReadAsStream(); + Assert.IsFalse(stream.CanWrite); + Assert.AreEqual(3, stream.Length); + } + + [TestMethod] + public void CopyTo_NullDestination_ThrowsArgumentNullException() + { + byte[] contentData = CreateSourceArray(); + var content = new ByteArrayContent(contentData); + + Assert.ThrowsException(typeof(ArgumentNullException), + () => + { + content.CopyTo(null); + }); + } + + [TestMethod] + public void CopyTo_UseWholeSourceArray_WholeContentCopied() + { + byte[] contentData = CreateSourceArray(); + var content = new ByteArrayContent(contentData); + + using var destination = new MemoryStream(); + content.CopyTo(destination); + + Assert.AreEqual(contentData.Length, destination.Length); + CheckResult(destination, 0); + } + + [TestMethod] + public void CopyTo_UsePartialSourceArray_PartialContentCopied() + { + byte[] contentData = CreateSourceArray(); + var content = new ByteArrayContent(contentData, 3, 5); + + using var destination = new MemoryStream(); + content.CopyTo(destination); + + Assert.AreEqual(5, destination.Length); + CheckResult(destination, 3); + } + + [TestMethod] + public void CopyTo_UseEmptySourceArray_NothingCopied() + { + var contentData = new byte[0]; + var content = new ByteArrayContent(contentData, 0, 0); + + using var destination = new MemoryStream(); + content.CopyTo(destination); + + Assert.AreEqual(0, destination.Length); + } + + #region Helper methods + + private static byte[] CreateSourceArray() + { + var contentData = new byte[10]; + for (int i = 0; i < contentData.Length; i++) + { + contentData[i] = (byte)(i % 256); + } + return contentData; + } + + private static void CheckResult(Stream destination, byte firstValue) + { + destination.Position = 0; + var destinationData = new byte[destination.Length]; + int read = destination.Read(destinationData, 0, destinationData.Length); + + Assert.AreEqual(destinationData.Length, read); + Assert.AreEqual(firstValue, destinationData[0]); + + for (int i = 1; i < read; i++) + { + Assert.IsTrue((destinationData[i] == (destinationData[i - 1] + 1)) || + ((destinationData[i] == 0) && (destinationData[i - 1] != 0))); + } + } + + #endregion + } +} diff --git a/tests/Cosmos.Network.Http.Tests/CertificateValidatorTests.cs b/tests/Cosmos.Network.Http.Tests/CertificateValidatorTests.cs deleted file mode 100644 index e5ef8c4..0000000 --- a/tests/Cosmos.Network.Http.Tests/CertificateValidatorTests.cs +++ /dev/null @@ -1,389 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using NUnit.Framework; -using Org.BouncyCastle.Tls; - -namespace Cosmos.Network.Http.Tests; - -/// -/// Chains made up with , each broken one way, checked -/// without a connection. -/// -[TestFixture] -public class CertificateValidatorTests -{ - private static readonly string[] s_names = ["server.test", "127.0.0.1"]; - - private X509Certificate2 _root = null!; - private X509Certificate2 _intermediate = null!; - private TrustedRoots _roots = null!; - - [OneTimeSetUp] - public void CreateRoot() - { - _root = TestPki.Root(); - _intermediate = TestPki.Authority(_root); - _roots = TrustedRoots.FromCertificates([_root.RawData]); - } - - [Test] - public void ValidChain_IsTrusted() - { - Assert.That(Validate([TestPki.Server(_intermediate, s_names), _intermediate], "server.test"), Is.Null); - } - - [Test] - public void ChainWithoutIntermediate_DirectlyUnderTheRoot_IsTrusted() - { - Assert.That(Validate([TestPki.Server(_root, s_names)], "server.test"), Is.Null); - } - - [Test] - public void IntermediatesInAnyOrder_WithStrangers_AreTrusted() - { - X509Certificate2 second = TestPki.Authority(_intermediate, "Second Intermediate"); - X509Certificate2 stranger = TestPki.Authority(TestPki.Root("Other Root"), "Stranger"); - - string? error = Validate([TestPki.Server(second, s_names), stranger, _intermediate, second], "server.test"); - - Assert.That(error, Is.Null); - } - - [Test] - public void CrossSignedCopyOfTheRoot_LeadsToTheRootItself() - { - // The server sends the root signed by an older root nobody trusts any - // more: the chain stops at the intermediate the trusted root signed. - ECDsa rootKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); - X509Certificate2 root = TestPki.Root("Current Root", rootKey); - X509Certificate2 crossSigned = TestPki.Authority(TestPki.Root("Retired Root"), "Current Root", key: rootKey); - X509Certificate2 intermediate = TestPki.Authority(root); - - string? error = CertificateValidator.Validate( - Bc(TestPki.Server(intermediate, s_names), intermediate, crossSigned), "server.test", - TrustedRoots.FromCertificates([root.RawData]), DateTime.UtcNow, out _); - - Assert.That(error, Is.Null); - } - - [Test] - public void MissingIntermediate_IsNotTrusted() - { - string? error = Validate([TestPki.Server(_intermediate, s_names)], "server.test", out short alert); - - Assert.That(error, Does.Contain("may have left out an intermediate")); - Assert.That(alert, Is.EqualTo(AlertDescription.unknown_ca)); - } - - [Test] - public void SelfSigned_IsNotTrusted() - { - Assert.That(Validate([TestPki.Server(null, s_names)], "server.test"), Is.EqualTo("the certificate is self-signed")); - } - - [Test] - public void UntrustedRoot_IsNotTrusted() - { - X509Certificate2 otherRoot = TestPki.Root("Other Root"); - X509Certificate2 intermediate = TestPki.Authority(otherRoot); - - string? error = Validate([TestPki.Server(intermediate, s_names), intermediate, otherRoot], "server.test"); - - Assert.That(error, Does.Contain("CN=Other Root, which is not a trusted root")); - } - - [Test] - public void ForgedSignature_IsNotTrusted() - { - // Same names as the real intermediate, another key: its signature cannot verify. - X509Certificate2 impostor = TestPki.Authority(TestPki.Root(), "Test Intermediate"); - - string? error = Validate([TestPki.Server(impostor, s_names), _intermediate], "server.test"); - - Assert.That(error, Does.Contain("which is not a trusted root")); - } - - [Test] - public void ExpiredServer_IsNotTrusted() - { - X509Certificate2 server = TestPki.Server(_intermediate, s_names, - notBefore: DateTimeOffset.UtcNow.AddDays(-30), notAfter: DateTimeOffset.UtcNow.AddDays(-1)); - - string? error = Validate([server, _intermediate], "server.test", out short alert); - - Assert.That(error, Does.StartWith("the certificate expired on")); - Assert.That(alert, Is.EqualTo(AlertDescription.certificate_expired)); - } - - [Test] - public void NotYetValidServer_IsNotTrusted() - { - X509Certificate2 server = TestPki.Server(_intermediate, s_names, - notBefore: DateTimeOffset.UtcNow.AddDays(1), notAfter: DateTimeOffset.UtcNow.AddDays(30)); - - Assert.That(Validate([server, _intermediate], "server.test"), Does.Contain("is not valid before")); - } - - [Test] - public void IssuerThatIsNoAuthority_IsNotTrusted() - { - X509Certificate2 notAnAuthority = TestPki.Server(_intermediate, ["middle.test"], usages: [TestPki.ServerAuth]); - - string? error = Validate([TestPki.Server(notAnAuthority, s_names), notAnAuthority, _intermediate], "server.test"); - - Assert.That(error, Does.Contain("is not a certificate authority")); - } - - [Test] - public void PathLengthConstraint_IsEnforced() - { - X509Certificate2 limited = TestPki.Authority(_root, "Limited", pathLength: 0); - X509Certificate2 below = TestPki.Authority(limited, "Below Limited"); - - string? error = Validate([TestPki.Server(below, s_names), below, limited], "server.test"); - - Assert.That(error, Does.Contain("allows 0 intermediate certificates under it")); - } - - [Test] - public void IssuerWhoseKeyMayNotSignCertificates_IsNotTrusted() - { - X509Certificate2 intermediate = TestPki.Authority(_root, "Signer Only", keyUsage: X509KeyUsageFlags.DigitalSignature); - - string? error = Validate([TestPki.Server(intermediate, s_names), intermediate], "server.test"); - - Assert.That(error, Does.Contain("may not sign certificates")); - } - - [Test] - public void ServerCertificateForClients_IsNotTrusted() - { - X509Certificate2 server = TestPki.Server(_intermediate, s_names, usages: [TestPki.ClientAuth]); - - Assert.That(Validate([server, _intermediate], "server.test"), Is.EqualTo("the certificate is not for TLS servers")); - } - - [Test] - public void IntermediateRestrictedToClients_IsNotTrusted() - { - X509Certificate2 intermediate = TestPki.Authority(_root, "Client CA", usages: [TestPki.ClientAuth]); - - string? error = Validate([TestPki.Server(intermediate, s_names), intermediate], "server.test"); - - Assert.That(error, Is.EqualTo("CN=Client CA is not for TLS servers")); - } - - [Test] - public void UnknownCriticalExtension_IsNotTrusted() - { - X509Certificate2 server = TestPki.Server(_intermediate, s_names, extensions: [TestPki.UnknownCritical()]); - - string? error = Validate([server, _intermediate], "server.test", out short alert); - - Assert.That(error, Does.Contain("critical extension that is not understood")); - Assert.That(alert, Is.EqualTo(AlertDescription.unsupported_certificate)); - } - - [Test] - public void NameConstraints_AllowWhatTheyPermit() - { - X509Certificate2 constrained = TestPki.Authority(_root, "Constrained", extensions: [TestPki.NameConstraints(["test"], [])]); - - Assert.That(Validate([TestPki.Server(constrained, ["server.test"]), constrained], "server.test"), Is.Null); - } - - [Test] - public void NameConstraints_RefuseWhatTheyDoNotPermit() - { - X509Certificate2 constrained = TestPki.Authority(_root, "Constrained", extensions: [TestPki.NameConstraints(["test"], [])]); - - string? error = Validate([TestPki.Server(constrained, ["bank.example"]), constrained], "bank.example"); - - Assert.That(error, Does.Contain("outside the names its issuer may certify")); - } - - [Test] - public void NameConstraints_RefuseWhatTheyExclude() - { - X509Certificate2 constrained = TestPki.Authority(_root, "Excluding", extensions: [TestPki.NameConstraints([], ["secret.test"])]); - - string? error = Validate([TestPki.Server(constrained, ["db.secret.test"]), constrained], "db.secret.test"); - - Assert.That(error, Does.Contain("outside the names its issuer may certify")); - } - - [Test] - public void NameConstraints_ApplyToTheHost_NotOnlyToAWildcard() - { - // *.allowed.test is within the permitted names, bad.allowed.test is excluded: - // the wildcard would cover it. - X509Certificate2 constrained = TestPki.Authority(_root, "Excluding", - extensions: [TestPki.NameConstraints(["allowed.test"], ["bad.allowed.test"])]); - X509Certificate2 server = TestPki.Server(constrained, ["*.allowed.test"]); - - Assert.That(Validate([server, constrained], "good.allowed.test"), Is.Null); - Assert.That(Validate([server, constrained], "bad.allowed.test"), Does.Contain("outside the names its issuer may certify")); - } - - [Test] - public void Sha1Signature_IsNotTrusted() - { - // The desktop signs SHA-1 with RSA only, so the issuer has an RSA key. - X509Certificate2 rsaIntermediate = TestPki.Authority(_root, "RSA Intermediate", key: RSA.Create(2048)); - X509Certificate2 server = TestPki.Server(rsaIntermediate, s_names, hash: HashAlgorithmName.SHA1); - - string? error = Validate([server, rsaIntermediate], "server.test"); - - Assert.That(error, Does.StartWith("the certificate is signed with SHA-1withRSA")); - Assert.That(error, Does.EndWith("which is too weak to trust")); - } - - [Test] - public void Sha1SignedIntermediate_IsNotTrusted() - { - X509Certificate2 root = TestPki.Root("RSA Root", RSA.Create(2048)); - X509Certificate2 intermediate = TestPki.Authority(root, "Old Intermediate", hash: HashAlgorithmName.SHA1); - - string? error = CertificateValidator.Validate(Bc(TestPki.Server(intermediate, s_names), intermediate), "server.test", - TrustedRoots.FromCertificates([root.RawData]), DateTime.UtcNow, out _); - - Assert.That(error, Does.StartWith("CN=Old Intermediate is signed with SHA-1withRSA")); - } - - [Test] - public void ShortRsaKey_IsNotTrusted() - { - X509Certificate2 server = TestPki.Server(_intermediate, s_names, key: RSA.Create(1024)); - - Assert.That(Validate([server, _intermediate], "server.test"), Does.Contain("1024-bit RSA key")); - } - - [Test] - public void RsaChain_IsTrusted() - { - X509Certificate2 root = TestPki.Root("RSA Root", RSA.Create(2048)); - X509Certificate2 intermediate = TestPki.Authority(root, "RSA Intermediate", key: RSA.Create(2048)); - X509Certificate2 server = TestPki.Server(intermediate, s_names, key: RSA.Create(2048)); - - string? error = CertificateValidator.Validate(Bc(server, intermediate), "server.test", - TrustedRoots.FromCertificates([root.RawData]), DateTime.UtcNow, out _); - - Assert.That(error, Is.Null); - } - - [Test] - public void Dates_AreCheckedLast() - { - // Expired and for another host: the host is what is reported, so a - // date error always means nothing else is wrong. - X509Certificate2 server = TestPki.Server(_intermediate, ["elsewhere.test"], - notBefore: DateTimeOffset.UtcNow.AddDays(-30), notAfter: DateTimeOffset.UtcNow.AddDays(-1)); - - Assert.That(Validate([server, _intermediate], "server.test"), Does.Contain("not server.test")); - } - - [Test] - public void ExpiredRoot_GivesWayToAValidOneOfTheSameName() - { - ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - X509Certificate2 expired = TestPki.Root("Renewed Root", key, - notBefore: DateTimeOffset.UtcNow.AddDays(-400), notAfter: DateTimeOffset.UtcNow.AddDays(-10)); - X509Certificate2 renewed = TestPki.Root("Renewed Root", key); - X509Certificate2 intermediate = TestPki.Authority(renewed); - TrustedRoots roots = TrustedRoots.FromCertificates([expired.RawData, renewed.RawData]); - - Assert.That(CertificateValidator.Validate(Bc(TestPki.Server(intermediate, s_names), intermediate), "server.test", - roots, DateTime.UtcNow, out _), Is.Null); - } - - [Test] - public void ExpiredRoot_IsReportedWhenNothingElseLeadsToARoot() - { - ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256); - X509Certificate2 expired = TestPki.Root("Old Root", key, - notBefore: DateTimeOffset.UtcNow.AddDays(-400), notAfter: DateTimeOffset.UtcNow.AddDays(-10)); - X509Certificate2 intermediate = TestPki.Authority(TestPki.Root("Old Root", key)); - - string? error = CertificateValidator.Validate(Bc(TestPki.Server(intermediate, s_names), intermediate), "server.test", - TrustedRoots.FromCertificates([expired.RawData]), DateTime.UtcNow, out short alert); - - Assert.That(error, Does.StartWith("the root CN=Old Root expired on")); - Assert.That(alert, Is.EqualTo(AlertDescription.certificate_expired)); - } - - [TestCase("server.test", "server.test")] - [TestCase("SERVER.test", "server.test")] - [TestCase("127.0.0.1", "127.0.0.1")] - public void Host_IsFoundInTheAlternativeNames(string host, string name) - { - Assert.That(Validate([TestPki.Server(_intermediate, [name]), _intermediate], host), Is.Null); - } - - [Test] - public void OtherHost_IsNotTrusted() - { - string? error = Validate([TestPki.Server(_intermediate, ["a.test", "b.test", "10.0.0.1"]), _intermediate], "c.test"); - - Assert.That(error, Is.EqualTo("the certificate is for a.test, b.test, 10.0.0.1, not c.test")); - } - - [Test] - public void AddressNamedOnlyAsADnsName_IsNotTrusted() - { - Assert.That(Validate([TestPki.Server(_intermediate, ["server.test"]), _intermediate], "10.0.0.1"), Does.Contain("not 10.0.0.1")); - } - - [Test] - public void CertificateWithoutAlternativeNames_IsNotTrusted() - { - // The common name is not looked at, as in browsers. - X509Certificate2 server = TestPki.Server(_intermediate, []); - - Assert.That(Validate([server, _intermediate], "no name"), Does.Contain("has no subject alternative names")); - } - - [TestCase("*.example.com", "www.example.com", true)] - [TestCase("*.example.com", "WWW.Example.COM", true)] - [TestCase("*.example.com", "example.com", false)] - [TestCase("*.example.com", "a.b.example.com", false)] - [TestCase("*.example.com", ".example.com", false)] - [TestCase("*.com", "example.com", false)] - [TestCase("*", "example", false)] - [TestCase("w*.example.com", "www.example.com", false)] - [TestCase("*.*.example.com", "a.b.example.com", false)] - [TestCase("example.com.", "example.com", true)] - [TestCase("example.com", "example.com.", true)] - [TestCase("example.com", "example.org", false)] - public void DnsNames_MatchAsRfc6125Says(string pattern, string host, bool matches) - { - Assert.That(CertificateValidator.MatchesDnsName(pattern, host), Is.EqualTo(matches)); - } - - [Test] - public void EmptyChain_IsNotTrusted() - { - Assert.That(Validate([], "server.test"), Is.EqualTo("the server sent no certificate")); - } - - [Test] - public void MozillaRoots_AreEmbedded() - { - TrustedRoots roots = TrustedRoots.Mozilla; - - Assert.That(roots.Count, Is.GreaterThan(100)); - Org.BouncyCastle.Asn1.X509.X509Name isrg = new("C=US,O=Internet Security Research Group,CN=ISRG Root X1"); - Assert.That(roots.FindBySubject(isrg).Count(), Is.EqualTo(1)); - } - - private string? Validate(X509Certificate2[] chain, string host) => Validate(chain, host, out _); - - private string? Validate(X509Certificate2[] chain, string host, out short alert) => - CertificateValidator.Validate(Bc(chain), host, _roots, DateTime.UtcNow, out alert); - - private static Org.BouncyCastle.X509.X509Certificate[] Bc(params X509Certificate2[] chain) => - chain.Select(TestPki.ToBouncyCastle).ToArray(); -} diff --git a/tests/Cosmos.Network.Http.Tests/CertificateVerifierTests.cs b/tests/Cosmos.Network.Http.Tests/CertificateVerifierTests.cs new file mode 100644 index 0000000..a471400 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/CertificateVerifierTests.cs @@ -0,0 +1,287 @@ +// Cosmos: tests of the certificate check mbedTLS runs natively for nanoFramework, which the port does itself. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; +using BcCertificate = Org.BouncyCastle.X509.X509Certificate; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class CertificateVerifierTests + { + private static BcCertificate[] Trusted => new[] { TestCertificates.Root.Certificate }; + + private static string Verify(string host, params TestCertificate[] chain) + { + return CertificateVerifier.Verify(Array.ConvertAll(chain, c => c.Certificate), host, Trusted, DateTime.UtcNow); + } + + [TestMethod] + public void Leaf_IssuedByTrustedRoot_IsValid() + { + Assert.IsNull(Verify("localhost", TestCertificates.RsaServer)); + } + + [TestMethod] + public void Leaf_WithRootInChain_IsValid() + { + Assert.IsNull(Verify("localhost", TestCertificates.EcServer, TestCertificates.Root)); + } + + [TestMethod] + public void Leaf_ThroughIntermediate_IsValid() + { + TestCertificate leaf = TestCertificates.CreateServer("www.example.com", TestCertificates.Intermediate, TestCertificates.Ec(), new[] { "www.example.com" }, null); + + Assert.IsNull(Verify("www.example.com", leaf, TestCertificates.Intermediate)); + } + + [TestMethod] + public void Leaf_WithoutItsIntermediate_IsRefused() + { + TestCertificate leaf = TestCertificates.CreateServer("www.example.com", TestCertificates.Intermediate, TestCertificates.Ec(), new[] { "www.example.com" }, null); + + Assert.IsNotNull(Verify("www.example.com", leaf)); + } + + [TestMethod] + public void Leaf_IssuedByUntrustedRoot_IsRefused() + { + TestCertificate leaf = TestCertificates.CreateServer("localhost", TestCertificates.OtherRoot, TestCertificates.Ec(), new[] { "localhost" }, null); + + Assert.IsNotNull(Verify("localhost", leaf)); + Assert.IsNotNull(Verify("localhost", leaf, TestCertificates.OtherRoot)); + } + + [TestMethod] + public void SelfSigned_Untrusted_IsRefused() + { + TestCertificate self = TestCertificates.CreateServer("localhost", null, TestCertificates.Ec(), new[] { "localhost" }, null); + + Assert.IsNotNull(Verify("localhost", self)); + } + + [TestMethod] + public void SelfSigned_Trusted_IsValid() + { + TestCertificate self = TestCertificates.CreateServer("localhost", null, TestCertificates.Ec(), new[] { "localhost" }, null); + + Assert.IsNull(CertificateVerifier.Verify(new[] { self.Certificate }, "localhost", new[] { self.Certificate }, DateTime.UtcNow)); + } + + [TestMethod] + public void Leaf_SignedByNonAuthority_IsRefused() + { + // The server certificate, whose basicConstraints say it is no authority, signs another. + TestCertificate leaf = TestCertificates.CreateServer("evil.example.com", TestCertificates.RsaServer, TestCertificates.Ec(), new[] { "evil.example.com" }, null); + + string error = Verify("evil.example.com", leaf, TestCertificates.RsaServer); + Assert.IsNotNull(error); + } + + [TestMethod] + public void Host_NotNamed_IsRefused() + { + string error = Verify("example.com", TestCertificates.RsaServer); + Assert.IsNotNull(error); + StringAssert.Contains(error, "example.com"); + } + + [TestMethod] + public void Host_IpAddress_MatchesIpSan() + { + Assert.IsNull(Verify("127.0.0.1", TestCertificates.RsaServer)); + Assert.IsNotNull(Verify("127.0.0.2", TestCertificates.RsaServer)); + } + + [TestMethod] + public void Host_IpAddress_DoesNotMatchDnsName() + { + TestCertificate leaf = TestCertificates.CreateServer("10.0.2.2", TestCertificates.Root, TestCertificates.Ec(), new[] { "10.0.2.2" }, null); + + Assert.IsNotNull(Verify("10.0.2.2", leaf)); + } + + [TestMethod] + public void CommonName_CountsOnlyWithoutDnsNames() + { + TestCertificate cnOnly = TestCertificates.CreateServer("cn.example.com", TestCertificates.Root, TestCertificates.Ec(), null, null); + Assert.IsNull(Verify("cn.example.com", cnOnly)); + + TestCertificate withSan = TestCertificates.CreateServer("cn.example.com", TestCertificates.Root, TestCertificates.Ec(), new[] { "san.example.com" }, null); + Assert.IsNotNull(Verify("cn.example.com", withSan)); + Assert.IsNull(Verify("san.example.com", withSan)); + } + + [TestMethod] + public void Expired_IsRefused() + { + TestCertificate expired = TestCertificates.Create("localhost", TestCertificates.Root, TestCertificates.Ec(), false, new[] { "localhost" }, null, + KeyPurposeID.id_kp_serverAuth, DateTime.UtcNow.AddDays(-30), DateTime.UtcNow.AddDays(-1)); + + string error = Verify("localhost", expired); + Assert.IsNotNull(error); + StringAssert.Contains(error, "expired"); + } + + [TestMethod] + public void NotYetValid_IsRefused() + { + TestCertificate future = TestCertificates.Create("localhost", TestCertificates.Root, TestCertificates.Ec(), false, new[] { "localhost" }, null, + KeyPurposeID.id_kp_serverAuth, DateTime.UtcNow.AddDays(1), DateTime.UtcNow.AddDays(30)); + + Assert.IsNotNull(Verify("localhost", future)); + } + + [TestMethod] + public void ClientCertificate_AsServer_IsRefused() + { + // Its extended key usage says clientAuth only. + Assert.IsNotNull(Verify("Cosmos Test Client", TestCertificates.Client)); + } + + [TestMethod] + public void ClientCertificate_AsClient_IsValid() + { + Assert.IsNull(Verify(null, TestCertificates.Client)); + Assert.IsNotNull(Verify(null, TestCertificates.RsaServer)); + } + + [TestMethod] + public void NoCertificate_IsRefused() + { + Assert.IsNotNull(CertificateVerifier.Verify(new BcCertificate[0], "localhost", Trusted, DateTime.UtcNow)); + Assert.IsNotNull(CertificateVerifier.Verify(null, "localhost", Trusted, DateTime.UtcNow)); + } + + [TestMethod] + public void PathLength_Exceeded_IsRefused() + { + // A root that may issue end-entity certificates only (pathLenConstraint 0), above an intermediate. + TestCertificate root = TestCertificates.Create("Cosmos PathLen Root", null, TestCertificates.Ec(), true, null, null, null, + DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddYears(1), pathLength: 0); + TestCertificate intermediate = TestCertificates.CreateAuthority("Cosmos PathLen Intermediate", root, TestCertificates.Ec()); + TestCertificate leaf = TestCertificates.CreateServer("www.example.com", intermediate, TestCertificates.Ec(), new[] { "www.example.com" }, null); + TestCertificate direct = TestCertificates.CreateServer("www.example.com", root, TestCertificates.Ec(), new[] { "www.example.com" }, null); + + BcCertificate[] trusted = { root.Certificate }; + string error = CertificateVerifier.Verify(new[] { leaf.Certificate, intermediate.Certificate }, "www.example.com", trusted, DateTime.UtcNow); + Assert.IsNotNull(error); + StringAssert.Contains(error, "intermediate"); + + Assert.IsNull(CertificateVerifier.Verify(new[] { direct.Certificate }, "www.example.com", trusted, DateTime.UtcNow)); + } + + [TestMethod] + public void CriticalNameConstraints_AreRefused() + { + // An intermediate restricted to example.org by a critical nameConstraints, which isn't enforced here. + TestCertificate intermediate = TestCertificates.Create("Cosmos Constrained Intermediate", TestCertificates.Root, TestCertificates.Ec(), true, null, null, null, + DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddYears(1), + customize: g => g.AddExtension(X509Extensions.NameConstraints, true, + new NameConstraints(new[] { new GeneralSubtree(new GeneralName(GeneralName.DnsName, "example.org")) }, null))); + TestCertificate leaf = TestCertificates.CreateServer("www.example.com", intermediate, TestCertificates.Ec(), new[] { "www.example.com" }, null); + + string error = Verify("www.example.com", leaf, intermediate); + Assert.IsNotNull(error); + StringAssert.Contains(error, "critical extension"); + } + + [DataTestMethod] + [DataRow(new byte[0])] + [DataRow(new byte[] { 0x04, 0x00 })] + [DataRow(new byte[] { 0x30, 0x03, 0x82, 0x01 })] + public void MalformedSubjectAltName_NamesNothing(byte[] value) + { + // An extension whose value is empty (BouncyCastle reads no names at all: null), not names, or cut short. + var extensions = new System.Collections.Generic.Dictionary + { + [X509Extensions.SubjectAlternativeName] = new X509Extension(false, new DerOctetString(value)), + }; + var tbs = NewTbs("CN=localhost", TestCertificates.EcServer.Certificate.SubjectPublicKeyInfo); + tbs.SetExtensions(new X509Extensions(extensions)); + var leaf = new BcCertificate(new X509CertificateStructure(tbs.GenerateTbsCertificate(), new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha256), new DerBitString(new byte[64]))); + + Assert.IsFalse(CertificateVerifier.NamesHost(leaf, "localhost")); + Assert.IsNotNull(CertificateVerifier.Verify(new[] { leaf }, "localhost", Trusted, DateTime.UtcNow)); + } + + private static V3TbsCertificateGenerator NewTbs(string name, SubjectPublicKeyInfo key) + { + var tbs = new V3TbsCertificateGenerator(); + tbs.SetSerialNumber(new DerInteger(7)); + tbs.SetIssuer(new X509Name(name)); + tbs.SetSubject(new X509Name(name)); + tbs.SetStartDate(new Time(DateTime.UtcNow.AddDays(-1))); + tbs.SetEndDate(new Time(DateTime.UtcNow.AddDays(1))); + tbs.SetSignature(new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha256)); + tbs.SetSubjectPublicKeyInfo(key); + return tbs; + } + + [TestMethod] + public void IssuerWithUnreadableKey_IsRefusedWithoutFailing() + { + // A certificate the peer sends as the leaf's issuer, whose EC key has no curve: BouncyCastle dereferences + // null reading it. + var tbs = NewTbs("CN=Cosmos Curveless", new SubjectPublicKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.IdECPublicKey), new byte[65])); + var curveless = new BcCertificate(new X509CertificateStructure(tbs.GenerateTbsCertificate(), new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha256), new DerBitString(new byte[64]))); + + Assert.IsFalse(CertificateVerifier.HasReadableKey(curveless)); + Assert.IsTrue(CertificateVerifier.HasReadableKey(TestCertificates.EcServer.Certificate)); + Assert.IsTrue(CertificateVerifier.HasReadableKey(TestCertificates.RsaServer.Certificate)); + + TestCertificate leaf = TestCertificates.Create("localhost", null, TestCertificates.Ec(), false, new[] { "localhost" }, null, + KeyPurposeID.id_kp_serverAuth, DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), + customize: g => g.SetIssuerDN(new X509Name("CN=Cosmos Curveless"))); + + Assert.IsNotNull(CertificateVerifier.Verify(new[] { leaf.Certificate, curveless }, "localhost", new[] { TestCertificates.Root.Certificate }, DateTime.UtcNow)); + } + + [DataTestMethod] + [DataRow("10.0.2.2", new byte[] { 10, 0, 2, 2 })] + [DataRow("255.255.255.255", new byte[] { 255, 255, 255, 255 })] + [DataRow("www.example.co.uk", null)] + [DataRow("1.2.3", null)] + [DataRow("1.2.3.4.5", null)] + [DataRow("1.2.3.256", null)] + [DataRow("1..3.4", null)] + [DataRow("::1", null)] + public void ParseIPv4(string host, byte[] expected) + { + byte[] actual = CertificateVerifier.ParseIPv4(host); + if (expected == null) + { + Assert.IsNull(actual); + } + else + { + CollectionAssert.AreEqual(expected, actual); + } + } + + [DataTestMethod] + [DataRow("*.example.com", "www.example.com", true)] + [DataRow("*.example.com", "WWW.Example.COM", true)] + [DataRow("*.example.com", "example.com", false)] + [DataRow("*.example.com", "a.b.example.com", false)] + [DataRow("www.example.com.", "www.example.com", true)] + [DataRow("*.example.com.", "www.example.com", true)] + [DataRow("www.example.com", "www.example.org", false)] + [DataRow("w*.example.com", "www.example.com", false)] + public void DnsName_Matching(string pattern, string host, bool expected) + { + Assert.AreEqual(expected, CertificateVerifier.MatchesDnsName(pattern, host)); + } + + [TestMethod] + public void Host_TrailingDot_IsNamed() + { + // A fully qualified host: NamesHost drops the dot before matching. + Assert.IsTrue(CertificateVerifier.NamesHost(TestCertificates.RsaServer.Certificate, "localhost.")); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/Cosmos.Network.Http.Tests.csproj b/tests/Cosmos.Network.Http.Tests/Cosmos.Network.Http.Tests.csproj index 4193a30..d4f7201 100644 --- a/tests/Cosmos.Network.Http.Tests/Cosmos.Network.Http.Tests.csproj +++ b/tests/Cosmos.Network.Http.Tests/Cosmos.Network.Http.Tests.csproj @@ -1,17 +1,31 @@ + net10.0 latest - enable + disable disable false + + false + + $(NoWarn);CS1591 - - + + + + + + + + diff --git a/tests/Cosmos.Network.Http.Tests/GlobalUsings.cs b/tests/Cosmos.Network.Http.Tests/GlobalUsings.cs new file mode 100644 index 0000000..502b0ed --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/GlobalUsings.cs @@ -0,0 +1,6 @@ +// nanoFramework.TestFramework's attributes are MSTest's. +global using TestClassAttribute = Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute; +global using TestMethodAttribute = Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute; +global using DataRowAttribute = Microsoft.VisualStudio.TestTools.UnitTesting.DataRowAttribute; +global using SetupAttribute = Microsoft.VisualStudio.TestTools.UnitTesting.TestInitializeAttribute; +global using CleanupAttribute = Microsoft.VisualStudio.TestTools.UnitTesting.TestCleanupAttribute; diff --git a/tests/Cosmos.Network.Http.Tests/HeadersTests.cs b/tests/Cosmos.Network.Http.Tests/HeadersTests.cs new file mode 100644 index 0000000..d1268e8 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/HeadersTests.cs @@ -0,0 +1,56 @@ +// Cosmos: tests of what the port changed in the headers: .NET's read methods, the headers a caller may set, and a +// Content-Type read as .NET reads it. + +using System; +using System.Collections.Generic; +using Cosmos.Network.Http.Headers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class HeadersTests + { + [TestMethod] + public void RequestHeaders_CallerHeaders_CanBeSet() + { + var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); + request.Headers.Add("Accept", "text/html"); + request.Headers.Add("User-Agent", "AuraOS/1"); + request.Headers.Add("Referer", "http://example.com/"); + request.Headers.Add("Range", "bytes=0-9"); + + Assert.IsTrue(request.Headers.Contains("user-agent")); + CollectionAssert.AreEqual(new[] { "text/html" }, new List(request.Headers.GetValues("Accept"))); + + Assert.IsTrue(request.Headers.Remove("Referer")); + Assert.IsFalse(request.Headers.Remove("Referer")); + Assert.IsFalse(request.Headers.TryGetValues("Referer", out _)); + + Assert.ThrowsException(() => request.Headers.Add("Host", "example.org")); + Assert.ThrowsException(() => request.Headers.Add("Transfer-Encoding", "chunked")); + Assert.ThrowsException(() => request.Headers.Add("User-Agent", "bad\r\nInjected: yes")); + } + + [TestMethod] + public void ContentType_Absent_IsNull() + { + var content = new ByteArrayContent(new byte[1]); + + Assert.IsNull(content.Headers.ContentType); + } + + [DataTestMethod] + [DataRow("text/html; Charset=UTF-8", "text/html", "UTF-8")] + [DataRow("multipart/form-data; boundary=x; charset=utf-8", "multipart/form-data", "utf-8")] + [DataRow("application/json; charset=\"utf-8\";", "application/json", "utf-8")] + [DataRow("image/png; name=a.png", "image/png", null)] + public void ContentType_Parameters_AreRead(string header, string mediaType, string charSet) + { + MediaTypeHeaderValue value = MediaTypeHeaderValue.Parse(header); + + Assert.AreEqual(mediaType, value.MediaType); + Assert.AreEqual(charSet, value.CharSet); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/HttpClientTest.cs b/tests/Cosmos.Network.Http.Tests/HttpClientTest.cs new file mode 100644 index 0000000..7c71f78 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/HttpClientTest.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.Threading; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class HttpClientTest + { + [Setup] + public void SetupHttpClientTest() + { + Assert.SkipTest("Can't run HttpClient unit tests on WIN32 nanoCLR. No network support."); + } + + [TestMethod] + public void Dispose_MultipleTimes_Success() + { + HttpClient client = CreateHttpClient(); + client.Dispose(); + client.Dispose(); + } + + [TestMethod] + public void DefaultRequestHeaders_Idempotent() + { + using (HttpClient client = CreateHttpClient()) + { + Assert.NotNull(client.DefaultRequestHeaders); + Assert.Same(client.DefaultRequestHeaders, client.DefaultRequestHeaders); + } + } + + [TestMethod] + public void BaseAddress_Roundtrip_Equal() + { + using (HttpClient client = CreateHttpClient()) + { + Assert.Null(client.BaseAddress); + + Uri uri = new Uri(CreateFakeUri()); + client.BaseAddress = uri; + Assert.Equal(uri.ToString(), client.BaseAddress.ToString()); + + client.BaseAddress = null; + Assert.Null(client.BaseAddress); + } + } + + [TestMethod] + public void BaseAddress_InvalidUri_Throws() + { + using (HttpClient client = CreateHttpClient()) + { + Assert.Throws(typeof(ArgumentException), + () => client.BaseAddress = new Uri("ftp://onlyhttpsupported")); + + Assert.Throws(typeof(ArgumentException), + () => client.BaseAddress = new Uri("/onlyabsolutesupported", UriKind.Relative)); + } + } + + [TestMethod] + public void Timeout_Roundtrip_Equal() + { + using (HttpClient client = CreateHttpClient()) + { + client.Timeout = Timeout.InfiniteTimeSpan; + Assert.Equal(Timeout.InfiniteTimeSpan.Ticks, client.Timeout.Ticks); + + client.Timeout = TimeSpan.FromSeconds(1); + Assert.Equal(TimeSpan.FromSeconds(1).Ticks, client.Timeout.Ticks); + } + } + + [TestMethod] + public void Timeout_OutOfRange_Throws() + { + using (HttpClient client = CreateHttpClient()) + { + Assert.Throws(typeof(ArgumentOutOfRangeException), + () => client.Timeout = TimeSpan.FromSeconds(-2)); + Assert.Throws(typeof(ArgumentOutOfRangeException), + () => client.Timeout = TimeSpan.FromSeconds(0)); + Assert.Throws(typeof(ArgumentOutOfRangeException), + () => client.Timeout = TimeSpan.FromSeconds(int.MaxValue)); + } + } + + [TestMethod] + public void MaxResponseContentBufferSize_ThrowsIfTooSmallForContent(int maxSize, int contentLength, bool exceptionExpected) + { + maxSize = 1; + contentLength = 2; + exceptionExpected = true; + + using HttpClient client = CreateHttpClient(); + + var server = LoopbackServer.CreateServer(); + + var waithandles = new WaitHandle[2] { new AutoResetEvent(false), new AutoResetEvent(false) }; + + Thread clientThread = new Thread(() => + { + string getTask = client.GetString(server.Uri.AbsoluteUri); + ((AutoResetEvent)waithandles[0]).Set(); + }); + + Thread serverThread = new Thread(() => + { + if (exceptionExpected) + { + Assert.Throws(typeof(HttpRequestException), + () => + { + _ = server.AcceptConnectionSendResponseAndClose(content: new string('s', contentLength)); + }); + } + + ((AutoResetEvent)waithandles[1]).Set(); + }); + + WaitHandle.WaitAll(waithandles); + } + + + private HttpClient CreateHttpClient() + { + return new HttpClient(); + } + + private static string CreateFakeUri() => $"http://{Guid.NewGuid()}"; + } +} diff --git a/tests/Cosmos.Network.Http.Tests/HttpContentTest.cs b/tests/Cosmos.Network.Http.Tests/HttpContentTest.cs new file mode 100644 index 0000000..29bc969 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/HttpContentTest.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.IO; +using System.Reflection; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class HttpContentTest + { + [TestMethod] + public void Dispose_BufferContentThenDisposeContent_BufferedStreamGetsDisposed() + { + MockContent content = new MockContent(); + content.LoadIntoBuffer(); + + Type type = typeof(HttpContent); + + FieldInfo bufferedContentField = type.GetField("_buffer", BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.IsNotNull(bufferedContentField, "_buffer field shouldn't be null"); + + MemoryStream bufferedContentStream = bufferedContentField.GetValue(content) as MemoryStream; + Assert.IsNotNull(bufferedContentStream, "bufferedContentStream field shouldn't be null"); + + content.Dispose(); + + // The following line will throw an ObjectDisposedException if the buffered-stream was correctly disposed. + Assert.ThrowsException(typeof(ObjectDisposedException), + () => + { + _ = bufferedContentStream.Length.ToString(); + }); + } + + [TestMethod] + public void LoadIntoBuffer_ContentLengthSmallerThanActualData_ActualDataLargerThanMaxSize_ThrowsException() + { + Assert.SkipTest("Skipping test because of missing implementation in HttpContent"); + + // TODO + // need to implement support for seeakable streams + BufferTestConfig[] bufferTests = new BufferTestConfig[] + { + new BufferTestConfig(1, 100, 99, 1), + new BufferTestConfig(1, 100, 50, 99), + new BufferTestConfig(1, 100, 98, 98), + new BufferTestConfig(1, 100, 99, 99), + new BufferTestConfig(1, 100, 99, 98), + new BufferTestConfig(3, 50, 100, 149), + new BufferTestConfig(3, 50, 149, 149) + }; + + foreach (var testConfig in bufferTests) + { + Assert.IsTrue((testConfig.MaxSize >= 1 && testConfig.MaxSize <= (testConfig.NumberOfWrites * testConfig.SizeOfEachWrite) - 1), "Config values out of range."); + + Assert.ThrowsException(typeof(HttpRequestException), + () => + { + LieAboutLengthContent c = new( + testConfig.NumberOfWrites, + testConfig.SizeOfEachWrite, + testConfig.ReportedLength); + + c.LoadIntoBuffer(); + }); + } + } + + private class BufferTestConfig + { + public int NumberOfWrites { get; set; } + public int SizeOfEachWrite { get; set; } + public int ReportedLength { get; set; } + public int MaxSize { get; set; } + + public BufferTestConfig( + int numberOfWrites, + int sizeOfEachWrite, + int reportedLength, + int maxSize1) + { + NumberOfWrites = numberOfWrites; + SizeOfEachWrite = sizeOfEachWrite; + ReportedLength = reportedLength; + MaxSize = maxSize1; + } + } + + private sealed class LieAboutLengthContent : HttpContent + { + private readonly int _numberOfWrites, _sizeOfEachWrite, _reportedLength; + + public LieAboutLengthContent(int numberOfWrites, int sizeOfEachWrite, int reportedLength) + { + _numberOfWrites = numberOfWrites; + _sizeOfEachWrite = sizeOfEachWrite; + _reportedLength = reportedLength; + } + + protected override void SerializeToStream(Stream stream) + { + byte[] bytes = new byte[_sizeOfEachWrite]; + + for (int i = 0; i < _numberOfWrites; i++) + { + stream.Write(bytes, 0, bytes.Length); + } + } + + protected internal override bool TryComputeLength(out long length) + { + length = _reportedLength; + return true; + } + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/HttpListenerRequestTests.cs b/tests/Cosmos.Network.Http.Tests/HttpListenerRequestTests.cs new file mode 100644 index 0000000..ce64651 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/HttpListenerRequestTests.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + + +using System.Net; +using nanoFramework.TestFramework; + +namespace Cosmos.Network.Http.Tests +{ + // Cosmos: a public test class, which MSTest runs; nanoFramework's has no [TestClass], and no test of it ran. + [TestClass] + public class HttpListenerRequestTests + { + // Verifies that malformed Authorization header (no space) does not cause a crash + [TestMethod] + public void Add_Authorization_NoSpaceMultipleChars_ShouldNotThrow() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: a111111"); + string value = headers["Authorization"]; + Assert.AreEqual("a111111", value); + } + + // Verifies that a properly formatted Authorization header (with space) is parsed and stored correctly + [TestMethod] + public void Add_Authorization_ValidBasicToken_ShouldSucceed() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: Basic dXNlcjpwYXNz"); + string value = headers["Authorization"]; + Assert.AreEqual("Basic dXNlcjpwYXNz", value); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/HttpLoopbackTests.cs b/tests/Cosmos.Network.Http.Tests/HttpLoopbackTests.cs new file mode 100644 index 0000000..665f558 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/HttpLoopbackTests.cs @@ -0,0 +1,852 @@ +// Cosmos: HttpClient against HttpListener over loopback, http and https. nanoFramework's tests skip HttpClient's +// (its desktop CLR has no network) and have none of HttpListener's serving. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class HttpLoopbackTests + { + private static readonly byte[] s_big = CreateBig(1024 * 1024 + 123); + + /// + /// An HttpListener served by one thread, as AuraOS serves it. + /// + private sealed class TestServer : IDisposable + { + private readonly HttpListener _listener; + private readonly Thread _thread; + private readonly List _errors = new List(); + private int _taken; + private int _handled; + + internal TestServer(bool https = false, SslProtocols protocols = SslProtocols.Tls12) + { + Port = FreePort(); + _listener = new HttpListener(https ? "https" : "http", Port); + if (https) + { + _listener.HttpsCert = TestCertificates.EcServer.WithKey(); + _listener.SslProtocols = protocols; + } + + _listener.Start(); + + _thread = new Thread(Serve) { IsBackground = true }; + _thread.Start(); + } + + internal int Port { get; } + + internal List ClientPorts { get; } = new List(); + + internal string Url(string path) => (_listener.HttpsCert != null ? "https" : "http") + "://localhost:" + Port + path; + + internal void Stop() => _listener.Stop(); + + internal void Start() => _listener.Start(); + + private void Serve() + { + while (_listener.IsListening) + { + HttpListenerContext context = _listener.GetContext(); + if (context == null) + { + break; + } + + Interlocked.Increment(ref _taken); + try + { + lock (ClientPorts) + { + ClientPorts.Add(context.Request.RemoteEndPoint.Port); + } + + Handle(context); + } + catch (Exception e) + { + lock (_errors) + { + _errors.Add(e); + } + } + + Interlocked.Increment(ref _handled); + } + + // As AuraOS's httpd: the sockets are the serving thread's to close. + _listener.Close(); + } + + private static void Handle(HttpListenerContext context) + { + HttpListenerRequest request = context.Request; + HttpListenerResponse response = context.Response; + + // nanoFramework's responses close the connection unless told otherwise. + response.KeepAlive = request.KeepAlive; + + switch (request.RawUrl) + { + case "/hello": + Respond(response, Encoding.UTF8.GetBytes("Hello, Cosmos")); + break; + + case "/echo": + { + var body = new MemoryStream(); + byte[] buffer = new byte[4096]; + long left = request.ContentLength64; + while (left > 0) + { + int read = request.InputStream.Read(buffer, 0, (int)Math.Min(buffer.Length, left)); + if (read <= 0) + { + break; + } + + body.Write(buffer, 0, read); + left -= read; + } + + response.ContentType = request.ContentType; + Respond(response, body.ToArray()); + break; + } + + case "/big": + Respond(response, s_big); + break; + + case "/chunked": + { + response.SendChunked = true; + for (int i = 0; i < 5; i++) + { + byte[] part = Encoding.ASCII.GetBytes("part" + i + ";"); + response.OutputStream.Write(part, 0, part.Length); + } + + response.Close(); + break; + } + + case "/header": + response.Headers.Add("X-Echo", request.Headers["X-Test"]); + Respond(response, Array.Empty()); + break; + + case "/postnoread": + // Answered without reading the body. + response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + Respond(response, Encoding.UTF8.GetBytes("no")); + break; + + case "/headchunked": + { + response.SendChunked = true; + if (request.HttpMethod != "HEAD") + { + byte[] part = Encoding.ASCII.GetBytes("chunk"); + response.OutputStream.Write(part, 0, part.Length); + } + + response.Close(); + break; + } + + case "/short": + { + // Says 100 bytes, sends 10. + response.ContentLength64 = 100; + byte[] part = new byte[10]; + response.OutputStream.Write(part, 0, part.Length); + response.Close(); + break; + } + + case "/unframed": + { + // Neither sized nor chunked: ends with the connection. + byte[] part = Encoding.ASCII.GetBytes("unframed body"); + response.OutputStream.Write(part, 0, part.Length); + response.Close(); + break; + } + + case "/ua": + Respond(response, Encoding.UTF8.GetBytes(request.UserAgent ?? "(none)")); + break; + + case "/host": + Respond(response, Encoding.UTF8.GetBytes(request.Headers["Host"] + "|" + request.UserHostName)); + break; + + case "/head": + // The length of the body a GET would have, which a HEAD response announces without sending. + response.ContentLength64 = s_big.Length; + if (request.HttpMethod != "HEAD") + { + response.OutputStream.Write(s_big, 0, s_big.Length); + } + + response.Close(); + break; + + case "/nocontent": + response.StatusCode = (int)HttpStatusCode.NoContent; + response.Close(); + break; + + case "/closeoutput": + { + // The output stream closed by the handler, then the response and the context. + byte[] body = Encoding.UTF8.GetBytes("closed output"); + response.ContentLength64 = body.Length; + using (Stream output = response.OutputStream) + { + output.Write(body, 0, body.Length); + } + + response.Close(); + context.Close(); + break; + } + + case "/ctxclose": + Respond(response, Encoding.UTF8.GetBytes("Hello, Cosmos")); + context.Close(); + break; + + case "/slow": + Thread.Sleep(3000); + Respond(response, Encoding.UTF8.GetBytes("late")); + break; + + default: + response.StatusCode = (int)HttpStatusCode.NotFound; + Respond(response, Encoding.UTF8.GetBytes("Not found")); + break; + } + + // Not context.Close(), which would close a kept-alive connection: the response's Close closes it + // unless it is kept alive. + } + + private static void Respond(HttpListenerResponse response, byte[] body) + { + response.ContentLength64 = body.Length; + response.OutputStream.Write(body, 0, body.Length); + response.Close(); + } + + internal void AssertNoError() + { + // The requests the server took handled to their end: a client has its response before the response's + // Close returns. + long start = Environment.TickCount64; + while (Volatile.Read(ref _handled) != Volatile.Read(ref _taken) && Environment.TickCount64 - start < 5000) + { + Thread.Sleep(10); + } + + lock (_errors) + { + Assert.AreEqual(0, _errors.Count, _errors.Count > 0 ? _errors[0].ToString() : null); + } + } + + public void Dispose() + { + try + { + _listener.Stop(); + } + catch (ObjectDisposedException) + { + // Stopped and closed by the serving thread already. + } + + Assert.IsTrue(_thread.Join(10_000), "GetContext didn't return after Stop."); + } + } + + /// + /// A server that answers one connection with raw bytes, then closes it. + /// + private sealed class RawServer : IDisposable + { + private readonly Socket _listener; + private readonly Thread _thread; + + internal RawServer(string response) + { + _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + _listener.Listen(1); + Port = ((IPEndPoint)_listener.LocalEndPoint).Port; + + _thread = new Thread(() => + { + try + { + using Socket socket = _listener.Accept(); + + // The request's head. + var head = new StringBuilder(); + byte[] buffer = new byte[1]; + while (!head.ToString().EndsWith("\r\n\r\n") && socket.Receive(buffer) == 1) + { + head.Append((char)buffer[0]); + } + + socket.Send(Encoding.ASCII.GetBytes(response)); + socket.Shutdown(SocketShutdown.Both); + } + catch + { + } + }) + { IsBackground = true }; + _thread.Start(); + } + + internal int Port { get; } + + public void Dispose() + { + _listener.Close(); + _thread.Join(5000); + } + } + + private static int FreePort() + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + int port = ((IPEndPoint)socket.LocalEndPoint).Port; + socket.Close(); + return port; + } + + private static byte[] CreateBig(int size) + { + byte[] data = new byte[size]; + new Random(42).NextBytes(data); + return data; + } + + private static HttpClient CreateClient(SslProtocols protocols = SslProtocols.Tls12) + { + return new HttpClient + { + HttpsAuthentCert = TestCertificates.Root.Public(), + SslProtocols = protocols, + Timeout = TimeSpan.FromSeconds(20), + }; + } + + [DataTestMethod] + [DataRow(false, SslProtocols.Tls12)] + [DataRow(true, SslProtocols.Tls12)] + [DataRow(true, SslProtocols.Tls13)] + public void GetString(bool https, SslProtocols protocols) + { + using var server = new TestServer(https, protocols); + using HttpClient client = CreateClient(protocols); + + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + } + + [DataTestMethod] + [DataRow(false)] + [DataRow(true)] + public void GetByteArray_Big(bool https) + { + using var server = new TestServer(https); + using HttpClient client = CreateClient(); + + CollectionAssert.AreEqual(s_big, client.GetByteArray(server.Url("/big"))); + + server.AssertNoError(); + } + + [DataTestMethod] + [DataRow(false)] + [DataRow(true)] + public void GetStream_Big(bool https) + { + using var server = new TestServer(https); + using HttpClient client = CreateClient(); + + using Stream stream = client.GetStream(server.Url("/big")); + var read = new MemoryStream(); + stream.CopyTo(read); + CollectionAssert.AreEqual(s_big, read.ToArray()); + + server.AssertNoError(); + } + + [DataTestMethod] + [DataRow(false)] + [DataRow(true)] + public void ResponseHeadersRead_Big(bool https) + { + using var server = new TestServer(https); + using HttpClient client = CreateClient(); + + using HttpResponseMessage response = client.Get(server.Url("/big"), HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + var received = new MemoryStream(); + response.Content.ReadAsStream().CopyTo(received); + CollectionAssert.AreEqual(s_big, received.ToArray()); + + server.AssertNoError(); + } + + [DataTestMethod] + [DataRow(false)] + [DataRow(true)] + public void Post_Echo(bool https) + { + using var server = new TestServer(https); + using HttpClient client = CreateClient(); + + string body = new string('x', 50_000) + "end"; + using HttpResponseMessage response = client.Post(server.Url("/echo"), new StringContent(body, Encoding.UTF8, "text/plain")); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(body, response.Content.ReadAsString()); + + server.AssertNoError(); + } + + [TestMethod] + public void Get_Chunked() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + + Assert.AreEqual("part0;part1;part2;part3;part4;", client.GetString(server.Url("/chunked"))); + + server.AssertNoError(); + } + + [TestMethod] + public void Get_NotFound() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + + using (HttpResponseMessage response = client.Get(server.Url("/missing"))) + { + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + Assert.AreEqual("Not Found", response.ReasonPhrase); + Assert.AreEqual("Not found", response.Content.ReadAsString()); + } + + Assert.ThrowsException(() => client.GetString(server.Url("/missing"))); + + server.AssertNoError(); + } + + [TestMethod] + public void Headers_RoundTrip() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.Add("X-Test", "cosmos"); + + using HttpResponseMessage response = client.Get(server.Url("/header")); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.IsTrue(response.Headers.TryGetValues("X-Echo", out IEnumerable values)); + CollectionAssert.AreEqual(new[] { "cosmos" }, new List(values)); + Assert.IsTrue(response.Headers.Contains("x-echo")); + Assert.IsFalse(response.Headers.Contains("X-Missing")); + Assert.ThrowsException(() => response.Headers.GetValues("X-Missing")); + + server.AssertNoError(); + } + + [DataTestMethod] + [DataRow(false)] + [DataRow(true)] + public void KeepAlive_ReusesTheConnection(bool https) + { + using var server = new TestServer(https); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + for (int i = 0; i < 3; i++) + { + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + } + + CollectionAssert.AreEqual(s_big, client.GetByteArray(server.Url("/big"))); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + Assert.AreEqual(5, server.ClientPorts.Count); + Assert.AreEqual(1, new HashSet(server.ClientPorts).Count, "The requests came over several connections: " + string.Join(", ", server.ClientPorts)); + } + + [TestMethod] + public void ConnectionClose_OpensOnePerRequest() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + + for (int i = 0; i < 3; i++) + { + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + } + + server.AssertNoError(); + Assert.AreEqual(3, new HashSet(server.ClientPorts).Count); + } + + [TestMethod] + public void Timeout_Throws() + { + using var server = new TestServer(); + using var client = new HttpClient { Timeout = TimeSpan.FromMilliseconds(500) }; + + Assert.ThrowsException(() => client.GetString(server.Url("/slow"))); + } + + [TestMethod] + public void Https_Untrusted_Throws() + { + using var server = new TestServer(https: true); + using var client = new HttpClient { HttpsAuthentCert = TestCertificates.OtherRoot.Public() }; + + Exception error = null; + try + { + client.GetString(server.Url("/hello")); + } + catch (Exception e) + { + error = e; + } + + Assert.IsNotNull(error, "The request succeeded with an untrusted server."); + Assert.IsTrue(error is HttpRequestException || error is AuthenticationException || error is WebException, error.ToString()); + } + + [TestMethod] + public void ConnectionRefused_Throws() + { + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + + Assert.ThrowsException(() => client.GetString("http://127.0.0.1:" + FreePort() + "/")); + } + + [DataTestMethod] + [DataRow("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nshort")] + [DataRow("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n")] + [DataRow("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n10\r\nhello")] + public void Response_CutShort_Throws(string raw) + { + using var server = new RawServer(raw); + using HttpClient client = CreateClient(); + + Assert.ThrowsException(() => client.GetString("http://127.0.0.1:" + server.Port + "/")); + } + + [DataTestMethod] + [DataRow("\r\n\r\n")] + [DataRow("ICY 200 OK\r\n\r\n")] + [DataRow("HTTP/1.1 OK\r\n\r\n")] + [DataRow("SSH-2.0-OpenSSH_9.6\r\n")] + public void Response_Malformed_Throws(string raw) + { + using var server = new RawServer(raw); + using HttpClient client = CreateClient(); + + Assert.ThrowsException(() => client.GetString("http://127.0.0.1:" + server.Port + "/")); + } + + [TestMethod] + public void Response_CloseDelimited_IsComplete() + { + // An HTTP/1.0 body that ends with the connection, after more than the read buffer holds. + string body = new string('b', 1000) + "end"; + using var server = new RawServer("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n" + body); + using HttpClient client = CreateClient(); + + Assert.AreEqual(body, client.GetString("http://127.0.0.1:" + server.Port + "/")); + } + + [TestMethod] + public void KeepAlive_RequestBodyLeftUnread_ClosesTheConnection() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + using (HttpResponseMessage response = client.Post(server.Url("/postnoread"), new StringContent(new string('p', 5000)))) + { + Assert.AreEqual(HttpStatusCode.MethodNotAllowed, response.StatusCode); + Assert.IsTrue(response.Headers.TryGetValues("Connection", out IEnumerable connection)); + CollectionAssert.AreEqual(new[] { "Close" }, new List(connection)); + } + + // Not read as a request: the next one gets its own answer. + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + Assert.AreEqual(2, new HashSet(server.ClientPorts).Count, "The connection the body was left on was reused: " + string.Join(", ", server.ClientPorts)); + } + + [TestMethod] + public void KeepAlive_HeadOfChunkedResponse_HasNoChunk() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + using (HttpResponseMessage response = client.Send(new HttpRequestMessage(HttpMethod.Head, server.Url("/headchunked")))) + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + Assert.AreEqual("chunk", client.GetString(server.Url("/headchunked"))); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + Assert.AreEqual(1, new HashSet(server.ClientPorts).Count); + } + + [TestMethod] + public void Response_ShorterThanItsLength_ClosesTheConnection() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + Assert.ThrowsException(() => client.GetString(server.Url("/short"))); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + } + + [TestMethod] + public void Response_Unframed_EndsWithTheConnection() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + Assert.AreEqual("unframed body", client.GetString(server.Url("/unframed"))); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + } + + [TestMethod] + public void UserAgent_CanBeSet() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.Add("User-Agent", "AuraOS/test"); + + Assert.AreEqual("AuraOS/test", client.GetString(server.Url("/ua"))); + + // Those the request sets itself stay refused. + Assert.ThrowsException(() => client.DefaultRequestHeaders.Add("Host", "example.com")); + Assert.ThrowsException(() => client.DefaultRequestHeaders.Add("Content-Length", "1")); + } + + [TestMethod] + public void Host_IncludesPort() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + + Assert.AreEqual("localhost:" + server.Port + "|localhost:" + server.Port, client.GetString(server.Url("/host"))); + } + + [TestMethod] + public void Head_OnKeptAliveConnection_DoesNotWait() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + client.Timeout = TimeSpan.FromSeconds(5); + + long start = Environment.TickCount64; + using (HttpResponseMessage response = client.Send(new HttpRequestMessage(HttpMethod.Head, server.Url("/head")))) + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(0, response.Content.ReadAsByteArray().Length); + } + + using (HttpResponseMessage response = client.Get(server.Url("/nocontent"))) + { + Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); + } + + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + Assert.IsTrue(Environment.TickCount64 - start < 3000, "Took " + (Environment.TickCount64 - start) + " ms."); + + server.AssertNoError(); + Assert.AreEqual(1, new HashSet(server.ClientPorts).Count, "The connection wasn't reused."); + } + + [TestMethod] + public void OutputStream_ClosedByHandler() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + Assert.AreEqual("closed output", client.GetString(server.Url("/closeoutput"))); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + server.AssertNoError(); + } + + [TestMethod] + public void ContextClose_KeepsAliveConnection() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + client.DefaultRequestHeaders.ConnectionClose = false; + + for (int i = 0; i < 3; i++) + { + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/ctxclose"))); + } + + server.AssertNoError(); + Assert.AreEqual(1, new HashSet(server.ClientPorts).Count, "The requests came over several connections."); + } + + [TestMethod] + public void Pool_DoesNotLendATlsConnectionTrustedOtherwise() + { + using var server = new TestServer(https: true); + + using HttpClient trusting = CreateClient(); + trusting.DefaultRequestHeaders.ConnectionClose = false; + Assert.AreEqual("Hello, Cosmos", trusting.GetString(server.Url("/hello"))); + + // The kept-alive connection was authenticated with the test root, which this client doesn't trust. + using var other = new HttpClient { HttpsAuthentCert = TestCertificates.OtherRoot.Public(), SslProtocols = SslProtocols.Tls12 }; + other.DefaultRequestHeaders.ConnectionClose = false; + Assert.ThrowsException(() => other.GetString(server.Url("/hello"))); + } + + [TestMethod] + public void Https_SilentClient_DoesNotHoldTheListener() + { + using var server = new TestServer(https: true); + + // A client that connects and never says hello. + using var silent = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + silent.Connect(new IPEndPoint(IPAddress.Loopback, server.Port)); + Thread.Sleep(200); + + long start = Environment.TickCount64; + using HttpClient client = CreateClient(); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + Assert.IsTrue(Environment.TickCount64 - start < 3000, "Took " + (Environment.TickCount64 - start) + " ms."); + + server.AssertNoError(); + } + + [TestMethod] + public void Listener_StopDuringRequest_AnswersItThenCloses() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + + string answer = null; + var request = new Thread(() => answer = client.GetString(server.Url("/slow"))); + request.Start(); + + // Stopped from this thread while the server's handles the request. + Thread.Sleep(1000); + server.Stop(); + + Assert.IsTrue(request.Join(10_000)); + Assert.AreEqual("late", answer); + server.AssertNoError(); + + // Closed by the serving thread once the request was answered. + long start = Environment.TickCount64; + while (true) + { + using var probe = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + probe.Connect(new IPEndPoint(IPAddress.Loopback, server.Port)); + } + catch (SocketException) + { + break; + } + + Assert.IsTrue(Environment.TickCount64 - start < 5000, "The listening socket stayed open."); + Thread.Sleep(50); + } + } + + [TestMethod] + public void Listener_StopThenStart_KeepsServing() + { + using var server = new TestServer(); + using HttpClient client = CreateClient(); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + + // From this thread, before the serving one acts on the Stop: undone. + server.Stop(); + server.Start(); + + Thread.Sleep(200); + Assert.AreEqual("Hello, Cosmos", client.GetString(server.Url("/hello"))); + server.AssertNoError(); + } + + [TestMethod] + public void Listener_StopWhileIdle_ReturnsNull() + { + int port = FreePort(); + var listener = new HttpListener("http", port); + listener.Start(); + + HttpListenerContext context = null; + var thread = new Thread(() => context = listener.GetContext()); + thread.Start(); + + Thread.Sleep(200); + listener.Stop(); + + Assert.IsTrue(thread.Join(5000)); + Assert.IsNull(context); + Assert.IsFalse(listener.IsListening); + + // It listens again. + listener.Start(); + listener.Close(); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/HttpRequestTests.cs b/tests/Cosmos.Network.Http.Tests/HttpRequestTests.cs deleted file mode 100644 index 132b768..0000000 --- a/tests/Cosmos.Network.Http.Tests/HttpRequestTests.cs +++ /dev/null @@ -1,390 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using NUnit.Framework; - -namespace Cosmos.Network.Http.Tests; - -/// -/// Sends requests to a loopback server that answers with bytes the tests -/// write out, and checks what went over the wire both ways. -/// -[TestFixture] -public class HttpRequestTests -{ - // Short, so a client that waits for a close it should not need fails fast. - private const int Timeout = 2000; - - [Test] - public void Get_SendsAMinimalRequestAndReturnsTheBody() - { - using TestServer server = new(); - server.Then("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 5\r\n\r\nhello"); - - HttpResponse response = new HttpRequest(server.Url("/page?x=1")) { Timeout = Timeout }.Send(); - - Assert.That(response.StatusCode, Is.EqualTo(200)); - Assert.That(response.GetString(), Is.EqualTo("hello")); - Assert.That(response.Url, Is.EqualTo(server.Url("/page?x=1"))); - - TestRequest request = server.Requests.Single(); - Assert.That(request.Head, Does.StartWith("GET /page?x=1 HTTP/1.1\r\n")); - Assert.That(request.Headers["Host"], Is.EqualTo($"127.0.0.1:{server.Port}")); - Assert.That(request.Headers["Connection"], Is.EqualTo("close")); - Assert.That(request.Headers["Accept-Encoding"], Is.EqualTo("identity")); - Assert.That(request.Headers["User-Agent"], Is.EqualTo(HttpRequest.DefaultUserAgent)); - Assert.That(request.Headers.ContainsKey("Content-Length"), Is.False); - } - - [Test] - public void ContentLength_DoesNotWaitForTheServerToClose() - { - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"); - connection.WaitForClientClose(); - }); - - HttpResponse response = new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send(); - - Assert.That(response.GetString(), Is.EqualTo("ok")); - } - - [Test] - public void Chunked_DoesNotWaitForTheServerToClose() - { - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.WriteSlowly("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n2\r\nde\r\n0\r\n\r\n"); - connection.WaitForClientClose(); - }); - - HttpResponse response = new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send(); - - Assert.That(response.GetString(), Is.EqualTo("abcde")); - } - - [Test] - public void NoLength_ReadsUntilTheServerCloses() - { - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.WriteSlowly("HTTP/1.0 200 OK\r\n\r\n"); - connection.Write(new string('x', 100_000)); - }); - - HttpResponse response = new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send(); - - Assert.That(response.Content, Has.Length.EqualTo(100_000)); - } - - [Test] - public void LargeBody_ArrivesWhole() - { - byte[] body = new byte[3 * 1024 * 1024]; - new Random(42).NextBytes(body); - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write($"HTTP/1.1 200 OK\r\nContent-Length: {body.Length}\r\n\r\n"); - connection.Write(body); - }); - - HttpResponse response = new HttpRequest(server.Url("/big.bin")) { Timeout = Timeout }.Send(); - - Assert.That(response.Content, Is.EqualTo(body)); - } - - [Test] - public void Head_ReturnsNoBody() - { - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\nContent-Length: 1234\r\n\r\n"); - connection.WaitForClientClose(); - }); - - HttpResponse response = new HttpRequest(server.Url("/")) { Method = "HEAD", Timeout = Timeout }.Send(); - - Assert.That(response.Content, Is.Empty); - Assert.That(response.GetHeader("Content-Length"), Is.EqualTo("1234")); - Assert.That(server.Requests.Single().Method, Is.EqualTo("HEAD")); - } - - [Test] - public void Post_SendsTheBodyWithItsLength() - { - using TestServer server = new(); - server.Then("HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n"); - - HttpResponse response = new HttpRequest(server.Url("/api")) - { - Method = "POST", - Body = Encoding.UTF8.GetBytes("{\"a\":1}"), - Headers = { ["Content-Type"] = "application/json" }, - Timeout = Timeout, - }.Send(); - - Assert.That(response.StatusCode, Is.EqualTo(201)); - TestRequest request = server.Requests.Single(); - Assert.That(request.Method, Is.EqualTo("POST")); - Assert.That(request.Headers["Content-Length"], Is.EqualTo("7")); - Assert.That(request.Headers["Content-Type"], Is.EqualTo("application/json")); - Assert.That(Encoding.UTF8.GetString(request.Body), Is.EqualTo("{\"a\":1}")); - } - - [Test] - public void PostWithoutBody_SendsAZeroLength() - { - using TestServer server = new(); - server.Then("HTTP/1.1 204 No Content\r\n\r\n"); - - new HttpRequest(server.Url("/")) { Method = "POST", Timeout = Timeout }.Send(); - - Assert.That(server.Requests.Single().Headers["Content-Length"], Is.EqualTo("0")); - } - - [Test] - public void Headers_ReplaceTheDefaultsAndAddToThem() - { - using TestServer server = new(); - server.Then("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); - - new HttpRequest(server.Url("/")) - { - Headers = - { - ["host"] = "virtual.example", - ["User-Agent"] = "AuraOS/1.0", - ["X-Extra"] = "yes", - }, - Timeout = Timeout, - }.Send(); - - TestRequest request = server.Requests.Single(); - Assert.That(request.Headers["Host"], Is.EqualTo("virtual.example")); - Assert.That(request.Headers["User-Agent"], Is.EqualTo("AuraOS/1.0")); - Assert.That(request.Headers["X-Extra"], Is.EqualTo("yes")); - Assert.That(request.Head.Split("\r\n").Count(line => line.StartsWith("Host:", StringComparison.OrdinalIgnoreCase)), Is.EqualTo(1)); - } - - [TestCase("Connection")] - [TestCase("content-length")] - [TestCase("Transfer-Encoding")] - public void Headers_TheRequestSetsItselfAreRefused(string name) - { - HttpRequest request = new("http://127.0.0.1:1/") { Headers = { [name] = "x" } }; - - Assert.Throws(() => request.Send()); - } - - [Test] - public void Headers_ThatBreakTheLineAreRefused() - { - HttpRequest request = new("http://127.0.0.1:1/") { Headers = { ["X-Evil"] = "a\r\nInjected: yes" } }; - - Assert.Throws(() => request.Send()); - } - - [Test] - public void ErrorStatus_IsReturned_AndEnsureSuccessThrows() - { - using TestServer server = new(); - server.Then("HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nnot found"); - - HttpResponse response = new HttpRequest(server.Url("/missing")) { Timeout = Timeout }.Send(); - - Assert.That(response.StatusCode, Is.EqualTo(404)); - Assert.That(response.GetString(), Is.EqualTo("not found")); - HttpException exception = Assert.Throws(() => response.EnsureSuccessStatusCode())!; - Assert.That(exception.StatusCode, Is.EqualTo(404)); - Assert.That(exception.Message, Does.Contain("404 Not Found")); - } - - [Test] - public void Redirects_AreFollowed() - { - using TestServer server = new(); - server.Then("HTTP/1.1 301 Moved Permanently\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n") - .Then($"HTTP/1.1 302 Found\r\nLocation: {server.Url("/last")}\r\nContent-Length: 0\r\n\r\n") - .Then("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\ndone"); - List log = []; - - HttpResponse response = new HttpRequest(server.Url("/first")) { Timeout = Timeout, Log = log.Add }.Send(); - - Assert.That(response.GetString(), Is.EqualTo("done")); - Assert.That(response.Url, Is.EqualTo(server.Url("/last"))); - Assert.That(server.Requests.Select(request => request.Target), Is.EqualTo(new[] { "/first", "/next", "/last" })); - Assert.That(log, Has.Count.EqualTo(5)); - } - - [Test] - public void SeeOther_TurnsAPostIntoAGet() - { - using TestServer server = new(); - server.Then("HTTP/1.1 303 See Other\r\nLocation: /result\r\nContent-Length: 0\r\n\r\n") - .Then("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); - - new HttpRequest(server.Url("/form")) { Method = "POST", Body = [1, 2, 3], Timeout = Timeout }.Send(); - - TestRequest second = server.Requests.Last(); - Assert.That(second.Method, Is.EqualTo("GET")); - Assert.That(second.Body, Is.Empty); - Assert.That(second.Headers.ContainsKey("Content-Length"), Is.False); - } - - [Test] - public void TemporaryRedirect_KeepsThePost() - { - using TestServer server = new(); - server.Then("HTTP/1.1 307 Temporary Redirect\r\nLocation: /elsewhere\r\nContent-Length: 0\r\n\r\n") - .Then("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); - - new HttpRequest(server.Url("/form")) { Method = "POST", Body = [1, 2, 3], Timeout = Timeout }.Send(); - - TestRequest second = server.Requests.Last(); - Assert.That(second.Method, Is.EqualTo("POST")); - Assert.That(second.Body, Is.EqualTo(new byte[] { 1, 2, 3 })); - } - - [Test] - public void Redirects_StopAtTheLimit() - { - using TestServer server = new(); - for (int i = 0; i < 3; i++) - { - server.Then("HTTP/1.1 302 Found\r\nLocation: /again\r\nContent-Length: 0\r\n\r\n"); - } - - HttpException exception = Assert.Throws(() => new HttpRequest(server.Url("/")) { MaxRedirects = 2, Timeout = Timeout }.Send())!; - Assert.That(exception.StatusCode, Is.EqualTo(302)); - } - - [Test] - public void Redirects_AreReturnedWithoutLimit() - { - using TestServer server = new(); - server.Then("HTTP/1.1 301 Moved Permanently\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n"); - - HttpResponse response = new HttpRequest(server.Url("/")) { MaxRedirects = 0, Timeout = Timeout }.Send(); - - Assert.That(response.StatusCode, Is.EqualTo(301)); - Assert.That(response.GetHeader("Location"), Is.EqualTo("/next")); - } - - [Test] - public void RedirectToAnotherScheme_Throws() - { - using TestServer server = new(); - server.Then("HTTP/1.1 301 Moved Permanently\r\nLocation: ftp://files.example/\r\nContent-Length: 0\r\n\r\n"); - - HttpException exception = Assert.Throws(() => new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send())!; - Assert.That(exception.StatusCode, Is.EqualTo(301)); - Assert.That(exception.Message, Does.Contain("ftp://files.example/")); - } - - [Test] - public void SilentServer_TimesOut() - { - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Hang(); - }); - - HttpException exception = Assert.Throws(() => new HttpRequest(server.Url("/")) { Timeout = 300 }.Send())!; - Assert.That(exception.Message, Does.Contain("sent nothing")); - } - - [Test] - public void TruncatedBody_Throws() - { - using TestServer server = new(); - server.Then("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabc"); - - HttpException exception = Assert.Throws(() => new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send())!; - Assert.That(exception.Message, Does.Contain("3 of 10")); - } - - [Test] - public void ClosedWithoutAnswer_Throws() - { - using TestServer server = new(); - server.Then(connection => connection.ReadRequest()); - - HttpException exception = Assert.Throws(() => new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send())!; - Assert.That(exception.Message, Does.Contain("without answering")); - } - - [Test] - public void RefusedConnection_Throws() - { - // A port nothing listens on: bound, then released. - TcpListener listener = new(IPAddress.Loopback, 0); - listener.Start(); - int port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - - HttpException exception = Assert.Throws(() => new HttpRequest($"http://127.0.0.1:{port}/") { Timeout = Timeout }.Send())!; - Assert.That(exception.Message, Does.StartWith($"Could not connect to 127.0.0.1:{port}")); - } - - [Test] - public void UnknownHost_Throws() - { - HttpException exception = Assert.Throws(() => new HttpRequest("http://no-such-host.invalid/") { Timeout = Timeout }.Send())!; - Assert.That(exception.Message, Does.StartWith("Could not resolve no-such-host.invalid")); - } - - [Test] - public void GetString_UsesTheCharsetAndSkipsTheByteOrderMark() - { - using TestServer server = new(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=\"ISO-8859-1\"\r\nContent-Length: 1\r\n\r\n"); - connection.Write([0xE9]); - }).Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n"); - connection.Write([0xEF, 0xBB, 0xBF, (byte)'{']); - }); - - Assert.That(new HttpRequest(server.Url("/latin1")) { Timeout = Timeout }.Send().GetString(), Is.EqualTo("é")); - Assert.That(new HttpRequest(server.Url("/bom")) { Timeout = Timeout }.Send().GetString(), Is.EqualTo("{")); - } - - [TestCase("GET POST")] - [TestCase("G\r\nET")] - [TestCase("")] - public void Method_MustBeAToken(string method) - { - Assert.Throws(() => _ = new HttpRequest("http://host/") { Method = method }); - } - - [Test] - public void Settings_AreValidated() - { - Assert.Throws(() => _ = new HttpRequest("http://host/") { Timeout = 0 }); - Assert.Throws(() => _ = new HttpRequest("http://host/") { MaxRedirects = -1 }); - Assert.Throws(() => _ = new HttpRequest(" ")); - Assert.Throws(() => _ = new HttpRequest("ftp://host/")); - } -} diff --git a/tests/Cosmos.Network.Http.Tests/HttpUrlTests.cs b/tests/Cosmos.Network.Http.Tests/HttpUrlTests.cs deleted file mode 100644 index 369e78c..0000000 --- a/tests/Cosmos.Network.Http.Tests/HttpUrlTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using NUnit.Framework; - -namespace Cosmos.Network.Http.Tests; - -[TestFixture] -public class HttpUrlTests -{ - [TestCase("http://httpforever.com/", "httpforever.com", 80, "/")] - [TestCase("http://httpforever.com", "httpforever.com", 80, "/")] - [TestCase("HTTP://Example.com/a/b.html", "Example.com", 80, "/a/b.html")] - [TestCase("httpforever.com/page", "httpforever.com", 80, "/page")] - [TestCase("http://10.0.2.2:8080/x?y=1", "10.0.2.2", 8080, "/x?y=1")] - [TestCase("http://host?q=1", "host", 80, "/?q=1")] - [TestCase("http://host/a#section", "host", 80, "/a")] - [TestCase("http://host:/a", "host", 80, "/a")] - [TestCase(" http://host/a ", "host", 80, "/a")] - [TestCase("http://[::1]:8000/", "::1", 8000, "/")] - [TestCase("host/search?u=http://other/", "host", 80, "/search?u=http://other/")] - [TestCase("https://example.com/", "example.com", 443, "/")] - [TestCase("HTTPS://example.com:8443/a?b", "example.com", 8443, "/a?b")] - [TestCase("https://host:/a", "host", 443, "/a")] - public void Parse_TakesTheUrlApart(string url, string host, int port, string target) - { - HttpUrl parsed = HttpUrl.Parse(url); - - Assert.Multiple(() => - { - Assert.That(parsed.Host, Is.EqualTo(host)); - Assert.That(parsed.Port, Is.EqualTo(port)); - Assert.That(parsed.Target, Is.EqualTo(target)); - }); - } - - [TestCase("http://host/", false)] - [TestCase("host/", false)] - [TestCase("https://host/", true)] - [TestCase("HtTpS://host/", true)] - public void Parse_TellsWhetherTheUrlIsSecure(string url, bool isSecure) - { - Assert.That(HttpUrl.Parse(url).IsSecure, Is.EqualTo(isSecure)); - } - - [TestCase("http://host/", "host")] - [TestCase("http://host:8080/", "host:8080")] - [TestCase("http://[::1]:8080/", "[::1]:8080")] - [TestCase("http://host:443/", "host:443")] - [TestCase("https://host:443/", "host")] - [TestCase("https://host:80/", "host:80")] - [TestCase("https://[::1]/", "[::1]")] - public void Authority_LeavesOutTheDefaultPort(string url, string authority) - { - Assert.That(HttpUrl.Parse(url).Authority, Is.EqualTo(authority)); - } - - [TestCase("https://Host/a", "https://Host/a")] - [TestCase("https://host:443/a", "https://host/a")] - [TestCase("https://host:8443", "https://host:8443/")] - public void ToString_WritesTheUrlOutInFull(string url, string expected) - { - Assert.That(HttpUrl.Parse(url).ToString(), Is.EqualTo(expected)); - } - - [Test] - public void Parse_PercentEncodesWhatARequestLineCannotCarry() - { - Assert.That(HttpUrl.Parse("http://host/a file/é?x=a b").Target, Is.EqualTo("/a%20file/%C3%A9?x=a%20b")); - Assert.That(HttpUrl.Parse("http://host/a%20b").Target, Is.EqualTo("/a%20b")); - } - - [TestCase("ftp://host/")] - [TestCase("wss://host/")] - [TestCase("http://user:pass@host/")] - [TestCase("https://user@host/")] - public void Parse_RefusesWhatItDoesNotSupport(string url) - { - Assert.Throws(() => HttpUrl.Parse(url)); - } - - [TestCase("http://")] - [TestCase("http:///path")] - [TestCase("http://host:0/")] - [TestCase("http://host:65536/")] - [TestCase("http://host:http/")] - [TestCase("http://[::1/")] - [TestCase("http://[::1]x/")] - public void Parse_RefusesMalformedUrls(string url) - { - Assert.Throws(() => HttpUrl.Parse(url)); - } - - [TestCase("http://other:81/x", "http://other:81/x")] - [TestCase("//other/x", "http://other/x")] - [TestCase("/x/y", "http://host:8080/x/y")] - [TestCase("?page=2", "http://host:8080/dir/file?page=2")] - [TestCase("other", "http://host:8080/dir/other")] - [TestCase("sub/other?a=1", "http://host:8080/dir/sub/other?a=1")] - [TestCase("#top", "http://host:8080/dir/file?q=1")] - [TestCase("/a b", "http://host:8080/a%20b")] - public void Resolve_FollowsEveryKindOfLocation(string location, string expected) - { - HttpUrl url = HttpUrl.Parse("http://host:8080/dir/file?q=1"); - - Assert.That(url.Resolve(location).ToString(), Is.EqualTo(expected)); - } - - [TestCase("http://host/a", "https://other/b", "https://other/b")] - [TestCase("https://host/a", "http://other/b", "http://other/b")] - [TestCase("https://host/a", "//other/b", "https://other/b")] - [TestCase("https://host:8443/a/b", "c", "https://host:8443/a/c")] - [TestCase("https://host/a", "/c?d", "https://host/c?d")] - public void Resolve_KeepsOrChangesTheScheme(string url, string location, string expected) - { - Assert.That(HttpUrl.Parse(url).Resolve(location).ToString(), Is.EqualTo(expected)); - } - - [Test] - public void Resolve_RefusesOtherSchemes() - { - Assert.Throws(() => HttpUrl.Parse("https://host/").Resolve("ftp://host/")); - } -} diff --git a/tests/Cosmos.Network.Http.Tests/HttpUtilityTest.cs b/tests/Cosmos.Network.Http.Tests/HttpUtilityTest.cs new file mode 100644 index 0000000..36bc3c7 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/HttpUtilityTest.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System.IO; +using System.Text; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class HttpUtilityTest + { + + [TestMethod] + public void UrlDecodeNoThrow() + { + string str = "../../&param2=%CURRREV%"; + + Assert.AreEqual(str, HttpUtility.UrlDecode(str)); + } + + [TestMethod] + public void UrlDecodeTest() + { + byte[] bIn; + + for (char c = char.MinValue; c < '\uD800'; c++) + { + bIn = Encoding.UTF8.GetBytes(c.ToString()); + using MemoryStream encodedValueBytes = new MemoryStream(); + + // build expected result for UrlEncode + for (int i = 0; i < bIn.Length; i++) + { + UrlEncodeChar((char)bIn[i], encodedValueBytes, false); + } + + byte[] bOut = encodedValueBytes.ToArray(); + string encodedValue = Encoding.UTF8.GetString(bOut, 0, bOut.Length); + + string decodedValue = HttpUtility.UrlDecode(encodedValue); + + Assert.AreEqual(c.ToString(), decodedValue, + $"Expecting UrlEncode of '{c}' ({(int)c}) as [{c}] got {decodedValue}"); + } + } + + [TestMethod] + public void UrlEncodeTest() + { + byte[] bIn; + for (char c = char.MinValue; c < '\uD800'; c++) + { + bIn = Encoding.UTF8.GetBytes(c.ToString()); + using MemoryStream expected = new MemoryStream(); + using MemoryStream expUnicode = new MemoryStream(); + + // build expected result for UrlEncode + for (int i = 0; i < bIn.Length; i++) + { + UrlEncodeChar((char)bIn[i], expected, false); + } + + // build expected result for UrlEncodeUnicode + UrlEncodeChar(c, expUnicode, true); + + byte[] bOut = expected.ToArray(); + + string expectedResult = Encoding.UTF8.GetString(bOut, 0, bOut.Length); + string actualResult = HttpUtility.UrlEncode(c.ToString()); + + Assert.AreEqual(expectedResult, actualResult, + $"Expecting UrlEncode of '{c}' ({(int)c}) as [{expectedResult}] got {actualResult}"); + } + } + + static void UrlEncodeChar(char c, Stream result, bool isUnicode) + { + if (c > 255) + { + int idx; + int i = (int)c; + + result.WriteByte((byte)'%'); + result.WriteByte((byte)'u'); + idx = i >> 12; + result.WriteByte((byte)hexChars[idx]); + idx = (i >> 8) & 0x0F; + result.WriteByte((byte)hexChars[idx]); + idx = (i >> 4) & 0x0F; + result.WriteByte((byte)hexChars[idx]); + idx = i & 0x0F; + result.WriteByte((byte)hexChars[idx]); + return; + } + + if (c > ' ' && notEncoded.IndexOf(c) != -1) + { + result.WriteByte((byte)c); + return; + } + if (c == ' ') + { + result.WriteByte((byte)'+'); + return; + } + if ((c < '0') || + (c < 'A' && c > '9') || + (c > 'Z' && c < 'a') || + (c > 'z')) + { + if (isUnicode && c > 127) + { + result.WriteByte((byte)'%'); + result.WriteByte((byte)'u'); + result.WriteByte((byte)'0'); + result.WriteByte((byte)'0'); + } + else + { + result.WriteByte((byte)'%'); + } + + int idx = ((int)c) >> 4; + result.WriteByte((byte)hexChars[idx]); + idx = c & 0x0F; + result.WriteByte((byte)hexChars[idx]); + } + else + { + result.WriteByte((byte)c); + } + } + + static char[] hexChars = "0123456789ABCDEF".ToCharArray(); + const string notEncoded = "~-._"; + } +} diff --git a/tests/Cosmos.Network.Http.Tests/HttpsTests.cs b/tests/Cosmos.Network.Http.Tests/HttpsTests.cs deleted file mode 100644 index 974ab94..0000000 --- a/tests/Cosmos.Network.Http.Tests/HttpsTests.cs +++ /dev/null @@ -1,379 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Diagnostics; -using System.Linq; -using System.Net.Security; -using System.Security.Authentication; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using NUnit.Framework; - -namespace Cosmos.Network.Http.Tests; - -/// -/// Sends https:// requests to a loopback server that speaks TLS through the -/// desktop's SslStream, with certificates from . -/// -[TestFixture] -public class HttpsTests -{ - private const int Timeout = 5000; - - private X509Certificate2 _root = null!; - private X509Certificate2 _intermediate = null!; - private TrustedRoots _roots = null!; - - [OneTimeSetUp] - public void CreatePki() - { - _root = TestPki.Root(); - _intermediate = TestPki.Authority(_root); - _roots = TrustedRoots.FromCertificates([_root.RawData]); - } - - [TestCase(SslProtocols.Tls13)] - [TestCase(SslProtocols.Tls12)] - public void Get_OverTls_ReturnsTheBody(SslProtocols protocol) - { - using TestServer server = Server(protocol); - server.Then("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nsecret"); - - HttpResponse response = Request(server.Url("/page")).Send(); - - Assert.That(response.StatusCode, Is.EqualTo(200)); - Assert.That(response.GetString(), Is.EqualTo("secret")); - Assert.That(response.Url, Is.EqualTo(server.Url("/page"))); - - TestHandshake handshake = server.Handshakes.Single(); - Assert.That(handshake.Protocol, Is.EqualTo(protocol)); - Assert.That(handshake.ApplicationProtocol, Is.EqualTo("http/1.1")); - Assert.That(server.Requests.Single().Headers["Host"], Is.EqualTo($"127.0.0.1:{server.Port}")); - } - - [Test] - public void HostName_IsSentAsServerName() - { - using TestServer server = Server(); - server.Then("HTTP/1.1 204 No Content\r\n\r\n"); - - Request(server.Url("/", "localhost")).Send(); - - Assert.That(server.Handshakes.Single().ServerName, Is.EqualTo("localhost")); - } - - [Test] - public void Address_IsNotSentAsServerName() - { - using TestServer server = Server(); - server.Then("HTTP/1.1 204 No Content\r\n\r\n"); - - Request(server.Url("/")).Send(); - - Assert.That(server.Handshakes.Single().ServerName, Is.Null); - } - - [Test] - public void Chunked_SentByteByByte_ComesWhole() - { - using TestServer server = Server(); - server.Then(connection => - { - connection.ReadRequest(); - connection.WriteSlowly("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n2\r\nde\r\n0\r\n\r\n"); - connection.WaitForClientClose(); - }); - - Assert.That(Request(server.Url("/")).Send().GetString(), Is.EqualTo("abcde")); - } - - [Test] - public void LargeBody_SpansManyRecords() - { - byte[] body = new byte[200_000]; - new Random(1).NextBytes(body); - using TestServer server = Server(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write($"HTTP/1.1 200 OK\r\nContent-Length: {body.Length}\r\n\r\n"); - connection.Write(body); - }); - - Assert.That(Request(server.Url("/")).Send().Content, Is.EqualTo(body)); - } - - [Test] - public void NoLength_EndsAtCloseNotify_WithoutWaitingForTheConnectionToClose() - { - using ManualResetEventSlim done = new(); - using TestServer server = Server(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\n\r\nuntil close_notify"); - connection.CloseNotify(); - // The connection stays open, unread: only close_notify ends the body. - done.Wait(10_000); - }); - - Stopwatch watch = Stopwatch.StartNew(); - HttpResponse response = Request(server.Url("/")).Send(); - long elapsed = watch.ElapsedMilliseconds; - done.Set(); - - Assert.That(response.GetString(), Is.EqualTo("until close_notify")); - Assert.That(elapsed, Is.LessThan(Timeout / 2)); - } - - [TestCase(SslProtocols.Tls13)] - [TestCase(SslProtocols.Tls12)] - public void RsaServer_IsTrusted(SslProtocols protocol) - { - X509Certificate2 root = TestPki.Root("RSA Root", RSA.Create(2048)); - X509Certificate2 intermediate = TestPki.Authority(root, "RSA Intermediate", key: RSA.Create(2048)); - X509Certificate2 certificate = TestPki.Server(intermediate, ["127.0.0.1"], key: RSA.Create(2048)); - using TestServer server = new(SslStreamCertificateContext.Create(certificate, [intermediate], offline: true), protocol); - server.Then("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nrsa"); - - HttpResponse response = new HttpRequest(server.Url("/")) - { - Timeout = Timeout, - TrustedRoots = TrustedRoots.FromCertificates([root.RawData]), - }.Send(); - - Assert.That(response.GetString(), Is.EqualTo("rsa")); - Assert.That(server.Handshakes.Single().Protocol, Is.EqualTo(protocol)); - } - - [Test] - public void NoLength_ReadsUntilTheServerCloses_WithoutCloseNotify() - { - using TestServer server = Server(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\n\r\nuntil the end"); - }); - - Assert.That(Request(server.Url("/")).Send().GetString(), Is.EqualTo("until the end")); - } - - [Test] - public void ContentLength_CutShort_Throws() - { - using TestServer server = Server(); - server.Then(connection => - { - connection.ReadRequest(); - connection.Write("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nhalf"); - connection.CloseNotify(); - }); - - HttpException exception = Assert.Throws(() => Request(server.Url("/")).Send())!; - Assert.That(exception.Message, Does.Contain("after 4 of 10 bytes")); - } - - [Test] - public void Post_SendsTheBodyEncrypted() - { - using TestServer server = Server(); - server.Then("HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n"); - - HttpResponse response = new HttpRequest(server.Url("/items")) - { - Method = "POST", - Body = "name=value"u8.ToArray(), - Timeout = Timeout, - TrustedRoots = _roots, - }.Send(); - - Assert.That(response.StatusCode, Is.EqualTo(201)); - Assert.That(server.Requests.Single().Body, Is.EqualTo("name=value"u8.ToArray())); - } - - [Test] - public void RedirectFromHttp_IsFollowed() - { - using TestServer secure = Server(); - secure.Then("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"); - using TestServer plain = new(); - plain.Then($"HTTP/1.1 301 Moved Permanently\r\nLocation: {secure.Url("/")}\r\nContent-Length: 0\r\n\r\n"); - - HttpResponse response = Request(plain.Url("/")).Send(); - - Assert.That(response.GetString(), Is.EqualTo("ok")); - Assert.That(response.Url, Is.EqualTo(secure.Url("/"))); - } - - [Test] - public void RedirectToHttp_IsNotFollowed() - { - using TestServer plain = new(); - using TestServer secure = Server(); - secure.Then($"HTTP/1.1 307 Temporary Redirect\r\nLocation: {plain.Url("/leak")}\r\nContent-Length: 0\r\n\r\n"); - - HttpException exception = Assert.Throws(() => new HttpRequest(secure.Url("/")) - { - Method = "POST", - Body = "password=hunter2"u8.ToArray(), - Headers = { ["Authorization"] = "Bearer secret" }, - Timeout = Timeout, - TrustedRoots = _roots, - }.Send())!; - - Assert.That(exception.StatusCode, Is.EqualTo(307)); - Assert.That(exception.Message, Does.Contain("it would leave TLS")); - Assert.That(plain.Requests, Is.Empty); - } - - [Test] - public void RedirectToAnotherServer_DropsTheHeadersMeantForTheFirst() - { - using TestServer other = Server(); - other.Then("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); - using TestServer first = Server(); - first.Then($"HTTP/1.1 302 Found\r\nLocation: /same\r\nContent-Length: 0\r\n\r\n"); - first.Then($"HTTP/1.1 302 Found\r\nLocation: {other.Url("/other")}\r\nContent-Length: 0\r\n\r\n"); - - new HttpRequest(first.Url("/")) - { - Headers = { ["Authorization"] = "Bearer secret", ["Cookie"] = "session=1", ["Host"] = "virtual.test", ["X-Trace"] = "kept" }, - Timeout = Timeout, - TrustedRoots = _roots, - }.Send(); - - TestRequest[] firstRequests = first.Requests.ToArray(); - Assert.That(firstRequests.Select(request => request.Headers["Authorization"]), Is.All.EqualTo("Bearer secret")); - Assert.That(firstRequests.Select(request => request.Headers["Host"]), Is.All.EqualTo("virtual.test")); - - TestRequest redirected = other.Requests.Single(); - Assert.That(redirected.Headers.ContainsKey("Authorization"), Is.False); - Assert.That(redirected.Headers.ContainsKey("Cookie"), Is.False); - Assert.That(redirected.Headers["Host"], Is.EqualTo($"127.0.0.1:{other.Port}")); - Assert.That(redirected.Headers["X-Trace"], Is.EqualTo("kept")); - } - - [Test] - public void UntrustedRoot_FailsTheHandshake() - { - using TestServer server = Server(); - server.Then(connection => connection.ReadRequest()); - - HttpException exception = Assert.Throws(() => - new HttpRequest(server.Url("/")) { Timeout = Timeout, TrustedRoots = TrustedRoots.FromCertificates([TestPki.Root("Other").RawData]) }.Send())!; - - Assert.That(exception.Message, Does.StartWith("The certificate of 127.0.0.1 is not trusted: the chain goes up to CN=Test Root")); - Assert.That(exception.StatusCode, Is.Zero); - Assert.That(server.Requests, Is.Empty); - } - - [Test] - public void MozillaRoots_DoNotTrustATestRoot() - { - using TestServer server = Server(); - server.Then(connection => connection.ReadRequest()); - - HttpException exception = Assert.Throws(() => new HttpRequest(server.Url("/")) { Timeout = Timeout }.Send())!; - - Assert.That(exception.Message, Does.Contain("is not trusted")); - } - - [Test] - public void OtherHost_FailsTheHandshake() - { - using TestServer server = Server(names: ["elsewhere.test"]); - server.Then(connection => connection.ReadRequest()); - - HttpException exception = Assert.Throws(() => Request(server.Url("/")).Send())!; - - Assert.That(exception.Message, Is.EqualTo("The certificate of 127.0.0.1 is not trusted: the certificate is for elsewhere.test, not 127.0.0.1.")); - } - - [Test] - public void Validation_CanTrustASelfSignedServer() - { - X509Certificate2 selfSigned = TestPki.Server(null, ["127.0.0.1"]); - using TestServer server = new(SslStreamCertificateContext.Create(selfSigned, null, offline: true)); - server.Then("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\npin!"); - ServerCertificate? seen = null; - - HttpResponse response = new HttpRequest(server.Url("/")) - { - Timeout = Timeout, - ServerCertificateValidation = certificate => - { - seen = certificate; - return certificate.Fingerprint == Convert.ToHexString(SHA256.HashData(selfSigned.RawData)); - }, - }.Send(); - - Assert.That(response.GetString(), Is.EqualTo("pin!")); - Assert.That(seen!.Error, Is.EqualTo("the certificate is self-signed")); - Assert.That(seen.Host, Is.EqualTo("127.0.0.1")); - Assert.That(seen.Subject, Is.EqualTo("CN=127.0.0.1")); - Assert.That(seen.Chain.Single(), Is.EqualTo(selfSigned.RawData)); - Assert.That(seen.NotAfter, Is.EqualTo(selfSigned.NotAfter.ToUniversalTime()).Within(TimeSpan.FromSeconds(1))); - } - - [Test] - public void Validation_CanRefuseATrustedServer() - { - using TestServer server = Server(); - server.Then(connection => connection.ReadRequest()); - - HttpException exception = Assert.Throws(() => - new HttpRequest(server.Url("/")) { Timeout = Timeout, TrustedRoots = _roots, ServerCertificateValidation = _ => false }.Send())!; - - Assert.That(exception.Message, Does.EndWith("is not trusted: ServerCertificateValidation refused it.")); - } - - [Test] - public void Validation_ThatThrows_FailsTheRequestWithItsException() - { - using TestServer server = Server(); - server.Then(connection => connection.ReadRequest()); - InvalidOperationException thrown = new("no way"); - - HttpException exception = Assert.Throws(() => - new HttpRequest(server.Url("/")) { Timeout = Timeout, TrustedRoots = _roots, ServerCertificateValidation = _ => throw thrown }.Send())!; - - Assert.That(exception.InnerException, Is.SameAs(thrown)); - Assert.That(exception.Message, Does.Contain("no way")); - } - - [Test] - public void SilentServer_TimesOutDuringTheHandshake() - { - using TestServer plain = new(); - plain.Then(connection => connection.Hang()); - - // A plain server that never answers the ClientHello. - string url = plain.Url("/").Replace("http://", "https://"); - HttpException exception = Assert.Throws(() => - new HttpRequest(url) { Timeout = 300, TrustedRoots = _roots }.Send())!; - - Assert.That(exception.Message, Does.Contain("did not finish the TLS handshake in 300 ms")); - } - - [Test] - public void PlainHttpServer_FailsTheHandshake() - { - using TestServer plain = new(); - plain.Then(connection => connection.Write("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")); - - string url = plain.Url("/").Replace("http://", "https://"); - HttpException exception = Assert.Throws(() => new HttpRequest(url) { Timeout = Timeout, TrustedRoots = _roots }.Send())!; - - Assert.That(exception.Message, Does.StartWith("The TLS handshake with 127.0.0.1:")); - } - - private TestServer Server(SslProtocols protocols = SslProtocols.None, string[]? names = null) - { - X509Certificate2 certificate = TestPki.Server(_intermediate, names ?? ["localhost", "127.0.0.1"]); - return new TestServer(SslStreamCertificateContext.Create(certificate, [_intermediate], offline: true), protocols); - } - - private HttpRequest Request(string url) => new(url) { Timeout = Timeout, TrustedRoots = _roots }; -} diff --git a/tests/Cosmos.Network.Http.Tests/LoopbackServer.cs b/tests/Cosmos.Network.Http.Tests/LoopbackServer.cs new file mode 100644 index 0000000..35a355d --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/LoopbackServer.cs @@ -0,0 +1,332 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; + +namespace Cosmos.Network.Http.Tests +{ + public sealed partial class LoopbackServer : IDisposable + { + private Socket _listenSocket; + private Options _options; + private Uri _uri; + + public Socket ListenSocket => _listenSocket; + public Uri Uri => _uri; + + // Use CreateServerAsync or similar to create + private LoopbackServer(Socket listenSocket, Options options) + { + _listenSocket = listenSocket; + _options = options; + + var localEndPoint = (IPEndPoint)listenSocket.LocalEndPoint; + + string host = options.Address.AddressFamily == AddressFamily.InterNetworkV6 ? + $"[{localEndPoint.Address}]" : + localEndPoint.Address.ToString(); + + string scheme = options.UseSsl ? "https" : "http"; + + if (options.WebSocketEndpoint) + { + scheme = options.UseSsl ? "wss" : "ws"; + } + + _uri = new Uri($"{scheme}://{host}:{localEndPoint.Port}/"); + } + + public static LoopbackServer CreateServer(Options options = null) + { + options = options ?? new Options(); + + using var listenSocket = new Socket(options.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + listenSocket.Bind(new IPEndPoint(options.Address, 0)); + listenSocket.Listen(options.ListenBacklog); + + using LoopbackServer server = new LoopbackServer(listenSocket, options); + + return server; + } + + public ArrayList AcceptConnection(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) + { + using Socket s = _listenSocket.Accept(); + + //s.NoDelay = true; + + Stream stream = new NetworkStream(s, ownsSocket: false); + + if (_options.UseSsl) + { + // TODO rework code to follow what we're doing in HttpWebRequest + //var sslStream = new SslStream(s); + + //using (var cert = Configuration.Certificates.GetServerCertificate()) + //{ + // sslStream.AuthenticateAsServer( + // cert, + // clientCertificateRequired: true, // allowed but not required + // enabledSslProtocols: _options.SslProtocols, + // checkCertificateRevocation: false); + //} + //stream = sslStream; + } + + if (_options.StreamWrapper != null) + { + stream = _options.StreamWrapper(stream); + } + + using var connection = new Connection(s, stream); + return connection.ReadRequestHeaderAndSendResponse(statusCode, additionalHeaders, content); + } + + public ArrayList AcceptConnectionSendResponseAndClose(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) + { + ArrayList lines = null; + + // Note, we assume there's no request body. + // We'll close the connection after reading the request header and sending the response. + return AcceptConnection(statusCode, additionalHeaders, content); ; + } + + public void Dispose() + { + if (_listenSocket != null) + { + _listenSocket.Close(); + _listenSocket = null; + } + } + + + // Stolen from HttpStatusDescription code in the product code + private static string GetStatusDescription(HttpStatusCode code) + { + switch ((int)code) + { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 102: + return "Processing"; + + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 207: + return "Multi-Status"; + + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 307: + return "Temporary Redirect"; + + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Request Entity Too Large"; + case 414: + return "Request-Uri Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Requested Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 422: + return "Unprocessable Entity"; + case 423: + return "Locked"; + case 424: + return "Failed Dependency"; + case 426: + return "Upgrade Required"; // RFC 2817 + + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "Http Version Not Supported"; + case 507: + return "Insufficient Storage"; + } + return null; + } + + public static string GetHttpResponse(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) => + $"HTTP/1.1 {(int)statusCode} {GetStatusDescription(statusCode)}\r\n" + + $"Date: {DateTime.UtcNow:R}\r\n" + + $"Content-Length: {(content == null ? 0 : content.Length)}\r\n" + + additionalHeaders + + "\r\n" + + content; + + public static string GetSingleChunkHttpResponse(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) => + $"HTTP/1.1 {(int)statusCode} {GetStatusDescription(statusCode)}\r\n" + + $"Date: {DateTime.UtcNow:R}\r\n" + + "Transfer-Encoding: chunked\r\n" + + additionalHeaders + + "\r\n" + + (string.IsNullOrEmpty(content) ? "" : + $"{content.Length:X}\r\n" + + $"{content}\r\n") + + $"0\r\n" + + $"\r\n"; + + public class Options + { + public IPAddress Address { get; set; } = IPAddress.Loopback; + public int ListenBacklog { get; set; } = 1; + public bool UseSsl { get; set; } = false; + public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; + public bool WebSocketEndpoint { get; set; } = false; + public Func StreamWrapper { get; set; } + public string Username { get; set; } + public string Domain { get; set; } + public string Password { get; set; } + public bool IsProxy { get; set; } = false; + } + + public sealed class Connection : IDisposable + { + private Socket _socket; + private Stream _stream; + private StreamReader _reader; + private StreamWriter _writer; + + public Connection(Socket socket, Stream stream) + { + _socket = socket; + _stream = stream; + + _reader = new StreamReader(stream); + _writer = new StreamWriter(stream); + } + + public Socket Socket => _socket; + public Stream Stream => _stream; + public StreamReader Reader => _reader; + public StreamWriter Writer => _writer; + + public void Dispose() + { + try + { + // Try to shutdown the send side of the socket. + // This seems to help avoid connection reset issues caused by buffered data + // that has not been sent/acked when the graceful shutdown timeout expires. + // This may throw if the socket was already closed, so eat any exception. + // TODO need to investigate what is the appropriate pattern to use with nanoFramework + //_socket.Shutdown(SocketShutdown.Send); + } + catch (Exception) { } + + _reader.Dispose(); + _writer.Dispose(); + _stream.Dispose(); + _socket = null; + } + + public ArrayList ReadRequestHeader() + { + ArrayList lines = new ArrayList(); + string line; + + while (!string.IsNullOrEmpty(line = _reader.ReadLine())) + { + lines.Add(line); + } + + if (line == null) + { + throw new Exception("Unexpected EOF trying to read request header"); + } + + return lines; + } + + public void SendResponse(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) + { + _writer.Write(GetHttpResponse(statusCode, additionalHeaders, content)); + } + + public ArrayList ReadRequestHeaderAndSendCustomResponse(string response) + { + ArrayList lines = ReadRequestHeader(); + _writer.Write(response); + return lines; + } + + public ArrayList ReadRequestHeaderAndSendResponse(HttpStatusCode statusCode = HttpStatusCode.OK, string additionalHeaders = null, string content = null) + { + ArrayList lines = ReadRequestHeader(); + SendResponse(statusCode, additionalHeaders, content); + return lines; + } + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/MediaTypeHeaderValueTest.cs b/tests/Cosmos.Network.Http.Tests/MediaTypeHeaderValueTest.cs new file mode 100644 index 0000000..d5b28dd --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/MediaTypeHeaderValueTest.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using Cosmos.Network.Http.Headers; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + // Cosmos: public, as MSTest runs only public test classes; nanoFramework's test framework runs internal ones. + public class MediaTypeHeaderValueTests + { + [TestMethod] + public void Ctor_MediaTypeNull_Throw() + { + Assert.ThrowsException(typeof(ArgumentException), + () => + { + new MediaTypeHeaderValue(null); + }); + } + + [TestMethod] + public void Ctor_MediaTypeEmpty_Throw() + { + // null and empty should be treated the same. So we also throw for empty strings. + Assert.ThrowsException(typeof(ArgumentException), + () => + { + new MediaTypeHeaderValue(string.Empty); + }); + } + + [TestMethod] + public void Ctor_MediaTypeInvalidFormat_ThrowFormatException() + { + AssertFormatException("text/plain; charset=utf-8; "); + AssertFormatException("text/plain;"); + AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters + } + + [TestMethod] + public void Ctor_MediaTypeValidFormat_SuccessfullyCreated() + { + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.AreEqual("text/plain", mediaType.MediaType); + Assert.IsNull(mediaType.CharSet); + } + + [TestMethod] + public void MediaType_SetAndGetMediaType_MatchExpectations() + { + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.AreEqual("text/plain", mediaType.MediaType); + + mediaType.MediaType = "application/xml"; + Assert.AreEqual("application/xml", mediaType.MediaType); + } + + [TestMethod] + public void CharSet_SetCharSetAndValidateObject_ParametersEntryForCharSetAdded() + { + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/plain") + { + CharSet = "mycharset" + }; + Assert.AreEqual("mycharset", mediaType.CharSet); + + mediaType.CharSet = null; + Assert.IsNull(mediaType.CharSet); + mediaType.CharSet = null; + } + + [TestMethod] + public void ToString_UseDifferentMediaTypes_AllSerializedCorrectly() + { + MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.AreEqual("text/plain", mediaType.ToString()); + + mediaType.CharSet = "utf-8"; + Assert.AreEqual("text/plain; charset=utf-8", mediaType.ToString()); + + mediaType.CharSet = null; + Assert.AreEqual("text/plain", mediaType.ToString()); + } + + [TestMethod] + public void GetHashCode_UseMediaTypeWithoutParameters_SameOrDifferentHashCodes() + { + MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/plain"); + MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("text/plain"); + mediaType2.CharSet = "utf-8"; + MediaTypeHeaderValue mediaType3 = new MediaTypeHeaderValue("text/plain"); + + Assert.AreNotEqual(mediaType1.GetHashCode(), mediaType2.GetHashCode()); + Assert.AreNotEqual(mediaType1.GetHashCode(), mediaType3.GetHashCode()); + Assert.AreNotEqual(mediaType2.GetHashCode(), mediaType3.GetHashCode()); + } + + [TestMethod] + public void Equals_UseMediaTypeWithoutParameters_EqualOrNotEqualNoExceptions() + { + MediaTypeHeaderValue mediaType1 = new MediaTypeHeaderValue("text/plain"); + MediaTypeHeaderValue mediaType2 = new MediaTypeHeaderValue("text/plain"); + mediaType2.CharSet = "utf-8"; + MediaTypeHeaderValue mediaType7 = new MediaTypeHeaderValue("text/other"); + + Assert.IsFalse(mediaType1.Equals(mediaType2), "No params vs. charset."); + Assert.IsFalse(mediaType2.Equals(mediaType1), "charset vs. no params."); + Assert.IsFalse(mediaType1.Equals(null), "No params vs. ."); + Assert.IsFalse(mediaType1.Equals(mediaType7), "text/plain vs. text/other."); + } + + [TestMethod] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + MediaTypeHeaderValue expected = new MediaTypeHeaderValue("text/plain"); + CheckValidParse(" text/plain", expected); + CheckValidParse(" text/plain ", expected); + CheckValidParse("text/plain", expected); + + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // media-type parser. + expected.CharSet = "utf-8"; + + OutputHelper.WriteLine($"Expecting {expected}"); + CheckValidParse("text/plain; charset=utf-8", expected); + CheckValidParse(" text/plain ;charset=utf-8", expected); + } + + [TestMethod] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse("", typeof(FormatException)); + CheckInvalidParse(" ", typeof(FormatException)); + CheckInvalidParse(null, typeof(ArgumentNullException)); + CheckInvalidParse("textplain", typeof(FormatException)); + CheckInvalidParse("textplain;charset=utf-8", typeof(FormatException)); + } + + #region Helper methods + + private static void AssertFormatException(string mediaType) + { + Assert.ThrowsException(typeof(FormatException), + () => + { + new MediaTypeHeaderValue(mediaType); + }); + } + + private void CheckValidParse( + string input, + MediaTypeHeaderValue expectedResult) + { + MediaTypeHeaderValue result = MediaTypeHeaderValue.Parse(input); + Assert.AreEqual(expectedResult.ToString(), result.ToString()); + } + + private void CheckInvalidParse(string input, Type exceptionType) + { + Assert.ThrowsException(exceptionType, + () => + { + MediaTypeHeaderValue.Parse(input); + }); + } + + #endregion + + } +} diff --git a/tests/Cosmos.Network.Http.Tests/MockContent.cs b/tests/Cosmos.Network.Http.Tests/MockContent.cs new file mode 100644 index 0000000..5c3b286 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/MockContent.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System; +using System.IO; +using System.Text; + +namespace Cosmos.Network.Http.Tests +{ + [Flags] + public enum MockOptions + { + None = 0x0, + ThrowInSerializeMethods = 0x1, + ReturnNullInCopyToAsync = 0x2, + UseWriteByteInCopyTo = 0x4, + DontOverrideCreateContentReadStream = 0x8, + CanCalculateLength = 0x10, + ThrowInTryComputeLength = 0x20, + ThrowInAsyncSerializeMethods = 0x40 + } + + public class MockException : Exception + { + public MockException() { } + public MockException(string message) : base(message) { } + public MockException(string message, Exception inner) : base(message, inner) { } + } + + public class MockContent : HttpContent + { + private byte[] _mockData; + private MockOptions _options; + private Exception _customException; + + public int TryComputeLengthCount { get; private set; } + public int SerializeToStreamAsyncCount { get; private set; } + public int CreateContentReadStreamCount { get; private set; } + public int DisposeCount { get; private set; } + + public byte[] MockData + { + get { return _mockData; } + } + + public MockContent() + : this((byte[])null, MockOptions.None) + { + } + + public MockContent(byte[] mockData) + : this(mockData, MockOptions.None) + { + } + + public MockContent(MockOptions options) + : this((byte[])null, options) + { + } + + public MockContent(Exception customException, MockOptions options) + : this((byte[])null, options) + { + _customException = customException; + } + + public MockContent(byte[] mockData, MockOptions options) + { + _options = options; + + if (mockData == null) + { + _mockData = Encoding.UTF8.GetBytes("data"); + } + else + { + _mockData = mockData; + } + } + + public byte[] GetMockData() + { + return _mockData; + } + + protected internal override bool TryComputeLength(out long length) + { + TryComputeLengthCount++; + + if ((_options & MockOptions.ThrowInTryComputeLength) != 0) + { + throw new MockException(); + } + + if ((_options & MockOptions.CanCalculateLength) != 0) + { + length = _mockData.Length; + return true; + } + else + { + length = 0; + return false; + } + } + + protected override void SerializeToStream(Stream stream) + { + SerializeToStreamAsyncCount++; + + if ((_options & MockOptions.ReturnNullInCopyToAsync) != 0) + { + return; + } + + if ((_options & MockOptions.ThrowInAsyncSerializeMethods) != 0) + { + throw _customException; + } + + stream.Write(_mockData, 0, _mockData.Length); + } + + protected override void Dispose(bool disposing) + { + DisposeCount++; + base.Dispose(disposing); + } + + private void CheckThrow() + { + if ((_options & MockOptions.ThrowInSerializeMethods) != 0) + { + throw _customException; + } + } + } + + public class MockMemoryStream : MemoryStream + { + public int DisposeCount { get; private set; } + + public MockMemoryStream(byte[] buffer) + : base(buffer) + { + } + + protected override void Dispose(bool disposing) + { + DisposeCount++; + base.Dispose(disposing); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/Properties/AssemblyInfo.cs b/tests/Cosmos.Network.Http.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a3735af --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright (c) 2021 nanoFramework contributors")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/Cosmos.Network.Http.Tests/ResponseReaderTests.cs b/tests/Cosmos.Network.Http.Tests/ResponseReaderTests.cs deleted file mode 100644 index 9c7f6c0..0000000 --- a/tests/Cosmos.Network.Http.Tests/ResponseReaderTests.cs +++ /dev/null @@ -1,243 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System.Text; -using NUnit.Framework; - -namespace Cosmos.Network.Http.Tests; - -/// -/// Feeds responses to the reader in every split a connection could deliver -/// them in, and checks it tells where they end. -/// -[TestFixture] -public class ResponseReaderTests -{ - private const string Url = "http://host/"; - - [Test] - public void ContentLength_CompletesWithoutTheConnectionClosing() - { - const string response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello"; - - foreach (ResponseReader reader in FeedInEverySplit(response)) - { - Assert.That(reader.IsComplete, Is.True); - HttpResponse parsed = reader.ToResponse(Url); - Assert.That(parsed.StatusCode, Is.EqualTo(200)); - Assert.That(parsed.ReasonPhrase, Is.EqualTo("OK")); - Assert.That(parsed.Version, Is.EqualTo("HTTP/1.1")); - Assert.That(parsed.ContentType, Is.EqualTo("text/plain")); - Assert.That(Encoding.ASCII.GetString(parsed.Content), Is.EqualTo("hello")); - } - } - - [Test] - public void ContentLength_IsNotCompleteBeforeTheLastByte() - { - ResponseReader reader = Feed("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhell"); - - Assert.That(reader.IsComplete, Is.False); - HttpException exception = Assert.Throws(() => reader.ToResponse(Url))!; - Assert.That(exception.Message, Does.Contain("4 of 5")); - } - - [Test] - public void ContentLength_IgnoresBytesPastTheBody() - { - HttpResponse response = Feed("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhello").ToResponse(Url); - - Assert.That(Encoding.ASCII.GetString(response.Content), Is.EqualTo("he")); - } - - [Test] - public void Chunked_CompletesAtTheLastChunkAndTrailers() - { - const string response = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" - + "5;name=value\r\nhello\r\n" - + "1\r\n,\r\n" - + "6 \r\n world\r\n" - + "0\r\nExpires: never\r\n\r\n"; - - foreach (ResponseReader reader in FeedInEverySplit(response)) - { - Assert.That(reader.IsComplete, Is.True); - Assert.That(Encoding.ASCII.GetString(reader.ToResponse(Url).Content), Is.EqualTo("hello, world")); - } - } - - [Test] - public void Chunked_IsNotCompleteBeforeTheBlankLineAfterTheLastChunk() - { - ResponseReader reader = Feed("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n"); - - Assert.That(reader.IsComplete, Is.False); - } - - [Test] - public void Chunked_TruncatedBodyThrows() - { - ResponseReader reader = Feed("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nA\r\nabc"); - - Assert.Throws(() => reader.ToResponse(Url)); - } - - [TestCase("zz")] - [TestCase("")] - [TestCase("5x")] - public void Chunked_MalformedSizeThrows(string size) - { - Assert.Throws(() => Feed($"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{size}\r\nhello\r\n0\r\n\r\n")); - } - - [Test] - public void TransferEncoding_WinsOverContentLength() - { - ResponseReader reader = Feed("HTTP/1.1 200 OK\r\nContent-Length: 100\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nok\r\n0\r\n\r\n"); - - Assert.That(reader.IsComplete, Is.True); - Assert.That(Encoding.ASCII.GetString(reader.ToResponse(Url).Content), Is.EqualTo("ok")); - } - - [Test] - public void NoLength_RunsUntilTheConnectionCloses() - { - ResponseReader reader = Feed("HTTP/1.0 200 OK\r\n\r\neverything until the end"); - - Assert.That(reader.IsComplete, Is.False); - Assert.That(Encoding.ASCII.GetString(reader.ToResponse(Url).Content), Is.EqualTo("everything until the end")); - } - - [TestCase(204)] - [TestCase(304)] - public void StatusesWithoutBody_CompleteAtTheHead(int status) - { - ResponseReader reader = Feed($"HTTP/1.1 {status} Whatever\r\nContent-Length: 10\r\n\r\n"); - - Assert.That(reader.IsComplete, Is.True); - Assert.That(reader.ToResponse(Url).Content, Is.Empty); - } - - [Test] - public void HeadRequest_CompletesAtTheHead() - { - ResponseReader reader = new(isHead: true); - byte[] bytes = Encoding.ASCII.GetBytes("HTTP/1.1 200 OK\r\nContent-Length: 1234\r\n\r\n"); - reader.Append(bytes, bytes.Length); - - Assert.That(reader.IsComplete, Is.True); - HttpResponse response = reader.ToResponse(Url); - Assert.That(response.Content, Is.Empty); - Assert.That(response.GetHeader("content-length"), Is.EqualTo("1234")); - } - - [Test] - public void InterimResponses_AreSkipped() - { - const string response = "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 103 Early Hints\r\nLink: \r\n\r\n" - + "HTTP/1.1 201 Created\r\nContent-Length: 2\r\n\r\nok"; - - foreach (ResponseReader reader in FeedInEverySplit(response)) - { - HttpResponse parsed = reader.ToResponse(Url); - Assert.That(parsed.StatusCode, Is.EqualTo(201)); - Assert.That(parsed.GetHeader("Link"), Is.Null); - Assert.That(Encoding.ASCII.GetString(parsed.Content), Is.EqualTo("ok")); - } - } - - [Test] - public void Headers_AreCaseInsensitiveAndRepeatsAreJoined() - { - HttpResponse response = Feed("HTTP/1.1 200 OK\r\nX-Thing: a\r\nx-thing: b\r\nSpaced : value \r\nContent-Length: 0\r\n\r\n").ToResponse(Url); - - Assert.That(response.GetHeader("X-THING"), Is.EqualTo("a, b")); - Assert.That(response.Headers["spaced"], Is.EqualTo("value")); - } - - [Test] - public void StatusLine_WithoutReasonPhrase() - { - HttpResponse response = Feed("HTTP/1.1 404\r\nContent-Length: 0\r\n\r\n").ToResponse(Url); - - Assert.That(response.StatusCode, Is.EqualTo(404)); - Assert.That(response.ReasonPhrase, Is.Empty); - Assert.That(response.IsSuccessStatusCode, Is.False); - } - - [TestCase("SSH-2.0-OpenSSH_9.6\r\n\r\n")] - [TestCase("HTTP/1.1 2000 OK\r\n\r\n")] - [TestCase("HTTP/1.1 abc OK\r\n\r\n")] - public void NotHttp_Throws(string response) - { - Assert.Throws(() => Feed(response)); - } - - [TestCase("abc")] - [TestCase("5, 6")] - [TestCase("-1")] - public void InvalidContentLength_Throws(string length) - { - Assert.Throws(() => Feed($"HTTP/1.1 200 OK\r\nContent-Length: {length}\r\n\r\n")); - } - - [Test] - public void RepeatedContentLength_IsTakenWhenTheCopiesAgree() - { - ResponseReader reader = Feed("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Length: 2\r\n\r\nok"); - - Assert.That(reader.IsComplete, Is.True); - } - - [Test] - public void OversizedHead_Throws() - { - string response = "HTTP/1.1 200 OK\r\nX-Big: " + new string('a', ResponseReader.MaxHeadLength) + "\r\n"; - - Assert.Throws(() => Feed(response)); - } - - [Test] - public void NoAnswer_Throws() - { - HttpException exception = Assert.Throws(() => new ResponseReader(isHead: false).ToResponse(Url))!; - Assert.That(exception.Message, Does.Contain("without answering")); - } - - [Test] - public void PartialHead_Throws() - { - ResponseReader reader = Feed("HTTP/1.1 200 OK\r\nContent-"); - - HttpException exception = Assert.Throws(() => reader.ToResponse(Url))!; - Assert.That(exception.Message, Does.Contain("middle of the response head")); - } - - private static ResponseReader Feed(string response) - { - ResponseReader reader = new(isHead: false); - byte[] bytes = Encoding.ASCII.GetBytes(response); - reader.Append(bytes, bytes.Length); - return reader; - } - - /// The reader after the response arrived in two parts, for every place it can be split, and then a byte at a time. - private static System.Collections.Generic.IEnumerable FeedInEverySplit(string response) - { - byte[] bytes = Encoding.ASCII.GetBytes(response); - for (int split = 0; split <= bytes.Length; split++) - { - ResponseReader reader = new(isHead: false); - reader.Append(bytes[..split], split); - reader.Append(bytes[split..], bytes.Length - split); - yield return reader; - } - - ResponseReader slow = new(isHead: false); - foreach (byte b in bytes) - { - slow.Append([b], 1); - } - - yield return slow; - } -} diff --git a/tests/Cosmos.Network.Http.Tests/SslStreamTests.cs b/tests/Cosmos.Network.Http.Tests/SslStreamTests.cs new file mode 100644 index 0000000..a42807c --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/SslStreamTests.cs @@ -0,0 +1,341 @@ +// Cosmos: tests of SslStream over loopback, client and server, as the port's TLS is managed code where nanoFramework's +// is native (its tests have none). + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class SslStreamTests + { + private const int Timeout = 20_000; + + /// + /// A TLS server for one connection on a thread of its own, which echoes what it reads until the client closes. + /// + private sealed class EchoServer : IDisposable + { + private readonly Socket _listener; + private readonly Thread _thread; + + internal EchoServer(X509Certificate certificate, SslProtocols protocols, SslVerification verification, X509Certificate ca = null) + { + _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + _listener.Listen(1); + Port = ((IPEndPoint)_listener.LocalEndPoint).Port; + + _thread = new Thread(() => + { + try + { + using Socket socket = _listener.Accept(); + using var stream = new SslStream(socket) { SslVerification = verification }; + stream.ReadTimeout = Timeout; + stream.Authenticate(true, null, certificate, ca, protocols); + + byte[] buffer = new byte[4096]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + stream.Write(buffer, 0, read); + } + } + catch (Exception e) + { + Error = e; + } + }); + _thread.IsBackground = true; + _thread.Start(); + } + + internal int Port { get; } + + internal Exception Error { get; private set; } + + internal void Join() => Assert.IsTrue(_thread.Join(Timeout), "The server didn't finish."); + + public void Dispose() + { + _listener.Close(); + _thread.Join(Timeout); + } + } + + private static SslStream Connect(EchoServer server, string host, SslProtocols protocols, X509Certificate ca, SslVerification verification = SslVerification.CertificateRequired, X509Certificate clientCertificate = null) + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(new IPEndPoint(IPAddress.Loopback, server.Port)); + + var stream = new SslStream(socket) { SslVerification = verification }; + stream.ReadTimeout = Timeout; + try + { + stream.AuthenticateAsClient(host, clientCertificate, ca, protocols); + } + catch + { + // So the server's handshake ends too. + socket.Close(); + throw; + } + + return stream; + } + + private static void AssertEcho(SslStream stream, int size) + { + byte[] sent = new byte[size]; + new Random(size).NextBytes(sent); + + stream.Write(sent, 0, sent.Length); + + byte[] received = new byte[size]; + int total = 0; + while (total < size) + { + int read = stream.Read(received, total, size - total); + Assert.IsTrue(read > 0, "The connection closed after " + total + " of " + size + " bytes."); + total += read; + } + + CollectionAssert.AreEqual(sent, received); + } + + [DataTestMethod] + [DataRow(SslProtocols.Tls13, false)] + [DataRow(SslProtocols.Tls13, true)] + [DataRow(SslProtocols.Tls12, false)] + [DataRow(SslProtocols.Tls12, true)] + [DataRow(SslProtocols.None, false)] + public void Handshake_AndEcho(SslProtocols protocols, bool rsa) + { + TestCertificate certificate = rsa ? TestCertificates.RsaServer : TestCertificates.EcServer; + + using var server = new EchoServer(certificate.WithKey(), protocols, SslVerification.NoVerification); + using (SslStream client = Connect(server, "localhost", protocols, TestCertificates.Root.Public())) + { + AssertEcho(client, 11); + AssertEcho(client, 70_000); + } + + server.Join(); + Assert.IsNull(server.Error, server.Error?.ToString()); + } + + [TestMethod] + public void Handshake_ByIpAddress() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + using (SslStream client = Connect(server, "127.0.0.1", SslProtocols.Tls13, TestCertificates.Root.Public())) + { + AssertEcho(client, 100); + } + + server.Join(); + } + + [TestMethod] + public void Handshake_TrustedThroughCertificateManager() + { + try + { + Assert.IsTrue(CertificateManager.AddCaCertificateBundle(new[] { TestCertificates.Root.Public() })); + + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls12, SslVerification.NoVerification); + using (SslStream client = Connect(server, "localhost", SslProtocols.Tls12, null)) + { + AssertEcho(client, 100); + } + + server.Join(); + } + finally + { + // The embedded roots again. + using Stream roots = typeof(CertificateManager).Assembly.GetManifestResourceStream("Cosmos.Network.Http.cacert.pem"); + var pem = new MemoryStream(); + roots.CopyTo(pem); + Assert.IsTrue(CertificateManager.AddCaCertificateBundle(pem.ToArray())); + } + } + + [DataTestMethod] + [DataRow(SslProtocols.Tls13)] + [DataRow(SslProtocols.Tls12)] + public void Handshake_WrongHost_Fails(SslProtocols protocols) + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), protocols, SslVerification.NoVerification); + + var error = Assert.ThrowsException(() => Connect(server, "example.com", protocols, TestCertificates.Root.Public())); + StringAssert.Contains(error.Message, "example.com"); + } + + [TestMethod] + public void Handshake_UntrustedServer_Fails() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + + Assert.ThrowsException(() => Connect(server, "localhost", SslProtocols.Tls13, TestCertificates.OtherRoot.Public())); + } + + [TestMethod] + public void Handshake_UntrustedServer_NoVerification_Succeeds() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + using (SslStream client = Connect(server, "example.com", SslProtocols.Tls13, TestCertificates.OtherRoot.Public(), SslVerification.NoVerification)) + { + AssertEcho(client, 100); + } + + server.Join(); + } + + [TestMethod] + public void Handshake_NoCommonVersion_Fails() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + + Assert.ThrowsException(() => Connect(server, "localhost", SslProtocols.Tls12, TestCertificates.Root.Public())); + } + + [TestMethod] + public void Handshake_UnsupportedProtocol_Throws() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + +#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete: refused here. + Assert.ThrowsException(() => Connect(server, "localhost", SslProtocols.Tls11, TestCertificates.Root.Public())); +#pragma warning restore SYSLIB0039 + } + + [DataTestMethod] + [DataRow(SslProtocols.Tls13)] + [DataRow(SslProtocols.Tls12)] + public void ClientCertificate_Required_Succeeds(SslProtocols protocols) + { + using var server = new EchoServer(TestCertificates.RsaServer.WithKey(), protocols, SslVerification.CertificateRequired, TestCertificates.Root.Public()); + using (SslStream client = Connect(server, "localhost", protocols, TestCertificates.Root.Public(), clientCertificate: TestCertificates.Client.WithKey())) + { + AssertEcho(client, 100); + } + + server.Join(); + Assert.IsNull(server.Error, server.Error?.ToString()); + } + + [DataTestMethod] + [DataRow(SslProtocols.Tls13)] + [DataRow(SslProtocols.Tls12)] + public void ClientCertificate_Missing_Fails(SslProtocols protocols) + { + using var server = new EchoServer(TestCertificates.RsaServer.WithKey(), protocols, SslVerification.CertificateRequired, TestCertificates.Root.Public()); + + try + { + // In TLS 1.3 the client may finish its handshake before the server refuses it: the refusal comes with + // the first read. + using SslStream client = Connect(server, "localhost", protocols, TestCertificates.Root.Public()); + byte[] buffer = new byte[1]; + client.Write(buffer, 0, 1); + client.Read(buffer, 0, 1); + Assert.Fail("The server accepted a client without a certificate."); + } + catch (AuthenticationException) + { + } + catch (IOException) + { + } + + server.Join(); + Assert.IsInstanceOfType(server.Error, typeof(AuthenticationException)); + } + + [TestMethod] + public void ServerCertificate_WithoutKey_Throws() + { + var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + client.Connect(listener.LocalEndPoint); + Socket socket = listener.Accept(); + + var stream = new SslStream(socket); + + Assert.ThrowsException(() => stream.Authenticate(true, null, TestCertificates.RsaServer.Public(), null, SslProtocols.Tls12)); + + socket.Close(); + client.Close(); + listener.Close(); + } + + [TestMethod] + public void Read_PeerClosed_ReturnsZero() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + using SslStream client = Connect(server, "localhost", SslProtocols.Tls13, TestCertificates.Root.Public()); + + AssertEcho(client, 10); + + // The server's echo loop ends when its Read returns 0, without an exception. + client.Close(); + server.Join(); + Assert.IsNull(server.Error, server.Error?.ToString()); + } + + [TestMethod] + public void Read_Timeout_Throws() + { + var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + + // A server that accepts and never answers the ClientHello. + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Connect(listener.LocalEndPoint); + Socket accepted = listener.Accept(); + + var stream = new SslStream(socket); + stream.ReadTimeout = 300; + + Assert.ThrowsException(() => stream.AuthenticateAsClient("localhost", SslProtocols.Tls13)); + + accepted.Close(); + listener.Close(); + socket.Close(); + } + + [TestMethod] + public void DataAvailable_AfterServerWrites() + { + using var server = new EchoServer(TestCertificates.EcServer.WithKey(), SslProtocols.Tls13, SslVerification.NoVerification); + using SslStream client = Connect(server, "localhost", SslProtocols.Tls13, TestCertificates.Root.Public()); + + byte[] data = Encoding.ASCII.GetBytes("ping"); + client.Write(data, 0, data.Length); + + long start = Environment.TickCount64; + while (!client.DataAvailable) + { + Assert.IsTrue(Environment.TickCount64 - start < Timeout, "No data became available."); + Thread.Sleep(5); + } + + Assert.IsTrue(client.Length > 0); + + byte[] buffer = new byte[16]; + int read = client.Read(buffer, 0, buffer.Length); + Assert.AreEqual("ping", Encoding.ASCII.GetString(buffer, 0, read)); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/StreamContentTest.cs b/tests/Cosmos.Network.Http.Tests/StreamContentTest.cs new file mode 100644 index 0000000..c8632d3 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/StreamContentTest.cs @@ -0,0 +1,432 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.IO; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class StreamContentTest + { + [TestMethod] + public void Ctor_NullStream_ThrowsArgumentNullException() + { + Assert.ThrowsException(typeof(ArgumentNullException), + () => new StreamContent(null)); + } + + [TestMethod] + public void Ctor_ZeroBufferSize_ThrowsArgumentOutOfRangeException() + { + Assert.ThrowsException(typeof(ArgumentOutOfRangeException), + () => new StreamContent(new MemoryStream(), 0)); + } + + [TestMethod] + public void Ctor_NullStreamAndZeroBufferSize_ThrowsArgumentNullException() + { + Assert.ThrowsException(typeof(ArgumentNullException), + () => new StreamContent(null, 0)); + } + + [TestMethod] + public void ContentLength_SetStreamSupportingSeeking_StreamLengthMatchesHeaderValue() + { + var source = new MockStream(new byte[10], true, true); + var content = new StreamContent(source); + + Assert.AreEqual(source.Length, content.Headers.ContentLength); + } + + [TestMethod] + public void ContentLength_SetStreamSupportingSeekingPartiallyConsumed_StreamLengthMatchesHeaderValueMinusConsumed() + { + int consumed = 4; + var source = new MockStream(new byte[10], true, true); + source.Read(new byte[consumed], 0, consumed); + var content = new StreamContent(source); + + Assert.AreEqual(source.Length - consumed, content.Headers.ContentLength); + } + + [TestMethod] + public void Dispose_UseMockStreamSourceAndDisposeContent_MockStreamGotDisposed() + { + var source = new MockStream(new byte[10]); + var content = new StreamContent(source); + content.Dispose(); + + Assert.AreEqual(1, source.DisposeCount); + } + + [TestMethod] + public void CopyToAsync_NullDestination_ThrowsArgumentnullException() + { + var source = new MockStream(new byte[10]); + var content = new StreamContent(source); + Assert.ThrowsException(typeof(ArgumentNullException), + () => + { + content.CopyTo(null); + }); + } + + [TestMethod] + + public void CopyTo_CallMultipleTimesWithStreamSupportingSeeking_ContentIsSerializedMultipleTimes() + { + var source = new MockStream(new byte[10], true, true); + var content = new StreamContent(source); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + Assert.AreEqual(source.Length, destination1.Length); + + using var destination2 = new MemoryStream(); + content.CopyTo(destination2); + Assert.AreEqual(source.Length, destination2.Length); + } + + [TestMethod] + public void CopyTo_CallMultipleTimesWithStreamSupportingSeekingPartiallyConsumed_ContentIsSerializedMultipleTimesFromInitialPoint() + { + int consumed = 4; + var source = new MockStream(new byte[10], true, true); + source.Read(new byte[consumed], 0, consumed); + var content = new StreamContent(source); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + Assert.AreEqual(source.Length - consumed, destination1.Length); + + using var destination2 = new MemoryStream(); + content.CopyTo(destination2); + Assert.AreEqual(source.Length - consumed, destination2.Length); + } + + [TestMethod] + public void CopyTo_CallMultipleTimesWithStreamNotSupportingSeeking_ThrowsInvalidOperationException() + { + var source = new MockStream(new byte[10], false, true); // doesn't support seeking. + var content = new StreamContent(source); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + + // Use hardcoded expected length, since source.Length would throw (source stream gets disposed if non-seekable). + Assert.AreEqual(10, destination1.Length); + + // Note that the InvalidOperationException is thrown in CopyToAsync(). It is not thrown inside the task. + using var destination2 = new MemoryStream(); + Assert.ThrowsException(typeof(InvalidOperationException), + () => + { + content.CopyTo(destination2); + }); + } + + [TestMethod] + public void CopyTo_CallMultipleTimesWithStreamNotSupportingSeekingButBufferedStream_ContentSerializedOnceToBuffer() + { + var source = new MockStream(new byte[10], false, true); // doesn't support seeking. + var content = new StreamContent(source); + + // After loading the content into a buffer, we should be able to copy the content to a destination stream + // multiple times, even though the stream doesn't support seeking. + content.LoadIntoBuffer(); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + // Use hardcoded expected length, since source.Length would throw (source stream gets disposed if non-seekable) + Assert.AreEqual(10, destination1.Length); + + using var destination2 = new MemoryStream(); + content.CopyTo(destination2); + Assert.AreEqual(10, destination2.Length); + } + + [TestMethod] + public void CopyTo_CallMultipleTimesWithStreamNotSupportingSeekingButBufferedStreamPartiallyConsumed_ContentSerializedOnceToBuffer() + { + int consumed = 4; + var source = new MockStream(new byte[10], false, true); // doesn't support seeking. + source.Read(new byte[consumed], 0, consumed); + var content = new StreamContent(source); + + // After loading the content into a buffer, we should be able to copy the content to a destination stream + // multiple times, even though the stream doesn't support seeking. + content.LoadIntoBuffer(); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + // Use hardcoded expected length, since source.Length would throw (source stream gets disposed if non-seekable). + Assert.AreEqual(10 - consumed, destination1.Length); + + using var destination2 = new MemoryStream(); + content.CopyTo(destination2); + Assert.AreEqual(10 - consumed, destination2.Length); + } + + [TestMethod] + public void CopyTo_NoLoadIntoBuffer_NotBuffered() + { + var initialSourceContent = new byte[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var sourceContent = new byte[10]; + Array.Copy(initialSourceContent, sourceContent, 10); + + var sourceStream = new MockStream(sourceContent, true, true); + + var content = new StreamContent(sourceStream); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + + Assert.AreEqual(10, destination1.Length); + CollectionAssert.AreEqual(initialSourceContent, destination1.ToArray()); + + // replace source content to ensure data was not buffered in the content + var replacedSourceContent = new byte[10] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; + Array.Copy(replacedSourceContent, sourceContent, 10); + + using var destination2 = new MemoryStream(); + content.CopyTo(destination2); + Assert.AreEqual(10, destination2.Length); + CollectionAssert.AreEqual(replacedSourceContent, destination2.ToArray()); + } + + [TestMethod] + public void CopyTo_LoadIntoBuffer_Buffered() + { + var initialSourceContent = new byte[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + var sourceContent = new byte[10]; + Array.Copy(initialSourceContent, sourceContent, 10); + + var sourceStream = new MockStream(sourceContent, true, true); + + var content = new StreamContent(sourceStream); + + // buffer so changing the source stream doesn't change the Content + content.LoadIntoBuffer(); + + using var destination1 = new MemoryStream(); + content.CopyTo(destination1); + + Assert.AreEqual(10, destination1.Length); + CollectionAssert.AreEqual(initialSourceContent, destination1.ToArray()); + + // replace source content to ensure data was buffered in the content + var replacedSourceContent = new byte[10] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; + Array.Copy(replacedSourceContent, sourceContent, 10); + + using var destination2 = new MemoryStream(); + content.CopyTo(destination2); + Assert.AreEqual(10, destination2.Length); + CollectionAssert.AreEqual(initialSourceContent, destination2.ToArray()); + } + + [TestMethod] + public void ContentReadStream_GetProperty_ReturnOriginalStream() + { + var source = new MockStream(new byte[10]); + var content = new StreamContent(source); + + using Stream stream = content.ReadAsStream(); + + Assert.IsFalse(stream.CanWrite); + Assert.AreEqual(source.Length, stream.Length); + Assert.AreEqual(0, source.ReadCount); + Assert.AreNotSame(source, stream); + } + + [TestMethod] + public void ContentReadStream_GetProperty_LoadIntoBuffer_ReturnOriginalStream() + { + var source = new MockStream(new byte[10]); + var content = new StreamContent(source); + + using Stream stream = content.ReadAsStream(); + Assert.IsFalse(stream.CanWrite); + Assert.AreEqual(source.Length, stream.Length); + Assert.AreEqual(0, source.ReadCount); + Assert.AreNotSame(source, stream); + } + + [TestMethod] + public void ContentReadStream_GetPropertyPartiallyConsumed_ReturnOriginalStream() + { + int consumed = 4; + var source = new MockStream(new byte[10]); + source.Read(new byte[consumed], 0, consumed); + var content = new StreamContent(source); + + using Stream stream = content.ReadAsStream(); + Assert.IsFalse(stream.CanWrite); + Assert.AreEqual(source.Length, stream.Length); + Assert.AreEqual(1, source.ReadCount); + Assert.AreEqual(consumed, stream.Position); + Assert.AreNotSame(source, stream); + } + + [TestMethod] + public void ContentReadStream_CheckResultProperties_ValuesRepresentReadOnlyStream() + { + byte[] data = new byte[10]; + + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)i; + } + + var source = new MockStream(data); + var content = new StreamContent(source); + using Stream contentReadStream = content.ReadAsStream(); + + // The following checks verify that the stream returned passes all read-related properties to the + // underlying MockStream and throws when using write-related members. + + Assert.IsFalse(contentReadStream.CanWrite); + Assert.IsTrue(contentReadStream.CanRead); + Assert.AreEqual(source.Length, contentReadStream.Length); + + Assert.AreEqual(1, source.CanSeekCount); + OutputHelper.WriteLine(contentReadStream.CanSeek.ToString()); + Assert.AreEqual(2, source.CanSeekCount); + + contentReadStream.Position = 3; // No exception. + Assert.AreEqual(3, contentReadStream.Position); + + byte byteOnIndex3 = (byte)contentReadStream.ReadByte(); + Assert.AreEqual(data[3], byteOnIndex3); + + byte[] byteOnIndex4 = new byte[1]; + int result = contentReadStream.Read(byteOnIndex4, 0, 1); + Assert.AreEqual(1, result); + + Assert.AreEqual(data[4], byteOnIndex4[0]); + + byte[] byteOnIndex5 = new byte[1]; + Assert.AreEqual(1, contentReadStream.Read(byteOnIndex5, 0, 1)); + Assert.AreEqual(data[5], byteOnIndex5[0]); + + byte[] byteOnIndex6 = new byte[1]; + Assert.AreEqual(1, contentReadStream.Read(new Span(byteOnIndex6, 0, 1))); + Assert.AreEqual(data[6], byteOnIndex6[0]); + + contentReadStream.ReadTimeout = 123; + Assert.AreEqual(123, source.ReadTimeout); + Assert.AreEqual(123, contentReadStream.ReadTimeout); + + Assert.AreEqual(0, source.CanTimeoutCount); + OutputHelper.WriteLine(contentReadStream.CanTimeout.ToString()); + Assert.AreEqual(1, source.CanTimeoutCount); + + Assert.AreEqual(0, source.SeekCount); + contentReadStream.Seek(0, SeekOrigin.Begin); + Assert.AreEqual(1, source.SeekCount); + + Assert.ThrowsException(typeof(NotSupportedException), () => { contentReadStream.WriteTimeout = 5; }); + Assert.ThrowsException(typeof(NotSupportedException), () => contentReadStream.WriteTimeout.ToString()); + Assert.ThrowsException(typeof(NotSupportedException), () => contentReadStream.Flush()); + Assert.ThrowsException(typeof(NotSupportedException), () => contentReadStream.SetLength(1)); + Assert.ThrowsException(typeof(NotSupportedException), () => contentReadStream.Write(null, 0, 0)); + Assert.ThrowsException(typeof(NotSupportedException), () => contentReadStream.WriteByte(1)); + + Assert.AreEqual(0, source.DisposeCount); + contentReadStream.Dispose(); + Assert.AreEqual(1, source.DisposeCount); + } + + #region Helper methods + + private class MockStream : MemoryStream + { + private bool _canSeek; + private bool _canRead; + private int _readTimeout; + + public int DisposeCount { get; private set; } + public int BufferSize { get; private set; } + public int ReadCount { get; private set; } + public int CanSeekCount { get; private set; } + public int CanTimeoutCount { get; private set; } + public int SeekCount { get; private set; } + + public override bool CanSeek + { + get + { + CanSeekCount++; + return _canSeek; + } + } + + public override bool CanRead + { + get { return _canRead; } + } + + public override int ReadTimeout + { + get { return _readTimeout; } + set { _readTimeout = value; } + } + + public override bool CanTimeout + { + get + { + CanTimeoutCount++; + return base.CanTimeout; + } + } + + public MockStream(byte[] data) + : this(data, true, true) + { + } + + public MockStream(byte[] data, bool canSeek, bool canRead) + : base(data) + { + _canSeek = canSeek; + _canRead = canRead; + } + + public override int Read(byte[] buffer, int offset, int count) + { + ReadCount++; + SetBufferSize(count); + return base.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin loc) + { + SeekCount++; + return base.Seek(offset, loc); + } + + protected override void Dispose(bool disposing) + { + DisposeCount++; + base.Dispose(disposing); + } + + private void SetBufferSize(int count) + { + if (BufferSize == 0) + { + BufferSize = count; + } + } + } + + #endregion + } +} diff --git a/tests/Cosmos.Network.Http.Tests/StringContentTest.cs b/tests/Cosmos.Network.Http.Tests/StringContentTest.cs new file mode 100644 index 0000000..d3a728f --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/StringContentTest.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.IO; +using System.Text; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class StringContentTest + { + [TestMethod] + public void Ctor_NullString_ThrowsArgumentNullException() + { + Assert.ThrowsException(typeof(ArgumentNullException), + () => new StringContent(null)); + } + + [TestMethod] + public void Ctor_EmptyString_Accept() + { + // Consider empty strings like null strings (null and empty strings should be treated equally). + var content = new StringContent(string.Empty); + using Stream result = content.ReadAsStream(); + Assert.AreEqual(0, result.Length); + } + + [TestMethod] + public void Ctor_DefineNoEncoding_DefaultEncodingUsed() + { + string sourceString = "\u00C4\u00E4\u00FC\u00DC"; + var content = new StringContent(sourceString); + Encoding defaultStringEncoding = Encoding.UTF8; + + // If no encoding is defined, the default encoding is used: utf-8 + Assert.AreEqual("text/plain", content.Headers.ContentType.MediaType); + Assert.AreEqual("utf-8", content.Headers.ContentType.CharSet); + + // Make sure the default encoding is also used when serializing the content. + var destination = new MemoryStream(); + content.CopyTo(destination); + + Assert.AreEqual(8, destination.Length); + + destination.Seek(0, SeekOrigin.Begin); + string roundTrip = new StreamReader(destination).ReadToEnd(); + Assert.AreEqual(sourceString, roundTrip); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/TestCertificates.cs b/tests/Cosmos.Network.Http.Tests/TestCertificates.cs new file mode 100644 index 0000000..a708787 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/TestCertificates.cs @@ -0,0 +1,237 @@ +// Cosmos: certificates for the TLS tests, made with BouncyCastle as the port's TLS runs on it. nanoFramework's tests +// have none, as its TLS is native. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Operators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.X509; +using BcCertificate = Org.BouncyCastle.X509.X509Certificate; + +namespace Cosmos.Network.Http.Tests +{ + /// + /// A certificate and its private key. + /// + internal sealed class TestCertificate + { + internal TestCertificate(BcCertificate certificate, AsymmetricKeyPair key) + { + Certificate = certificate; + Key = key; + } + + internal BcCertificate Certificate { get; } + + internal AsymmetricKeyPair Key { get; } + + internal byte[] Der => Certificate.GetEncoded(); + + internal string Pem + { + get + { + var writer = new StringWriter(); + new PemWriter(writer).WriteObject(Certificate); + return writer.ToString(); + } + } + + /// + /// The private key as PEM: PKCS#8, or PKCS#1/SEC1 when . + /// + internal string KeyPem(bool traditional = false) + { + var writer = new StringWriter(); + if (traditional) + { + new PemWriter(writer).WriteObject(Key.Private); + } + else + { + new PemWriter(writer).WriteObject(new Pkcs8Generator(Key.Private)); + } + + return writer.ToString(); + } + + /// + /// The certificate with its private key, as a TLS peer uses it. + /// + internal X509Certificate2 WithKey() => new X509Certificate2(Pem, KeyPem(), null); + + /// + /// The certificate alone, as a trusted one. + /// + internal X509Certificate Public() => new X509Certificate(Der); + } + + internal sealed class AsymmetricKeyPair + { + internal AsymmetricKeyPair(AsymmetricCipherKeyPair pair, string signatureAlgorithm) + { + Public = pair.Public; + Private = pair.Private; + SignatureAlgorithm = signatureAlgorithm; + } + + internal AsymmetricKeyParameter Public { get; } + + internal AsymmetricKeyParameter Private { get; } + + internal string SignatureAlgorithm { get; } + } + + internal static class TestCertificates + { + private static readonly SecureRandom s_random = new SecureRandom(); + private static long s_serial = 1; + + private static readonly object s_lock = new object(); + private static TestCertificate s_root; + private static TestCertificate s_otherRoot; + private static TestCertificate s_intermediate; + private static TestCertificate s_rsaServer; + private static TestCertificate s_ecServer; + private static TestCertificate s_client; + + /// + /// A root certificate authority, with an EC P-256 key. + /// + internal static TestCertificate Root => Lazy(ref s_root, () => CreateAuthority("Cosmos Test Root", null, Ec())); + + /// + /// A root nobody trusts. + /// + internal static TestCertificate OtherRoot => Lazy(ref s_otherRoot, () => CreateAuthority("Cosmos Other Root", null, Ec())); + + /// + /// An intermediate authority under . + /// + internal static TestCertificate Intermediate => Lazy(ref s_intermediate, () => CreateAuthority("Cosmos Test Intermediate", Root, Ec())); + + /// + /// A server certificate for localhost and 127.0.0.1 with an RSA key, issued by . + /// + internal static TestCertificate RsaServer => Lazy(ref s_rsaServer, () => CreateServer("localhost", Root, Rsa(), new[] { "localhost" }, new[] { "127.0.0.1" })); + + /// + /// A server certificate for localhost and 127.0.0.1 with an EC P-256 key, issued by . + /// + internal static TestCertificate EcServer => Lazy(ref s_ecServer, () => CreateServer("localhost", Root, Ec(), new[] { "localhost" }, new[] { "127.0.0.1" })); + + /// + /// A client certificate issued by . + /// + internal static TestCertificate Client => Lazy(ref s_client, () => Create("Cosmos Test Client", Root, Ec(), false, null, null, KeyPurposeID.id_kp_clientAuth, DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30))); + + internal static AsymmetricKeyPair Rsa() + { + var generator = new RsaKeyPairGenerator(); + generator.Init(new RsaKeyGenerationParameters(BigInteger.ValueOf(65537), s_random, 2048, 25)); + return new AsymmetricKeyPair(generator.GenerateKeyPair(), "SHA256WITHRSA"); + } + + internal static AsymmetricKeyPair Ec() + { + var generator = new ECKeyPairGenerator(); + generator.Init(new ECKeyGenerationParameters(SecObjectIdentifiers.SecP256r1, s_random)); + return new AsymmetricKeyPair(generator.GenerateKeyPair(), "SHA256WITHECDSA"); + } + + internal static TestCertificate CreateAuthority(string name, TestCertificate issuer, AsymmetricKeyPair key) + { + return Create(name, issuer, key, true, null, null, null, DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddYears(1)); + } + + internal static TestCertificate CreateServer(string name, TestCertificate issuer, AsymmetricKeyPair key, string[] dnsNames, string[] addresses) + { + return Create(name, issuer, key, false, dnsNames, addresses, KeyPurposeID.id_kp_serverAuth, DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30)); + } + + internal static TestCertificate Create( + string name, + TestCertificate issuer, + AsymmetricKeyPair key, + bool authority, + string[] dnsNames, + string[] addresses, + KeyPurposeID purpose, + DateTime notBefore, + DateTime notAfter, + int pathLength = -1, + Action customize = null) + { + var subject = new X509Name("CN=" + name); + + var generator = new X509V3CertificateGenerator(); + lock (s_lock) + { + generator.SetSerialNumber(BigInteger.ValueOf(s_serial++)); + } + + generator.SetSubjectDN(subject); + generator.SetIssuerDN(issuer != null ? issuer.Certificate.SubjectDN : subject); + generator.SetNotBefore(notBefore); + generator.SetNotAfter(notAfter); + generator.SetPublicKey(key.Public); + + if (authority) + { + generator.AddExtension(X509Extensions.BasicConstraints, true, pathLength >= 0 ? new BasicConstraints(pathLength) : new BasicConstraints(true)); + generator.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.KeyCertSign | KeyUsage.CrlSign)); + } + else + { + generator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(false)); + generator.AddExtension(X509Extensions.KeyUsage, true, new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment)); + } + + if (purpose != null) + { + generator.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(purpose)); + } + + var names = new List(); + foreach (string dnsName in dnsNames ?? Array.Empty()) + { + names.Add(new GeneralName(GeneralName.DnsName, dnsName)); + } + + foreach (string address in addresses ?? Array.Empty()) + { + names.Add(new GeneralName(GeneralName.IPAddress, new DerOctetString(IPAddress.Parse(address).GetAddressBytes()))); + } + + if (names.Count > 0) + { + generator.AddExtension(X509Extensions.SubjectAlternativeName, false, new GeneralNames(names.ToArray())); + } + + customize?.Invoke(generator); + + AsymmetricKeyPair signer = issuer != null ? issuer.Key : key; + BcCertificate certificate = generator.Generate(new Asn1SignatureFactory(signer.SignatureAlgorithm, signer.Private, s_random)); + + return new TestCertificate(certificate, key); + } + + private static TestCertificate Lazy(ref TestCertificate field, Func create) + { + lock (s_lock) + { + return field ??= create(); + } + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/TestFramework.cs b/tests/Cosmos.Network.Http.Tests/TestFramework.cs new file mode 100644 index 0000000..36aad3f --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/TestFramework.cs @@ -0,0 +1,82 @@ +// nanoFramework.TestFramework, as nanoFramework's tests use it, over MSTest: its attributes are MSTest's (aliased in +// GlobalUsings.cs), and its Assert and OutputHelper forward to MSTest's. + +using System; +using MSTestAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + +namespace nanoFramework.TestFramework +{ + /// + /// nanoFramework.TestFramework's Assert: MSTest's, with the xUnit-style names it also has. + /// + public static class Assert + { + public static void AreEqual(T expected, T actual, string message = "") => MSTestAssert.AreEqual(expected, actual, message); + + public static void AreNotEqual(T notExpected, T actual, string message = "") => MSTestAssert.AreNotEqual(notExpected, actual, message); + + public static void Equal(T expected, T actual, string message = "") => MSTestAssert.AreEqual(expected, actual, message); + + public static void IsTrue(bool condition, string message = "") => MSTestAssert.IsTrue(condition, message); + + public static void IsFalse(bool condition, string message = "") => MSTestAssert.IsFalse(condition, message); + + public static void IsNull(object value, string message = "") => MSTestAssert.IsNull(value, message); + + public static void Null(object value, string message = "") => MSTestAssert.IsNull(value, message); + + public static void IsNotNull(object value, string message = "") => MSTestAssert.IsNotNull(value, message); + + public static void NotNull(object value, string message = "") => MSTestAssert.IsNotNull(value, message); + + public static void AreSame(object expected, object actual, string message = "") => MSTestAssert.AreSame(expected, actual, message); + + public static void Same(object expected, object actual, string message = "") => MSTestAssert.AreSame(expected, actual, message); + + public static void AreNotSame(object notExpected, object actual, string message = "") => MSTestAssert.AreNotSame(notExpected, actual, message); + + /// + /// Passes if throws exactly, as nanoFramework's does. + /// + public static void ThrowsException(Type exceptionType, Action action, string message = "") + { + try + { + action(); + } + catch (Exception e) + { + if (e.GetType() != exceptionType) + { + MSTestAssert.Fail($"Expected {exceptionType.Name}, got {e.GetType().Name}: {e.Message}. {message}"); + } + + return; + } + + MSTestAssert.Fail($"Expected {exceptionType.Name}, nothing was thrown. {message}"); + } + + public static void Throws(Type exceptionType, Action action, string message = "") => ThrowsException(exceptionType, action, message); + + public static void SkipTest(string message = "") => MSTestAssert.Inconclusive(message); + } + + /// + /// nanoFramework.TestFramework's CollectionAssert: MSTest's. + /// + public static class CollectionAssert + { + public static void AreEqual(System.Collections.ICollection expected, System.Collections.ICollection actual, string message = "") => Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert.AreEqual(expected, actual, message); + } + + /// + /// nanoFramework.TestFramework's OutputHelper: the test's output. + /// + public static class OutputHelper + { + public static void WriteLine(string message) => Console.WriteLine(message); + + public static void Write(string message) => Console.Write(message); + } +} diff --git a/tests/Cosmos.Network.Http.Tests/TestPki.cs b/tests/Cosmos.Network.Http.Tests/TestPki.cs deleted file mode 100644 index 99ee631..0000000 --- a/tests/Cosmos.Network.Http.Tests/TestPki.cs +++ /dev/null @@ -1,169 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Generic; -using System.Formats.Asn1; -using System.Net; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -namespace Cosmos.Network.Http.Tests; - -/// -/// Certificates made up for the tests with the desktop's own cryptography: -/// roots, intermediates and server certificates, each as broken as a test -/// needs it. Keys are P-256 unless told otherwise, which is quick to -/// generate; signatures are SHA-256 unless told otherwise. -/// -internal static class TestPki -{ - public const string ServerAuth = "1.3.6.1.5.5.7.3.1"; - public const string ClientAuth = "1.3.6.1.5.5.7.3.2"; - - /// A self-signed root, valid from a year ago to a year ahead unless told otherwise. - public static X509Certificate2 Root(string name = "Test Root", AsymmetricAlgorithm? key = null, - DateTimeOffset? notBefore = null, DateTimeOffset? notAfter = null) => - Create($"CN={name}", issuer: null, isAuthority: true, key: key, - notBefore: notBefore ?? DateTimeOffset.UtcNow.AddDays(-365), notAfter: notAfter ?? DateTimeOffset.UtcNow.AddDays(365)); - - /// An intermediate certificate authority. - public static X509Certificate2 Authority(X509Certificate2 issuer, string name = "Test Intermediate", int? pathLength = null, - X509KeyUsageFlags keyUsage = X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, string[]? usages = null, - IEnumerable? extensions = null, AsymmetricAlgorithm? key = null, HashAlgorithmName? hash = null) => - Create($"CN={name}", issuer, isAuthority: true, pathLength: pathLength, keyUsage: keyUsage, usages: usages, - extensions: extensions, key: key, hash: hash, - notBefore: DateTimeOffset.UtcNow.AddDays(-300), notAfter: DateTimeOffset.UtcNow.AddDays(300)); - - /// - /// A server certificate for , DNS names or IP - /// addresses, valid from an hour ago to 30 days ahead unless told - /// otherwise; self-signed without an issuer. - /// - public static X509Certificate2 Server(X509Certificate2? issuer, string[] names, string[]? usages = null, - DateTimeOffset? notBefore = null, DateTimeOffset? notAfter = null, IEnumerable? extensions = null, - bool isAuthority = false, AsymmetricAlgorithm? key = null, HashAlgorithmName? hash = null) => - Create($"CN={(names.Length > 0 ? names[0] : "no name")}", issuer, isAuthority, names: names, usages: usages ?? [ServerAuth], - keyUsage: X509KeyUsageFlags.DigitalSignature, extensions: extensions, key: key, hash: hash, - notBefore: notBefore ?? DateTimeOffset.UtcNow.AddHours(-1), notAfter: notAfter ?? DateTimeOffset.UtcNow.AddDays(30)); - - /// A name constraints extension (RFC 5280, section 4.2.1.10) on DNS names, critical as it must be. - public static X509Extension NameConstraints(string[] permitted, string[] excluded) - { - AsnWriter writer = new(AsnEncodingRules.DER); - using (writer.PushSequence()) - { - WriteSubtrees(writer, 0, permitted); - WriteSubtrees(writer, 1, excluded); - } - - return new X509Extension("2.5.29.30", writer.Encode(), critical: true); - } - - /// An extension nobody understands, critical, which must make a certificate refused. - public static X509Extension UnknownCritical() => new("1.3.6.1.4.1.99999.1", [0x05, 0x00], critical: true); - - public static Org.BouncyCastle.X509.X509Certificate ToBouncyCastle(X509Certificate2 certificate) => new(certificate.RawData); - - private static void WriteSubtrees(AsnWriter writer, int tag, string[] names) - { - if (names.Length == 0) - { - return; - } - - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, tag))) - { - foreach (string name in names) - { - using (writer.PushSequence()) - { - // dNSName is [2] IA5String. - writer.WriteCharacterString(UniversalTagNumber.IA5String, name, new Asn1Tag(TagClass.ContextSpecific, 2)); - } - } - } - } - - private static X509Certificate2 Create(string subject, X509Certificate2? issuer, bool isAuthority, - DateTimeOffset notBefore, DateTimeOffset notAfter, int? pathLength = null, - X509KeyUsageFlags keyUsage = X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, - string[]? names = null, string[]? usages = null, IEnumerable? extensions = null, - AsymmetricAlgorithm? key = null, HashAlgorithmName? hash = null) - { - key ??= ECDsa.Create(ECCurve.NamedCurves.nistP256); - CertificateRequest request = key is RSA rsa - ? new CertificateRequest(subject, rsa, hash ?? HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) - : new CertificateRequest(subject, (ECDsa)key, hash ?? HashAlgorithmName.SHA256); - request.CertificateExtensions.Add(new X509BasicConstraintsExtension(isAuthority, pathLength.HasValue, pathLength ?? 0, critical: true)); - request.CertificateExtensions.Add(new X509KeyUsageExtension(keyUsage, critical: true)); - request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, critical: false)); - - if (names is { Length: > 0 }) - { - SubjectAlternativeNameBuilder alternativeNames = new(); - foreach (string name in names) - { - if (IPAddress.TryParse(name, out IPAddress? address)) - { - alternativeNames.AddIpAddress(address); - } - else - { - alternativeNames.AddDnsName(name); - } - } - - request.CertificateExtensions.Add(alternativeNames.Build()); - } - - if (usages is not null) - { - OidCollection oids = []; - foreach (string usage in usages) - { - oids.Add(new Oid(usage)); - } - - request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(oids, critical: false)); - } - - if (extensions is not null) - { - foreach (X509Extension extension in extensions) - { - request.CertificateExtensions.Add(extension); - } - } - - if (issuer is null) - { - return request.CreateSelfSigned(notBefore, notAfter); - } - - // Signed through a generator rather than Create(issuer, ...), which - // refuses an issuer that is no authority: some tests need one. - byte[] serial = RandomNumberGenerator.GetBytes(16); - serial[0] &= 0x7F; - X509SignatureGenerator generator = issuer.GetRSAPrivateKey() is RSA issuerRsa - ? hash == HashAlgorithmName.SHA1 - ? new Sha1RsaSignatureGenerator(issuerRsa) - : X509SignatureGenerator.CreateForRSA(issuerRsa, RSASignaturePadding.Pkcs1) - : X509SignatureGenerator.CreateForECDsa(issuer.GetECDsaPrivateKey()!); - using X509Certificate2 issued = request.Create(issuer.SubjectName, generator, notBefore, notAfter, serial); - return key is RSA rsaKey ? issued.CopyWithPrivateKey(rsaKey) : issued.CopyWithPrivateKey((ECDsa)key); - } - - /// Signs with SHA-1 and RSA, which the desktop's own generators refuse to do any more. - private sealed class Sha1RsaSignatureGenerator(RSA key) : X509SignatureGenerator - { - // AlgorithmIdentifier { sha1WithRSAEncryption, NULL } - private static readonly byte[] s_sha1WithRsa = [0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05, 0x05, 0x00]; - - public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) => s_sha1WithRsa; - - public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) => - key.SignData(data, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); - - protected override PublicKey BuildPublicKey() => new(key); - } -} diff --git a/tests/Cosmos.Network.Http.Tests/TestServer.cs b/tests/Cosmos.Network.Http.Tests/TestServer.cs deleted file mode 100644 index 6a99121..0000000 --- a/tests/Cosmos.Network.Http.Tests/TestServer.cs +++ /dev/null @@ -1,258 +0,0 @@ -// This code is licensed under the BSD 3-Clause license (see LICENSE.txt for details) - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Text; -using System.Threading; - -namespace Cosmos.Network.Http.Tests; - -/// -/// A loopback server that answers each connection with the next handler a -/// test queued, so a test writes the response bytes exactly as a server -/// would send them. Given a certificate, it speaks TLS (the desktop's -/// SslStream) before handing the connection over. -/// -internal sealed class TestServer : IDisposable -{ - private readonly TcpListener _listener; - private readonly Thread _thread; - private readonly ConcurrentQueue> _handlers = new(); - private readonly SslStreamCertificateContext? _certificate; - private readonly SslProtocols _protocols; - private volatile bool _disposed; - - /// The certificate, and the chain, to speak TLS with; plain HTTP without one. - /// The TLS versions to accept: the system's choice by default. - public TestServer(SslStreamCertificateContext? certificate = null, SslProtocols protocols = SslProtocols.None) - { - _certificate = certificate; - _protocols = protocols; - _listener = new TcpListener(IPAddress.Loopback, 0); - _listener.Start(); - _thread = new Thread(Serve) { IsBackground = true }; - _thread.Start(); - } - - public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port; - - /// The requests received, in order. - public ConcurrentQueue Requests { get; } = new(); - - /// The TLS handshakes completed, in order: the server name the client asked for, and what was agreed on. - public ConcurrentQueue Handshakes { get; } = new(); - - /// The first failure of a handler, rethrown by so the test sees it. - private Exception? _failure; - - public string Url(string target, string host = "127.0.0.1") => - $"{(_certificate is null ? "http" : "https")}://{host}:{Port}{target}"; - - /// Queues the handler for the next connection. - public TestServer Then(Action handler) - { - _handlers.Enqueue(handler); - return this; - } - - /// Queues a handler that reads the request and sends , then closes. - public TestServer Then(string response) => Then(connection => - { - connection.ReadRequest(); - connection.Write(response); - }); - - public void Dispose() - { - _disposed = true; - _listener.Stop(); - _thread.Join(5000); - if (_failure is not null) - { - throw new InvalidOperationException("A test server handler failed.", _failure); - } - } - - private void Serve() - { - while (!_disposed) - { - TcpClient client; - try - { - client = _listener.AcceptTcpClient(); - } - catch (SocketException) - { - return; - } - - using (client) - { - if (!_handlers.TryDequeue(out Action? handler)) - { - continue; - } - - try - { - handler(new TestConnection(this, client, Secure(client))); - } - catch (Exception exception) when (exception is IOException or SocketException or AuthenticationException) - { - // The client went away, which some tests make it do, - // refusing the server's certificate among others. - } - catch (Exception exception) - { - _failure ??= exception; - } - } - } - } - - internal void Record(TestRequest request) => Requests.Enqueue(request); - - /// The connection's stream, after a TLS handshake when the server has a certificate. - private Stream Secure(TcpClient client) - { - NetworkStream stream = client.GetStream(); - if (_certificate is null) - { - return stream; - } - - SslStream ssl = new(stream); - ssl.AuthenticateAsServer(new SslServerAuthenticationOptions - { - ServerCertificateContext = _certificate, - EnabledSslProtocols = _protocols, - ApplicationProtocols = [SslApplicationProtocol.Http11], - }); - - // On a server, TargetHostName is the name the client asked for (SNI). - string? serverName = ssl.TargetHostName.Length == 0 ? null : ssl.TargetHostName; - Handshakes.Enqueue(new TestHandshake(serverName, ssl.SslProtocol, ssl.NegotiatedApplicationProtocol.ToString())); - return ssl; - } -} - -/// A TLS handshake as the test server saw it. -internal sealed record TestHandshake(string? ServerName, SslProtocols Protocol, string ApplicationProtocol); - -/// A request as the test server received it. -internal sealed record TestRequest(string Method, string Target, Dictionary Headers, byte[] Body, string Head); - -/// One connection to the test server. -internal sealed class TestConnection -{ - private readonly TestServer _server; - private readonly Stream _stream; - private readonly MemoryStream _pending = new(); - - public TestConnection(TestServer server, TcpClient client, Stream stream) - { - _server = server; - client.GetStream().ReadTimeout = 10_000; - _stream = stream; - } - - /// Reads a request head and the body its Content-Length announces. - public TestRequest ReadRequest() - { - byte[] buffer = new byte[4096]; - int headEnd; - while ((headEnd = IndexOfHeadEnd(_pending.GetBuffer(), (int)_pending.Length)) < 0) - { - int read = _stream.Read(buffer, 0, buffer.Length); - if (read == 0) - { - throw new IOException("The client closed before sending a whole head."); - } - - _pending.Write(buffer, 0, read); - } - - string head = Encoding.UTF8.GetString(_pending.GetBuffer(), 0, headEnd); - string[] lines = head.Split("\r\n", StringSplitOptions.RemoveEmptyEntries); - string[] requestLine = lines[0].Split(' '); - Dictionary headers = new(StringComparer.OrdinalIgnoreCase); - for (int i = 1; i < lines.Length; i++) - { - int colon = lines[i].IndexOf(':'); - headers[lines[i].Substring(0, colon).Trim()] = lines[i].Substring(colon + 1).Trim(); - } - - int length = headers.TryGetValue("Content-Length", out string? value) ? int.Parse(value) : 0; - while (_pending.Length < headEnd + length) - { - int read = _stream.Read(buffer, 0, buffer.Length); - if (read == 0) - { - throw new IOException("The client closed before sending a whole body."); - } - - _pending.Write(buffer, 0, read); - } - - byte[] body = new byte[length]; - Buffer.BlockCopy(_pending.GetBuffer(), headEnd, body, 0, length); - - TestRequest request = new(requestLine[0], requestLine[1], headers, body, head); - _server.Record(request); - return request; - } - - public void Write(string text) => Write(Encoding.UTF8.GetBytes(text)); - - public void Write(byte[] data) - { - _stream.Write(data, 0, data.Length); - _stream.Flush(); - } - - /// Writes a byte at a time, so the client sees every split there is. - public void WriteSlowly(string text) - { - foreach (byte b in Encoding.UTF8.GetBytes(text)) - { - _stream.WriteByte(b); - _stream.Flush(); - Thread.Sleep(1); - } - } - - /// Keeps the connection open until the client closes it, as a server that ignores Connection: close would. - public void WaitForClientClose() - { - byte[] buffer = new byte[256]; - while (_stream.Read(buffer, 0, buffer.Length) > 0) - { - } - } - - /// Stays silent until the client gives up. - public void Hang() => WaitForClientClose(); - - /// Sends TLS close_notify, as a server that closes properly does before closing the connection. - public void CloseNotify() => ((SslStream)_stream).ShutdownAsync().GetAwaiter().GetResult(); - - private static int IndexOfHeadEnd(byte[] data, int length) - { - for (int i = 0; i + 3 < length; i++) - { - if (data[i] == '\r' && data[i + 1] == '\n' && data[i + 2] == '\r' && data[i + 3] == '\n') - { - return i + 4; - } - } - - return -1; - } -} diff --git a/tests/Cosmos.Network.Http.Tests/UriUnitTests.cs b/tests/Cosmos.Network.Http.Tests/UriUnitTests.cs new file mode 100644 index 0000000..c9ae215 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/UriUnitTests.cs @@ -0,0 +1,484 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using nanoFramework.TestFramework; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class UriUnitTests + { + [TestMethod] + public void NullCtor_Should_Throw_Exception() + { + Assert.ThrowsException(typeof(ArgumentNullException), + () => _ = new Uri(null), "Expected ArgumentNullException"); + } + + [TestMethod] + [DataRow("ftp://username:password@ftp.example.com")] // ftp with username and password + [DataRow("ftp://ftp.example.com/folder/file.txt")] // ftp with file path + [DataRow("ftp://username:password@ftp.example.com:22/folder/file.pe")] // ftp with username and password, custom post, and file + [DataRow("\tftp://abc.com ")] // ftp with tabs + [DataRow("file://")] // Empty file path + [DataRow("file:///")] + [DataRow("file:////")] + [DataRow("file://c")] + [DataRow("file:///c")] + [DataRow("file:////c")] + [DataRow("file:///C:/path/to/file.txt")] // local file path + [DataRow("file:///home/user/file.txt")] // unix file path + [DataRow("file://hostname/share/path/file.txt")] // network share + [DataRow("file:///\\\\server\\folder\\file.txt")] // Windows share style file + [DataRow("file:///c:\\rbllog")] // Windows drive style file + [DataRow("mailto:user@example.com")] // email + [DataRow("mailto:user@example.com?subject=Hello&body=World")] // email with subject and body + [DataRow("postgresql://user:password@localhost:5432/database")] // postgresql + [DataRow("mongodb://user:password@localhost:27017/database")] // mongo db + [DataRow("ws://example.com/socket")] // web socket + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5")] // web socket with query, path, and port + [DataRow("wss://ws.pusherapp.com/app/?client=js&version=1.9.3&protocol=5")] // secure web socket with query, path, and port + [DataRow("ssh://user@hostname:22")] // SSH + [DataRow("telnet://user:pass@hostname:22")] // Telnet + [DataRow("sftp://user@hostname/path/to/file")] // SFTP + [DataRow("rtsp://media.example.com/video")] // RTSP + [DataRow("http://[2001:db8::1]:8080")] // http with ipv6 + [DataRow("ftp://[2001:db8::1]/folder")] // ftp with ipv6 + [DataRow("urn:isbn:0262531968")] // isbn urn + [DataRow("urn:uuid:123e4567-e89b-12d3-a456-426614174000")] // uuid urn + [DataRow("urn:ietf:rfc:2648")] // IETF namespace urn + [DataRow("urn:uci:I001+SBSi-B10000083052")] // UCI urn + [DataRow("urn:isan:0000-0000-9E59-0000-O-0000-0000-2")] // ISAN urn + [DataRow("urn:issn:0167-6423")] // ISSN urn + [DataRow("urn:mpeg:mpeg7:schema:2001")] // mpeg urn + [DataRow("urn:oid:2.216.840")] // urn oid + [DataRow("uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66")] // UUID + [DataRow("geo:47.645812,-122.134544")] // geo location + [DataRow("news:comp.infosystems.www.servers.unix")] // news + [DataRow("tel:+1-816-555-1212")] // Telephone number + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256")] // h323 with user, ipv4, and query values + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one")] // ldap + [DataRow("http://www.co1ntoso.com")] // Http + [DataRow("http://user:password@www.co5ntoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName")] // Http with bells and whistles + [DataRow("https://www.co3ntoso.com/Home/Index.htm?q1=v1&q2=v2")] // Https with path and query + [DataRow("https://www.co4ntoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName")] // Https with path, query, fragment, and standard port + [DataRow("https://www.co4ntoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName")] // Https with path, query, fragment, and non-standard port + [DataRow("https://user:password@www.co5ntoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName")] // Https with user credentials and non-standard port + [DataRow("h1tp://foo.com")] // typos + [DataRow("http5://foo.com")] // typos + [DataRow("HtTp://example.com")] // mixed case http + [DataRow("FTp://example.com")] // mixed case ftp + [DataRow("http://example.com/path%20with%20spaces")] // unusual but valid characters + [DataRow("http://example.com/?q=complex%20query%21%40%23")] // including special characters + [DataRow("file:///C:/path%20with%20spaces/file.txt")] // file path with encoded spaces + [DataRow("magnet:?xt=urn:btih:1234567890abcdef")] + [DataRow("http://example.com/path?redirect=192.168.0.1#section")] // ip in query + [DataRow("http://example.com/path#192.168.0.1")] // ip in fragment + [DataRow("http://example.com#")] // empty fragment + [DataRow("http://example.com/../../etc/passwd")] // path traversal characters + [DataRow("blob:http://example.com/550e8400-e29b-41d4-a716-446655440000")] // blob uri + //[DataRow("urn:example:animal:ferret:nose?color=black")] // urn with query parameters **urn parsing is not fully implemented + public void UriCtor_Should_Not_Throw_Exception(string uri) + { + Console.WriteLine("uri: " + uri); + Uri createdUri = new(uri); + + Assert.IsNotNull(createdUri); + } + + [DataRow("http://user:password@www.co5ntoso.com/Home/Sub?q1=v1&q3=v3#Fragment", "Index.html?q1=v1&q2=v2#FragmentName", "http://user:password@www.co5ntoso.com/Home/Index.html?q1=v1&q2=v2#FragmentName")] + [DataRow("http://user:password@www.co5ntoso.com/Home/Sub", "Index.html?q1=v1&q2=v2#FragmentName", "http://user:password@www.co5ntoso.com/Home/Index.html?q1=v1&q2=v2#FragmentName")] + [DataRow("http://user:password@www.co5ntoso.com/Home/Sub/", "Index.html?q1=v1&q2=v2#FragmentName", "http://user:password@www.co5ntoso.com/Home/Sub/Index.html?q1=v1&q2=v2#FragmentName")] + [DataRow("http://user:password@www.co5ntoso.com/Home/Sub", "/Index.html?q1=v1&q2=v2#FragmentName", "http://user:password@www.co5ntoso.com/Index.html?q1=v1&q2=v2#FragmentName")] + [DataRow("http://user:password@www.co5ntoso.com/Home/Sub/", "/Index.html?q1=v1&q2=v2#FragmentName", "http://user:password@www.co5ntoso.com/Index.html?q1=v1&q2=v2#FragmentName")] + [DataRow("http://user:password@www.co5ntoso.com/Home/Sub/", "http://usr:pwd@www.co6ntoso.com/Index.html?q1=v1&q2=v2#FragmentName", "http://usr:pwd@www.co6ntoso.com/Index.html?q1=v1&q2=v2#FragmentName")] + public void UriWithParamaters(string uri, string paramaters, string correctFullUri) + { + Uri baseUri = new(uri); + Uri fullUri = new(correctFullUri); + Uri parmUri = new(paramaters, UriKind.Relative); + Uri finalUri = new(baseUri, parmUri.OriginalString); + Console.WriteLine($"{finalUri.OriginalString} == {fullUri.OriginalString}"); + Assert.AreEqual(finalUri.OriginalString, correctFullUri); + Assert.AreEqual(fullUri.OriginalString, correctFullUri); + } + + [TestMethod] + [DataRow("", typeof(ArgumentNullException), "ExpectedArgumentNullException")] + [DataRow("foo", typeof(ArgumentException), "Expected ArgumentException")] + [DataRow("file:///c:", typeof(ArgumentException), "Expected ArgumentException")] + [DataRow("http:abc/d/", typeof(ArgumentException), "Expected ArgumentException")] + [DataRow("file:/server", typeof(ArgumentException), "Expected ArgumentException")] + [DataRow("http:", typeof(ArgumentException), "Expected ArgumentException")] + [DataRow("1ttp://foo.com", typeof(ArgumentException), "Expected ArgumentException")] + [DataRow("h@tp://foo.com", typeof(ArgumentException), "Expected ArgumentException")] + public void UriCtor_Invalid_Should_Throw_Exception(string uri, Type exceptionType, string message) + { + try + { + Console.WriteLine("uri: " + uri); + Assert.ThrowsException(exceptionType, () => + { + _ = new Uri(uri); + }, message); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "#FragmentName")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "")] + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256", "")] + public void UriCtor_Should_Parse_Fragment(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.Fragment, $"actual: {uri.Fragment} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("http://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", false)] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", false)] + [DataRow("file://server/filename.ext", true)] + public void UriCtor_IsUnc_Should_Be_Valid(string uriString, bool expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.IsUnc, $"actual: {uri.IsUnc} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("http://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", false)] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", false)] + [DataRow("file://server/filename.ext", true)] + public void UriCtor_IsFile_Should_Be_Valid(string uriString, bool expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.IsFile, $"actual: {uri.IsFile} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", false)] + [DataRow("file://server/filename.ext", false)] + [DataRow("file:///C:/path/to/file.txt", true)] + [DataRow("mailto:John.Doe@example.com", false)] + public void UriCtor_IsLoopback_Should_Be_Valid(string uriString, bool expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.IsLoopback, $"actual: {uri.IsLoopback} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName")] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5")] + [DataRow("\tftp://abc.com ")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one")] + [DataRow("mailto:John.Doe@example.com")] + [DataRow("news:comp.infosystems.www.servers.unix")] + [DataRow("tel:+1-816-555-1212")] + public void UriCtor_OriginalString_Should_Be_Valid(string uriString) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(uriString, uri.OriginalString, $"actual: {uri.OriginalString} expected: {uriString}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", 443)] + [DataRow("ftp://user:password@ftp.contoso.com/directory/file.txt", 21)] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", 389)] + [DataRow("news:comp.infosystems.www.servers.unix", -1)] + [DataRow("tel:+1-816-555-1212", -1)] + [DataRow("file:///home/user/file.txt", -1)] + public void UriCtor_Port_Should_Be_Valid(string uriString, int expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.Port, $"actual: {uri.Port} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("http://example.com:5001/path#192.168.0.1", false)] + [DataRow("http://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", true)] + [DataRow("ssh://user@hostname:22", true)] + [DataRow("rtsp://media.example.com/video", true)] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5", true)] + [DataRow("ftp://[2001:db8::1]/folder", true)] + [DataRow("telnet://192.0.2.16:80/", false)] + [DataRow("file:///home/user/file.txt", true)] + + public void UriCtor_IsDefaultPort_Should_Return_Default_Port(string uriString, bool expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.IsDefaultPort, $"actual: {uri.IsDefaultPort} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "user:password")] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5", "")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "user:password")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", "")] + [DataRow("mailto:John.Doe@example.com", "John.Doe")] + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256", "")] + public void UriCtor_UserInfo_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.UserInfo, $"actual: {uri.UserInfo} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "https")] + [DataRow("ws://ws.pusherapp.com/app/?client=js&version=1.9.3&protocol=5", "ws")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "ftp")] + [DataRow("\thttp://abc.com ", "http")] + [DataRow("mailto:John.Doe@example.com", "mailto")] + [DataRow("h323:caller@192.168.1.100", "h323")] + [DataRow("file:///C:/path/to/file.txt", "file")] + public void UriCtor_Scheme_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.Scheme, $"actual: {uri.Scheme} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "www.contoso.com")] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5", "ws.pusherapp.com")] + [DataRow("ftp://user:password@ftp.contoso.com/directory/file.txt", "ftp.contoso.com")] + [DataRow("\tftp://abc.com ", "abc.com")] + [DataRow("file:///c", "")] + [DataRow("file://c", "c")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", "[2001:db8::7]")] + [DataRow("mailto:John.Doe@example.com", "example.com")] + [DataRow("news:comp.infosystems.www.servers.unix", "")] + [DataRow("telnet://192.0.2.16:80/", "192.0.2.16")] + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256", "")] + public void UriCtor_Host_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.Host, $"actual: {uri.Host} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "/Home/Index.htm")] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5", "/app/")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "/directory/file.txt")] + [DataRow("\tftp://abc.com ", "/")] + [DataRow("file:///", "/")] + [DataRow("file:///c", "/c")] + [DataRow("file:////", "//")] + [DataRow("file://c", "/")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", "/c=GB")] + [DataRow("mailto:John.Doe@example.com", "")] + [DataRow("news:comp.infosystems.www.servers.unix", "comp.infosystems.www.servers.unix")] + [DataRow("tel:+1-816-555-1212", "+1-816-555-1212")] + [DataRow("telnet://192.0.2.16:80/", "/")] + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256", "caller@192.168.1.100")] + public void UriCtor_Absolute_Path_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.AbsolutePath, $"actual: {uri.AbsolutePath} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com/Home/Index.htm?q1=v1&q2=v2#FragmentName", 2)] + [DataRow("file:///", 1)] + [DataRow("file://c", 2)] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", 4)] + [DataRow("telnet://192.0.2.16:80/", 3)] + [DataRow("tel:+1-816-555-1212", 0)] + [DataRow("mailto:John.Doe@example.com", 2)] + [DataRow("\tftp://abc.com ", 2)] + [DataRow("h323:caller@192.168.1.100", 0)] + public void UriCtor_HostNameType_Should_Be_Valid(string uriString, int expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, (int)uri.HostNameType, $"actual: {uri.HostNameType} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", 3)] + [DataRow("file:///", 2)] + [DataRow("\tftp://abc.com ", 2)] + [DataRow("file:////c", 2)] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", 2)] + [DataRow("mailto:John.Doe@example.com", 1)] + public void UriCtor_SegmentsLength_Should_Be_Valid(string uriString, int expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.Segments.Length, $"actual: {uri.Segments.Length} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "?q1=v1&q2=v2")] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5", "?client=js&version=1.9.3&protocol=5")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "")] + [DataRow("\tftp://abc.com ", "")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", "?objectClass?one")] + [DataRow("telnet://192.0.2.16:80/", "")] + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256", "?codec=g729&bandwidth=256")] + [DataRow("file:///", "")] + public void UriCtor_Query_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.Query, $"actual: {uri.Query} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "/Home/Index.htm?q1=v1&q2=v2")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "/directory/file.txt")] + [DataRow("\tftp://abc.com ", "/")] + [DataRow("file:////c", "/")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", "/c=GB?objectClass?one")] + [DataRow("mailto:user@example.com?subject=Hello&body=World", "?subject=Hello&body=World")] + [DataRow("news:comp.infosystems.www.servers.unix", "comp.infosystems.www.servers.unix")] + [DataRow("tel:+1-816-555-1212", "+1-816-555-1212")] + public void UriCtor_PathAndQuery_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + Assert.AreEqual(expectedValue, uri.PathAndQuery, $"actual: {uri.PathAndQuery} expected: {expectedValue}"); + } + + [TestMethod] + [DataRow("https://user:password@www.contoso.com:443/Home/Index.htm?q1=v1&q2=v2#FragmentName", "https://user:password@www.contoso.com/Home/Index.htm?q1=v1&q2=v2#FragmentName")] + [DataRow("ws://ws.pusherapp.com:80/app/?client=js&version=1.9.3&protocol=5", "ws://ws.pusherapp.com/app/?client=js&version=1.9.3&protocol=5")] + [DataRow("ftp://user:password@ftp.contoso.com:21/directory/file.txt", "ftp://user:password@ftp.contoso.com/directory/file.txt")] + [DataRow("\tftp://abc.com ", "ftp://abc.com/")] + [DataRow("file://", "file:///")] + [DataRow("file:///", "file:///")] + [DataRow("file:////", "file:////")] + [DataRow("file:///c", "file:///c")] + [DataRow("file://c", "file://c/")] + [DataRow("file:////c", "file://c/")] + [DataRow("ldap://[2001:db8::7]/c=GB?objectClass?one", "ldap://[2001:db8::7]/c=GB?objectClass?one")] + [DataRow("mailto:John.Doe@example.com", "mailto:John.Doe@example.com")] + [DataRow("news:comp.infosystems.www.servers.unix", "news:comp.infosystems.www.servers.unix")] + [DataRow("tel:+1-816-555-1212", "tel:+1-816-555-1212")] + [DataRow("telnet://192.0.2.16:80/", "telnet://192.0.2.16:80/")] + [DataRow("h323:caller@192.168.1.100?codec=g729&bandwidth=256", "h323:caller@192.168.1.100?codec=g729&bandwidth=256")] + public void UriCtor_AbsoluteUri_Should_Be_Valid(string uriString, string expectedValue) + { + Console.WriteLine(uriString); + Uri uri = new(uriString); + PrintUriPropertiesToConsole(uri); + + Assert.IsTrue(uri.IsAbsoluteUri); + Assert.AreEqual(expectedValue, uri.AbsoluteUri, $"actual: {uri.AbsoluteUri} expected: {expectedValue}"); + } + + + [TestMethod] + public void UriCtor_CombineCtor_Should_ParseIntoUri() + { + Uri baseUri = new(@"http://www.contoso.com/"); + Uri uri = new(baseUri, "catalog/shownew.htm?date=today"); + PrintUriPropertiesToConsole(uri); + + Assert.AreEqual(@"/catalog/shownew.htm", uri.AbsolutePath, $"actual: {uri.AbsolutePath} expected: /catalog/shownew.htm"); + Assert.AreEqual(@"http://www.contoso.com/catalog/shownew.htm?date=today", uri.AbsoluteUri, $"actual: {uri.AbsoluteUri} expected: http://www.contoso.com/catalog/shownew.htm?date=today"); + Assert.AreEqual(@"www.contoso.com", uri.Host, $"actual: {uri.Host} expected: www.contoso.com"); + Assert.AreEqual(UriHostNameType.Dns, uri.HostNameType, $"actual: {uri.HostNameType} expected: {UriHostNameType.Dns}"); + Assert.IsTrue(uri.IsAbsoluteUri, "IsAbsoluteUri was false, but expected true"); + Assert.IsTrue(uri.IsDefaultPort, "IsDefaultPort was false, but expected true"); + Assert.IsFalse(uri.IsLoopback, "IsLoopback was true, but expected false"); + Assert.AreEqual(@"http://www.contoso.com/catalog/shownew.htm?date=today", uri.OriginalString, $"actual: {uri.OriginalString} expected: http://www.contoso.com/catalog/shownew.htm?date=today"); + Assert.AreEqual(@"/catalog/shownew.htm?date=today", uri.PathAndQuery, $"actual: {uri.PathAndQuery} expected: /catalog/shownew.htm?date=today"); + Assert.AreEqual(80, uri.Port, $"actual: {uri.Port} expected: {80}"); + Assert.AreEqual(@"?date=today", uri.Query, $"actual: {uri.Query} expected: ?date=today"); + Assert.AreEqual(@"http", uri.Scheme, $"actual: {uri.Scheme} expected: http"); + Assert.AreEqual(3, uri.Segments.Length, $"actual: {uri.Segments.Length} expected: 3"); + Assert.AreEqual(@"", uri.UserInfo, $"actual: {uri.UserInfo} expected: ''"); + } + + [TestMethod] + public void UriCtor_CombineCtor_Absolute_Should_ParseIntoUri() + { + Uri baseUri = new(@"http://www.contoso.com/", UriKind.Absolute); + var uri = new Uri(baseUri, "catalog/shownew.htm?date=today"); + PrintUriPropertiesToConsole(uri); + + Assert.AreEqual(@"/catalog/shownew.htm", uri.AbsolutePath, $"actual: {uri.AbsolutePath} expected: /catalog/shownew.htm"); + Assert.AreEqual(@"http://www.contoso.com/catalog/shownew.htm?date=today", uri.AbsoluteUri, $"actual: {uri.AbsoluteUri} expected: http://www.contoso.com/catalog/shownew.htm?date=today"); + Assert.AreEqual(@"www.contoso.com", uri.Host, $"actual: {uri.Host} expected: www.contoso.com"); + Assert.AreEqual(UriHostNameType.Dns, uri.HostNameType, $"actual: {uri.HostNameType} expected: {UriHostNameType.Dns}"); + Assert.IsTrue(uri.IsAbsoluteUri, "IsAbsoluteUri was false, but expected true"); + Assert.IsTrue(uri.IsDefaultPort, "IsDefaultPort was false, but expected true"); + Assert.AreEqual(@"http://www.contoso.com/catalog/shownew.htm?date=today", uri.OriginalString, $"actual: {uri.OriginalString} expected: http://www.contoso.com/catalog/shownew.htm?date=today"); + Assert.AreEqual(@"/catalog/shownew.htm?date=today", uri.PathAndQuery, $"actual: {uri.PathAndQuery} expected: /catalog/shownew.htm?date=today"); + Assert.AreEqual(80, uri.Port, $"actual: {uri.Port} expected: {80}"); + Assert.AreEqual(@"?date=today", uri.Query, $"actual: {uri.Query} expected: ?date=today"); + Assert.AreEqual(@"http", uri.Scheme, $"actual: {uri.Scheme} expected: http"); + Assert.AreEqual(3, uri.Segments.Length, $"actual: {uri.Segments.Length} expected: 3"); + Assert.AreEqual(@"", uri.UserInfo, $"actual: {uri.UserInfo} expected: ''"); + } + + [TestMethod] + public void UriCtor_Ctor_RelativeUri_Should_ParseIntoUri() + { + Uri uri = new("catalog/shownew.htm?date=today", UriKind.Relative); + + Console.WriteLine($"Fragment: {uri.Fragment}"); + Console.WriteLine($"HostNameType: {uri.HostNameType}"); + Console.WriteLine($"IsAbsoluteUri: {uri.IsAbsoluteUri}"); + Console.WriteLine($"IsDefaultPort: {uri.IsDefaultPort}"); + Console.WriteLine($"IsFile: {uri.IsFile}"); + Console.WriteLine($"OriginalString: {uri.OriginalString}"); + Console.WriteLine($"Query: {uri.Query}"); + Console.WriteLine($"UserInfo: {uri.UserInfo}"); + + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.AbsolutePath; }, "No exception thrown when accessing AbsolutePath"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.AbsoluteUri; }, "No exception thrown when accessing AbsoluteUri"); + Assert.AreEqual(@"", uri.Fragment, $"actual: {uri.Fragment} expected: ''"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.Host; }, "No exception thrown when accessing Host"); + Assert.AreEqual(UriHostNameType.Unknown, uri.HostNameType, $"actual: {uri.HostNameType} expected: {UriHostNameType.Unknown}"); + Assert.IsFalse(uri.IsAbsoluteUri, "IsAbsoluteUri was true, but expected false"); + Assert.IsTrue(uri.IsDefaultPort, "IsDefaultPort was true, but expected false"); + Assert.IsFalse(uri.IsFile, "IsFile was true, but expected false"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.IsLoopback; }, "No exception thrown when accessing IsLoopback"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.IsUnc; }, "No exception thrown when accessing IsUnc"); + Assert.AreEqual(@"catalog/shownew.htm?date=today", uri.OriginalString); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.PathAndQuery; }, "No exception thrown when PathAndQuery"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.Port; }, "No exception thrown when accessing Port"); + Assert.AreEqual(@"?date=today", uri.Query, $"actual: {uri.Query} expected: ?date=today"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.Scheme; }, "No exception thrown when accessing Scheme"); + Assert.ThrowsException(typeof(InvalidOperationException), () => { _ = uri.Segments.Length; }, "No exception thrown when accessing Scheme"); + Assert.AreEqual(@"", uri.UserInfo, $"actual: {uri.UserInfo} expected: ''"); + } + + private static void PrintUriPropertiesToConsole(Uri uri) + { + Console.WriteLine($"AbsolutePath: {uri.AbsolutePath}"); + Console.WriteLine($"AbsoluteUri: {uri.AbsoluteUri}"); + Console.WriteLine($"Fragment: {uri.Fragment}"); + Console.WriteLine($"Host: {uri.Host}"); + Console.WriteLine($"HostNameType: {uri.HostNameType}"); + Console.WriteLine($"IsAbsoluteUri: {uri.IsAbsoluteUri}"); + Console.WriteLine($"IsDefaultPort: {uri.IsDefaultPort}"); + Console.WriteLine($"IsFile: {uri.IsFile}"); + Console.WriteLine($"IsLoopback: {uri.IsLoopback}"); + Console.WriteLine($"IsUnc: {uri.IsUnc}"); + Console.WriteLine($"OriginalString: {uri.OriginalString}"); + Console.WriteLine($"PathAndQuery: {uri.PathAndQuery}"); + Console.WriteLine($"Port: {uri.Port}"); + Console.WriteLine($"Query: {uri.Query}"); + Console.WriteLine($"Scheme: {uri.Scheme}"); + Console.WriteLine($"Segments: {uri.Segments.Length}"); + Console.WriteLine($"UserInfo: {uri.UserInfo}"); + } + } +} + diff --git a/tests/Cosmos.Network.Http.Tests/WebHeaderCollectionTests.cs b/tests/Cosmos.Network.Http.Tests/WebHeaderCollectionTests.cs new file mode 100644 index 0000000..983b059 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/WebHeaderCollectionTests.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.Net; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class WebHeaderCollectionTests + { + [TestMethod] + public void Add_Authorization_BearerWithSpaceAndNoValue() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: Bearer "); + } + + [TestMethod] + public void Add_Authorization_NoSpaceSingleChar() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: 1"); + } + + [TestMethod] + public void Add_Authorization_ValidBearer() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: Bearer a11111"); + string value = headers["Authorization"]; + Assert.AreEqual("Bearer a11111", value); + } + + [TestMethod] + public void Add_Authorization_ValidTestValue() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: test 1"); + string value = headers["Authorization"]; + Assert.AreEqual("test 1", value); + } + + [TestMethod] + public void Add_Authorization_ValidSingleLetterPair() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: a b"); + string value = headers["Authorization"]; + Assert.AreEqual("a b", value); + } + [TestMethod] + public void Add_Authorization_EmptyValue() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization:"); + string value = headers["Authorization"]; + Assert.AreEqual(string.Empty, value); + } + + [TestMethod] + public void Add_Authorization_ColonWithSpaceOnly() + { + var headers = new WebHeaderCollection(); + headers.Add("Authorization: "); + string value = headers["Authorization"]; + Assert.AreEqual(string.Empty, value); + } + + [TestMethod] + public void Add_NullHeader_ThrowsArgumentNullException() + { + var headers = new WebHeaderCollection(); + Assert.ThrowsException(typeof(ArgumentNullException), () => headers.Add(null)); + } + + [TestMethod] + public void Add_EmptyHeader_ThrowsArgumentNullException() + { + var headers = new WebHeaderCollection(); + Assert.ThrowsException(typeof(ArgumentNullException), () => headers.Add(string.Empty)); + } + + [TestMethod] + public void Add_HeaderWithNoColon_ThrowsArgumentException() + { + var headers = new WebHeaderCollection(); + Assert.ThrowsException(typeof(ArgumentException), () => headers.Add("Authorization")); + } + + [TestMethod] + public void Add_HeaderNameWithSpace_ThrowsArgumentException() + { + var headers = new WebHeaderCollection(); + Assert.ThrowsException(typeof(ArgumentException), () => headers.Add("My Header: value")); + } + } +} diff --git a/tests/Cosmos.Network.Http.Tests/X509Certificate2Tests.cs b/tests/Cosmos.Network.Http.Tests/X509Certificate2Tests.cs new file mode 100644 index 0000000..6374bd3 --- /dev/null +++ b/tests/Cosmos.Network.Http.Tests/X509Certificate2Tests.cs @@ -0,0 +1,211 @@ +// Cosmos: tests of what nanoFramework parses natively: certificates and private keys, PEM and DER. + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; + +namespace Cosmos.Network.Http.Tests +{ + [TestClass] + public class X509Certificate2Tests + { + private static TestCertificate Rsa => TestCertificates.RsaServer; + + private static TestCertificate Ec => TestCertificates.EcServer; + + [TestMethod] + public void Certificate_FromPemString_IsParsed() + { + var certificate = new X509Certificate(Rsa.Pem); + + StringAssert.Contains(certificate.Subject, "CN=localhost"); + StringAssert.Contains(certificate.Issuer, "CN=Cosmos Test Root"); + Assert.AreEqual(Rsa.Certificate.NotAfter, certificate.GetExpirationDate()); + Assert.AreEqual(Rsa.Certificate.NotBefore, certificate.GetEffectiveDate()); + } + + [TestMethod] + public void Certificate_FromDer_IsParsed() + { + var certificate = new X509Certificate(Rsa.Der); + + StringAssert.Contains(certificate.Subject, "CN=localhost"); + Assert.AreEqual(1, certificate.GetCertificates().Length); + } + + [TestMethod] + public void Certificate_DerEndingWithZero_IsParsed() + { + // A DER certificate may end with a zero byte, which isn't nanoFramework's string terminator. + for (int i = 0; i < 2048; i++) + { + TestCertificate candidate = TestCertificates.CreateServer("zero" + i, TestCertificates.Root, TestCertificates.Root.Key, null, null); + byte[] der = candidate.Der; + if (der[der.Length - 1] == 0) + { + var certificate = new X509Certificate(der); + Assert.AreEqual(1, certificate.GetCertificates().Length); + CollectionAssert.AreEqual(der, certificate.GetCertificates()[0].GetEncoded()); + return; + } + } + + Assert.Inconclusive("No certificate ending with a zero byte was made."); + } + + [TestMethod] + public void Certificate_PemBundle_HoldsEveryCertificate() + { + string bundle = TestCertificates.Root.Pem + TestCertificates.OtherRoot.Pem + TestCertificates.Intermediate.Pem; + + var certificate = new X509Certificate(bundle); + + Assert.AreEqual(3, certificate.GetCertificates().Length); + StringAssert.Contains(certificate.Subject, "CN=Cosmos Test Root"); + } + + [TestMethod] + public void Certificate_Garbage_Throws() + { + Assert.ThrowsException(() => new X509Certificate(Encoding.UTF8.GetBytes("not a certificate"))); + } + + [TestMethod] + public void Key_Pkcs8Pem_IsDecoded() + { + AssertSameKey(Rsa, new X509Certificate2(Rsa.Pem, Rsa.KeyPem(), null)); + AssertSameKey(Ec, new X509Certificate2(Ec.Pem, Ec.KeyPem(), null)); + } + + [TestMethod] + public void Key_TraditionalPem_IsDecoded() + { + // "RSA PRIVATE KEY" (PKCS#1) and "EC PRIVATE KEY" (SEC1). + AssertSameKey(Rsa, new X509Certificate2(Rsa.Pem, Rsa.KeyPem(traditional: true), null)); + AssertSameKey(Ec, new X509Certificate2(Ec.Pem, Ec.KeyPem(traditional: true), null)); + } + + [TestMethod] + public void Key_EncryptedPkcs8Pem_IsDecoded() + { + var writer = new StringWriter(); + new PemWriter(writer).WriteObject(new Pkcs8Generator(Ec.Key.Private, Pkcs8Generator.PbeWithShaAnd3KeyTripleDesCbc) { Password = "secret".ToCharArray() }); + + AssertSameKey(Ec, new X509Certificate2(Ec.Pem, writer.ToString(), "secret")); + Assert.ThrowsException(() => new X509Certificate2(Ec.Pem, writer.ToString(), "wrong")); + } + + [TestMethod] + public void Key_EncryptedTraditionalPem_IsDecoded() + { + var writer = new StringWriter(); + new PemWriter(writer).WriteObject(Rsa.Key.Private, "AES-128-CBC", "secret".ToCharArray(), new SecureRandom()); + + AssertSameKey(Rsa, new X509Certificate2(Rsa.Pem, writer.ToString(), "secret")); + } + + [TestMethod] + public void Key_Pkcs8Der_IsDecoded() + { + byte[] der = PrivateKeyInfoFactory.CreatePrivateKeyInfo(Rsa.Key.Private).GetEncoded(); + + AssertSameKey(Rsa, new X509Certificate2(Rsa.Der, der, null)); + } + + [TestMethod] + public void Key_EncryptedPkcs8Der_IsDecoded() + { + byte[] salt = new byte[8]; + new SecureRandom().NextBytes(salt); + byte[] der = EncryptedPrivateKeyInfoFactory.CreateEncryptedPrivateKeyInfo( + PkcsObjectIdentifiers.PbeWithShaAnd3KeyTripleDesCbc, "secret".ToCharArray(), salt, 2048, Ec.Key.Private).GetEncoded(); + + AssertSameKey(Ec, new X509Certificate2(Ec.Der, der, "secret")); + } + + [TestMethod] + public void Key_Pkcs1Der_IsDecoded() + { + byte[] der = PrivateKeyInfoFactory.CreatePrivateKeyInfo(Rsa.Key.Private).ParsePrivateKey().GetEncoded(); + + AssertSameKey(Rsa, new X509Certificate2(Rsa.Der, der, null)); + } + + [TestMethod] + public void Key_Sec1Der_IsDecoded() + { + byte[] der = PrivateKeyInfoFactory.CreatePrivateKeyInfo(Ec.Key.Private).ParsePrivateKey().GetEncoded(); + + AssertSameKey(Ec, new X509Certificate2(Ec.Der, der, null)); + } + + [TestMethod] + public void Key_DerWithTerminator_IsDecoded() + { + byte[] der = PrivateKeyInfoFactory.CreatePrivateKeyInfo(Ec.Key.Private).GetEncoded(); + byte[] terminated = new byte[der.Length + 1]; + Array.Copy(der, terminated, der.Length); + + AssertSameKey(Ec, new X509Certificate2(Ec.Der, terminated, null)); + } + + [TestMethod] + public void Key_EcWithoutCurve_Throws() + { + // SEC1 lets the curve be left out (RFC 5915), which BouncyCastle can't build a key without. + var ec = (Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters)Ec.Key.Private; + byte[] der = new Org.BouncyCastle.Asn1.Sec.ECPrivateKeyStructure(256, ec.D).GetEncoded(); + + Assert.ThrowsException(() => new X509Certificate2(Ec.Der, der, null)); + + var writer = new StringWriter(); + new Org.BouncyCastle.Utilities.IO.Pem.PemWriter(writer).WriteObject(new Org.BouncyCastle.Utilities.IO.Pem.PemObject("EC PRIVATE KEY", der)); + Assert.ThrowsException(() => new X509Certificate2(Ec.Pem, writer.ToString(), null)); + } + + [TestMethod] + public void Key_PemAfterParameters_IsDecoded() + { + // As openssl ecparam -genkey writes it: the curve's block first, then the key's. + string pem = "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n" + Ec.KeyPem(traditional: true); + + AssertSameKey(Ec, new X509Certificate2(Ec.Pem, pem, null)); + } + + [TestMethod] + public void Key_Garbage_Throws() + { + Assert.ThrowsException(() => new X509Certificate2(Rsa.Pem, "not a key", null)); + } + + private static void AssertSameKey(TestCertificate expected, X509Certificate2 certificate) + { + Assert.IsTrue(certificate.HasPrivateKey); + Assert.IsNotNull(certificate.Key); + Assert.IsTrue(certificate.Key.IsPrivate); + + switch (expected.Key.Private) + { + case RsaPrivateCrtKeyParameters rsa: + Assert.AreEqual(rsa.Modulus, ((RsaKeyParameters)certificate.Key).Modulus); + Assert.AreEqual(rsa.Exponent, ((RsaKeyParameters)certificate.Key).Exponent); + break; + + case ECPrivateKeyParameters ec: + Assert.AreEqual(ec.D, ((ECPrivateKeyParameters)certificate.Key).D); + break; + + default: + Assert.Fail("Unexpected key type " + expected.Key.Private.GetType()); + break; + } + } + } +}