Skip to content
Browse files

Merge pull request #124 from adamipc/master

Basic HTTP Authentication for DefaultWebClient.
  • Loading branch information...
2 parents b2f781a + e520f2b commit 39bd9d0829dfde22976bce4ba01341a5038cf69c @RubenWillems RubenWillems committed Jun 24, 2012
View
27 project/Remote/CruiseServerHttpClient.cs
@@ -18,6 +18,8 @@ public class CruiseServerHttpClient
private readonly string serverUri;
private string targetServer;
private WebClient client;
+ private IWebFunctions webFunctions;
+
#endregion
#region Constructors
@@ -39,6 +41,7 @@ public CruiseServerHttpClient(string serverUri, WebClient client)
{
this.serverUri = serverUri.EndsWith("/", StringComparison.CurrentCulture) ? serverUri.Substring(0, serverUri.Length - 1) : serverUri;
this.client = client;
+ this.webFunctions = new DefaultWebFunctions();
}
#endregion
@@ -87,7 +90,27 @@ public override ProjectStatus[] GetProjectStatus()
{
// Retrieve the XML from the server
var url = GenerateUrl("XmlStatusReport.aspx");
- var response = client.DownloadString(url);
+ var uri = new Uri(url);
+ webFunctions.SetCredentials(client, uri, false);
+ string response;
+ try
+ {
+ response = client.DownloadString(url);
+ }
+ catch (WebException error)
+ {
+ if (error.Message.Contains("(403) Forbidden"))
+ {
+ // Jenkins doesn't give a challenge for HTTP Authentication
+ // So we need to force an Authorization header
+ webFunctions.SetCredentials(client, uri, true);
+ response = client.DownloadString(url);
+ }
+ else
+ {
+ throw;
+ }
+ }
if (string.IsNullOrEmpty(response)) throw new CommunicationsException("No data retrieved");
// Load the XML and parse it
@@ -183,6 +206,7 @@ public override CruiseServerSnapshot GetCruiseServerSnapshot()
{
// Retrieve the XML from the server - 1.3 or later
var url = GenerateUrl("XmlServerReport.aspx");
+ webFunctions.SetCredentials(client, new Uri(url), false);
response = client.DownloadString(url);
}
catch (Exception)
@@ -382,6 +406,7 @@ private void SendButtonPush(string command, string project)
values.Add("serverName", TargetServer);
try
{
+ webFunctions.SetCredentials(client, new Uri(url), false);
client.UploadValues(url, values);
}
catch (Exception error)
View
19 project/Remote/DefaultWebClient.cs
@@ -12,15 +12,26 @@ public class DefaultWebClient
{
#region Private fields
private WebClient innerClient;
+ private IWebFunctions webFunctions;
+
#endregion
#region Constructors
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DefaultWebClient"/> class
+ /// </summary>
+ public DefaultWebClient() : this(new WebClient())
+ {
+ }
+
/// <summary>
/// Initializes a new instance of the <see cref="DefaultWebClient"/> class.
/// </summary>
- public DefaultWebClient()
+ /// <param name="webClient">instance of <see cref="WebClient"/> to use</param>
+ public DefaultWebClient(WebClient webClient)
{
- this.innerClient = new WebClient();
+ this.innerClient = webClient;
this.innerClient.UploadValuesCompleted += (o, e) =>
{
// Pass on the event
@@ -29,6 +40,7 @@ public DefaultWebClient()
this.UploadValuesCompleted(this, new BinaryDataEventArgs(e.Result, e.Error, e.Cancelled, e.UserState));
}
};
+ this.webFunctions = new DefaultWebFunctions();
}
#endregion
@@ -43,8 +55,10 @@ public DefaultWebClient()
/// <returns>The response data.</returns>
public byte[] UploadValues(Uri address, string method, NameValueCollection data)
{
+ this.webFunctions.SetCredentials(this.innerClient, address, false);
return this.innerClient.UploadValues(address, method, data);
}
+
#endregion
#region
@@ -56,6 +70,7 @@ public byte[] UploadValues(Uri address, string method, NameValueCollection data)
/// <param name="data">The data.</param>
public void UploadValuesAsync(Uri address, string method, NameValueCollection data)
{
+ this.webFunctions.SetCredentials(this.innerClient, address, false);
this.innerClient.UploadValuesAsync(address, method, data);
}
#endregion
View
47 project/Remote/DefaultWebFunctions.cs
@@ -0,0 +1,47 @@
+namespace ThoughtWorks.CruiseControl.Remote
+{
+ using System;
+ using System.Net;
+ using System.Text;
+
+ public class DefaultWebFunctions : IWebFunctions
+ {
+ /// <summary>
+ /// Sets credentials on client if address contains user info.
+ /// </summary>
+ /// <param name="webClient">The <see cref="WebClient"/> to set credentials on.</param>
+ /// <param name="address">The address to check for user info.</param>
+ /// <param name="forceAuthorization">Whether to force an Authorization header or allow WebClient credentials to handle it.</param>
+ public void SetCredentials(WebClient webClient, Uri address, bool forceAuthorization)
+ {
+ if (address.UserInfo.Length <= 0) return;
+
+ var userInfoValues = address.UserInfo.Split(':');
+ var credentials = new NetworkCredential
+ {
+ UserName = userInfoValues[0]
+ };
+
+ if (userInfoValues.Length > 1)
+ credentials.Password = userInfoValues[1];
+
+ if (forceAuthorization)
+ webClient.Headers.Add("Authorization", GenerateAuthorizationFromCredentials(credentials));
+ else
+ webClient.Credentials = credentials;
+ }
+
+ /// <summary>
+ /// Generates the body of a Basic HTTP Authorization header.
+ /// </summary>
+ /// <param name="credentials">The <see cref="NetworkCredential"/> to pull username and password from."/></param>
+ /// <returns>The body of a basic HTTP Authorization header.</returns>
+ private static string GenerateAuthorizationFromCredentials(NetworkCredential credentials)
+ {
+ string credentialsText = String.Format("{0}:{1}", credentials.UserName, credentials.Password);
+ byte[] bytes = Encoding.ASCII.GetBytes(credentialsText);
+ string base64 = Convert.ToBase64String(bytes);
+ return String.Concat("Basic ", base64);
+ }
+ }
+}
View
16 project/Remote/IWebFunctions.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Net;
+
+namespace ThoughtWorks.CruiseControl.Remote
+{
+ public interface IWebFunctions
+ {
+ /// <summary>
+ /// Sets credentials on client if address contains user info.
+ /// </summary>
+ /// <param name="webClient">The <see cref="WebClient"/> to set credentials on.</param>
+ /// <param name="address">The address to check for user info.</param>
+ /// <param name="forceAuthorization">Whether to force an Authorization header or allow WebClient credentials to handle it.</param>
+ void SetCredentials(WebClient webClient, Uri address, bool forceAuthorization);
+ }
+}
View
2 project/Remote/Remote.csproj
@@ -144,11 +144,13 @@
<Compile Include="CommunicationsException.cs" />
<Compile Include="CruiseServerHttpClient.cs" />
<Compile Include="DefaultWebClient.cs" />
+ <Compile Include="DefaultWebFunctions.cs" />
<Compile Include="EncryptingConnection.cs" />
<Compile Include="ICruiseServerClientFactory.cs" />
<Compile Include="IWebClient.cs" />
<Compile Include="IWebClientFactory.cs" />
<Compile Include="BuildSummary.cs" />
+ <Compile Include="IWebFunctions.cs" />
<Compile Include="Messages\CommunicationsMessage.cs" />
<Compile Include="Messages\EncryptedRequest.cs" />
<Compile Include="Messages\EncryptedResponse.cs" />
View
79 project/UnitTests/Remote/CruiseServerHttpClientTests.cs
@@ -80,6 +80,85 @@ public class CruiseServerHttpClientTests
#endregion
[Test]
+ public void GetCruiseServerSnapshotSetsCredentialsOnWebClient()
+ {
+ var webClient = mocks.DynamicMock<WebClient>();
+ Expect.Call(webClient.DownloadString(""))
+ .IgnoreArguments()
+ .Return(xmlFrom11);
+ SetupResult.For(webClient.Credentials).PropertyBehavior();
+ var url = "http://test1:test2@test3";
+ var client = new CruiseServerHttpClient("http://test1:test2@test3", webClient);
+
+ mocks.ReplayAll();
+ client.GetCruiseServerSnapshot();
+
+ Assert.IsNotNull(webClient.Credentials, "No credentials set");
+ var cred = webClient.Credentials.GetCredential(new Uri(url), "Basic");
+ Assert.AreEqual("test1", cred.UserName, "Unexpected username");
+ Assert.AreEqual("test2", cred.Password, "Unexpected password");
+ }
+
+ [Test]
+ public void GetProjectStatusSetsCredentialsOnWebClient()
+ {
+ var webClient = mocks.DynamicMock<WebClient>();
+ Expect.Call(webClient.DownloadString(""))
+ .IgnoreArguments()
+ .Return(xmlFrom11);
+ SetupResult.For(webClient.Credentials).PropertyBehavior();
+ var url = "http://test1:test2@test3";
+ var client = new CruiseServerHttpClient("http://test1:test2@test3", webClient);
+
+ mocks.ReplayAll();
+ client.GetProjectStatus();
+
+ Assert.IsNotNull(webClient.Credentials, "No credentials set");
+ var cred = webClient.Credentials.GetCredential(new Uri(url), "Basic");
+ Assert.AreEqual("test1", cred.UserName, "Unexpected username");
+ Assert.AreEqual("test2", cred.Password, "Unexpected password");
+ }
+
+ [Test]
+ public void StartProjectSetsCredentialsOnWebClient()
+ {
+ var webClient = mocks.DynamicMock<WebClient>();
+ SetupResult.For(webClient.Credentials).PropertyBehavior();
+ var url = "http://test1:test2@test3";
+ var client = new CruiseServerHttpClient("http://test1:test2@test3", webClient);
+
+ mocks.ReplayAll();
+ client.StartProject(null);
+
+ Assert.IsNotNull(webClient.Credentials, "No credentials set");
+ var cred = webClient.Credentials.GetCredential(new Uri(url), "Basic");
+ Assert.AreEqual("test1", cred.UserName, "Unexpected username");
+ Assert.AreEqual("test2", cred.Password, "Unexpected password");
+ }
+
+ [Test]
+ public void GetProjectStatusForcesAuthorizationIf403ForbiddenIsReceived()
+ {
+ var webClient = mocks.DynamicMock<WebClient>();
+ Expect.Call(webClient.DownloadString(""))
+ .IgnoreArguments()
+ .Throw(new WebException("The remote server returned an error: (403) Forbidden."));
+ Expect.Call(webClient.DownloadString(""))
+ .IgnoreArguments()
+ .Return(xmlFrom11);
+ SetupResult.For(webClient.Credentials).PropertyBehavior();
+ SetupResult.For(webClient.Headers).PropertyBehavior();
+ webClient.Headers = new WebHeaderCollection();
+ var url = "http://test1:test2@test3";
+ var client = new CruiseServerHttpClient("http://test1:test2@test3", webClient);
+
+ mocks.ReplayAll();
+ client.GetProjectStatus();
+
+ Assert.AreEqual("Basic dGVzdDE6dGVzdDI=", webClient.Headers["Authorization"], "Unexpected Authorization header");
+ }
+
+ [Test]
public void GetProjectStatusCorrectlyHandlesRelativePath()
{
var webClient = mocks.StrictMock<WebClient>();
View
50 project/UnitTests/Remote/DefaultWebClientTests.cs
@@ -0,0 +1,50 @@
+namespace ThoughtWorks.CruiseControl.UnitTests.Remote
+{
+ using NUnit.Framework;
+ using CruiseControl.Remote;
+ using System;
+ using System.Collections.Specialized;
+ using System.Net;
+ using Rhino.Mocks;
+
+ [TestFixture]
+ public class DefaultWebClientTests
+ {
+ private readonly MockRepository _mocks = new MockRepository();
+ private WebClient _mockClient;
+ private DefaultWebClient _client;
+ private Uri _uri;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _mockClient = _mocks.DynamicMock<WebClient>();
+ SetupResult.For(_mockClient.Credentials).PropertyBehavior();
+ _client = new DefaultWebClient(_mockClient);
+ _uri = new Uri("http://test1:test2@test3/");
+ }
+
+ [Test]
+ public void UploadValuesSetsCredentialsOnWebClientWhenUserInfoInUrl()
+ {
+ _client.UploadValues(_uri, "", new NameValueCollection());
+
+ AssertCredentialsMatch();
+ }
+
+ [Test]
+ public void UploadValuesAsyncSetsCredentialsOnWebClientWhenUserInfoInUrl()
+ {
+ _client.UploadValuesAsync(_uri, "", new NameValueCollection());
+
+ AssertCredentialsMatch();
+ }
+
+ private void AssertCredentialsMatch()
+ {
+ var cred = _mockClient.Credentials.GetCredential(_uri, "Basic");
+ Assert.AreEqual("test1", cred.UserName);
+ Assert.AreEqual("test2", cred.Password);
+ }
+ }
+}
View
1 project/UnitTests/UnitTests.csproj
@@ -429,6 +429,7 @@
<Compile Include="IntegrationTests\TriggerTests.cs" />
<Compile Include="Remote\CommunicationsEventArgsTests.cs" />
<Compile Include="Remote\CruiseServerClientBaseTests.cs" />
+ <Compile Include="Remote\DefaultWebClientTests.cs" />
<Compile Include="Remote\EncryptingConnectionTests.cs" />
<Compile Include="Remote\HttpConnectionTests.cs" />
<Compile Include="Remote\ItemStatusTests.cs" />

0 comments on commit 39bd9d0

Please sign in to comment.
Something went wrong with that request. Please try again.