Browse files

Create a SSH key pair when initializing repositories

* Updated SSHKeyManager to allow multiple invocation of SetPrivateKey
* Extract public key from private key when invoked
* Support back compat by extracting public key from private key when
  GetKey is invoked

Fixes #408
  • Loading branch information...
1 parent 7f729e9 commit 0ba5181bd914672f7686eb8362780883eb17011d @pranavkm pranavkm committed Mar 26, 2013
View
7 Kudu.Client/SSHKey/RemoteSSHKeyManager.cs
@@ -23,12 +23,9 @@ public Task SetPrivateKey(string key)
return Client.PutAsync(String.Empty, param);
}
- public async Task<string> GetPublicKey(string key)
+ public Task<string> GetPublicKey()
{
- var param = new KeyValuePair<string, string>("key", key);
- string publicKey = await Client.PostJsonAsync<KeyValuePair<string, string>, string>(String.Empty, param);
-
- return publicKey;
+ return Client.GetJsonAsync<string>("");
}
}
}
View
19 Kudu.Contracts/SSHKey/ISSHKeyManager.cs
@@ -1,10 +1,23 @@
-using System;
-
+
namespace Kudu.Core.SSHKey
{
public interface ISSHKeyManager
{
+ /// <summary>
+ /// Sets a private key
+ /// </summary>
void SetPrivateKey(string key);
- string GetOrCreateKey(bool forceCreate);
+
+ /// <summary>
+ /// Reads an exisiting public key or generates a new key pair and returns it.
+ /// </summary>
+ /// <returns></returns>
+ string GetKey();
+
+ /// <summary>
+ /// Create a new key pair overwritting any existing key files on disk.
+ /// </summary>
+ /// <returns></returns>
+ string CreateKey();
}
}
View
1 Kudu.Core.Test/Kudu.Core.Test.csproj
@@ -73,6 +73,7 @@
<Compile Include="PathUtilityFacts.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PurgeDeploymentsTests.cs" />
+ <Compile Include="RepositoryFactoryFacts.cs" />
<Compile Include="SSHKeyManagerFacts.cs" />
</ItemGroup>
<ItemGroup>
View
32 Kudu.Core.Test/RepositoryFactoryFacts.cs
@@ -0,0 +1,32 @@
+using System;
+using Kudu.Contracts.Settings;
+using Kudu.Core.SourceControl;
+using Kudu.Core.SSHKey;
+using Kudu.Core.Tracing;
+using Moq;
+using Xunit;
+using Xunit.Extensions;
+
+namespace Kudu.Core.Test
+{
+ public class RepositoryFactoryFacts
+ {
+ [Theory]
+ [InlineData(RepositoryType.Git, false, true, "Expected a 'Git' repository but found a 'Mercurial' repository at path ''.")]
+ [InlineData(RepositoryType.Mercurial, true, false, "Expected a 'Mercurial' repository but found a 'Git' repository at path ''.")]
+ public void EnsuringGitRepositoryThrowsIfDifferentRepositoryAlreadyExists(RepositoryType repoType, bool isGit, bool isMercurial, string message)
+ {
+ // Arrange
+ var repoFactory = new Mock<RepositoryFactory>(Mock.Of<IEnvironment>(), Mock.Of<IDeploymentSettingsManager>(), Mock.Of<ITraceFactory>(), Mock.Of<ISSHKeyManager>()) { CallBase = true };
+ repoFactory.SetupGet(f => f.IsGitRepository)
+ .Returns(isGit);
+ repoFactory.SetupGet(f => f.IsHgRepository)
+ .Returns(isMercurial);
+
+ // Act and Assert
+ var ex = Assert.Throws<InvalidOperationException>(() => repoFactory.Object.EnsureRepository(repoType));
+
+ Assert.Equal(message, ex.Message);
+ }
+ }
+}
View
334 Kudu.Core.Test/SSHKeyManagerFacts.cs
@@ -1,13 +1,22 @@
using System;
+using System.Collections.Generic;
using System.IO.Abstractions;
using System.Security.Cryptography;
using Moq;
using Xunit;
+using Xunit.Extensions;
namespace Kudu.Core.SSHKey.Test
{
public class SSHKeyManagerFacts
{
+ private const string _privateKey = @"-----BEGIN RSA PRIVATE KEY-----
+MIGpAgEAAiEAuP52TyQ82vNoHmlxc3bFZnPBBguVXwp/LX4/IAWyEUUCASUCIFT/
+Sx1xg76Lgt2KZI7/OBmHRuKr8nGmemgPyMdnd9MtAhEA9wWeggZekx/tUBIZAE2J
+ZQIRAL+3tm2sgZSREmYsvm992mECEHgsP0YsnLZG4ib0DCmpLhUCEQCwLEbFpXAn
+p+dkzyuJC91tAhEAqZQQlB/blelwf7hrrbEOfw==
+-----END RSA PRIVATE KEY-----
+";
[Fact]
public void ConstructorThrowsIfEnvironmentIsNull()
{
@@ -36,14 +45,17 @@ public void SetPrivateKeySetsByPassKeyCheckAndKeyIfFile()
{
// Arrange
string sshPath = @"x:\path\.ssh";
- var fileBase = new Mock<FileBase>(MockBehavior.Strict);
- fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub")).Returns(false).Verifiable();
- fileBase.Setup(s => s.WriteAllText(sshPath + @"\config", "HOST *\r\n StrictHostKeyChecking no")).Verifiable();
- fileBase.Setup(s => s.WriteAllText(sshPath + @"\id_rsa", "my super secret key")).Verifiable();
+ var fileBase = new Mock<FileBase>();
+ fileBase.Setup(s => s.WriteAllText(sshPath + @"\config", "HOST *\r\n StrictHostKeyChecking no"))
+ .Verifiable();
+ fileBase.Setup(s => s.WriteAllText(sshPath + @"\id_rsa", _privateKey))
+ .Verifiable();
+ fileBase.Setup(s => s.WriteAllText(sshPath + @"\id_rsa.pub", It.IsAny<string>()))
+ .Verifiable();
- var directory = new Mock<DirectoryBase>(MockBehavior.Strict);
+ var directory = new Mock<DirectoryBase>();
directory.Setup(d => d.Exists(sshPath)).Returns(true).Verifiable();
- var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ var fileSystem = new Mock<IFileSystem>();
fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
fileSystem.SetupGet(f => f.Directory).Returns(directory.Object);
@@ -53,7 +65,7 @@ public void SetPrivateKeySetsByPassKeyCheckAndKeyIfFile()
var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
// Act
- sshKeyManager.SetPrivateKey("my super secret key");
+ sshKeyManager.SetPrivateKey(_privateKey);
// Assert
fileBase.Verify();
@@ -64,7 +76,7 @@ public void SetPrivateKeyCreatesSSHDirectoryIfItDoesNotExist()
{
// Arrange
string sshPath = @"x:\path\.ssh";
- var fileBase = new Mock<FileBase>(MockBehavior.Strict);
+ var fileBase = new Mock<FileBase>();
fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub")).Returns(false).Verifiable();
fileBase.Setup(s => s.WriteAllText(sshPath + @"\config", "HOST *\r\n StrictHostKeyChecking no")).Verifiable();
fileBase.Setup(s => s.WriteAllText(sshPath + @"\id_rsa", "my super secret key")).Verifiable();
@@ -82,25 +94,43 @@ public void SetPrivateKeyCreatesSSHDirectoryIfItDoesNotExist()
var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
// Act
- sshKeyManager.SetPrivateKey("my super secret key");
+ sshKeyManager.SetPrivateKey(_privateKey);
// Assert
directory.Verify();
}
[Fact]
- public void SetPrivateKeyAllowsRepeatedInvocationIfNoPublicKeyIsPresent()
+ public void SetPrivateKeyAllowsRepeatedInvocation()
{
// Arrange
+ string key1 = @"-----BEGIN RSA PRIVATE KEY-----
+MIICWgIBAAKBgQCRLka+LfMP4esyl2Br+PEZ+QgQk8jDMa5rXVt+Idf/M1/2RVeW
+E/odUygVPmOfpHa7crCq4aXJwYjRHxaGlidKSYBzd+JI0UBx6+1S8cGwUlH7riMU
+ZAUyySIXpIkEpLTjYHbYsSXzblBh7olNm1vExaMaCh4m5D3tOs/kqCDbiwIBJQKB
+gFZS3fSKBiUei9jkYthqgYUQnQLwFoHmMFuDni9SZMFBJE09/LoZt0+052aTzIhv
+oIshmXpcp8QSNazGYGuzOfPnKtPwJikIZnEa36YEMorlrcG+sP25coqYWA3Q1US5
+XsfyXmxBib5Enx3up/JwyFyceLwZzhjPoK/etWg5zTb9AkEA7xbPft5FHWAQ98oN
+n9x1TXkAo/rRMNfUSSSCf4ZSFM0emNUvVx8SKEXcZhqWP1+VcDu9IE9PmITwvUe4
+m7WxVQJBAJtzEPxmvrFiortOCzOQOiV6kkly9ZKPo/QjrHRWM1gldIF3ORpZZxhz
+R45UQocITcKcTxt/3B0Td54/zjbX2V8CQCBPMMv0heFgAkr/oPntW/W2Z97O3f+u
+dqIZsMUf/UEUzMiLgvAY9J2ok2e+Z1SsDUaEnQRdvqXoc48zNJ9rlIECQAyaoIMq
+7N3zPaB78xIExnG91IJ+8VESkMDEn0ez9lNBTqK2o8PdvEBAssZZ2+FvYEA2L+15
+EdjYEJ4g2V5khz8CQQDgIy+FkYp3GWHCOjdCXG88KxiCgDa/Tz42f/9ecS/XyzK+
+AP1a+ov5cqO34vONhqb7iikB5o0X8Mm0hua4HlRu
+-----END RSA PRIVATE KEY-----
+";
+
+ string publicKey = @"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAkS5Gvi3zD+HrMpdga/jxGfkIEJPIwzGua11bfiHX/zNf9kVXlhP6HVMoFT5jn6R2u3KwquGlycGI0R8WhpYnSkmAc3fiSNFAcevtUvHBsFJR+64jFGQFMskiF6SJBKS042B22LEl825QYe6JTZtbxMWjGgoeJuQ97TrP5Kgg24s=";
+
string sshPath = @"x:\path\.ssh";
- var fileBase = new Mock<FileBase>(MockBehavior.Strict);
- fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub")).Returns(false);
+ var fileBase = new Mock<FileBase>();
fileBase.Setup(s => s.WriteAllText(sshPath + @"\config", "HOST *\r\n StrictHostKeyChecking no"));
fileBase.Setup(s => s.WriteAllText(sshPath + @"\id_rsa", It.IsAny<string>()));
- var directory = new Mock<DirectoryBase>(MockBehavior.Strict);
+ var directory = new Mock<DirectoryBase>();
directory.Setup(d => d.Exists(sshPath)).Returns(true);
- var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ var fileSystem = new Mock<IFileSystem>();
fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
fileSystem.SetupGet(f => f.Directory).Returns(directory.Object);
@@ -110,20 +140,23 @@ public void SetPrivateKeyAllowsRepeatedInvocationIfNoPublicKeyIsPresent()
var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
// Act
- sshKeyManager.SetPrivateKey("key 1");
- sshKeyManager.SetPrivateKey("key 2");
+ sshKeyManager.SetPrivateKey(key1);
+ sshKeyManager.SetPrivateKey(_privateKey);
// Assert
fileBase.Verify(s => s.WriteAllText(sshPath + @"\id_rsa", It.IsAny<string>()), Times.Exactly(2));
+ fileBase.Verify(s => s.WriteAllText(sshPath + @"\id_rsa.pub", publicKey));
}
[Fact]
- public void SetPrivateKeyThrowsIfAPublicKeyAlreadyExistsOnFileSystem()
+ public void GetSSHKeyReturnsExistingKeyIfPresentOnDisk()
{
// Arrange
string sshPath = @"x:\path\.ssh";
+ string expected = "my-public-key";
var fileBase = new Mock<FileBase>(MockBehavior.Strict);
- fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub")).Returns(true).Verifiable();
+ fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub")).Returns(true);
+ fileBase.Setup(s => s.ReadAllText(sshPath + "\\id_rsa.pub")).Returns(expected);
var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
@@ -133,20 +166,29 @@ public void SetPrivateKeyThrowsIfAPublicKeyAlreadyExistsOnFileSystem()
var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
- // Act and Assert
- var ex = Assert.Throws<InvalidOperationException>(() => sshKeyManager.SetPrivateKey("my super secret key"));
- Assert.Equal("Cannot set key. A key pair already exists on disk. To generate a new key set 'forceCreate' to true.", ex.Message);
+ // Act
+ var actual = sshKeyManager.GetKey();
+
+ // Assert
+ Assert.Equal(expected, actual);
}
[Fact]
- public void GetSSHKeyReturnsExistingKeyIfPresentOnDisk()
+ public void GetSSHKeyCreatesKeyIfPublicAndPrivateKeyDoesNotAlreadyExist()
{
// Arrange
string sshPath = @"x:\path\.ssh";
- string expected = "my-public-key";
+ string keyOnDisk = null;
var fileBase = new Mock<FileBase>(MockBehavior.Strict);
- fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub")).Returns(true);
- fileBase.Setup(s => s.ReadAllText(sshPath + "\\id_rsa.pub")).Returns(expected);
+ fileBase.Setup(s => s.Exists(It.IsAny<string>()))
+ .Returns(false);
+ fileBase.Setup(s => s.WriteAllText(sshPath + "\\id_rsa.pub", It.IsAny<string>()))
+ .Callback((string name, string value) => { keyOnDisk = value; })
+ .Verifiable();
+ fileBase.Setup(s => s.WriteAllText(sshPath + "\\id_rsa", It.IsAny<string>()))
+ .Verifiable();
+ fileBase.Setup(s => s.WriteAllText(sshPath + @"\config", "HOST *\r\n StrictHostKeyChecking no"))
+ .Verifiable();
var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
@@ -157,26 +199,24 @@ public void GetSSHKeyReturnsExistingKeyIfPresentOnDisk()
var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
// Act
- var actual = sshKeyManager.GetOrCreateKey(forceCreate: false);
+ var actual = sshKeyManager.GetKey();
// Assert
- Assert.Equal(expected, actual);
+ fileBase.Verify();
+ Assert.Equal(keyOnDisk, actual);
}
[Fact]
- public void GetSSHKeyCreatesKeyIfForceCreateIsSet()
+ public void GetSSHKeyReturnsPublicKeyIfItExists()
{
// Arrange
string sshPath = @"x:\path\.ssh";
- string keyOnDisk = null;
+ string publicKey = "this-is-my-public-key";
var fileBase = new Mock<FileBase>(MockBehavior.Strict);
- fileBase.Setup(s => s.WriteAllText(sshPath + "\\id_rsa.pub", It.IsAny<string>()))
- .Callback((string name, string value) => { keyOnDisk = value; })
- .Verifiable();
- fileBase.Setup(s => s.WriteAllText(sshPath + "\\id_rsa", It.IsAny<string>()))
- .Verifiable();
- fileBase.Setup(s => s.WriteAllText(sshPath + @"\config", "HOST *\r\n StrictHostKeyChecking no"))
- .Verifiable();
+ fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa.pub"))
+ .Returns(true);
+ fileBase.Setup(s => s.ReadAllText(sshPath + "\\id_rsa.pub"))
+ .Returns(publicKey);
var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
@@ -187,11 +227,58 @@ public void GetSSHKeyCreatesKeyIfForceCreateIsSet()
var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
// Act
- var actual = sshKeyManager.GetOrCreateKey(forceCreate: true);
+ var actual = sshKeyManager.GetKey();
// Assert
- fileBase.Verify();
- Assert.Equal(keyOnDisk, actual);
+ Assert.Equal(publicKey, actual);
+ }
+
+ [Fact]
+ public void GetSSHKeyDecodesPublicKeyFromPrivateKeyIfItExists()
+ {
+ // Arrange
+ string sshPath = @"x:\path\.ssh";
+ string privateKey = @"-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDGpVEX9AjH9sAZvORj6lU8BD8E/VY9nqFtZRlOvItLc4n6ssmZ
+ZN7bvGGij+y5MRSvrI5pRV5LFODTQu/CyLWqx3Wkxxm2OwHwIeSF2eyntbGoZgjL
+SKX0N0IzLqa/xnzQl/S3zBqGinoqVvnBEomnGMjIgQiDFTBa4ykG5LC4fQIDAQAB
+AoGAKaWHNupm3OWSqNK9X2VFsWuCet1SM2EKnxDPGX7WBV+X0gOh2JMZViBMp/Rc
+wQbVO2+F+/QbLMqXyDMEaWYDEAhqBeF2VPKuoHPWyxpiOxYUiqgskB7FH4QWdml2
+eAZp5DGL1f98JMGpb2NVqe2+Dxg92Yf7aKwjlf8OGVrKJVECQQDjDgRuUXpQHsoT
+d1UVk7HIAhWLFEW45l/ueI+OwKvT/HxK9DfIxhrXg5OwvjvbIpO8MisQJuYfwB2O
+7z+FYcz/AkEA3/gqdG19aGYIuKj7Fe38tGGt5S4NEFvR0i2up+5lr9aoTZj9moFI
+CqP+Ojkvvr5n1RSueTX3JQ5rorNmLDEugwJAVxvOmWBK86gMUNGMY/3Iy/n4t+Xs
+JdbEYSIBuXuzsF2CdeMh77YJIDuLktg48IZgdWgt20GBMhcrf+XL0elGkwJBAJVU
+MXpPRj5FSatVf5Ovib37IqabfbpafhtUug7dtI744F5ckzpg2Fe/39GSL3NOIIzB
+rVLD2HSsmLdyRb1RTWECQQDZ20WQgZk43lFZyrl0A4bjaeO9vOtElQ66Hasdur1J
+9OzikqfmQg95sRA6oXENazTTmnTruaK6ZEaEtndujEl7
+-----END RSA PRIVATE KEY-----
+";
+ var publicKeyParams = new RSAParameters
+ {
+ Exponent = new byte[] { 1, 0, 1 },
+ Modulus = Convert.FromBase64String("xqVRF/QIx/bAGbzkY+pVPAQ/BP1WPZ6hbWUZTryLS3OJ+rLJmWTe27xhoo/suTEUr6yOaUVeSxTg00Lvwsi1qsd1pMcZtjsB8CHkhdnsp7WxqGYIy0il9DdCMy6mv8Z80Jf0t8wahop6Klb5wRKJpxjIyIEIgxUwWuMpBuSwuH0=")
+ };
+ string publicKey = SSHEncoding.GetString(publicKeyParams);
+ var fileBase = new Mock<FileBase>();
+ fileBase.Setup(s => s.Exists(sshPath + "\\id_rsa"))
+ .Returns(true);
+ fileBase.Setup(s => s.ReadAllText(sshPath + "\\id_rsa"))
+ .Returns(privateKey);
+
+ var fileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ fileSystem.SetupGet(f => f.File).Returns(fileBase.Object);
+
+ var environment = new Mock<IEnvironment>();
+ environment.SetupGet(e => e.SSHKeyPath).Returns(sshPath);
+
+ var sshKeyManager = new SSHKeyManager(environment.Object, fileSystem.Object, traceFactory: null);
+
+ // Act
+ var actual = sshKeyManager.GetKey();
+
+ // Assert
+ Assert.Equal(publicKey, actual);
}
[Fact]
@@ -250,5 +337,174 @@ public void GetPEMEncodedStringEncodesPrivateKey()
// Assert
Assert.Equal(expected, output);
}
+
+
+ public static IEnumerable<object[]> PrivateKey
+ {
+ get
+ {
+ yield return new object[]
+ {
+ "yhP4LGiDCwMvd/Q+aQz++0RE6swY9aiVNxa+teB/JODRLMaMXHJU4ZLOrx3swBDzyCfQ2p7v/p0MBIWJEwsLU6JRHQLlAsOevpXw0oxSpqe/MQGNCyBdSylD56TZSFs/L5p+B1Or9cU+AG/XMGhOR80KrFYwIVPDZPff7ZOmaBM=",
+ new byte[] { 1, 0, 1 },
+ @"-----BEGIN RSA PRIVATE KEY-----
+MIICXgIBAAKBgQDKE/gsaIMLAy939D5pDP77RETqzBj1qJU3Fr614H8k4NEsxoxc
+clThks6vHezAEPPIJ9Danu/+nQwEhYkTCwtTolEdAuUCw56+lfDSjFKmp78xAY0L
+IF1LKUPnpNlIWz8vmn4HU6v1xT4Ab9cwaE5HzQqsVjAhU8Nk99/tk6ZoEwIDAQAB
+AoGBAIzky347CFMfT3N1aiZYl1edy+dhkm2FszQLucCZ3ExcK7vqW2cBmEkG0PCs
+DqwDpdWCXU5wzqhZ200zxdTvOF9CS/GbQ3uYb1A/CxwPN9/FtwNenh0NU0Z2CFgp
++4QsU83hYGtokYauWKkz3Taz9w8i5uCu4KEBdaloBiJH7fZxAkEA/8eVbuuWbY/S
+LBVZhDqeSbUB+VrG5aNZWGliKZZcbBx9FwdIXS1TI0YaN/S1Z9Sg9QqzBiyLRbhd
+G0FgT/3HTwJBAMpAinwLq9iuRtuom3OShReZ1ireECEh4hOkVUPA9ymR0Nx2mIvz
+2s3OTxgJrvmhWyHI2QVtJhsA5YUzYsCU4f0CQAXAtWmzPsTkETQQntzMfLbnrU2w
+bvzHOcE1TZHl4dpEocOc1FHULSSD9R8BD/tv2tboELK42cENrnpodAQYjx0CQQDK
+KAzDxF62LCxDLpqCwHcrieaZ3nA8zcNNYrqfCGeEM22SjzAW411WzNod6r/sYC3Y
+7QqO8/RclV7U7vHMEIR5AkEAqqC8CgH3XWuBnXc935oGGvtc0hrKctSefnRBv/HI
+n76PtB67TZo31AX3NBvZNenRyYaG8fgPUhsSU/yA6WydLQ==
+-----END RSA PRIVATE KEY-----
+"
+ };
+
+ yield return new object[]
+ {
+
+ "xqVRF/QIx/bAGbzkY+pVPAQ/BP1WPZ6hbWUZTryLS3OJ+rLJmWTe27xhoo/suTEUr6yOaUVeSxTg00Lvwsi1qsd1pMcZtjsB8CHkhdnsp7WxqGYIy0il9DdCMy6mv8Z80Jf0t8wahop6Klb5wRKJpxjIyIEIgxUwWuMpBuSwuH0=",
+ new byte[] { 1, 0, 1 },
+ @"-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDGpVEX9AjH9sAZvORj6lU8BD8E/VY9nqFtZRlOvItLc4n6ssmZ
+ZN7bvGGij+y5MRSvrI5pRV5LFODTQu/CyLWqx3Wkxxm2OwHwIeSF2eyntbGoZgjL
+SKX0N0IzLqa/xnzQl/S3zBqGinoqVvnBEomnGMjIgQiDFTBa4ykG5LC4fQIDAQAB
+AoGAKaWHNupm3OWSqNK9X2VFsWuCet1SM2EKnxDPGX7WBV+X0gOh2JMZViBMp/Rc
+wQbVO2+F+/QbLMqXyDMEaWYDEAhqBeF2VPKuoHPWyxpiOxYUiqgskB7FH4QWdml2
+eAZp5DGL1f98JMGpb2NVqe2+Dxg92Yf7aKwjlf8OGVrKJVECQQDjDgRuUXpQHsoT
+d1UVk7HIAhWLFEW45l/ueI+OwKvT/HxK9DfIxhrXg5OwvjvbIpO8MisQJuYfwB2O
+7z+FYcz/AkEA3/gqdG19aGYIuKj7Fe38tGGt5S4NEFvR0i2up+5lr9aoTZj9moFI
+CqP+Ojkvvr5n1RSueTX3JQ5rorNmLDEugwJAVxvOmWBK86gMUNGMY/3Iy/n4t+Xs
+JdbEYSIBuXuzsF2CdeMh77YJIDuLktg48IZgdWgt20GBMhcrf+XL0elGkwJBAJVU
+MXpPRj5FSatVf5Ovib37IqabfbpafhtUug7dtI744F5ckzpg2Fe/39GSL3NOIIzB
+rVLD2HSsmLdyRb1RTWECQQDZ20WQgZk43lFZyrl0A4bjaeO9vOtElQ66Hasdur1J
+9OzikqfmQg95sRA6oXENazTTmnTruaK6ZEaEtndujEl7
+-----END RSA PRIVATE KEY-----
+"
+ };
+
+ yield return new object[]
+ {
+ "wmC8M8FGoMGZMImAy828M2cjlHOvOwAL+1EjnXbMjfU6xNaakuPs5gGmKyU2jeTDKufxyZ2bujM62JTllxePQFqByOXPfF6w2dfqO8SMDa6rb2Fdx2R6APjlwVfRPNR7YR0yfyZ0ysWJkBtRegBm/p8utalIgesC8um+bOYtgdoFD1IMcpp+Mx7HOp47Jfqu9DUzxEqb6lpXWlihTCNqeEpr4hDBlL9nNraSMllxnkWWBExPvTikHtiYSD/MqOeAhVt7eoMPmTISayJoa6ikMD6hbZtA4xoSwhbv3ltONebf3GjLjrm29l+rbmIpU30s1Zzuq2BC0Slv8DRoUAkzx8WsjtCsiD3kqdzKJznz4gjGNBWPE5vqaBDttRJTBrVq0WdBGyXg/rp3YN4gfGFxszlCihUed9YaEEYdMTx+bUZ1uRW9AnTi5u2w4ZDuPDYKMR2Y7oJmXpWXGnzGxDTqUxyMjNf5QFq7IbN+EifXU05Gst9t0GrjHWDGJg/99T4t",
+ new byte[] { 1, 0, 1},
+ @"-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
+"
+ };
+
+ yield return new object[]
+ {
+ "1YXT32rLPUWv3eDmv/B9ZWDm6LFx+RN+PJSgVga0gyyCNzuEiuRXJYT/rB2XG8KycQ+df//0eYZ2rcUr+HE8UsqUQTCo3oH4L2xReD6uPCpOzrBhzowrokLUyUn4SpPWoO9ikBZaj0GfA5RRQWaQ84rqK0cGCm1tCwmEkAjZkz1Zvjbix9fI+F2WqhMZuIwby+L69jRu8gA4fv2QQRJyTLd3QD/Hci0SggPQfaMheLOgK0b1PqxmZjm7SZNyeNSR5W72GYT/YZUsc3bmp/0OyfPngoFnUqM1Vs08RL3e3mZgz+wIAiNs02JQCPqIuUl7cKjVkbkpvGohMRVWqfAZngKpM58siwBke7N2Ll6dc+hl9S86jSirdA8He4pjq4N1J2THlSETkAyj0mDVtLzQVHh8naG9Nuk67kg3J6lmyLHBXd8HMQ2jFDrOps96tcieOGNykZd0phF653BTvhdZihLgsqCqb5wuSm06AJX3rKWB8VqrE1aK13bTI3V743Oj3nT1uc9Qmd9ghLyaPSqzrL34ls141WQRb3GKIB3pZE6J0I30hwZpGlm/TLWH5Ph3ei2MCRFAdTF76UlXGUzGixqqa3y+ksl92VUoWDZL2yVtV2iqBXfUJjwn2CHMA5aqaenZuO0nnyvFsClnJtpRj110ndAnnZc5OHf8E+ubKVk=",
+ new byte[] { 1, 0, 1 },
+ @"-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
+"
+ };
+
+ }
+ }
+
+ [Theory]
+ [PropertyData("PrivateKey")]
+ public void ExtractPublicKeysExtractsKeysFromPEMEncodedPrivateKey(string expectedModulus, byte[] expectedExponent, string privateKey)
+ {
+ // Act
+ RSAParameters output = PEMEncoding.ExtractPublicKey(privateKey);
+
+ // Assert
+ Assert.Equal(expectedModulus, Convert.ToBase64String(output.Modulus));
+ Assert.Equal(expectedExponent, output.Exponent);
+ }
}
}
View
185 Kudu.Core/SSHKey/PEMEncoder.cs
@@ -22,6 +22,46 @@ internal static class PEMEncoding
private static byte[] zeroAsn1 = new byte[] { 0x00 };
+
+ public static RSAParameters ExtractPublicKey(string privateKey)
+ {
+ string pem;
+ if (!TryParsePEM(privateKey, out pem))
+ {
+ throw new FormatException("Unsupported private key format.");
+ }
+
+ byte[] encoding = Convert.FromBase64String(pem);
+
+ using (var mem = new MemoryStream(encoding))
+ {
+
+ int tag = mem.ReadTag();
+ if (tag != (SequenceTag | ConstructedTag))
+ {
+ throw new ArgumentException("Unexpected tag in PEM!");
+ }
+
+ int length = mem.ReadLength(encoding.Length - (int)mem.Position);
+ if (length != (encoding.Length - (int)mem.Position))
+ {
+ throw new ArgumentException("Unexpected PEM length!");
+ }
+
+ byte[] version = mem.ReadAsn1(encoding.Length - (int)mem.Position);
+ if (version.Length != zeroAsn1.Length || version[0] != zeroAsn1[0])
+ {
+ throw new ArgumentException("Unsupported PEM version 0!");
+ }
+
+ return new RSAParameters
+ {
+ Modulus = FromAsn1(mem.ReadAsn1(encoding.Length - (int)mem.Position)),
+ Exponent = FromAsn1(mem.ReadAsn1(encoding.Length - (int)mem.Position))
+ };
+ }
+ }
+
/// <summary>
/// Get PEM encoding string
/// </summary>
@@ -104,6 +144,33 @@ private static byte[] ToAsn1(byte[] bytes)
return bytes;
}
+ // Convert ASN.1 to RSAParameters (big-endian)
+ private static byte[] FromAsn1(byte[] bytes)
+ {
+ Debug.Assert(bytes != null && bytes.Length > 0);
+
+ // find the highest bit that is not zero
+ byte highest = 0x00;
+ for (int i = 0; i < bytes.Length; ++i)
+ {
+ if (bytes[i] != 0x00)
+ {
+ highest = bytes[i];
+ break;
+ }
+ }
+
+ // pretty much reverse process of ToAsn1
+ if (0x80 == (highest & 0x80))
+ {
+ byte[] temp = new byte[bytes.Length - 1];
+ Array.Copy(bytes, 1, temp, 0, temp.Length);
+ bytes = temp;
+ }
+
+ return bytes;
+ }
+
private static void WriteLength(this MemoryStream mem, int length)
{
if (length > 127)
@@ -133,5 +200,123 @@ private static void WriteTag(this MemoryStream mem, int tag)
{
mem.WriteByte((byte)tag);
}
+
+ private static int ReadTag(this MemoryStream mem)
+ {
+ return mem.ReadByte();
+ }
+
+ private static bool TryParsePEM(string text, out string pem)
+ {
+ pem = null;
+ using (var reader = new StringReader(text.Trim()))
+ {
+ string line = reader.ReadLine();
+ if (line == null || !line.Trim().Equals(PEMHeader, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var builder = new StringBuilder();
+ // Read header
+ while ((line = reader.ReadLine()) != null && !String.IsNullOrEmpty(line = line.Trim()) && !line.Equals(PEMFooter, StringComparison.OrdinalIgnoreCase))
+ {
+ string[] header = line.Split(new string[] { ": " }, StringSplitOptions.None);
+ if (header.Length == 1)
+ {
+ builder.Append(header[0].Trim());
+ break;
+ }
+ else if (header.Length == 2)
+ {
+ if (header[0].Trim() == "Proc-Type" && header[1].Trim() == "4,ENCRYPTED")
+ {
+ throw new FormatException("Passphrase is not supported!");
+ }
+ }
+ else
+ {
+ throw new FormatException("Invalid PEM format!");
+ }
+ }
+
+ while ((line = reader.ReadLine()) != null && !string.IsNullOrEmpty(line = line.Trim()) && !line.Equals(PEMFooter, StringComparison.OrdinalIgnoreCase))
+ {
+ builder.Append(line);
+ }
+
+ if (line == null || !line.Equals(PEMFooter, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new FormatException("Missing PEM footer!");
+ }
+
+ pem = builder.ToString();
+ return pem.Length > 0;
+ }
+ }
+
+ private static byte[] ReadAsn1(this MemoryStream mem, int limit)
+ {
+ int tag = mem.ReadTag();
+ if (tag != IntegerTag)
+ {
+ throw new ArgumentException("Invalid Integer tag!");
+ }
+
+ int length = mem.ReadLength(limit);
+ byte[] bytes = new byte[length];
+ mem.Read(bytes, 0, bytes.Length);
+ return bytes;
+ }
+
+ private static int ReadLength(this MemoryStream mem, int limit)
+ {
+ int length = mem.ReadByte();
+ if (length < 0)
+ {
+ throw new ArgumentException("EOF found when length expected");
+ }
+
+ if (length == 0x80)
+ {
+ throw new ArgumentException("Indefinite-length encoding");
+ }
+
+ if (length > 127)
+ {
+ int size = length & 0x7f;
+
+ // Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
+ if (size > 4)
+ {
+ throw new ArgumentException("DER length more than 4 bytes: " + size);
+ }
+
+ length = 0;
+ for (int i = 0; i < size; i++)
+ {
+ int next = mem.ReadByte();
+
+ if (next < 0)
+ {
+ throw new ArgumentException("EOF found reading length");
+ }
+
+ length = (length << 8) + next;
+ }
+
+ if (length < 0)
+ {
+ throw new ArgumentException("Corrupted stream - negative length found");
+ }
+
+ if (length >= limit) // after all we must have read at least 1 byte
+ {
+ throw new ArgumentException("Corrupted stream - out of bounds length found");
+ }
+ }
+
+ return length;
+ }
}
}
View
73 Kudu.Core/SSHKey/SSHKeyManager.cs
@@ -47,11 +47,8 @@ public void SetPrivateKey(string key)
ITracer tracer = _traceFactory.GetTracer();
using (tracer.Step("SSHKeyManager.SetPrivateKey"))
{
- if (_fileSystem.File.Exists(_id_rsaPub))
- {
- // If we have a public key on disk, we will disallow the ability to set a private key.
- throw new InvalidOperationException(Resources.Error_KeyAlreadyExists);
- }
+ RSAParameters publicKeyParameters = PEMEncoding.ExtractPublicKey(key);
+ string publicKey = SSHEncoding.GetString(publicKeyParameters);
FileSystemHelpers.EnsureDirectory(_fileSystem, _sshPath);
@@ -60,51 +57,75 @@ public void SetPrivateKey(string key)
// This overrides if file exists
_fileSystem.File.WriteAllText(_id_rsa, key);
+
+ _fileSystem.File.WriteAllText(_id_rsaPub, publicKey);
}
}
/// <summary>
/// Gets an existing created public key or creates a new one and returns the public key
/// </summary>
- public string GetOrCreateKey(bool forceCreate)
+ public string GetKey()
{
ITracer tracer = _traceFactory.GetTracer();
- using (tracer.Step("SSHKeyManager.CreatePrivateKey"))
+ using (tracer.Step("SSHKeyManager.GetKey"))
{
- if (!forceCreate && _fileSystem.File.Exists(_id_rsaPub))
+ if (_fileSystem.File.Exists(_id_rsaPub))
{
+ tracer.Trace("Public key exists.");
+ // If a public key exists, return it.
return _fileSystem.File.ReadAllText(_id_rsaPub);
}
+ if (_fileSystem.File.Exists(_id_rsa))
+ {
+ tracer.Trace("Private key exists without public key.");
+ // If a private key exists without a public key, extract the public key to disk and return it.
+ // This might occur in back-compat scenarios where we've an existing private key from pre S21 scenarios.
+ string privateKey = _fileSystem.File.ReadAllText(_id_rsa);
+ RSAParameters publicKeyParameters = PEMEncoding.ExtractPublicKey(privateKey);
+
+ string publicKey = SSHEncoding.GetString(publicKeyParameters);
+ _fileSystem.File.WriteAllText(_id_rsaPub, publicKey);
+
+ return publicKey;
+ }
+
+ tracer.Trace("Generating SSH key pair.");
+ // Neither exists. Proceed to create a new key-pair.
return CreateKey();
}
}
- private string CreateKey()
+ public string CreateKey()
{
- RSACryptoServiceProvider rsa = null;
- try
+ ITracer tracer = _traceFactory.GetTracer();
+ using (tracer.Step("SSHKeyManager.CreateKey"))
{
- rsa = new RSACryptoServiceProvider(dwKeySize: KeySize);
- RSAParameters privateKeyParam = rsa.ExportParameters(includePrivateParameters: true);
- RSAParameters publicKeyParam = rsa.ExportParameters(includePrivateParameters: false);
+ RSACryptoServiceProvider rsa = null;
+ try
+ {
+ rsa = new RSACryptoServiceProvider(dwKeySize: KeySize);
+ RSAParameters privateKeyParam = rsa.ExportParameters(includePrivateParameters: true);
+ RSAParameters publicKeyParam = rsa.ExportParameters(includePrivateParameters: false);
- string privateKey = PEMEncoding.GetString(privateKeyParam);
- string publicKey = SSHEncoding.GetString(publicKeyParam);
+ string privateKey = PEMEncoding.GetString(privateKeyParam);
+ string publicKey = SSHEncoding.GetString(publicKeyParam);
- _fileSystem.File.WriteAllText(_id_rsa, privateKey);
- _fileSystem.File.WriteAllText(_id_rsaPub, publicKey);
+ _fileSystem.File.WriteAllText(_id_rsa, privateKey);
+ _fileSystem.File.WriteAllText(_id_rsaPub, publicKey);
- _fileSystem.File.WriteAllText(_config, ConfigContent);
+ _fileSystem.File.WriteAllText(_config, ConfigContent);
- return publicKey;
- }
- finally
- {
- if (rsa != null)
+ return publicKey;
+ }
+ finally
{
- rsa.PersistKeyInCsp = false;
- rsa.Dispose();
+ if (rsa != null)
+ {
+ rsa.PersistKeyInCsp = false;
+ rsa.Dispose();
+ }
}
}
}
View
34 Kudu.Core/SourceControl/RepositoryFactory.cs
@@ -7,6 +7,7 @@
using Kudu.Contracts.Tracing;
using Kudu.Core.Infrastructure;
using Kudu.Core.SourceControl.Git;
+using Kudu.Core.SSHKey;
using Kudu.Core.Tracing;
namespace Kudu.Core.SourceControl
@@ -17,18 +18,20 @@ public class RepositoryFactory : IRepositoryFactory
private readonly IEnvironment _environment;
private readonly ITraceFactory _traceFactory;
private readonly IDeploymentSettingsManager _settings;
+ private readonly ISSHKeyManager _sshKeyManager;
- public RepositoryFactory(IEnvironment environment, IDeploymentSettingsManager settings, ITraceFactory traceFactory)
+ public RepositoryFactory(IEnvironment environment, IDeploymentSettingsManager settings, ITraceFactory traceFactory, ISSHKeyManager sshKeyManager)
{
_environment = environment;
_settings = settings;
_traceFactory = traceFactory;
+ _sshKeyManager = sshKeyManager;
}
/// <summary>
/// Hieruistically guesses if there's a Mercurial repository at the repositoryPath
/// </summary>
- private bool IsHgRepository
+ public virtual bool IsHgRepository
{
get
{
@@ -38,45 +41,44 @@ private bool IsHgRepository
}
}
- private bool IsGitRepository
+ public virtual bool IsGitRepository
{
get
{
string gitRepoFiles = Path.Combine(_environment.RepositoryPath, ".git");
return Directory.Exists(gitRepoFiles) &&
- Directory.GetFiles(gitRepoFiles).Length > 0;
+ Directory.EnumerateFiles(gitRepoFiles).Any();
}
}
public IRepository EnsureRepository(RepositoryType repositoryType)
{
+ IRepository repository;
if (repositoryType == RepositoryType.Mercurial)
{
if (IsGitRepository)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_MismatchRepository, repositoryType, RepositoryType.Git, _environment.RepositoryPath));
}
FileSystemHelpers.EnsureDirectory(_environment.RepositoryPath);
- var hgRepository = new HgRepository(_environment.RepositoryPath, _environment.SiteRootPath, _settings, _traceFactory);
- if (!hgRepository.Exists)
- {
- hgRepository.Initialize();
- }
- return hgRepository;
+ repository = new HgRepository(_environment.RepositoryPath, _environment.SiteRootPath, _settings, _traceFactory);
}
else
{
if (IsHgRepository)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.Error_MismatchRepository, repositoryType, RepositoryType.Mercurial, _environment.RepositoryPath));
}
- var gitRepository = new GitExeRepository(_environment.RepositoryPath, _environment.SiteRootPath, _settings, _traceFactory);
- if (!gitRepository.Exists)
- {
- gitRepository.Initialize();
- }
- return gitRepository;
+ repository = new GitExeRepository(_environment.RepositoryPath, _environment.SiteRootPath, _settings, _traceFactory);
+ }
+
+ if (!repository.Exists)
+ {
+ repository.Initialize();
+ // Attempt to create a key pair when creating a repository. This would allow for fetching SSH-based repository urls to work.
+ _sshKeyManager.GetKey();
}
+ return repository;
}
public IRepository GetRepository()
View
13 Kudu.FunctionalTests/DeploymentManagerTests.cs
@@ -272,12 +272,12 @@ public void DeleteKuduSiteCleansProperly()
}
[Fact]
- public void PullApiTestGitHubFormat()
+ public async Task PullApiTestGitHubFormat()
{
string githubPayload = @"{ ""after"": ""ea1c6d7ea669c816dd5f86206f7b47b228fdcacd"", ""before"": ""7e2a599e2d28665047ec347ab36731c905c95e8b"", ""commits"": [ { ""added"": [], ""author"": { ""email"": ""prkrishn@hotmail.com"", ""name"": ""Pranav K"", ""username"": ""pranavkm"" }, ""id"": ""43acf30efa8339103e2bed5c6da1379614b00572"", ""message"": ""Changes from master again"", ""modified"": [ ""Hello.txt"" ], ""timestamp"": ""2012-12-17T17:32:15-08:00"" } ], ""compare"": ""https://github.com/KuduApps/GitHookTest/compare/7e2a599e2d28...7e2a599e2d28"", ""created"": false, ""deleted"": false, ""forced"": false, ""head_commit"": { ""added"": [ "".gitignore"", ""SimpleWebApplication.sln"", ""SimpleWebApplication/About.aspx"", ""SimpleWebApplication/About.aspx.cs"", ""SimpleWebApplication/About.aspx.designer.cs"", ""SimpleWebApplication/Account/ChangePassword.aspx"", ""SimpleWebApplication/Account/ChangePassword.aspx.cs"", ""SimpleWebApplication/Account/ChangePassword.aspx.designer.cs"", ""SimpleWebApplication/Account/ChangePasswordSuccess.aspx"", ""SimpleWebApplication/Account/ChangePasswordSuccess.aspx.cs"", ""SimpleWebApplication/Account/ChangePasswordSuccess.aspx.designer.cs"", ""SimpleWebApplication/Account/Login.aspx"", ""SimpleWebApplication/Account/Login.aspx.cs"", ""SimpleWebApplication/Account/Login.aspx.designer.cs"", ""SimpleWebApplication/Account/Register.aspx"", ""SimpleWebApplication/Account/Register.aspx.cs"", ""SimpleWebApplication/Account/Register.aspx.designer.cs"", ""SimpleWebApplication/Account/Web.config"", ""SimpleWebApplication/Default.aspx"", ""SimpleWebApplication/Default.aspx.cs"", ""SimpleWebApplication/Default.aspx.designer.cs"", ""SimpleWebApplication/Global.asax"", ""SimpleWebApplication/Global.asax.cs"", ""SimpleWebApplication/Properties/AssemblyInfo.cs"", ""SimpleWebApplication/Scripts/jquery-1.4.1-vsdoc.js"", ""SimpleWebApplication/Scripts/jquery-1.4.1.js"", ""SimpleWebApplication/Scripts/jquery-1.4.1.min.js"", ""SimpleWebApplication/SimpleWebApplication.csproj"", ""SimpleWebApplication/Site.Master"", ""SimpleWebApplication/Site.Master.cs"", ""SimpleWebApplication/Site.Master.designer.cs"", ""SimpleWebApplication/Styles/Site.css"", ""SimpleWebApplication/Web.Debug.config"", ""SimpleWebApplication/Web.Release.config"", ""SimpleWebApplication/Web.config"" ], ""author"": { ""email"": ""david.ebbo@microsoft.com"", ""name"": ""davidebbo"", ""username"": ""davidebbo"" }, ""committer"": { ""email"": ""david.ebbo@microsoft.com"", ""name"": ""davidebbo"", ""username"": ""davidebbo"" }, ""distinct"": false, ""id"": ""7e2a599e2d28665047ec347ab36731c905c95e8b"", ""message"": ""Initial"", ""modified"": [], ""removed"": [], ""timestamp"": ""2011-11-21T23:07:42-08:00"", ""url"": ""https://github.com/KuduApps/GitHookTest/commit/7e2a599e2d28665047ec347ab36731c905c95e8b"" }, ""pusher"": { ""name"": ""none"" }, ""ref"": ""refs/heads/master"", ""repository"": { ""created_at"": ""2012-06-28T00:07:55-07:00"", ""description"": """", ""fork"": false, ""forks"": 1, ""has_downloads"": true, ""has_issues"": true, ""has_wiki"": true, ""language"": ""ASP"", ""name"": ""GitHookTest"", ""open_issues"": 0, ""organization"": ""KuduApps"", ""owner"": { ""email"": ""kuduapps@hotmail.com"", ""name"": ""KuduApps"" }, ""private"": false, ""pushed_at"": ""2012-06-28T00:11:48-07:00"", ""size"": 188, ""url"": ""https://github.com/KuduApps/SimpleWebApplication"", ""watchers"": 1 } }";
string appName = "PullApiTestGitHubFormat";
- ApplicationManager.Run(appName, appManager =>
+ await ApplicationManager.RunAsync(appName, async appManager =>
{
var post = new Dictionary<string, string>
{
@@ -290,11 +290,18 @@ public void PullApiTestGitHubFormat()
return client.PostAsync("deploy?scmType=GitHub", new FormUrlEncodedContent(post));
});
- var results = appManager.DeploymentManager.GetResultsAsync().Result.ToList();
+ var resultsTask = appManager.DeploymentManager.GetResultsAsync();
+ var sshKeyTask = appManager.SSHKeyManager.GetPublicKey();
+
+ await Task.WhenAll(resultsTask, sshKeyTask);
+
+ var results = resultsTask.Result.ToList();
Assert.Equal(1, results.Count);
Assert.Equal(DeployStatus.Success, results[0].Status);
Assert.Equal("GitHub", results[0].Deployer);
KuduAssert.VerifyUrl(appManager.SiteUrl, "Welcome to ASP.NET!");
+
+ Assert.True(sshKeyTask.Result.StartsWith("ssh-rsa"));
});
}
View
6 Kudu.Services.Test/SSHKeyControllerTests.cs
@@ -15,7 +15,7 @@ public void GetPublicKeyDoesNotForceRecreatePublicKeyByDefault()
// Arrange
var sshKeyManager = new Mock<ISSHKeyManager>(MockBehavior.Strict);
string expected = "public-key";
- sshKeyManager.Setup(s => s.GetOrCreateKey(It.Is<bool>(v => !v))).Returns(expected).Verifiable();
+ sshKeyManager.Setup(s => s.GetKey()).Returns(expected).Verifiable();
var tracer = Mock.Of<ITracer>();
var operationLock = new Mock<IOperationLock>();
operationLock.Setup(l => l.Lock()).Returns(true);
@@ -30,12 +30,12 @@ public void GetPublicKeyDoesNotForceRecreatePublicKeyByDefault()
}
[Fact]
- public void GetPublicKeyForcesRecreateIfParameterIsSet()
+ public void CreatePublicKeyForcesRecreateIfParameterIsSet()
{
// Arrange
var sshKeyManager = new Mock<ISSHKeyManager>(MockBehavior.Strict);
string expected = "public-key";
- sshKeyManager.Setup(s => s.GetOrCreateKey(It.Is<bool>(v => v))).Returns(expected).Verifiable();
+ sshKeyManager.Setup(s => s.CreateKey()).Returns(expected).Verifiable();
var tracer = Mock.Of<ITracer>();
var operationLock = new Mock<IOperationLock>();
operationLock.Setup(l => l.Lock()).Returns(true);
View
2 Kudu.Services.Web/App_Start/NinjectServices.cs
@@ -252,7 +252,7 @@ public static void RegisterRoutes(IKernel kernel, RouteCollection routes)
routes.MapHttpRoute("one-deployment-log-details", "deployments/{id}/log/{logId}", new { controller = "Deployment", action = "GetLogEntryDetails" });
// SSHKey
- routes.MapHttpRoute("post-sshkey", "sshkey", new { controller = "SSHKey", action = "GetPublicKey" }, new { verb = new HttpMethodConstraint("POST") });
+ routes.MapHttpRoute("get-sshkey", "sshkey", new { controller = "SSHKey", action = "GetPublicKey" }, new { verb = new HttpMethodConstraint("GET") });
routes.MapHttpRoute("put-sshkey", "sshkey", new { controller = "SSHKey", action = "SetPrivateKey" }, new { verb = new HttpMethodConstraint("PUT") });
// Environment
View
7 Kudu.Services/SSHKey/SSHKeyController.cs
@@ -1,13 +1,13 @@
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Kudu.Contracts.Infrastructure;
using Kudu.Contracts.Tracing;
using Kudu.Core.SSHKey;
using Newtonsoft.Json.Linq;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
namespace Kudu.Services.SSHKey
{
@@ -80,7 +80,6 @@ public void SetPrivateKey()
}
}
- [HttpPost]
public string GetPublicKey(bool forceCreate = false)
{
using (_tracer.Step("SSHKeyController.GetPublicKey"))
@@ -90,7 +89,7 @@ public string GetPublicKey(bool forceCreate = false)
{
try
{
- key = _sshKeyManager.GetOrCreateKey(forceCreate);
+ key = forceCreate ? _sshKeyManager.CreateKey() : _sshKeyManager.GetKey();
}
catch (InvalidOperationException ex)
{
View
23 Kudu.TestHarness/ApplicationManager.cs
@@ -151,12 +151,25 @@ public string GetKuduUpTime()
private static bool TestFailureOccurred = false;
public static void Run(string testName, Action<ApplicationManager> action)
{
- // If StopAfterFirstTestFailure is set, don't do anything after the first failure
- if (KuduUtils.StopAfterFirstTestFailure && TestFailureOccurred) return;
+ Func<ApplicationManager, Task> asyncAction = (appManager) =>
+ {
+ action(appManager);
+ return TaskHelpers.Completed();
+ };
+
+ RunAsync(testName, asyncAction).Wait();
+ }
+
+ public static async Task RunAsync(string testName, Func<ApplicationManager, Task> action)
+ {
+ if (KuduUtils.StopAfterFirstTestFailure && TestFailureOccurred)
+ {
+ return;
+ }
try
{
- RunNoCatch(testName, action);
+ await RunNoCatch(testName, action);
}
catch
{
@@ -165,7 +178,7 @@ public static void Run(string testName, Action<ApplicationManager> action)
}
}
- public static void RunNoCatch(string testName, Action<ApplicationManager> action)
+ public static async Task RunNoCatch(string testName, Func<ApplicationManager, Task> action)
{
TestTracer.Trace("Running test - {0}", testName);
@@ -195,7 +208,7 @@ public static void RunNoCatch(string testName, Action<ApplicationManager> action
{
using (StartLogStream(appManager))
{
- action(appManager);
+ await action(appManager);
}
KuduUtils.DownloadDump(appManager.ServiceUrl, dumpPath);

0 comments on commit 0ba5181

Please sign in to comment.