├── icon.png ├── NtlmProxy.Tests ├── packages.config ├── NtlmProxy.Tests.csproj └── NtlmProxyTests.cs ├── .gitignore ├── PACKAGING.md ├── LICENSE ├── NtlmProxy ├── MikeRogers.NtlmProxy.nuspec ├── Properties │ └── AssemblyInfo.cs ├── SimpleHttpServerOptions.cs ├── NtlmProxy.csproj ├── SimpleHttpServer.cs └── NtlmProxy.cs ├── NtlmProxy.sln └── README.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-rogers/NtlmProxy/HEAD/icon.png -------------------------------------------------------------------------------- /NtlmProxy.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Rr]elease/ 12 | x64/ 13 | build/ 14 | [Bb]in/ 15 | [Oo]bj/ 16 | 17 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 18 | !packages/*/build/ 19 | NuGet.exe 20 | 21 | *_i.c 22 | *_p.c 23 | *.ilk 24 | *.meta 25 | *.obj 26 | *.pch 27 | *.pdb 28 | *.pgc 29 | *.pgd 30 | *.rsp 31 | *.sbr 32 | *.tlb 33 | *.tli 34 | *.tlh 35 | *.tmp 36 | *.tmp_proj 37 | *.log 38 | #*.vspscc 39 | #*.vssscc 40 | .builds 41 | *.pidb 42 | *.log 43 | *.scc 44 | 45 | # ReSharper is a .NET coding add-in 46 | _ReSharper*/ 47 | *.[Rr]e[Ss]harper 48 | *.DotSettings 49 | 50 | # GhostDoc 51 | *.GhostDoc.xml 52 | 53 | # NuGet Packages Directory 54 | packages/ 55 | 56 | # YES, I USE EMACS 57 | *~ 58 | 59 | ## Windows detritus 60 | 61 | # Windows image file caches 62 | Thumbs.db 63 | ehthumbs.db 64 | 65 | # Folder config file 66 | Desktop.ini 67 | 68 | # Recycle Bin used on file shares 69 | $RECYCLE.BIN/ 70 | 71 | ## Mac detritus 72 | .DS_Store 73 | -------------------------------------------------------------------------------- /PACKAGING.md: -------------------------------------------------------------------------------- 1 | # How to package (because I can never remember) 2 | 3 | ## Update files 4 | 5 | Make sure to update the assembly version, the release notes and version number in `NtlmProxy/MikeRogers.NtlmProxy.nuspec`, and the version number in the README file's example NuGet XML. 6 | 7 | Update the build version number in Appveyor. 8 | 9 | ## Do the NuGet dance 10 | 11 | See [here](http://docs.nuget.org/docs/creating-packages/creating-and-publishing-a-package) for details. 12 | 13 | You'll probably want to install the Chocolatey `NuGet.CommandLine` package. 14 | 15 | ## The command that pays 16 | 17 | ... after compiling in Release mode, is this: 18 | 19 | (cd NtlmProxy && NuGet Pack NtlmProxy.csproj -Prop Configuration=Release) 20 | 21 | ## Package it up 22 | 23 | Make a ZIP file with: 24 | 25 | * `LICENSE` 26 | * `README.md` 27 | * `MikeRogers.NtlmProxy.dll` 28 | 29 | Name it `MikeRogers.NtlmProxy-${version}.zip`. 30 | 31 | ## Version it 32 | 33 | Create a tag with the version number and push it to GitHub. 34 | 35 | ## Release it 36 | 37 | Upload the ZIP file and the NuGet file to GitHub. 38 | 39 | ```bat 40 | > nuget setApiKey ${myKey} 41 | 42 | > nuget push ${packageName} 43 | ``` 44 | 45 | ## Party hat 46 | 47 | Put on a party hat. 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, R. Michael Rogers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /NtlmProxy/MikeRogers.NtlmProxy.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MikeRogers.NtlmProxy 5 | 1.2.1.0 6 | NtlmProxy Library 7 | Mike Rogers 8 | Mike Rogers 9 | https://raw.githubusercontent.com/mike-rogers/NtlmProxy/master/LICENSE 10 | https://github.com/mike-rogers/NtlmProxy 11 | https://raw.githubusercontent.com/mike-rogers/NtlmProxy/master/icon.png 12 | false 13 | Provides a proxy that adds NTLM data for the current user, for use on systems requiring NTLM 14 | 15 | 1.2.1 - Addressed bug (issue #3), allowing dynamic configuration of proxy port 16 | number 17 | 1.2 --- Added support for proxying options, including 18 | * Optional proxying of HTTP headers 19 | * Support for specific Angular.JS Content-Type 20 | 1.1 --- Added support for POST, PUT, DELETE verbs 21 | 1.0 --- Provides a proxy that adds NTLM data for the current user into a web request. 22 | 23 | Copyright 2015 24 | Selenium PhantomJS headless proxy ntlm 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /NtlmProxy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.21005.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NtlmProxy", "NtlmProxy\NtlmProxy.csproj", "{79DF1E9F-06F9-4392-AB0D-3CAC3191EB5F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NtlmProxy.Tests", "NtlmProxy.Tests\NtlmProxy.Tests.csproj", "{8D8540E1-C7B6-437A-AB55-95211669A2CC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {79DF1E9F-06F9-4392-AB0D-3CAC3191EB5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {79DF1E9F-06F9-4392-AB0D-3CAC3191EB5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {79DF1E9F-06F9-4392-AB0D-3CAC3191EB5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {79DF1E9F-06F9-4392-AB0D-3CAC3191EB5F}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {8D8540E1-C7B6-437A-AB55-95211669A2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {8D8540E1-C7B6-437A-AB55-95211669A2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {8D8540E1-C7B6-437A-AB55-95211669A2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {8D8540E1-C7B6-437A-AB55-95211669A2CC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /NtlmProxy/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("NtlmProxy Library")] 8 | [assembly: AssemblyDescription("Provides a proxy that adds NTLM data for the current user, for use on systems requiring NTLM")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Mike Rogers")] 11 | [assembly: AssemblyProduct("NtlmProxyLibrary")] 12 | [assembly: AssemblyCopyright("Copyright ©2015, All Rights Reserved")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("970844e3-a3ea-4527-9fc4-2eef2e87475c")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.2.1.0")] 35 | [assembly: AssemblyFileVersion("1.2.1.0")] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NTLM Proxy 2 | 3 | Current status: [![Build status](https://ci.appveyor.com/api/projects/status/jtyvkirqtc7ki0kh?svg=true)](https://ci.appveyor.com/project/mike-rogers/ntlmproxy) 4 | 5 | So my team is using [Selenium](http://docs.seleniumhq.org/) [WebDriver](http://docs.seleniumhq.org/projects/webdriver/) to test our web application, and it takes balls forever to run on our test browser. Our test browser is [Firefox](http://www.mozilla.org/en-US/firefox/fx/), which, with a name like that, you'd think would be... I dunno, zippy. 6 | 7 | Anyway. 8 | 9 | I wanted to use [PhantomJS](http://phantomjs.org/) to run headlessly, which would increase the speed of the test runs. Sounds good, right? Too bad we're using Windows Authentication. PhantomJS (currently, 20140205) doesn't like NTLM, and neither do I. The only way to pass Windows credentials is to use some sort of proxy. 10 | 11 | ## Some sort of proxy 12 | 13 | That's why I wrote this little library. 14 | 15 | With a simple `using` directive it will create a tiny web proxy and naively forward any requests sent to it on to a target URL, with NTLM headers for the current user. 16 | 17 | It's pretty simple to use: 18 | 19 | ```csharp 20 | using (var proxy = new NtlmProxy(new Uri("http://localhost:8081/"))) 21 | { 22 | // Make your requests here to http://localhost:3999/{0}, proxy.Port 23 | } 24 | ``` 25 | 26 | You can pass in your own custom options: 27 | 28 | ```csharp 29 | var options = new SimpleHttpServerOptions 30 | { 31 | Port = 3999, 32 | AuthenticationScheme = AuthenticationSchemes.None 33 | }; 34 | 35 | using (var proxy = new NtlmProxy(new Uri("http://localhost:8081/"), options)) 36 | { 37 | // Make your requests here to http://localhost:3999/whatever 38 | } 39 | ``` 40 | 41 | To see a comprehensive list of options, see the comments for [SimpleHttpServerOptions.cs](https://github.com/mike-rogers/NtlmProxy/blob/master/NtlmProxy/SimpleHttpServerOptions.cs). 42 | 43 | ## How do I install it? 44 | 45 | You can download the zipped DLL in the Releases section above, or you can use NuGet: 46 | 47 | ```xml 48 | 49 | ``` 50 | 51 | ## Why do you keep saying 'naively'? 52 | 53 | I'm no NTLM expert, but I think after the first authorization challenge the client should be holding onto the credentials and including the appropriate NTLM headers. I'm pretty sure this proxy server won't do that. 54 | 55 | ## It's still pretty cool 56 | 57 | I know, right? 58 | 59 | ## License? 60 | 61 | As always, we rock the BSD. 62 | -------------------------------------------------------------------------------- /NtlmProxy/SimpleHttpServerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | 4 | namespace MikeRogers.NtlmProxy 5 | { 6 | /// 7 | /// Class SimpleHttpServerOptions. This class cannot be inherited. 8 | /// 9 | public sealed class SimpleHttpServerOptions 10 | { 11 | /// 12 | /// Gets or sets a value indicating whether this instance has an Angular content-type header. 13 | /// 14 | /// true if this instance has angular content type; otherwise, false. 15 | public bool HasAngularContentType { get; set; } 16 | 17 | /// 18 | /// Gets or sets the request headers. 19 | /// 20 | /// The request headers. 21 | public Dictionary RequestHeaders { get; set; } 22 | 23 | /// 24 | /// Gets the port. 25 | /// 26 | /// The port. 27 | public int Port { get; set; } 28 | 29 | /// 30 | /// Gets or sets the authentication scheme. 31 | /// 32 | /// The authentication scheme. 33 | public AuthenticationSchemes AuthenticationScheme { get; set; } 34 | 35 | /// 36 | /// Should the proxy duplicate individual request headers? 37 | /// 38 | public bool AreHeadersDuplicated { get; set; } 39 | 40 | /// 41 | /// List of headers to not repeat when AreHeadersDuplicated is true 42 | /// 43 | public List ExcludedHeaders { get; private set; } 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// 48 | public SimpleHttpServerOptions() 49 | { 50 | RequestHeaders = new Dictionary(); 51 | AuthenticationScheme = AuthenticationSchemes.Anonymous; 52 | ExcludedHeaders = new List { "Host", "Accept-Encoding" }; 53 | } 54 | 55 | /// 56 | /// The default options 57 | /// 58 | public static SimpleHttpServerOptions GetDefaultOptions() 59 | { 60 | return new SimpleHttpServerOptions 61 | { 62 | Port = 0, 63 | AuthenticationScheme = AuthenticationSchemes.Anonymous, 64 | HasAngularContentType = false, 65 | AreHeadersDuplicated = false, 66 | RequestHeaders = new Dictionary() 67 | }; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /NtlmProxy/NtlmProxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {79DF1E9F-06F9-4392-AB0D-3CAC3191EB5F} 8 | Library 9 | Properties 10 | MikeRogers.NtlmProxy 11 | MikeRogers.NtlmProxy 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 60 | -------------------------------------------------------------------------------- /NtlmProxy.Tests/NtlmProxy.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8D8540E1-C7B6-437A-AB55-95211669A2CC} 8 | Library 9 | Properties 10 | MikeRogers.NtlmProxy.Tests 11 | MikeRogers.NtlmProxy.Tests 12 | v4.5 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | ..\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll 40 | 41 | 42 | ..\packages\NUnit.2.6.3\lib\nunit.framework.dll 43 | 44 | 45 | ..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {79df1e9f-06f9-4392-ab0d-3cac3191eb5f} 60 | NtlmProxy 61 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | -------------------------------------------------------------------------------- /NtlmProxy/SimpleHttpServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Sockets; 7 | using System.Threading.Tasks; 8 | 9 | namespace MikeRogers.NtlmProxy 10 | { 11 | /// 12 | /// A simple HTTP server that can be set to a random unused port 13 | /// and expect a given type of authentication. 14 | /// 15 | public sealed class SimpleHttpServer : IDisposable 16 | { 17 | #region Fields 18 | 19 | /// 20 | /// The instance for creating a small proxy server 21 | /// 22 | private readonly HttpListener _listener; 23 | 24 | /// 25 | /// The function that handles incoming proxied requests 26 | /// 27 | private readonly Func> _requestHandler; 28 | 29 | #endregion 30 | 31 | 32 | #region Properties 33 | 34 | /// 35 | /// Gets or sets the port. 36 | /// 37 | /// The port. 38 | public int Port { get; private set; } 39 | 40 | #endregion Properties 41 | 42 | 43 | #region Constructors 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// 48 | /// The function that handles incoming proxied requests. 49 | /// Configuration options for the server. 50 | public SimpleHttpServer( 51 | Func> handler, 52 | SimpleHttpServerOptions options = null) 53 | { 54 | options = options ?? SimpleHttpServerOptions.GetDefaultOptions(); 55 | 56 | if (options.RequestHeaders == null) 57 | { 58 | throw new ArgumentNullException("options"); 59 | } 60 | 61 | Port = options.Port == 0 ? GetEmptyPort() : options.Port; 62 | _requestHandler = handler; 63 | 64 | _listener = new HttpListener 65 | { 66 | AuthenticationSchemes = options.AuthenticationScheme, 67 | IgnoreWriteExceptions = true 68 | }; 69 | 70 | _listener.Prefixes.Add(string.Format("http://localhost:{0}/", Port.ToString(CultureInfo.InvariantCulture))); 71 | 72 | _listener.Start(); 73 | StartListenLoop(); 74 | } 75 | 76 | #endregion 77 | 78 | 79 | #region Public Methods 80 | 81 | /// 82 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 83 | /// 84 | public void Dispose() 85 | { 86 | _listener.Stop(); 87 | } 88 | 89 | #endregion 90 | 91 | 92 | #region Private Methods 93 | 94 | /// 95 | /// Gets the empty port. 96 | /// 97 | /// An unused port number 98 | private static int GetEmptyPort() 99 | { 100 | // from http://stackoverflow.com/a/3978040/996184 101 | var listener = new TcpListener(IPAddress.Any, 0); 102 | listener.Start(); 103 | var port = ((IPEndPoint)listener.LocalEndpoint).Port; 104 | listener.Stop(); 105 | return port; 106 | } 107 | 108 | /// 109 | /// Starts the listen loop. 110 | /// 111 | private async void StartListenLoop() 112 | { 113 | while (true) 114 | { 115 | var context = await _listener.GetContextAsync(); 116 | var response = await _requestHandler(context); 117 | 118 | using (var stream = context.Response.OutputStream) 119 | { 120 | var bytes = await response.Content.ReadAsByteArrayAsync(); 121 | 122 | // Copy response attributes back to the proxy 123 | context.Response.ContentLength64 = bytes.Length; 124 | context.Response.StatusCode = (int)response.StatusCode; 125 | context.Response.StatusDescription = response.StatusCode.ToString(); 126 | if (response.Content.Headers.ContentType != null) 127 | { 128 | context.Response.ContentType = response.Content.Headers.ContentType.ToString(); 129 | } 130 | 131 | stream.Write(bytes, 0, bytes.Count()); 132 | stream.Close(); 133 | } 134 | } 135 | } 136 | 137 | #endregion 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /NtlmProxy/NtlmProxy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace MikeRogers.NtlmProxy 10 | { 11 | /// 12 | /// Class NtlmProxy. This class cannot be inherited. 13 | /// 14 | public sealed class NtlmProxy : IDisposable 15 | { 16 | #region Fields 17 | 18 | /// 19 | /// The hostname to which incoming requests will be proxied 20 | /// 21 | private readonly Uri _hostname; 22 | 23 | /// 24 | /// The simple HTTP server that accepts proxied requests 25 | /// 26 | private readonly SimpleHttpServer _server; 27 | 28 | /// 29 | /// The options for the server 30 | /// 31 | private readonly SimpleHttpServerOptions _options; 32 | 33 | #endregion 34 | 35 | 36 | #region Properties 37 | 38 | /// 39 | /// Gets the proxy server port. 40 | /// 41 | public int Port 42 | { 43 | get { return _server.Port; } 44 | } 45 | 46 | #endregion 47 | 48 | 49 | #region Constructors 50 | 51 | /// 52 | /// Initializes a new instance of the class. 53 | /// 54 | /// The proxied hostname. 55 | /// Configuration options for the server. 56 | public NtlmProxy(Uri proxiedHostname, SimpleHttpServerOptions serverOptions = null) 57 | { 58 | _options = serverOptions ?? SimpleHttpServerOptions.GetDefaultOptions(); 59 | _server = new SimpleHttpServer(ProcessRequest, serverOptions); 60 | _hostname = proxiedHostname; 61 | } 62 | 63 | #endregion 64 | 65 | 66 | #region Public Methods 67 | 68 | /// 69 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 70 | /// 71 | public void Dispose() 72 | { 73 | _server.Dispose(); 74 | } 75 | 76 | #endregion 77 | 78 | 79 | #region Private Methods 80 | 81 | /// 82 | /// Processes the request. 83 | /// 84 | /// The context. 85 | /// A task thread that resolves into an HTTP response. 86 | private async Task ProcessRequest(HttpListenerContext context) 87 | { 88 | var credential = CredentialCache.DefaultNetworkCredentials; 89 | var myCache = new CredentialCache 90 | { 91 | {_hostname, "NTLM", credential} 92 | }; 93 | 94 | var handler = new HttpClientHandler 95 | { 96 | AllowAutoRedirect = true, 97 | Credentials = myCache 98 | }; 99 | 100 | using (var client = new HttpClient(handler)) 101 | { 102 | var httpMethod = new HttpMethod(context.Request.HttpMethod); 103 | var target = new Uri(_hostname, context.Request.Url.PathAndQuery); 104 | var content = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding).ReadToEnd(); 105 | 106 | // New implementation suggested by https://github.com/blabla4 107 | var request = new HttpRequestMessage(httpMethod, target); 108 | if (content != String.Empty) 109 | { 110 | var contentType = context.Request.ContentType; 111 | 112 | if (_options.HasAngularContentType && contentType != null) 113 | { 114 | // Thank you to https://github.com/svantreeck 115 | contentType = Regex.Replace(contentType, ";charset=(.)*", string.Empty); 116 | } 117 | 118 | request.Content = new StringContent(content, context.Request.ContentEncoding, contentType); 119 | } 120 | 121 | // Add headers ('thank you' to https://github.com/svantreeck) 122 | _options.RequestHeaders.ToList().ForEach(x => request.Headers.Add(x.Key, x.Value)); 123 | 124 | if (_options.AreHeadersDuplicated) 125 | { 126 | foreach (var key in context.Request.Headers.AllKeys) 127 | { 128 | if (!_options.ExcludedHeaders.Contains(key)) 129 | { 130 | request.Headers.TryAddWithoutValidation(key, context.Request.Headers[key]); 131 | } 132 | } 133 | } 134 | 135 | return await client.SendAsync(request); 136 | } 137 | } 138 | 139 | #endregion 140 | } 141 | } -------------------------------------------------------------------------------- /NtlmProxy.Tests/NtlmProxyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Net.Http; 6 | using NUnit.Framework; 7 | using RestSharp; 8 | using System.Text; 9 | 10 | namespace MikeRogers.NtlmProxy.Tests 11 | { 12 | /// 13 | /// These are the worst tests I have ever written. 14 | /// 15 | [TestFixture] 16 | public sealed class NtlmProxyTests 17 | { 18 | /// 19 | /// Sample text string to be served by the fake server. 20 | /// 21 | private const string ExpectedResultText = "I enjoy cows."; 22 | 23 | /// 24 | /// The types of HTTP methods supported. 25 | /// 26 | private static readonly object[] HttpMethods = 27 | { 28 | Method.GET, 29 | Method.POST, 30 | Method.DELETE, 31 | Method.PUT 32 | }; 33 | 34 | /// 35 | /// Passes if the response from server is returned correctly, 36 | /// and if the credentials make it all the way from the web request through 37 | /// proxy to server. 38 | /// 39 | /// The HTTP method to be tested. 40 | [Test, TestCaseSource("HttpMethods")] 41 | public void ShouldProxyCredentialsOnRequest(Method method) 42 | { 43 | var serverOptions = SimpleHttpServerOptions.GetDefaultOptions(); 44 | var proxyOptions = SimpleHttpServerOptions.GetDefaultOptions(); 45 | serverOptions.AuthenticationScheme = AuthenticationSchemes.Ntlm; 46 | proxyOptions.AuthenticationScheme = AuthenticationSchemes.Ntlm; 47 | 48 | var serverAssertion = new Action(context => 49 | { 50 | var currentCredentials = System.Security.Principal.WindowsIdentity.GetCurrent(); 51 | if (currentCredentials == null) 52 | { 53 | Assert.Fail("Unable to determine current Windows user credentials"); 54 | } 55 | else 56 | { 57 | Assert.That(context.User.Identity.Name, Is.EqualTo(currentCredentials.Name)); 58 | } 59 | }); 60 | 61 | var clientAssertion = new Action(proxy => 62 | { 63 | var proxyUrl = string.Format("http://localhost:{0}/", proxy.Port); 64 | 65 | var client = new RestClient(proxyUrl) 66 | { 67 | Authenticator = new NtlmAuthenticator() 68 | }; 69 | 70 | var request = new RestRequest("/", method); 71 | 72 | var response = client.Execute(request); 73 | 74 | Assert.That(response.Content, Is.EqualTo(ExpectedResultText)); 75 | }); 76 | 77 | ExecuteTestInContext(serverOptions, proxyOptions, serverAssertion, clientAssertion); 78 | } 79 | 80 | /// 81 | /// https://github.com/svantreeck reported in https://github.com/mike-rogers/NtlmProxy/pull/2 that 82 | /// Angular.js has a weird behavior with certain ContentType strings. The option under test 83 | /// removes the 'charset' string from the ContentType. 84 | /// 85 | [Test] 86 | public void ShouldRemoveCharsetForAngularOption() 87 | { 88 | var options = SimpleHttpServerOptions.GetDefaultOptions(); 89 | options.HasAngularContentType = true; 90 | 91 | var serverAssertion = new Action(context => 92 | { 93 | var contentType = context.Request.ContentType; 94 | Assert.That(contentType == null || !contentType.Contains("charset=")); 95 | }); 96 | 97 | var clientAssertion = new Action(proxy => 98 | { 99 | var proxyUrl = string.Format("http://localhost:{0}/", proxy.Port); 100 | 101 | var client = new RestClient(proxyUrl); 102 | 103 | var request = new RestRequest("/", Method.GET); 104 | 105 | client.Execute(request); 106 | }); 107 | 108 | ExecuteTestInContext(options, SimpleHttpServerOptions.GetDefaultOptions(), serverAssertion, clientAssertion); 109 | } 110 | 111 | /// 112 | /// Should be able to inject headers into proxied requests. 113 | /// 114 | [Test] 115 | public void ShouldInjectHeadersIntoProxiedRequest() 116 | { 117 | var serverOptions = SimpleHttpServerOptions.GetDefaultOptions(); 118 | var proxyOptions = SimpleHttpServerOptions.GetDefaultOptions(); 119 | proxyOptions.RequestHeaders = new Dictionary 120 | { 121 | {"HeaderOne", "Donkey"}, 122 | {"HeaderTwo", "Pony"} 123 | }; 124 | 125 | var serverAssertion = new Action(context => 126 | { 127 | Assert.That(context.Request.Headers["HeaderOne"], Is.EqualTo("Donkey")); 128 | Assert.That(context.Request.Headers["HeaderTwo"], Is.EqualTo("Pony")); 129 | }); 130 | 131 | var clientAssertion = new Action(proxy => 132 | { 133 | var proxyUrl = string.Format("http://localhost:{0}/", proxy.Port); 134 | 135 | var client = new RestClient(proxyUrl); 136 | 137 | var request = new RestRequest("/", Method.GET); 138 | 139 | client.Execute(request); 140 | }); 141 | 142 | ExecuteTestInContext(serverOptions, proxyOptions, serverAssertion, clientAssertion); 143 | } 144 | 145 | /// 146 | /// The responses from the server should be added to the response object 147 | /// passed back to the executing context. 148 | /// 149 | [Test] 150 | public void ShouldDuplicateHeadersInProxiedRequest() 151 | { 152 | var proxyOptions = new SimpleHttpServerOptions { AreHeadersDuplicated = true }; 153 | 154 | var serverAssertion = new Action(context => 155 | { 156 | Assert.That(context.Request.Headers["HeaderOne"], Is.EqualTo("Donkey")); 157 | Assert.That(context.Request.Headers["HeaderTwo"], Is.EqualTo("Pony")); 158 | }); 159 | 160 | var clientAssertion = new Action(proxy => 161 | { 162 | var proxyUrl = string.Format("http://localhost:{0}/", proxy.Port); 163 | 164 | var client = new RestClient(proxyUrl); 165 | 166 | var request = new RestRequest("/", Method.GET); 167 | request.AddHeader("HeaderOne", "Donkey"); 168 | request.AddHeader("HeaderTwo", "Pony"); 169 | 170 | client.Execute(request); 171 | }); 172 | 173 | ExecuteTestInContext(SimpleHttpServerOptions.GetDefaultOptions(), proxyOptions, serverAssertion, clientAssertion); 174 | } 175 | 176 | /// 177 | /// The responses from the server should be added to the response object 178 | /// passed back to the executing context. 179 | /// 180 | [Test] 181 | public void ShouldIncludeProxiedResponseDataBackToClient() 182 | { 183 | var options = SimpleHttpServerOptions.GetDefaultOptions(); 184 | 185 | var clientAssertion = new Action(proxy => 186 | { 187 | var proxyUrl = string.Format("http://localhost:{0}/", proxy.Port); 188 | 189 | var client = new RestClient(proxyUrl); 190 | 191 | var request = new RestRequest("/", Method.GET); 192 | 193 | var response = client.Execute(request); 194 | 195 | Assert.That(response.ContentLength, Is.EqualTo(13)); 196 | Assert.That(response.ContentType, Is.Empty); 197 | Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 198 | Assert.That(response.StatusDescription, Is.EqualTo("OK")); 199 | }); 200 | 201 | ExecuteTestInContext(options, SimpleHttpServerOptions.GetDefaultOptions(), null, clientAssertion); 202 | } 203 | 204 | /// 205 | /// Should proxy simple GET request per read me instructions. 206 | /// 207 | [Test] 208 | public void ShouldProxySimpleGetRequestOnCustomPort() 209 | { 210 | var proxyOptions = new SimpleHttpServerOptions 211 | { 212 | Port = 3999 213 | }; 214 | 215 | var serverOptions = SimpleHttpServerOptions.GetDefaultOptions(); 216 | 217 | var clientAssertion = new Action(proxy => 218 | { 219 | var proxyUrl = string.Format("http://localhost:{0}/", proxy.Port); 220 | 221 | var client = new RestClient(proxyUrl); 222 | 223 | var request = new RestRequest("/", Method.GET); 224 | 225 | var response = client.Execute(request); 226 | 227 | Assert.That(response.ContentLength, Is.EqualTo(13)); 228 | Assert.That(response.ContentType, Is.Empty); 229 | Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 230 | Assert.That(response.StatusDescription, Is.EqualTo("OK")); 231 | }); 232 | 233 | ExecuteTestInContext(serverOptions, proxyOptions, null, clientAssertion); 234 | } 235 | 236 | /// 237 | /// This method creates a stub server (server). 238 | /// It will then create the proxy (proxy). 239 | /// Finally it issues a web request to the proxy. 240 | /// 241 | /// The options for the running HTTP server. 242 | /// The options for the running proxy server. 243 | /// The configuration/assertion code to run in the context of the server. 244 | /// The configuration/assertion code to run in the context of the client. 245 | private static void ExecuteTestInContext( 246 | SimpleHttpServerOptions serverOptions, 247 | SimpleHttpServerOptions proxyOptions, 248 | Action serverAssertion, 249 | Action clientAssertion) 250 | { 251 | // Unfortunately I'm not familiar enough with await/async to correct the following: 252 | #pragma warning disable 1998 253 | using (var server = new SimpleHttpServer(async context => 254 | #pragma warning restore 1998 255 | { 256 | var response = new HttpResponseMessage 257 | { 258 | Content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(ExpectedResultText))) 259 | }; 260 | 261 | if (serverAssertion != null) 262 | { 263 | serverAssertion(context); 264 | } 265 | 266 | return response; 267 | }, serverOptions)) 268 | { 269 | var serverUri = new Uri(string.Format("http://localhost:{0}/", server.Port)); 270 | using (var proxy = new NtlmProxy(serverUri, proxyOptions)) 271 | { 272 | clientAssertion(proxy); 273 | } 274 | } 275 | } 276 | } 277 | } 278 | --------------------------------------------------------------------------------