├── .gitattributes ├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.md └── src ├── HttpOverStream.Client ├── DialMessageHandler.cs └── HttpOverStream.Client.csproj ├── HttpOverStream.NamedPipe ├── HttpOverStream.NamedPipe.csproj ├── NamedPipeDialer.cs ├── NamedPipeHttpClientBuilder.cs └── NamedPipeListener.cs ├── HttpOverStream.Server.Owin.Tests ├── EndToEndTests.cs ├── HttpOverStream.Server.Owin.Tests.csproj ├── HttpOverStream.Server.Owin.Tests.csproj.user ├── OnceTests.cs ├── Properties │ └── AssemblyInfo.cs ├── TestLoggingHandler.cs ├── app.config └── packages.config ├── HttpOverStream.Server.Owin ├── CustomListenerHost.cs ├── HttpOverStream.Server.Owin.csproj ├── HttpOverStream.Server.Owin.csproj.user ├── HttpOverStream.Server.Owin.nuspec ├── Once.cs ├── app.config └── packages.config ├── HttpOverStream.Tests ├── BodyStreamTests.cs ├── ByLineReaderTests.cs └── HttpOverStream.Tests.csproj ├── HttpOverStream.sln ├── HttpOverStream ├── BodyStream.cs ├── ByLineReader.cs ├── HttpHeaderWriter.cs ├── HttpOverStream.csproj ├── HttpParser.cs ├── IDial.cs ├── IListen.cs └── Logging │ ├── ILoggerHttpOverStream.cs │ └── NoopLogger.cs ├── build.cake └── build.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /**/.vs/ 6 | /**/bin/ 7 | /**/obj/ 8 | /src/packages/ 9 | /src/TestResults/ 10 | /src/tools/ -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | // -*- groovy -*- 2 | 3 | pipeline { 4 | agent { 5 | label { 6 | label 'windows-2019' 7 | } 8 | } 9 | options { 10 | timestamps() 11 | buildDiscarder(logRotator(numToKeepStr: '10')) 12 | timeout(time: 1, unit: 'HOURS') 13 | } 14 | stages { 15 | stage('Build') { 16 | steps { 17 | bat 'docker run --rm -v %cd%:C:/work -w C:/work/src mcr.microsoft.com/dotnet/framework/sdk:4.8-20190910-windowsservercore-ltsc2019 powershell -File build.ps1 --target=Build' 18 | } 19 | } 20 | stage('Test') { 21 | steps { 22 | bat 'docker run --rm -v %cd%:C:/work -w C:/work/src mcr.microsoft.com/dotnet/framework/sdk:4.8-20190910-windowsservercore-ltsc2019 powershell -File build.ps1 --target=Test' 23 | } 24 | } 25 | stage('Nuget-pack') { 26 | steps { 27 | bat 'docker run --rm -v %cd%:C:/work -w C:/work/src -e TAG_NAME mcr.microsoft.com/dotnet/framework/sdk:4.8-20190910-windowsservercore-ltsc2019 powershell -File build.ps1 --target=Nuget-pack' 28 | } 29 | } 30 | stage('Nuget-push') { 31 | when { tag "*" } 32 | environment { 33 | NugetAPIKey = credentials('nuget-api-key') 34 | } 35 | steps { 36 | bat 'docker run --rm -v %cd%:C:/work -w C:/work/src -e NugetAPIKey -e TAG_NAME mcr.microsoft.com/dotnet/framework/sdk:4.8-20190910-windowsservercore-ltsc2019 powershell -File build.ps1 --target=Nuget-push' 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HttpOverStream 2 | [![NuGet](https://img.shields.io/nuget/v/HttpOverStream?color=green)](https://www.nuget.org/packages/HttpOverStream/) 3 | [![Build Status](https://ci-next.docker.com/public/job/HttpOverStream/job/master/badge/icon)](https://ci-next.docker.com/public/job/HttpOverStream/job/master/) 4 | 5 | Used by Docker Desktop. 6 | 7 | .NET library for using HTTP 1.1 over streams, especially Windows Named Pipes. 8 | 9 | This library essentially allows inter-process communication over named pipes using HTTP which doesn't require opening ports on the host machine like a standard web server. 10 | 11 | There is both a client and server implementation and these have been tested across languages (GoLang and Javascript so far can both successfully send and receive messages here). 12 | Server implementation in OWIN is more production ready than the .NET Core version. 13 | 14 | Server usage (OWIN) is like this: 15 | ``` 16 | var server = CustomListenerHost.Start(startupAction, new NamedPipeListener(pipeName)); 17 | ``` 18 | 19 | Client usage: 20 | 21 | ``` 22 | _httpClient = new NamedPipeHttpClientBuilder("myPipeName") 23 | .WithPerRequestTimeout(TimeSpan.FromSeconds(5)) 24 | .Build(); 25 | var response = await _httpClient.GetAsync("/api/endpoint"); 26 | ``` 27 | -------------------------------------------------------------------------------- /src/HttpOverStream.Client/DialMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using HttpOverStream.Logging; 8 | 9 | namespace HttpOverStream.Client 10 | { 11 | public class DialMessageHandler : HttpMessageHandler 12 | { 13 | public const string UnderlyingStreamProperty = "DIAL_UNDERLYING_STREAM"; 14 | private readonly IDial _dial; 15 | private readonly Version _httpVersion; 16 | private readonly ILoggerHttpOverStream _logger; 17 | 18 | public DialMessageHandler(IDial dial) : this(dial, null, null) 19 | { 20 | } 21 | 22 | public DialMessageHandler(IDial dial, ILoggerHttpOverStream logger) : this(dial, logger, null) 23 | { 24 | } 25 | 26 | public DialMessageHandler(IDial dial, Version httpVersion) : this(dial, null, httpVersion) 27 | { 28 | } 29 | 30 | public DialMessageHandler(IDial dial, ILoggerHttpOverStream logger, Version httpVersion) 31 | { 32 | _dial = dial ?? throw new ArgumentNullException(nameof(dial)); 33 | _logger = logger ?? new NoopLogger(); 34 | _httpVersion = httpVersion ?? HttpVersion.Version10; 35 | } 36 | 37 | private class DialResponseContent : HttpContent 38 | { 39 | private Stream _stream; 40 | private long? _length; 41 | 42 | public void SetContent(Stream unread, long? length) 43 | { 44 | _stream = unread; 45 | _length = length; 46 | } 47 | 48 | protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) 49 | { 50 | return _stream.CopyToAsync(stream); 51 | } 52 | 53 | protected override bool TryComputeLength(out long length) 54 | { 55 | if (_length.HasValue) 56 | { 57 | length = _length.Value; 58 | return true; 59 | } 60 | length = 0; 61 | return false; 62 | } 63 | 64 | protected override void Dispose(bool disposing) 65 | { 66 | base.Dispose(disposing); 67 | if (disposing) 68 | { 69 | _stream.Dispose(); 70 | } 71 | } 72 | 73 | protected override Task CreateContentReadStreamAsync() 74 | { 75 | return Task.FromResult(_stream); 76 | } 77 | } 78 | 79 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 80 | { 81 | ValidateAndNormalizeRequest(request); 82 | Stream stream = null; 83 | try 84 | { 85 | _logger.LogVerbose("HttpOS Client: Trying to connect.."); 86 | stream = await _dial.DialAsync(request, cancellationToken).ConfigureAwait(false); 87 | 88 | _logger.LogVerbose("HttpOS Client: Connected."); 89 | request.Properties.Add(UnderlyingStreamProperty, stream); 90 | 91 | _logger.LogVerbose("HttpOS Client: Writing request"); 92 | await stream.WriteClientMethodAndHeadersAsync(request, cancellationToken).ConfigureAwait(false); 93 | 94 | // as soon as headers are sent, we should begin reading the response, and send the request body concurrently 95 | // This is because if the server 404s nothing will ever read the response and it'll hang waiting 96 | // for someone to read it 97 | var writeContentTask = Task.Run(async () => // Cancel this task if server response detected 98 | { 99 | if (request.Content != null) 100 | { 101 | _logger.LogVerbose("HttpOS Client: Writing request request.Content.CopyToAsync"); 102 | await request.Content.CopyToAsync(stream).ConfigureAwait(false); 103 | } 104 | 105 | _logger.LogVerbose("HttpOS Client: stream.FlushAsync"); 106 | await stream.FlushAsync(cancellationToken).ConfigureAwait(false); 107 | _logger.LogVerbose("HttpOS Client: Finished writing request"); 108 | }, cancellationToken); 109 | 110 | var responseContent = new DialResponseContent(); 111 | var response = new HttpResponseMessage {RequestMessage = request, Content = responseContent}; 112 | 113 | _logger.LogVerbose("HttpOS Client: Waiting for response"); 114 | string statusLine = await stream.ReadLineAsync(cancellationToken).ConfigureAwait(false); 115 | _logger.LogVerbose("HttpOS Client: Read 1st response line"); 116 | ParseStatusLine(response, statusLine); 117 | _logger.LogVerbose("HttpOS Client: ParseStatusLine"); 118 | for (;;) 119 | { 120 | var line = await stream.ReadLineAsync(cancellationToken).ConfigureAwait(false); 121 | if (line.Length == 0) 122 | { 123 | _logger.LogVerbose("HttpOS Client: Found empty line, end of response headers"); 124 | break; 125 | } 126 | 127 | try 128 | { 129 | _logger.LogVerbose("HttpOS Client: Parsing line:" + line); 130 | (var name, var value) = HttpParser.ParseHeaderNameValues(line); 131 | if (!response.Headers.TryAddWithoutValidation(name, value)) 132 | { 133 | response.Content.Headers.TryAddWithoutValidation(name, value); 134 | } 135 | } 136 | catch (FormatException ex) 137 | { 138 | throw new HttpRequestException("Error parsing header", ex); 139 | } 140 | } 141 | 142 | _logger.LogVerbose("HttpOS Client: Finished reading response header lines"); 143 | responseContent.SetContent( 144 | new BodyStream(stream, response.Content.Headers.ContentLength, closeOnReachEnd: true), 145 | response.Content.Headers.ContentLength); 146 | return response; 147 | } 148 | catch (TimeoutException) 149 | { 150 | _logger.LogWarning("HttpOS Client: connection timed out."); 151 | stream?.Dispose(); 152 | throw; 153 | } 154 | catch(Exception e) 155 | { 156 | _logger.LogError("HttpOS Client: Exception:" + e.Message); 157 | stream?.Dispose(); 158 | throw; 159 | } 160 | } 161 | 162 | private void ParseStatusLine(HttpResponseMessage response, string line) 163 | { 164 | const int MinStatusLineLength = 12; // "HTTP/1.x 123" 165 | if (line.Length < MinStatusLineLength || line[8] != ' ') 166 | { 167 | throw new HttpRequestException("Invalid response, expecting HTTP/1.0 or 1.1, was:" + line); 168 | } 169 | 170 | if (!line.StartsWith("HTTP/1.")) 171 | { 172 | throw new HttpRequestException("Invalid response, expecting HTTP/1.0 or 1.1, was:" + line); 173 | } 174 | response.Version = _httpVersion; 175 | // Set the status code 176 | if (int.TryParse(line.Substring(9,3), out int statusCode)) 177 | { 178 | response.StatusCode = (HttpStatusCode)statusCode; 179 | } 180 | else 181 | { 182 | throw new HttpRequestException("Invalid response, can't parse status code. Line was:" + line); 183 | } 184 | // Parse (optional) reason phrase 185 | if (line.Length == MinStatusLineLength) 186 | { 187 | response.ReasonPhrase = string.Empty; 188 | } 189 | else if (line[MinStatusLineLength] == ' ') 190 | { 191 | response.ReasonPhrase = line.Substring(MinStatusLineLength + 1); 192 | } 193 | else 194 | { 195 | throw new HttpRequestException("Invalid response"); 196 | } 197 | } 198 | 199 | private void ValidateAndNormalizeRequest(HttpRequestMessage request) 200 | { 201 | request.Version = HttpVersion.Version10; 202 | // Add headers to define content transfer, if not present 203 | if (request.Headers.TransferEncodingChunked.GetValueOrDefault()) 204 | { 205 | throw new HttpRequestException("DialMessageHandler does not support chunked encoding"); 206 | } 207 | 208 | // HTTP 1.0 does not support Expect: 100-continue; just disable it. 209 | if (request.Headers.ExpectContinue == true) 210 | { 211 | request.Headers.ExpectContinue = false; 212 | } 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/HttpOverStream.Client/HttpOverStream.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | false 9 | 10 | 11 | 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/HttpOverStream.NamedPipe/HttpOverStream.NamedPipe.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/HttpOverStream.NamedPipe/NamedPipeDialer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipes; 4 | using System.Net.Http; 5 | using System.Security.Principal; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace HttpOverStream.NamedPipe 10 | { 11 | public class NamedPipeDialer : IDial 12 | { 13 | private readonly string _pipeName; 14 | private readonly string _serverName; 15 | private readonly PipeOptions _pipeOptions; 16 | private readonly int _timeoutMs; 17 | private readonly TokenImpersonationLevel _impersonationLevel; 18 | 19 | public NamedPipeDialer(string pipeName) 20 | : this(pipeName, ".", PipeOptions.Asynchronous, 0) 21 | { 22 | } 23 | 24 | public NamedPipeDialer(string pipeName, int timeoutMs) 25 | : this(pipeName, ".", PipeOptions.Asynchronous, timeoutMs) 26 | { 27 | } 28 | 29 | public NamedPipeDialer(string pipeName, string serverName, PipeOptions pipeOptions, int timeoutMs, TokenImpersonationLevel impersonationLevel = TokenImpersonationLevel.Identification) 30 | { 31 | _pipeName = pipeName; 32 | _serverName = serverName; 33 | _pipeOptions = pipeOptions; 34 | _timeoutMs = timeoutMs; 35 | _impersonationLevel = impersonationLevel; 36 | } 37 | 38 | public async ValueTask DialAsync(HttpRequestMessage request, CancellationToken cancellationToken) 39 | { 40 | var pipeStream = new NamedPipeClientStream(_serverName, _pipeName, PipeDirection.InOut, _pipeOptions, _impersonationLevel); 41 | await pipeStream.ConnectAsync(_timeoutMs, cancellationToken).ConfigureAwait(false); 42 | if (cancellationToken.CanBeCanceled) 43 | { 44 | cancellationToken.Register(() => 45 | { 46 | try 47 | { 48 | pipeStream.Dispose(); 49 | } 50 | catch (Exception) { } 51 | } 52 | ); 53 | } 54 | return pipeStream; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/HttpOverStream.NamedPipe/NamedPipeHttpClientBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Security.Principal; 4 | using HttpOverStream.Client; 5 | using HttpOverStream.Logging; 6 | 7 | namespace HttpOverStream.NamedPipe 8 | { 9 | public class NamedPipeHttpClientBuilder 10 | { 11 | private string _pipeName; 12 | private ILoggerHttpOverStream _logger; 13 | private DelegatingHandler _outerHandler; 14 | private TimeSpan? _perRequestTimeout; 15 | private Version _httpVersion; 16 | private TimeSpan? _namedPipeConnectionTimeout; 17 | private TokenImpersonationLevel _impersonationLevel = TokenImpersonationLevel.Identification; 18 | 19 | public NamedPipeHttpClientBuilder(string pipeName) 20 | { 21 | _pipeName = pipeName; 22 | } 23 | 24 | public NamedPipeHttpClientBuilder WithLogger(ILoggerHttpOverStream logger) 25 | { 26 | _logger = logger; 27 | return this; 28 | } 29 | 30 | public NamedPipeHttpClientBuilder WithDelegatingHandler(DelegatingHandler outerHandler) 31 | { 32 | _outerHandler = outerHandler; 33 | return this; 34 | } 35 | 36 | public NamedPipeHttpClientBuilder WithPerRequestTimeout(TimeSpan perRequestTimeout) 37 | { 38 | _perRequestTimeout = perRequestTimeout; 39 | return this; 40 | } 41 | 42 | public NamedPipeHttpClientBuilder WithHttpVersion(Version httpVersion) 43 | { 44 | _httpVersion = httpVersion; 45 | return this; 46 | } 47 | 48 | public NamedPipeHttpClientBuilder WithNamedPipeConnectionTimeout(TimeSpan timeSpan) 49 | { 50 | _namedPipeConnectionTimeout = timeSpan; 51 | return this; 52 | } 53 | 54 | public NamedPipeHttpClientBuilder WithImpersonationLevel(TokenImpersonationLevel impersonationLevel) 55 | { 56 | _impersonationLevel = impersonationLevel; 57 | return this; 58 | } 59 | 60 | public HttpClient Build() 61 | { 62 | var connectionTimeout = 0; 63 | if (_namedPipeConnectionTimeout.HasValue) 64 | { 65 | connectionTimeout = (int)_namedPipeConnectionTimeout.Value.TotalMilliseconds; 66 | } 67 | 68 | var dialer = new NamedPipeDialer(_pipeName, ".", System.IO.Pipes.PipeOptions.Asynchronous, connectionTimeout, _impersonationLevel); 69 | 70 | var innerHandler = new DialMessageHandler(dialer, _logger, _httpVersion); 71 | HttpClient httpClient; 72 | if (_outerHandler != null) 73 | { 74 | _outerHandler.InnerHandler = innerHandler; 75 | httpClient = new HttpClient(_outerHandler); 76 | } 77 | else 78 | { 79 | httpClient = new HttpClient(innerHandler); 80 | } 81 | 82 | httpClient.BaseAddress = new Uri("http://localhost"); 83 | if (_perRequestTimeout != null) 84 | { 85 | httpClient.Timeout = _perRequestTimeout.Value; 86 | } 87 | 88 | return httpClient; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/HttpOverStream.NamedPipe/NamedPipeListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.IO.Pipes; 6 | using System.Linq; 7 | using System.Security.Principal; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using HttpOverStream.Logging; 11 | 12 | namespace HttpOverStream.NamedPipe 13 | { 14 | public class NamedPipeListener : IListen 15 | { 16 | private Task _listenTask; 17 | private CancellationTokenSource _listenTcs; 18 | private readonly string _pipeName; 19 | private readonly PipeOptions _pipeOptions; 20 | private readonly PipeTransmissionMode _pipeTransmissionMode; 21 | private readonly int _maxAllowedServerInstances; 22 | private static readonly int _numServerThreads = 5; 23 | private readonly ILoggerHttpOverStream _logger; 24 | private readonly PipeSecurity _pipeSecurity; 25 | 26 | public NamedPipeListener(string pipeName, ILoggerHttpOverStream logger = null, PipeSecurity pipeSecurity = null) 27 | : this(pipeName, PipeOptions.Asynchronous, PipeTransmissionMode.Byte, NamedPipeServerStream.MaxAllowedServerInstances, logger, pipeSecurity) 28 | { 29 | } 30 | 31 | public NamedPipeListener(string pipeName, PipeOptions pipeOptions, PipeTransmissionMode pipeTransmissionMode, int maxAllowedServerInstances, ILoggerHttpOverStream logger, PipeSecurity pipeSecurity = null) 32 | { 33 | _pipeName = pipeName; 34 | _pipeOptions = pipeOptions; 35 | _pipeTransmissionMode = pipeTransmissionMode; 36 | _maxAllowedServerInstances = maxAllowedServerInstances; 37 | _logger = logger ?? new NoopLogger(); 38 | _pipeSecurity = pipeSecurity; 39 | } 40 | 41 | public Task StartAsync(Action onConnection, CancellationToken cancellationToken) 42 | { 43 | _listenTcs = new CancellationTokenSource(); 44 | var ct = _listenTcs.Token; 45 | _listenTask = StartServerAndDummyThreads(onConnection, ct); 46 | return Task.CompletedTask; 47 | } 48 | 49 | private Task StartServerAndDummyThreads(Action onConnection, CancellationToken cancellationToken) 50 | { 51 | var tasks = new List(); 52 | 53 | // We block on creating the dummy server/client to ensure we definitely have that set up before doing anything else 54 | var (dummyClient, dummyServer) = ConnectDummyClientAndServer(cancellationToken); 55 | tasks.Add(Task.Run(() => DisposeWhenCancelled(dummyClient, "client", cancellationToken))); 56 | tasks.Add(Task.Run(() => DisposeWhenCancelled(dummyServer, "server", cancellationToken))); 57 | 58 | // This runs synchronously until we've created the first server listener to ensure we can handle at least the first client connection 59 | var listenTask = CreateServerStreamAndListen(-1, onConnection, cancellationToken); 60 | tasks.Add(listenTask); 61 | 62 | // We don't technically need more than 1 thread but its faster 63 | for (int i = 0; i < _numServerThreads - 1; i++) 64 | { 65 | var i1 = i; // Capture value immediately to prevent late closure binding 66 | tasks.Add(Task.Run(() => CreateServerStreamAndListen(i1, onConnection, cancellationToken), cancellationToken)); 67 | } 68 | 69 | return Task.WhenAll(tasks); 70 | } 71 | 72 | // We dont use the cancellation token but other implementations might 73 | public async Task StopAsync(CancellationToken cancellationToken) 74 | { 75 | try 76 | { 77 | _listenTcs.Cancel(); 78 | } 79 | catch (AggregateException a) when (a.InnerExceptions.All(e => e is ObjectDisposedException)) 80 | { 81 | // NamedPipe cancellations can throw ObjectDisposedException 82 | // They will be grouped in an AggregateException and this shouldnt break 83 | } 84 | try 85 | { 86 | await _listenTask.ConfigureAwait(false); 87 | } 88 | catch (OperationCanceledException) 89 | { 90 | } 91 | } 92 | 93 | private async Task CreateServerStreamAndListen(int threadNumber, Action onConnection, 94 | CancellationToken cancelToken) 95 | { 96 | try 97 | { 98 | while (!cancelToken.IsCancellationRequested) 99 | { 100 | var serverStream = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, _maxAllowedServerInstances, _pipeTransmissionMode, _pipeOptions, 0, 0, _pipeSecurity); 101 | try 102 | { 103 | _logger.LogVerbose("[ thread " + threadNumber + "] Waiting for connection.."); 104 | await serverStream.WaitForConnectionAsync(cancelToken).ConfigureAwait(false); 105 | _logger.LogVerbose("[ thread " + threadNumber + "] Found connection!"); 106 | // MP: We deliberately don't await this because we want to kick off the work on a background thead 107 | // and immediately check for the next client connecting 108 | #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed 109 | Task.Run(() => onConnection(serverStream), cancelToken); 110 | #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed 111 | 112 | } 113 | catch (OperationCanceledException) // Thrown when cancellationToken is cancelled 114 | { 115 | _logger.LogVerbose("[ thread " + threadNumber + "] Cancelling server wait."); 116 | serverStream.Dispose(); 117 | } 118 | catch (IOException ex) // Thrown if client disconnects early 119 | { 120 | if (ex.Message.Contains("The pipe is being closed.")) 121 | { 122 | _logger.LogVerbose("[ thread " + threadNumber + "] IOException: Could not read Named Pipe message - client disconnected before server finished reading."); 123 | } 124 | else 125 | { 126 | _logger.LogWarning("[ thread " + threadNumber + "] IOException, possibly because the client disconnected early:" + ex); 127 | } 128 | serverStream.Dispose(); 129 | } 130 | catch (Exception e) 131 | { 132 | _logger.LogError("[ thread " + threadNumber + "] Exception thrown during server stream wait:" + e); 133 | serverStream.Dispose(); 134 | } 135 | } 136 | _logger.LogVerbose("[ thread " + threadNumber + "] Stopping thread - cancelled"); 137 | } 138 | catch (Exception e) 139 | { 140 | _logger.LogError("[ thread " + threadNumber + "] Exception creating server stream:" + e); 141 | } 142 | } 143 | 144 | private (NamedPipeClientStream, NamedPipeServerStream) ConnectDummyClientAndServer(CancellationToken cancellationToken) 145 | { 146 | const int MAX_DUMMYCONNECTION_RETRIES = 500; 147 | for (int i = 0; i < MAX_DUMMYCONNECTION_RETRIES; i++) 148 | { 149 | // Always have another stream active so if HandleStream finishes really quickly theres 150 | // no chance of the named pipe being removed altogether. 151 | // This is the same pattern as microsofts go library -> https://github.com/Microsoft/go-winio/pull/80/commits/ecd994be061f4ae21f463bbf08166d8edc96cadb 152 | var serverStream = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, _maxAllowedServerInstances, _pipeTransmissionMode, _pipeOptions, 0, 0, _pipeSecurity); 153 | serverStream.WaitForConnectionAsync(cancellationToken); 154 | 155 | var dummyClientStream = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); 156 | try 157 | { 158 | dummyClientStream.Connect(10); // 10ms timeout to connect to itself 159 | } 160 | catch (Exception) 161 | { 162 | _logger.LogVerbose("[DUMMY] Dummy client couldn't connect, usually because a real client is trying to connect before the server is ready. Closing the pending connection and restarting server.."); 163 | serverStream.Disconnect(); 164 | continue; 165 | } 166 | 167 | _logger.LogVerbose("[DUMMY] Connected!"); 168 | return (dummyClientStream, serverStream); 169 | } 170 | 171 | var errorMessage = 172 | $"Could not start server - dummy connection could not be made after {MAX_DUMMYCONNECTION_RETRIES} retries. " + 173 | "This could be because there are too many pending connections on this named pipe. Ensure clients don't spam the server before it's ready."; 174 | _logger.LogError(errorMessage); 175 | throw new Exception(errorMessage); 176 | } 177 | 178 | private async Task DisposeWhenCancelled(IDisposable disposable, string threadName, 179 | CancellationToken cancellationToken) 180 | { 181 | await cancellationToken.WhenCanceled().ConfigureAwait(false); 182 | 183 | try 184 | { 185 | disposable.Dispose(); 186 | } 187 | catch (Exception e) 188 | { 189 | _logger.LogError($"Exception disposing dummy {threadName} stream: {e}"); 190 | } 191 | } 192 | 193 | public IPrincipal GetTransportIdentity(Stream connection) 194 | { 195 | var serverStream = connection as NamedPipeServerStream; 196 | if (serverStream == null) 197 | { 198 | throw new InvalidOperationException("connection should be a NamedPipeServerStream"); 199 | } 200 | 201 | WindowsPrincipal principal = null; 202 | try 203 | { 204 | serverStream.RunAsClient(() => 205 | { 206 | principal = new WindowsPrincipal(WindowsIdentity.GetCurrent()); 207 | }); 208 | } 209 | catch 210 | { 211 | // this can fail if client explicitly connected with ImpersonationLevel.None. 212 | // default is ImpersonationLevel.Identify which runs fine. 213 | } 214 | 215 | return principal; 216 | } 217 | } 218 | 219 | internal static class CancellationTokenExtensions 220 | { 221 | // Taken from https://github.com/dotnet/corefx/issues/2704#issuecomment-131221355 222 | public static Task WhenCanceled(this CancellationToken cancellationToken) 223 | { 224 | var tcs = new TaskCompletionSource(); 225 | cancellationToken.Register(s => ((TaskCompletionSource)s).SetResult(true), tcs); 226 | return tcs.Task; 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/EndToEndTests.cs: -------------------------------------------------------------------------------- 1 | using HttpOverStream.Client; 2 | using HttpOverStream.NamedPipe; 3 | using Microsoft.Owin.Logging; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Owin; 6 | using System; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Net; 10 | using System.Net.Http; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using System.Web.Http; 15 | 16 | namespace HttpOverStream.Server.Owin.Tests 17 | { 18 | [RoutePrefix("api/e2e-tests")] 19 | public class EndToEndApiController : ApiController 20 | { 21 | [Route("hello-world")] 22 | [HttpGet()] 23 | public string GetHelloWorld() 24 | { 25 | return "Hello World"; 26 | } 27 | 28 | [Route("hello")] 29 | [HttpPost()] 30 | public WelcomeMessage PostHello([FromBody] PersonMessage person) 31 | { 32 | return new WelcomeMessage { Text = $"Hello {person.Name}" }; 33 | } 34 | 35 | [Route("timeout")] 36 | [HttpGet()] 37 | public async Task GetTimeoutAsync() 38 | { 39 | await Task.Delay(TimeSpan.FromSeconds(5)); 40 | return "This should have timed out."; 41 | } 42 | } 43 | 44 | public class PersonMessage 45 | { 46 | public string Name { get; set; } 47 | } 48 | 49 | public class WelcomeMessage 50 | { 51 | public string Text { get; set; } 52 | } 53 | 54 | [TestClass] 55 | public class EndToEndTests 56 | { 57 | public TestContext TestContext { get; set; } 58 | 59 | [TestMethod] 60 | public async Task TestGet_Http10() 61 | { 62 | await TestGet_Impl(1, HttpVersion.Version10); 63 | } 64 | [TestMethod] 65 | public async Task TestGet_Http11() 66 | { 67 | await TestGet_Impl(1, HttpVersion.Version11); 68 | } 69 | 70 | [TestMethod] 71 | public async Task TestGetStressTest() 72 | { 73 | for (int i = 0; i < 100; i++) 74 | { 75 | await TestGet_Impl(); 76 | GC.Collect(); 77 | GC.WaitForPendingFinalizers(); 78 | 79 | if (Debug.Listeners.Count > 1) 80 | { 81 | Debug.Listeners.RemoveAt(1); 82 | } 83 | } 84 | } 85 | 86 | [TestMethod] 87 | public async Task TestGetStressTest_SingleServer() 88 | { 89 | for (int i = 0; i < 5; i++) 90 | { 91 | await TestGet_Impl(100); 92 | GC.Collect(); 93 | GC.WaitForPendingFinalizers(); 94 | 95 | if (Debug.Listeners.Count > 1) 96 | { 97 | Debug.Listeners.RemoveAt(1); 98 | } 99 | } 100 | } 101 | 102 | private async Task TestGet_Impl(int numberOfRequests = 1, Version httpVersion = null) 103 | { 104 | using (CustomListenerHost.Start(SetupDefaultAppBuilder, new NamedPipeListener(TestContext.TestName))) 105 | { 106 | for (int i = 0; i < numberOfRequests; i++) 107 | { 108 | var client = new NamedPipeHttpClientBuilder(TestContext.TestName) 109 | .WithPerRequestTimeout(TimeSpan.FromSeconds(5)) 110 | .WithHttpVersion(httpVersion) 111 | .WithDelegatingHandler(new TestLoggingHandler()) 112 | .Build(); 113 | var result = await client.GetAsync("http://localhost/api/e2e-tests/hello-world"); 114 | Assert.AreEqual("Hello World", await result.Content.ReadAsAsync()); 115 | } 116 | } 117 | } 118 | 119 | [TestMethod] 120 | public async Task TestGetStressTest_ClientBeforeServer() 121 | { 122 | for (int i = 0; i < 5; i++) 123 | { 124 | await TestClientStartsFirst_ServerDropsClientButComesUpAfterwards_Impl(100); 125 | GC.Collect(); 126 | GC.WaitForPendingFinalizers(); 127 | 128 | if (Debug.Listeners.Count > 1) 129 | { 130 | Debug.Listeners.RemoveAt(1); 131 | } 132 | } 133 | } 134 | 135 | private async Task TestClientStartsFirst_ServerDropsClientButComesUpAfterwards_Impl(int numberOfRequests) 136 | { 137 | var client = new HttpClient(new DialMessageHandler(new NamedPipeDialer(TestContext.TestName, (int)TimeSpan.FromSeconds(30).TotalMilliseconds))); 138 | client.Timeout = TimeSpan.FromSeconds(30); 139 | var clientTask = client.GetAsync("http://localhost/api/e2e-tests/hello-world"); 140 | 141 | using (CustomListenerHost.Start(SetupDefaultAppBuilder, new NamedPipeListener(TestContext.TestName))) 142 | { 143 | for (int i = 0; i < numberOfRequests; i++) 144 | { 145 | var client2 = new NamedPipeHttpClientBuilder(TestContext.TestName) 146 | .WithPerRequestTimeout(TimeSpan.FromSeconds(5)) 147 | .Build(); 148 | var result = await client2.GetAsync("http://localhost/api/e2e-tests/hello-world"); 149 | Assert.AreEqual("Hello World", await result.Content.ReadAsAsync()); 150 | } 151 | } 152 | } 153 | 154 | [TestMethod] 155 | public async Task TestBadRequestStressTest() 156 | { 157 | for (int i = 0; i < 5; i++) 158 | { 159 | await TestBadRequest_BadMediaType_Impl(); 160 | } 161 | } 162 | 163 | private async Task TestBadRequest_BadMediaType_Impl() 164 | { 165 | using (CustomListenerHost.Start(SetupDefaultAppBuilder, new NamedPipeListener(TestContext.TestName))) 166 | { 167 | var client = new NamedPipeHttpClientBuilder(TestContext.TestName) 168 | .WithPerRequestTimeout(TimeSpan.FromSeconds(1)) 169 | .Build(); 170 | var badContent = new StringContent("{ ", Encoding.UTF8, "application/broken"); 171 | var result = await client.PostAsJsonAsync("http://localhost/api/e2e-tests/hello", badContent); 172 | var wlcMsg = await result.Content.ReadAsAsync(); 173 | Assert.AreEqual("Hello ", wlcMsg.Text); 174 | } 175 | } 176 | 177 | [TestMethod] 178 | public async Task TestBadRequest_NonexistentEndpoint_StressTest() 179 | { 180 | for (int i = 0; i < 5; i++) 181 | { 182 | Debug.WriteLine(i); 183 | await TestBadRequest_NonexistentEndpoint_Impl(); 184 | } 185 | } 186 | 187 | [TestMethod] 188 | public async Task TestBadRequest_NonexistentEndpoint() 189 | { 190 | await TestBadRequest_NonexistentEndpoint_Impl(); 191 | } 192 | private async Task TestBadRequest_NonexistentEndpoint_Impl() 193 | { 194 | using (CustomListenerHost.Start(SetupDefaultAppBuilder, new NamedPipeListener(TestContext.TestName))) 195 | { 196 | var client = new NamedPipeHttpClientBuilder(TestContext.TestName).Build(); 197 | try 198 | { 199 | var badContent = new StringContent("{ }"); 200 | var result = await client.PostAsJsonAsync("http://localhost/api/this-doesnt-exist", badContent); 201 | Debug.WriteLine("Client: Posted Json "); 202 | 203 | Assert.AreEqual(result.StatusCode, System.Net.HttpStatusCode.NotFound); 204 | } 205 | catch(Exception e) 206 | { 207 | Debug.WriteLine("EXCEPTION " + e); 208 | throw; 209 | } 210 | } 211 | } 212 | 213 | [TestMethod] 214 | public async Task TestBodyStream() 215 | { 216 | await TestBodyStream_Impl(); 217 | } 218 | 219 | [TestMethod] 220 | public async Task TestBodyStreamStressTest() 221 | { 222 | for (int i = 0; i < 50; i++) 223 | { 224 | await TestBodyStream_Impl(); 225 | } 226 | } 227 | 228 | private async Task TestBodyStream_Impl() 229 | { 230 | var listener = new NamedPipeListener(TestContext.TestName); 231 | var payload = Encoding.UTF8.GetBytes("Hello world"); 232 | await listener.StartAsync(con => 233 | { 234 | Task.Run(async () => 235 | { 236 | try 237 | { 238 | await con.WriteAsync(payload, 0, payload.Length); 239 | } 240 | catch (Exception e) 241 | { 242 | Debug.WriteLine("... WriteAsync exception:" + e.Message); 243 | } 244 | }); 245 | }, CancellationToken.None); 246 | 247 | 248 | var dialer = new NamedPipeDialer(TestContext.TestName); 249 | var stream = await dialer.DialAsync(new HttpRequestMessage(), CancellationToken.None); 250 | var bodyStream = new BodyStream(stream, payload.Length); 251 | var data = new byte[4096]; 252 | var read = await bodyStream.ReadAsync(data, 0, data.Length); 253 | Assert.AreEqual(payload.Length, read); 254 | read = await bodyStream.ReadAsync(data, 0, data.Length); 255 | Assert.AreEqual(0, read); 256 | 257 | // Clean up 258 | await listener.StopAsync(CancellationToken.None); 259 | } 260 | 261 | [TestMethod] 262 | public async Task TestPost() 263 | { 264 | await TestPost_Impl(); 265 | } 266 | 267 | [TestMethod] 268 | public async Task TestPostStressTest() 269 | { 270 | for (int i = 0; i < 50; i++) 271 | { 272 | await TestPost_Impl(); 273 | } 274 | } 275 | 276 | private async Task TestPost_Impl() 277 | { 278 | using (CustomListenerHost.Start(SetupDefaultAppBuilder, new NamedPipeListener(TestContext.TestName))) 279 | { 280 | var client = new NamedPipeHttpClientBuilder(TestContext.TestName).Build(); 281 | var result = await client.PostAsJsonAsync("http://localhost/api/e2e-tests/hello", new PersonMessage { Name = "Test" }); 282 | var wlcMsg = await result.Content.ReadAsAsync(); 283 | Assert.AreEqual("Hello Test", wlcMsg.Text); 284 | } 285 | } 286 | 287 | [TestMethod] 288 | public async Task TestPost_WhenNoServerListening_ThrowsTimeoutException() 289 | { 290 | var client = new NamedPipeHttpClientBuilder(TestContext.TestName).Build(); 291 | await Assert.ThrowsExceptionAsync(async () => await client.PostAsJsonAsync("http://localhost/api/e2e-tests/hello", new PersonMessage { Name = "Test" })); 292 | } 293 | 294 | [TestMethod] 295 | public async Task TestStreamInteruption() 296 | { 297 | await TestStreamInterruption_Impl(); 298 | } 299 | 300 | [TestMethod] 301 | public async Task TestStreamInteruptionStressTest() 302 | { 303 | for (int i = 0; i < 50; i++) 304 | { 305 | await TestStreamInterruption_Impl(); 306 | } 307 | } 308 | 309 | private async Task TestStreamInterruption_Impl() 310 | { 311 | var logFactory = new TestLoggerFactory(); 312 | using (CustomListenerHost.Start(app => 313 | { 314 | SetupDefaultAppBuilder(app); 315 | app.SetLoggerFactory(logFactory); 316 | }, new NamedPipeListener(TestContext.TestName))) 317 | { 318 | var dialer = new NamedPipeDialer(TestContext.TestName); 319 | using (var fuzzyStream = await dialer.DialAsync(new HttpRequestMessage(), CancellationToken.None)) 320 | { 321 | // just write the first line of a valid http request, and drop the connection 322 | var payload = Encoding.ASCII.GetBytes("GET /docs/index.html HTTP/1.0\n"); 323 | await fuzzyStream.WriteAsync(payload, 0, payload.Length); 324 | } 325 | var ex = await logFactory.ExceptionReceived.Task; 326 | Assert.IsInstanceOfType(ex, typeof(EndOfStreamException)); 327 | 328 | // Test the stream still works afterwards 329 | var client = new HttpClient(new DialMessageHandler(dialer)); 330 | var result = await client.PostAsJsonAsync("http://localhost/api/e2e-tests/hello", new PersonMessage { Name = "Test" }); 331 | var wlcMsg = await result.Content.ReadAsAsync(); 332 | Assert.AreEqual("Hello Test", wlcMsg.Text); 333 | } 334 | } 335 | 336 | [TestMethod] 337 | public async Task TestClientTimeoutIsRespectedWhenServerTakesTooLong() 338 | { 339 | using (CustomListenerHost.Start(SetupDefaultAppBuilder, new NamedPipeListener(TestContext.TestName))) 340 | { 341 | var client = new NamedPipeHttpClientBuilder(TestContext.TestName) 342 | .WithPerRequestTimeout(TimeSpan.FromMilliseconds(100)) 343 | .Build(); 344 | var sw = Stopwatch.StartNew(); 345 | await Assert.ThrowsExceptionAsync(async () => 346 | { 347 | await client.GetAsync("http://localhost/api/e2e-tests/timeout"); 348 | }); 349 | sw.Stop(); 350 | Assert.IsTrue(sw.ElapsedMilliseconds < 1000, $"Client Timeout wasn't respected - GetAsync took too long ({sw.ElapsedMilliseconds} ms)"); 351 | } 352 | } 353 | 354 | private static void SetupDefaultAppBuilder(IAppBuilder appBuilder) 355 | { 356 | HttpConfiguration config = new HttpConfiguration(); 357 | config.MapHttpAttributeRoutes(); 358 | config.SuppressDefaultHostAuthentication(); 359 | config.SuppressHostPrincipal(); 360 | appBuilder.UseWebApi(config); 361 | } 362 | 363 | class TestLoggerFactory : ILoggerFactory, ILogger 364 | { 365 | public TaskCompletionSource ExceptionReceived { get; } = new TaskCompletionSource(); 366 | public ILogger Create(string name) => this; 367 | 368 | public bool WriteCore(TraceEventType eventType, int eventId, object state, Exception exception, Func formatter) 369 | { 370 | if (exception != null) 371 | { 372 | ExceptionReceived.TrySetResult(exception); 373 | } 374 | return true; 375 | } 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/HttpOverStream.Server.Owin.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | false 7 | true 8 | Debug 9 | AnyCPU 10 | {96388CE9-B97A-4754-A3B3-C898CD195969} 11 | Library 12 | Properties 13 | HttpOverStream.Server.Owin.Tests 14 | HttpOverStream.Server.Owin.Tests 15 | v4.6.2 16 | 512 17 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 18 | 15.0 19 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 20 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 21 | False 22 | UnitTest 23 | 24 | 25 | 26 | 27 | 28 | true 29 | full 30 | false 31 | bin\Debug\ 32 | DEBUG;TRACE 33 | prompt 34 | 4 35 | 36 | 37 | pdbonly 38 | true 39 | bin\Release\ 40 | TRACE 41 | prompt 42 | 4 43 | 44 | 45 | 46 | ..\packages\Microsoft.Owin.4.0.1\lib\net45\Microsoft.Owin.dll 47 | 48 | 49 | ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll 50 | 51 | 52 | ..\packages\MSTest.TestFramework.1.4.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll 53 | 54 | 55 | ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll 56 | 57 | 58 | ..\packages\Owin.1.0\lib\net40\Owin.dll 59 | 60 | 61 | 62 | 63 | 64 | ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll 65 | 66 | 67 | 68 | ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 69 | 70 | 71 | ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll 72 | 73 | 74 | ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll 75 | 76 | 77 | ..\packages\Microsoft.AspNet.WebApi.Core.5.2.6\lib\net45\System.Web.Http.dll 78 | 79 | 80 | ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.6\lib\net45\System.Web.Http.Owin.dll 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {03090A81-4647-47BC-93BE-49D36484C94C} 96 | HttpOverStream.NamedPipe 97 | 98 | 99 | {8e94720f-04f5-4734-ac2d-e14d208af59e} 100 | HttpOverStream 101 | 102 | 103 | {fd513277-f032-4106-abcd-9719ada8f3db} 104 | HttpOverStream.Client 105 | 106 | 107 | {008edf79-f050-49c8-bf5f-25842466369b} 108 | HttpOverStream.Server.Owin 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/HttpOverStream.Server.Owin.Tests.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | true 5 | 6 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/OnceTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace HttpOverStream.Server.Owin.Tests 10 | { 11 | 12 | [TestClass] 13 | public class OnceTests 14 | { 15 | [TestMethod] 16 | public void TestOnceCallsOnlyOnce() 17 | { 18 | int value = 0; 19 | var once = new Once(() => Interlocked.Increment(ref value)); 20 | Parallel.For(0, 1000, _ => 21 | { 22 | once.EnsureDone(); 23 | }); 24 | Assert.AreEqual(1, value); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("HttpOverStream.WebApiTest")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("HttpOverStream.WebApiTest")] 10 | [assembly: AssemblyCopyright("Copyright © 2018")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("96388ce9-b97a-4754-a3b3-c898cd195969")] 17 | 18 | // [assembly: AssemblyVersion("1.0.*")] 19 | [assembly: AssemblyVersion("1.0.0.0")] 20 | [assembly: AssemblyFileVersion("1.0.0.0")] 21 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/TestLoggingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace HttpOverStream.Server.Owin.Tests 8 | { 9 | public class TestLoggingHandler : DelegatingHandler 10 | { 11 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 12 | { 13 | var correlationId = Guid.NewGuid(); 14 | LogRequest(request, correlationId); 15 | var stopwatch = Stopwatch.StartNew(); 16 | var response = await base.SendAsync(request, cancellationToken); 17 | stopwatch.Stop(); 18 | LogResponse(request, response, stopwatch.Elapsed, correlationId); 19 | return response; 20 | } 21 | 22 | private void LogRequest(HttpRequestMessage request, Guid correlationId) 23 | { 24 | if (request == null) 25 | { 26 | Console.WriteLine("Null request"); 27 | return; 28 | } 29 | 30 | Console.WriteLine($"[{correlationId}] {request.Method?.Method} {request.RequestUri}"); 31 | } 32 | 33 | private void LogResponse(HttpRequestMessage request, HttpResponseMessage response, TimeSpan stopwatchElapsed, Guid correlationId) 34 | { 35 | Console.WriteLine($"[{correlationId}] {request?.Method?.Method} {request?.RequestUri} -> {(int)response.StatusCode} {response.StatusCode} took {(int)stopwatchElapsed.TotalMilliseconds}ms"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/CustomListenerHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Owin; 2 | using Microsoft.Owin.Logging; 3 | using Microsoft.Owin.Hosting; 4 | using Microsoft.Owin.Hosting.Engine; 5 | using Microsoft.Owin.Hosting.ServerFactory; 6 | using Microsoft.Owin.Hosting.Services; 7 | using Owin; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using System.Text; 15 | 16 | namespace HttpOverStream.Server.Owin 17 | { 18 | public class CustomListenerHost : IDisposable 19 | { 20 | private readonly IListen _listener; 21 | private readonly Func, Task> _app; 22 | private readonly ILogger _logger; 23 | private bool _disposed; 24 | static readonly byte[] _eol = Encoding.ASCII.GetBytes("\n"); 25 | 26 | private CustomListenerHost(IListen listener, Func, Task> app, IAppBuilder builder) 27 | { 28 | _listener = listener; 29 | _app = app; 30 | _logger = builder.CreateLogger("HttpOS.Owin.CLH"); 31 | } 32 | 33 | 34 | private void Start() 35 | { 36 | _listener.StartAsync(OnAccept, CancellationToken.None).Wait(); 37 | } 38 | 39 | private async void OnAccept(Stream stream) 40 | { 41 | try 42 | { 43 | using (stream) 44 | { 45 | var onSendingHeadersCallbacks = new List<(Action, object)>(); 46 | var owinContext = new OwinContext(); 47 | // this is an owin extension 48 | owinContext.Set, object>>("server.OnSendingHeaders", (callback, state) => 49 | { 50 | onSendingHeadersCallbacks.Add((callback, state)); 51 | }); 52 | owinContext.Set("owin.Version", "1.0"); 53 | 54 | _logger.WriteVerbose("Server: reading message.."); 55 | await PopulateRequestAsync(stream, owinContext.Request, CancellationToken.None).ConfigureAwait(false); 56 | 57 | // some transports (shuch as Named Pipes) may require the server to read some data before being able to get the 58 | // client identity. So we get the client identity after reading the request headers 59 | var transportIdentity = _listener.GetTransportIdentity(stream); 60 | owinContext.Request.User = transportIdentity; 61 | 62 | _logger.WriteVerbose("Server: finished reading message"); 63 | Func sendHeadersAsync = async () => 64 | { 65 | // notify we are sending headers 66 | foreach ((var callback, var state) in onSendingHeadersCallbacks) 67 | { 68 | callback(state); 69 | } 70 | // send status and headers 71 | string statusCode = owinContext.Response.StatusCode.ToString(); 72 | _logger.WriteVerbose("Server: Statuscode was " + statusCode); 73 | await stream.WriteServerResponseStatusAndHeadersAsync(owinContext.Request.Protocol, statusCode, owinContext.Response.ReasonPhrase, owinContext.Response.Headers.Select(i => new KeyValuePair>(i.Key, i.Value)), _logger.WriteVerbose, CancellationToken.None).ConfigureAwait(false); 74 | _logger.WriteVerbose("Server: Wrote status and headers."); 75 | await stream.FlushAsync().ConfigureAwait(false); 76 | }; 77 | var body = new WriteInterceptStream(stream, sendHeadersAsync); 78 | owinContext.Response.Body = body; 79 | // execute higher level middleware 80 | _logger.WriteVerbose("Server: executing middleware.."); 81 | try 82 | { 83 | await _app(owinContext.Environment).ConfigureAwait(false); 84 | } 85 | catch (Exception e) 86 | { 87 | await HandleMiddlewareException(e, owinContext, body); 88 | } 89 | _logger.WriteVerbose("Server: finished executing middleware.."); 90 | await body.FlushAsync().ConfigureAwait(false); 91 | _logger.WriteVerbose("Server: Finished request. Disposing connection."); 92 | } 93 | } 94 | catch (EndOfStreamException e) 95 | { 96 | _logger.WriteWarning("Server: Error handling client stream, (Client disconnected early / invalid HTTP request)", e); 97 | } 98 | catch (Exception e) 99 | { 100 | _logger.WriteError("Server: Error handling client stream " + e); 101 | } 102 | } 103 | 104 | private async Task HandleMiddlewareException(Exception e, OwinContext owinContext, WriteInterceptStream body) 105 | { 106 | var logMessage = "Exception trying to execute middleware: " + e; 107 | _logger.WriteError(logMessage); 108 | 109 | owinContext.Response.StatusCode = 500; 110 | var payload = Encoding.ASCII.GetBytes("Exception trying to execute middleware. See logs for details."); 111 | await body.WriteAsync(payload, 0, payload.Length).ConfigureAwait(false); 112 | await body.WriteAsync(_eol, 0, _eol.Length).ConfigureAwait(false); 113 | } 114 | 115 | static Uri _localhostUri = new Uri("http://localhost/"); 116 | 117 | private async Task PopulateRequestAsync(Stream stream, IOwinRequest request, CancellationToken cancellationToken) 118 | { 119 | var firstLine = await stream.ReadLineAsync(cancellationToken).ConfigureAwait(false); 120 | var parts = firstLine.Split(' '); 121 | if (parts.Length < 3) 122 | { 123 | throw new FormatException($"{firstLine} is not a valid request status"); 124 | } 125 | 126 | _logger.WriteVerbose("Incoming request:" + firstLine); 127 | request.Method = parts[0]; 128 | request.Protocol = parts[2]; 129 | var uri = new Uri(parts[1], UriKind.RelativeOrAbsolute); 130 | if (!uri.IsAbsoluteUri) 131 | { 132 | uri = new Uri(_localhostUri, uri); 133 | } 134 | for (; ; ) 135 | { 136 | var line = await stream.ReadLineAsync(cancellationToken).ConfigureAwait(false); 137 | if (line.Length == 0) 138 | { 139 | break; 140 | } 141 | _logger.WriteVerbose("Incoming header:" + line); 142 | (var name, var values) = HttpParser.ParseHeaderNameValues(line); 143 | request.Headers.Add(name, values.ToArray()); 144 | } 145 | request.Scheme = uri.Scheme; 146 | request.Path = PathString.FromUriComponent(uri); 147 | request.QueryString = QueryString.FromUriComponent(uri); 148 | 149 | long? length = null; 150 | 151 | var contentLengthValues = request.Headers.GetValues("Content-Length"); 152 | if (contentLengthValues != null && contentLengthValues.Count > 0) 153 | { 154 | length = long.Parse(contentLengthValues[0]); 155 | } 156 | request.Body = new BodyStream(stream, length); 157 | } 158 | 159 | public void Dispose() 160 | { 161 | if (_disposed) 162 | { 163 | return; 164 | } 165 | _disposed = true; 166 | _listener.StopAsync(CancellationToken.None).Wait(); 167 | } 168 | 169 | public static IDisposable Start(IListen listener) 170 | { 171 | var options = new StartOptions(); 172 | options.AppStartup = typeof(TStartup).AssemblyQualifiedName; 173 | var context = new StartContext(options); 174 | return Start(context, listener); 175 | } 176 | 177 | public static IDisposable Start(Action startup, IListen listener) 178 | { 179 | var options = new StartOptions(); 180 | options.AppStartup = startup.Method.ReflectedType.FullName; 181 | var context = new StartContext(options); 182 | context.Startup = startup; 183 | return Start(context, listener); 184 | } 185 | 186 | private static IDisposable Start(StartContext context, IListen listener) 187 | { 188 | IServiceProvider services = ServicesFactory.Create(); 189 | var engine = services.GetService(); 190 | context.ServerFactory = new ServerFactoryAdapter(new CustomListenerHostFactory(listener)); 191 | return engine.Start(context); 192 | } 193 | 194 | private class CustomListenerHostFactory 195 | { 196 | private readonly IListen _listener; 197 | private IAppBuilder _builder; 198 | 199 | public CustomListenerHostFactory(IListen listener) 200 | { 201 | _listener = listener; 202 | } 203 | 204 | public IDisposable Create(Func, Task> app, IDictionary properties) 205 | { 206 | var host = new CustomListenerHost(_listener, app, _builder); 207 | host.Start(); 208 | return host; 209 | } 210 | 211 | public void Initialize(IAppBuilder builder) 212 | { 213 | _builder = builder; 214 | } 215 | } 216 | 217 | private class WriteInterceptStream : Stream 218 | { 219 | private readonly Stream _innerStream; 220 | private readonly Once _onFirstWrite; 221 | 222 | public WriteInterceptStream(Stream innerStream, Func onFirstWrite) 223 | { 224 | _innerStream = innerStream; 225 | _onFirstWrite = new Once(onFirstWrite); 226 | } 227 | 228 | public override void Flush() 229 | { 230 | _onFirstWrite.EnsureDone().Wait(); 231 | _innerStream.Flush(); 232 | } 233 | public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); 234 | public override void SetLength(long value) => _innerStream.SetLength(value); 235 | public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException(); 236 | public override void Write(byte[] buffer, int offset, int count) 237 | { 238 | _onFirstWrite.EnsureDone().Wait(); 239 | _innerStream.Write(buffer, offset, count); 240 | } 241 | 242 | public override bool CanRead => false; 243 | public override bool CanSeek => _innerStream.CanSeek; 244 | public override bool CanWrite => _innerStream.CanWrite; 245 | public override long Length => _innerStream.Length; 246 | public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } 247 | 248 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 249 | { 250 | var task = WriteAsync(buffer, offset, count); 251 | var tcs = new TaskCompletionSource(state); 252 | task.ContinueWith(t => 253 | { 254 | if (t.IsFaulted) 255 | tcs.TrySetException(t.Exception.InnerExceptions); 256 | else if (t.IsCanceled) 257 | tcs.TrySetCanceled(); 258 | else 259 | tcs.TrySetResult(0); 260 | 261 | callback?.Invoke(tcs.Task); 262 | }, TaskScheduler.Default); 263 | return tcs.Task; 264 | } 265 | public override void EndWrite(IAsyncResult asyncResult) 266 | { 267 | ((Task)asyncResult).Wait(); 268 | } 269 | public override Task FlushAsync(CancellationToken cancellationToken) 270 | { 271 | return _onFirstWrite 272 | .EnsureDone() 273 | .ContinueWith(previous => _innerStream.FlushAsync(cancellationToken), cancellationToken) 274 | .Unwrap(); 275 | } 276 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 277 | { 278 | return _onFirstWrite 279 | .EnsureDone() 280 | .ContinueWith(previous => _innerStream.WriteAsync(buffer, offset, count, cancellationToken), cancellationToken) 281 | .Unwrap(); 282 | } 283 | public override void WriteByte(byte value) 284 | { 285 | _onFirstWrite.EnsureDone().Wait(); 286 | _innerStream.WriteByte(value); 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/HttpOverStream.Server.Owin.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net461 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/HttpOverStream.Server.Owin.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectFiles 5 | 6 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/HttpOverStream.Server.Owin.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HttpOverStream.Server.Owin 5 | 1.0.0 6 | HttpOverStream.Server.Owin 7 | HttpOverStream.Server.Owin 8 | HttpOverStream.Server.Owin 9 | false 10 | Package Description 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/Once.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace HttpOverStream.Server.Owin 5 | { 6 | public class Once 7 | { 8 | private T _result; 9 | private Func _todo; 10 | public Once(Func todo) 11 | { 12 | _todo = todo; 13 | } 14 | 15 | public T EnsureDone() 16 | { 17 | var todo = Interlocked.Exchange(ref _todo, null); 18 | if (todo != null) 19 | { 20 | _result = todo(); 21 | } 22 | return _result; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/HttpOverStream.Server.Owin/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/HttpOverStream.Tests/BodyStreamTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace HttpOverStream.Tests 9 | { 10 | public class BodyStreamTests 11 | { 12 | class TestStream : MemoryStream 13 | { 14 | public bool Disposed { get; private set; } 15 | public TestStream(byte[] data) : base(data) { } 16 | protected override void Dispose(bool disposing) 17 | { 18 | base.Dispose(disposing); 19 | Disposed = true; 20 | } 21 | 22 | } 23 | [Fact] 24 | public async Task TestBodyStreamCloseOnReadEnd() 25 | { 26 | byte[] payload = new byte[10]; 27 | var ms = new TestStream(payload); 28 | var bodyStream = new BodyStream(ms, 10, closeOnReachEnd: true); 29 | var result = new byte[10]; 30 | var read = await bodyStream.ReadAsync(result, 0, 10); 31 | Assert.Equal(10, read); 32 | Assert.True(ms.Disposed); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/HttpOverStream.Tests/ByLineReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace HttpOverStream.Tests 8 | { 9 | public class ByLineReaderTests 10 | { 11 | [Fact] 12 | public async Task TestWithCrLf() 13 | { 14 | var ms = new MemoryStream(); 15 | ms.Write(Encoding.UTF8.GetBytes("first line\r\nsecond longer line\r\nthird line\r\nhello\r\n\r\n")); 16 | ms.Flush(); 17 | ms.Position = 0; 18 | Assert.Equal("first line", await ms.ReadLineAsync(CancellationToken.None)); 19 | Assert.Equal("second longer line", await ms.ReadLineAsync(CancellationToken.None)); 20 | Assert.Equal("third line", await ms.ReadLineAsync(CancellationToken.None)); 21 | Assert.Equal("hello", await ms.ReadLineAsync(CancellationToken.None)); 22 | Assert.Equal("", await ms.ReadLineAsync(CancellationToken.None)); 23 | } 24 | [Fact] 25 | public async Task TestWithLf() 26 | { 27 | var ms = new MemoryStream(); 28 | ms.Write(Encoding.UTF8.GetBytes("first line\nsecond longer line\nthird line\nhello\n\n")); 29 | ms.Flush(); 30 | ms.Position = 0; 31 | Assert.Equal("first line", await ms.ReadLineAsync(CancellationToken.None)); 32 | Assert.Equal("second longer line", await ms.ReadLineAsync(CancellationToken.None)); 33 | Assert.Equal("third line", await ms.ReadLineAsync(CancellationToken.None)); 34 | Assert.Equal("hello", await ms.ReadLineAsync(CancellationToken.None)); 35 | Assert.Equal("", await ms.ReadLineAsync(CancellationToken.None)); 36 | } 37 | 38 | [Fact] 39 | public async Task TestMalformedHeaders() 40 | { 41 | var ms = new MemoryStream(); 42 | ms.Write(Encoding.UTF8.GetBytes("aaa\nsecond longer line\n")); 43 | ms.Flush(); 44 | ms.Position = 0; 45 | Assert.Equal("aaa", await ms.ReadLineAsync(CancellationToken.None)); 46 | Assert.Equal("second longer line", await ms.ReadLineAsync(CancellationToken.None)); 47 | await Assert.ThrowsAsync(async ()=> await ms.ReadLineAsync(CancellationToken.None)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/HttpOverStream.Tests/HttpOverStream.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/HttpOverStream.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29519.181 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpOverStream", "HttpOverStream\HttpOverStream.csproj", "{8E94720F-04F5-4734-AC2D-E14D208AF59E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpOverStream.Tests", "HttpOverStream.Tests\HttpOverStream.Tests.csproj", "{E4DF4D76-1729-427C-A362-EF4E122C7F2E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpOverStream.Client", "HttpOverStream.Client\HttpOverStream.Client.csproj", "{FD513277-F032-4106-ABCD-9719ADA8F3DB}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpOverStream.Server.Owin", "HttpOverStream.Server.Owin\HttpOverStream.Server.Owin.csproj", "{008EDF79-F050-49C8-BF5F-25842466369B}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpOverStream.Server.Owin.Tests", "HttpOverStream.Server.Owin.Tests\HttpOverStream.Server.Owin.Tests.csproj", "{96388CE9-B97A-4754-A3B3-C898CD195969}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpOverStream.NamedPipe", "HttpOverStream.NamedPipe\HttpOverStream.NamedPipe.csproj", "{03090A81-4647-47BC-93BE-49D36484C94C}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {8E94720F-04F5-4734-AC2D-E14D208AF59E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {8E94720F-04F5-4734-AC2D-E14D208AF59E}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {8E94720F-04F5-4734-AC2D-E14D208AF59E}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {8E94720F-04F5-4734-AC2D-E14D208AF59E}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {E4DF4D76-1729-427C-A362-EF4E122C7F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {E4DF4D76-1729-427C-A362-EF4E122C7F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {E4DF4D76-1729-427C-A362-EF4E122C7F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {E4DF4D76-1729-427C-A362-EF4E122C7F2E}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {FD513277-F032-4106-ABCD-9719ADA8F3DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {FD513277-F032-4106-ABCD-9719ADA8F3DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {FD513277-F032-4106-ABCD-9719ADA8F3DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {FD513277-F032-4106-ABCD-9719ADA8F3DB}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {008EDF79-F050-49C8-BF5F-25842466369B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {008EDF79-F050-49C8-BF5F-25842466369B}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {008EDF79-F050-49C8-BF5F-25842466369B}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {008EDF79-F050-49C8-BF5F-25842466369B}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {96388CE9-B97A-4754-A3B3-C898CD195969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {96388CE9-B97A-4754-A3B3-C898CD195969}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {96388CE9-B97A-4754-A3B3-C898CD195969}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {96388CE9-B97A-4754-A3B3-C898CD195969}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {03090A81-4647-47BC-93BE-49D36484C94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {03090A81-4647-47BC-93BE-49D36484C94C}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {03090A81-4647-47BC-93BE-49D36484C94C}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {03090A81-4647-47BC-93BE-49D36484C94C}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {21FDAFB7-9661-403C-A19D-36403118786F} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /src/HttpOverStream/BodyStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace HttpOverStream 7 | { 8 | public class BodyStream : Stream 9 | { 10 | long? _length; 11 | long _read; 12 | bool _closeOnReachEnd; 13 | public Stream Underlying { get; private set; } 14 | 15 | public BodyStream(Stream underlying, long? length, bool closeOnReachEnd = false) 16 | { 17 | Underlying = underlying; 18 | _length = length; 19 | _closeOnReachEnd = closeOnReachEnd; 20 | } 21 | 22 | private void CloseIfReachedEnd() 23 | { 24 | if (!_closeOnReachEnd || !_length.HasValue) 25 | { 26 | return; 27 | } 28 | if (_read >= _length.Value) 29 | { 30 | Close(); 31 | } 32 | } 33 | 34 | private int BoundedCount(int count) 35 | { 36 | if (!_length.HasValue) 37 | { 38 | return count; 39 | } 40 | return (int)Math.Min((long)count, _length.Value - _read); 41 | } 42 | 43 | 44 | public override void Flush() => throw new NotSupportedException(); 45 | public override int Read(byte[] buffer, int offset, int count) 46 | { 47 | count = BoundedCount(count); 48 | if (count == 0) 49 | { 50 | return 0; 51 | } 52 | var read = Underlying.Read(buffer, offset, count); 53 | _read += read; 54 | CloseIfReachedEnd(); 55 | return read; 56 | } 57 | public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); 58 | public override void SetLength(long value) => throw new NotSupportedException(); 59 | public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); 60 | 61 | public override bool CanRead => true; 62 | public override bool CanSeek => false; 63 | public override bool CanWrite => false; 64 | public override long Length 65 | { 66 | get 67 | { 68 | if (!_length.HasValue) 69 | { 70 | return Underlying.Length; 71 | } 72 | return _length.Value; 73 | } 74 | } 75 | public override long Position 76 | { 77 | get => throw new InvalidOperationException("Cannot seek"); 78 | set => throw new InvalidOperationException("Cannot seek"); 79 | } 80 | 81 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 82 | { 83 | count = BoundedCount(count); 84 | if (count == 0) 85 | { 86 | return 0; 87 | } 88 | var read = await Underlying.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); 89 | _read += read; 90 | CloseIfReachedEnd(); 91 | return read; 92 | } 93 | public override void Close() => Underlying.Close(); 94 | protected override void Dispose(bool disposing) { 95 | base.Dispose(disposing); 96 | if (disposing) 97 | { 98 | Underlying.Dispose(); 99 | } 100 | } 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/HttpOverStream/ByLineReader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.IO.Pipes; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace HttpOverStream 9 | { 10 | public static class ByLineReader 11 | { 12 | public static async ValueTask ReadLineAsync(this Stream stream, CancellationToken cancellationToken) 13 | { 14 | var bytes = new List(); 15 | var buffer = new byte[1]; 16 | const byte lineSeparator = (byte)'\n'; 17 | while(true) 18 | { 19 | var read = await stream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); 20 | if (read == 0) 21 | { 22 | if (cancellationToken.IsCancellationRequested) 23 | { 24 | throw new TaskCanceledException("Cancellation token triggered before we finished reading from the stream."); 25 | } 26 | // EOF -> throw 27 | throw new EndOfStreamException("Reached end of stream before finding a line seperator"); 28 | } 29 | if (buffer[0] == lineSeparator) 30 | { 31 | break; 32 | } 33 | bytes.Add(buffer[0]); 34 | } 35 | // handle \r\n eol markers 36 | const byte carriageReturn = (byte)'\r'; 37 | if (bytes.Count > 0 && bytes[bytes.Count - 1] == carriageReturn) 38 | { 39 | bytes.RemoveAt(bytes.Count - 1); 40 | } 41 | return Encoding.ASCII.GetString(bytes.ToArray()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/HttpOverStream/HttpHeaderWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace HttpOverStream 11 | { 12 | public static class HttpHeaderWriter 13 | { 14 | static byte[] _eol = Encoding.ASCII.GetBytes("\n"); 15 | public static async Task WriteServerResponseStatusAndHeadersAsync(this Stream stream, string protocol, 16 | string statusCode, string reasonPhrase, IEnumerable>> headers, 17 | Action log, CancellationToken cancellationToken) 18 | { 19 | var statusLine = $"{protocol} {statusCode} {reasonPhrase}\n"; 20 | var statusLineBytes = Encoding.ASCII.GetBytes(statusLine); 21 | log("Status line:" + statusLine); 22 | await stream.WriteAsync(statusLineBytes, 0, statusLineBytes.Length, cancellationToken).ConfigureAwait(false); 23 | await WriteHeadersAsync(stream, headers, log, cancellationToken).ConfigureAwait(false); 24 | await stream.WriteAsync(_eol, 0, _eol.Length, cancellationToken).ConfigureAwait(false); 25 | } 26 | 27 | private static async Task WriteHeadersAsync(Stream stream, 28 | IEnumerable>> headers, Action log, 29 | CancellationToken cancellationToken) 30 | { 31 | foreach(var header in headers) 32 | { 33 | var separator = header.Key == "Server" ? " " : ", "; 34 | var values = string.Join(separator, header.Value); 35 | var line = $"{header.Key}: {values}\n"; 36 | log(header.Key + " Header:" + line); 37 | var payload = Encoding.ASCII.GetBytes(line); 38 | await stream.WriteAsync(payload, 0, payload.Length, cancellationToken).ConfigureAwait(false); 39 | } 40 | } 41 | 42 | public static async Task WriteClientMethodAndHeadersAsync(this Stream stream, HttpRequestMessage request, CancellationToken cancellationToken) 43 | { 44 | var firstLine = $"{request.Method.Method} {request.RequestUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped)} HTTP/{request.Version}\n"; 45 | var payload = Encoding.ASCII.GetBytes(firstLine); 46 | 47 | Debug.WriteLine("-- Client: Writing request - first line"); 48 | await stream.WriteAsync(payload, 0, payload.Length, cancellationToken).ConfigureAwait(false); 49 | Debug.WriteLine("-- Client: Writing request - headers"); 50 | await WriteHeadersAsync(stream, request.Headers, _ => { }, cancellationToken).ConfigureAwait(false); 51 | if(request.Content != null) 52 | { 53 | if (!request.Content.Headers.ContentLength.HasValue) 54 | { 55 | await request.Content.LoadIntoBufferAsync(); 56 | // force populating the underlying headers collection 57 | request.Content.Headers.ContentLength = request.Content.Headers.ContentLength; 58 | } 59 | await WriteHeadersAsync(stream, request.Content.Headers, _ => { }, cancellationToken).ConfigureAwait(false); 60 | } 61 | await stream.WriteAsync(_eol, 0, _eol.Length, cancellationToken).ConfigureAwait(false); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/HttpOverStream/HttpOverStream.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | 9 | true 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/HttpOverStream/HttpParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace HttpOverStream 6 | { 7 | public static class HttpParser 8 | { 9 | public static (string name, List values) ParseHeaderNameValues(string line) 10 | { 11 | var pos = line.IndexOf(':'); 12 | if(pos == -1) 13 | { 14 | throw new FormatException("Invalid header format"); 15 | } 16 | var name = line.Substring(0, pos).Trim(); 17 | var values = line.Substring(pos + 1) 18 | .Split(',') 19 | .Select(v => v.Trim()) 20 | .ToList(); 21 | return (name, values); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HttpOverStream/IDial.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace HttpOverStream 7 | { 8 | public interface IDial 9 | { 10 | ValueTask DialAsync(HttpRequestMessage request, CancellationToken cancellationToken); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/HttpOverStream/IListen.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Principal; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace HttpOverStream 8 | { 9 | public interface IListen 10 | { 11 | Task StartAsync(Action onConnection, CancellationToken cancellationToken); 12 | Task StopAsync(CancellationToken cancellationToken); 13 | IPrincipal GetTransportIdentity(Stream connection); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/HttpOverStream/Logging/ILoggerHttpOverStream.cs: -------------------------------------------------------------------------------- 1 | namespace HttpOverStream.Logging 2 | { 3 | public interface ILoggerHttpOverStream 4 | { 5 | void LogError(string message); 6 | void LogWarning(string message); 7 | void LogVerbose(string message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/HttpOverStream/Logging/NoopLogger.cs: -------------------------------------------------------------------------------- 1 | namespace HttpOverStream.Logging 2 | { 3 | public class NoopLogger : ILoggerHttpOverStream 4 | { 5 | public void LogVerbose(string message) 6 | { 7 | } 8 | 9 | public void LogError(string message) 10 | { 11 | } 12 | 13 | public void LogWarning(string message) 14 | { 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/build.cake: -------------------------------------------------------------------------------- 1 | var target = Argument("target", "Build"); 2 | var configuration = Argument("configuration", "Release"); 3 | var msbuildVerbosity = Argument("msbuild-verbosity", "Minimal"); 4 | var coverage = Argument("coverage", "false"); 5 | 6 | void SetMSBuildConfiguration(MSBuildSettings settings){ 7 | settings.Configuration = configuration; 8 | settings.Verbosity = (Verbosity)Enum.Parse(typeof(Verbosity), msbuildVerbosity); 9 | } 10 | 11 | Task("Nuget-Restore") 12 | .Does(() => 13 | { 14 | NuGetRestore("."); 15 | DotNetCoreRestore(); 16 | }); 17 | Task("Build") 18 | .IsDependentOn("Nuget-Restore") 19 | .Does(() => 20 | { 21 | MSBuild(".", SetMSBuildConfiguration); 22 | }); 23 | 24 | Task("Test") 25 | .IsDependentOn("Nuget-Restore") 26 | .Does(() => 27 | { 28 | var settings = new DotNetCoreTestSettings 29 | { 30 | Configuration = configuration, 31 | NoRestore = true, 32 | }; 33 | var projectFiles = GetFiles("./**/*Test*.csproj"); 34 | foreach (var file in projectFiles) 35 | { 36 | DotNetCoreTest(file.FullPath, settings); 37 | } 38 | }); 39 | 40 | Task("Nuget-pack") 41 | .IsDependentOn("Nuget-Restore") 42 | .Does(()=>{ 43 | var version = "0.1.0"; 44 | if(HasEnvironmentVariable("TAG_NAME")){ 45 | version = EnvironmentVariable("TAG_NAME"); 46 | } 47 | var settings = new DotNetCorePackSettings 48 | { 49 | Configuration = "Release", 50 | OutputDirectory = "./nupkgs/", 51 | MSBuildSettings = new DotNetCoreMSBuildSettings().WithProperty("PackageVersion", version), 52 | 53 | }; 54 | 55 | DotNetCorePack("./", settings); 56 | }); 57 | 58 | Task("Nuget-push") 59 | .IsDependentOn("Nuget-pack") 60 | .Does(()=>{ 61 | if(!HasEnvironmentVariable("TAG_NAME")){ 62 | return; 63 | } 64 | // Get the paths to the packages. 65 | var packages = GetFiles("./nupkgs/*.nupkg"); 66 | 67 | // Push the package. 68 | NuGetPush(packages, new NuGetPushSettings { 69 | Source = "https://api.nuget.org/v3/index.json", 70 | ApiKey = EnvironmentVariable("NugetAPIKey") 71 | }); 72 | }); 73 | 74 | RunTarget(target); -------------------------------------------------------------------------------- /src/build.ps1: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the Cake bootstrapper script for PowerShell. 3 | # This file was downloaded from https://github.com/cake-build/resources 4 | # Feel free to change this file to fit your needs. 5 | ########################################################################## 6 | 7 | <# 8 | 9 | .SYNOPSIS 10 | This is a Powershell script to bootstrap a Cake build. 11 | 12 | .DESCRIPTION 13 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 14 | and execute your Cake build script with the parameters you provide. 15 | 16 | .PARAMETER Script 17 | The build script to execute. 18 | .PARAMETER Target 19 | The build script target to run. 20 | .PARAMETER Configuration 21 | The build configuration to use. 22 | .PARAMETER Verbosity 23 | Specifies the amount of information to be displayed. 24 | .PARAMETER ShowDescription 25 | Shows description about tasks. 26 | .PARAMETER DryRun 27 | Performs a dry run. 28 | .PARAMETER SkipToolPackageRestore 29 | Skips restoring of packages. 30 | .PARAMETER ScriptArgs 31 | Remaining arguments are added here. 32 | 33 | .LINK 34 | https://cakebuild.net 35 | 36 | #> 37 | 38 | [CmdletBinding()] 39 | Param( 40 | [string]$Script = "build.cake", 41 | [string]$Target, 42 | [string]$Configuration, 43 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 44 | [string]$Verbosity, 45 | [switch]$ShowDescription, 46 | [Alias("WhatIf", "Noop")] 47 | [switch]$DryRun, 48 | [switch]$SkipToolPackageRestore, 49 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 50 | [string[]]$ScriptArgs 51 | ) 52 | 53 | # Attempt to set highest encryption available for SecurityProtocol. 54 | # PowerShell will not set this by default (until maybe .NET 4.6.x). This 55 | # will typically produce a message for PowerShell v2 (just an info 56 | # message though) 57 | try { 58 | # Set TLS 1.2 (3072), then TLS 1.1 (768), then TLS 1.0 (192), finally SSL 3.0 (48) 59 | # Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't 60 | # exist in .NET 4.0, even though they are addressable if .NET 4.5+ is 61 | # installed (.NET 4.5 is an in-place upgrade). 62 | [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 63 | } catch { 64 | Write-Output 'Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to upgrade to .NET Framework 4.5+ and PowerShell v3' 65 | } 66 | 67 | [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null 68 | function MD5HashFile([string] $filePath) 69 | { 70 | if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) 71 | { 72 | return $null 73 | } 74 | 75 | [System.IO.Stream] $file = $null; 76 | [System.Security.Cryptography.MD5] $md5 = $null; 77 | try 78 | { 79 | $md5 = [System.Security.Cryptography.MD5]::Create() 80 | $file = [System.IO.File]::OpenRead($filePath) 81 | return [System.BitConverter]::ToString($md5.ComputeHash($file)) 82 | } 83 | finally 84 | { 85 | if ($file -ne $null) 86 | { 87 | $file.Dispose() 88 | } 89 | } 90 | } 91 | 92 | function GetProxyEnabledWebClient 93 | { 94 | $wc = New-Object System.Net.WebClient 95 | $proxy = [System.Net.WebRequest]::GetSystemWebProxy() 96 | $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials 97 | $wc.Proxy = $proxy 98 | return $wc 99 | } 100 | 101 | Write-Host "Preparing to run build script..." 102 | 103 | if(!$PSScriptRoot){ 104 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 105 | } 106 | 107 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 108 | $ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" 109 | $MODULES_DIR = Join-Path $TOOLS_DIR "Modules" 110 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 111 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 112 | $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 113 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 114 | $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" 115 | $ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" 116 | $MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" 117 | 118 | # Make sure tools folder exists 119 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 120 | Write-Verbose -Message "Creating tools directory..." 121 | New-Item -Path $TOOLS_DIR -Type directory | out-null 122 | } 123 | 124 | # Make sure that packages.config exist. 125 | if (!(Test-Path $PACKAGES_CONFIG)) { 126 | Write-Verbose -Message "Downloading packages.config..." 127 | try { 128 | $wc = GetProxyEnabledWebClient 129 | $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) 130 | } catch { 131 | Throw "Could not download packages.config." 132 | } 133 | } 134 | 135 | # Try find NuGet.exe in path if not exists 136 | if (!(Test-Path $NUGET_EXE)) { 137 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 138 | $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } 139 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 140 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 141 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 142 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 143 | } 144 | } 145 | 146 | # Try download NuGet.exe if not exists 147 | if (!(Test-Path $NUGET_EXE)) { 148 | Write-Verbose -Message "Downloading NuGet.exe..." 149 | try { 150 | $wc = GetProxyEnabledWebClient 151 | $wc.DownloadFile($NUGET_URL, $NUGET_EXE) 152 | } catch { 153 | Throw "Could not download NuGet.exe." 154 | } 155 | } 156 | 157 | # Save nuget.exe path to environment to be available to child processed 158 | $ENV:NUGET_EXE = $NUGET_EXE 159 | 160 | # Restore tools from NuGet? 161 | if(-Not $SkipToolPackageRestore.IsPresent) { 162 | Push-Location 163 | Set-Location $TOOLS_DIR 164 | 165 | # Check for changes in packages.config and remove installed tools if true. 166 | [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) 167 | if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or 168 | ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { 169 | Write-Verbose -Message "Missing or changed package.config hash..." 170 | Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | 171 | Remove-Item -Recurse 172 | } 173 | 174 | Write-Verbose -Message "Restoring tools from NuGet..." 175 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 176 | 177 | if ($LASTEXITCODE -ne 0) { 178 | Throw "An error occurred while restoring NuGet tools." 179 | } 180 | else 181 | { 182 | $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" 183 | } 184 | Write-Verbose -Message ($NuGetOutput | out-string) 185 | 186 | Pop-Location 187 | } 188 | 189 | # Restore addins from NuGet 190 | if (Test-Path $ADDINS_PACKAGES_CONFIG) { 191 | Push-Location 192 | Set-Location $ADDINS_DIR 193 | 194 | Write-Verbose -Message "Restoring addins from NuGet..." 195 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" 196 | 197 | if ($LASTEXITCODE -ne 0) { 198 | Throw "An error occurred while restoring NuGet addins." 199 | } 200 | 201 | Write-Verbose -Message ($NuGetOutput | out-string) 202 | 203 | Pop-Location 204 | } 205 | 206 | # Restore modules from NuGet 207 | if (Test-Path $MODULES_PACKAGES_CONFIG) { 208 | Push-Location 209 | Set-Location $MODULES_DIR 210 | 211 | Write-Verbose -Message "Restoring modules from NuGet..." 212 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" 213 | 214 | if ($LASTEXITCODE -ne 0) { 215 | Throw "An error occurred while restoring NuGet modules." 216 | } 217 | 218 | Write-Verbose -Message ($NuGetOutput | out-string) 219 | 220 | Pop-Location 221 | } 222 | 223 | # Make sure that Cake has been installed. 224 | if (!(Test-Path $CAKE_EXE)) { 225 | Throw "Could not find Cake.exe at $CAKE_EXE" 226 | } 227 | 228 | 229 | 230 | # Build Cake arguments 231 | $cakeArguments = @("$Script"); 232 | if ($Target) { $cakeArguments += "-target=$Target" } 233 | if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } 234 | if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } 235 | if ($ShowDescription) { $cakeArguments += "-showdescription" } 236 | if ($DryRun) { $cakeArguments += "-dryrun" } 237 | $cakeArguments += $ScriptArgs 238 | 239 | # Start Cake 240 | Write-Host "Running build script..." 241 | &$CAKE_EXE $cakeArguments 242 | exit $LASTEXITCODE 243 | --------------------------------------------------------------------------------