Skip to content

Commit 5f5e0d4

Browse files
committed
Add SSH key support with libssh2
1 parent a6489de commit 5f5e0d4

15 files changed

+527
-32
lines changed

LibGit2Sharp.Tests/GlobalSettingsFixture.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public void LoadFromSpecifiedPath(string architecture)
6161
{
6262
Skip.IfNot(Platform.IsRunningOnNetFramework(), ".NET Framework only test.");
6363

64-
var nativeDllFileName = NativeDllName.Name + ".dll";
6564
var testDir = Path.GetDirectoryName(typeof(GlobalSettingsFixture).Assembly.Location);
6665
var testAppExe = Path.Combine(testDir, $"NativeLibraryLoadTestApp.{architecture}.exe");
6766
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
@@ -71,12 +70,14 @@ public void LoadFromSpecifiedPath(string architecture)
7170
try
7271
{
7372
Directory.CreateDirectory(platformDir);
74-
File.Copy(Path.Combine(libraryPath, nativeDllFileName), Path.Combine(platformDir, nativeDllFileName));
73+
foreach (var file in Directory.GetFiles(libraryPath, "*.dll"))
74+
{
75+
File.Copy(file, Path.Combine(platformDir, Path.GetFileName(file)));
76+
}
7577

7678
var (output, exitCode) = ProcessHelper.RunProcess(testAppExe, arguments: $@"{NativeDllName.Name} ""{platformDir}""", workingDirectory: tempDir);
7779
78-
Assert.Empty(output);
79-
Assert.Equal(0, exitCode);
80+
Assert.True(exitCode == 0, $"Test app exited with code {exitCode}. Output: {output}");
8081
}
8182
finally
8283
{

LibGit2Sharp.Tests/NetworkFixture.cs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public void CanListRemoteReferences(string url)
2222
Remote remote = repo.Network.Remotes.Add(remoteName, url);
2323
IList<Reference> references = repo.Network.ListReferences(remote).ToList();
2424

25-
2625
foreach (var reference in references)
2726
{
2827
// None of those references point to an existing
@@ -136,6 +135,134 @@ public void CanListRemoteReferencesWithCredentials()
136135
}
137136
}
138137

138+
[Theory]
139+
[InlineData("http://github.com/libgit2/TestGitRepository")]
140+
[InlineData("https://github.com/libgit2/TestGitRepository")]
141+
public void CanListRemoteReferencesWithListRemoteOptions(string url)
142+
{
143+
string remoteName = "testRemote";
144+
145+
string repoPath = InitNewRepository();
146+
147+
using (var repo = new Repository(repoPath))
148+
{
149+
Remote remote = repo.Network.Remotes.Add(remoteName, url);
150+
var options = new ListRemoteOptions
151+
{
152+
ProxyOptions = new ProxyOptions()
153+
};
154+
155+
IList<Reference> references = repo.Network.ListReferences(remote, options).ToList();
156+
157+
foreach (var reference in references)
158+
{
159+
Assert.Null(reference.ResolveToDirectReference().Target);
160+
}
161+
162+
List<Tuple<string, string>> actualRefs = references.
163+
Select(directRef => new Tuple<string, string>(directRef.CanonicalName, directRef.ResolveToDirectReference()
164+
.TargetIdentifier)).ToList();
165+
166+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs.Count, actualRefs.Count);
167+
Assert.True(references.Single(reference => reference.CanonicalName == "HEAD") is SymbolicReference);
168+
for (int i = 0; i < TestRemoteRefs.ExpectedRemoteRefs.Count; i++)
169+
{
170+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2);
171+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1);
172+
}
173+
}
174+
}
175+
176+
[Theory]
177+
[InlineData("http://github.com/libgit2/TestGitRepository")]
178+
[InlineData("https://github.com/libgit2/TestGitRepository")]
179+
public void CanListRemoteReferencesFromUrlWithListRemoteOptions(string url)
180+
{
181+
string repoPath = InitNewRepository();
182+
183+
using (var repo = new Repository(repoPath))
184+
{
185+
var options = new ListRemoteOptions
186+
{
187+
ProxyOptions = new ProxyOptions()
188+
};
189+
190+
IList<Reference> references = repo.Network.ListReferences(url, options).ToList();
191+
192+
foreach (var reference in references)
193+
{
194+
Assert.Null(reference.ResolveToDirectReference().Target);
195+
}
196+
197+
List<Tuple<string, string>> actualRefs = references.
198+
Select(directRef => new Tuple<string, string>(directRef.CanonicalName, directRef.ResolveToDirectReference()
199+
.TargetIdentifier)).ToList();
200+
201+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs.Count, actualRefs.Count);
202+
Assert.True(references.Single(reference => reference.CanonicalName == "HEAD") is SymbolicReference);
203+
for (int i = 0; i < TestRemoteRefs.ExpectedRemoteRefs.Count; i++)
204+
{
205+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2);
206+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1);
207+
}
208+
}
209+
}
210+
211+
[Theory]
212+
[InlineData("https://github.com/libgit2/TestGitRepository")]
213+
[InlineData("git@github.com:libgit2/TestGitRepository")]
214+
public void CanListRemoteReferencesWithCertificateCheckCallback(string url)
215+
{
216+
string repoPath = InitNewRepository();
217+
218+
bool certificateCheckCalled = false;
219+
220+
using (var repo = new Repository(repoPath))
221+
{
222+
var options = new ListRemoteOptions
223+
{
224+
CertificateCheck = (cert, valid, host) =>
225+
{
226+
certificateCheckCalled = true;
227+
return true;
228+
}
229+
};
230+
231+
IList<Reference> references = repo.Network.ListReferences(url, options).ToList();
232+
233+
Assert.True(certificateCheckCalled);
234+
Assert.NotEmpty(references);
235+
}
236+
}
237+
238+
[SkippableFact]
239+
public void CanListRemoteReferencesWithCredentialsInListRemoteOptions()
240+
{
241+
InconclusiveIf(() => string.IsNullOrEmpty(Constants.PrivateRepoUrl),
242+
"Populate Constants.PrivateRepo* to run this test");
243+
244+
string remoteName = "origin";
245+
246+
string repoPath = InitNewRepository();
247+
248+
using (var repo = new Repository(repoPath))
249+
{
250+
Remote remote = repo.Network.Remotes.Add(remoteName, Constants.PrivateRepoUrl);
251+
252+
var options = new ListRemoteOptions
253+
{
254+
CredentialsProvider = Constants.PrivateRepoCredentials
255+
};
256+
257+
var references = repo.Network.ListReferences(remote, options);
258+
259+
foreach (var reference in references)
260+
{
261+
Assert.NotNull(reference);
262+
}
263+
}
264+
}
265+
139266
[Theory]
140267
[InlineData(FastForwardStrategy.Default)]
141268
[InlineData(FastForwardStrategy.NoFastForward)]

LibGit2Sharp.Tests/PushFixture.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ public void CanInvokePrePushCallbackAndFail()
135135
Assert.True(prePushHandlerCalled);
136136
}
137137

138+
[Fact]
139+
public void CanPushWithRemoteProgressCallback()
140+
{
141+
PushOptions options = new PushOptions()
142+
{
143+
OnPushStatusError = OnPushStatusError,
144+
OnPushRemoteProgress = (progress) => { return true; },
145+
};
146+
147+
AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options));
148+
}
149+
138150
[Fact]
139151
public void PushingABranchThatDoesNotTrackAnUpstreamBranchThrows()
140152
{

LibGit2Sharp.Tests/RepositoryFixture.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,30 @@ public void CanListRemoteReferences(string url)
724724
}
725725
}
726726

727+
[Theory]
728+
[InlineData("http://github.com/libgit2/TestGitRepository")]
729+
[InlineData("https://github.com/libgit2/TestGitRepository")]
730+
public void CanListRemoteReferencesWithListRemoteOptions(string url)
731+
{
732+
var options = new ListRemoteOptions
733+
{
734+
ProxyOptions = new ProxyOptions()
735+
};
736+
737+
IEnumerable<Reference> references = Repository.ListRemoteReferences(url, options).ToList();
738+
739+
List<Tuple<string, string>> actualRefs = references.
740+
Select(reference => new Tuple<string, string>(reference.CanonicalName, reference.ResolveToDirectReference().TargetIdentifier)).ToList();
741+
742+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs.Count, actualRefs.Count);
743+
Assert.True(references.Single(reference => reference.CanonicalName == "HEAD") is SymbolicReference);
744+
for (int i = 0; i < TestRemoteRefs.ExpectedRemoteRefs.Count; i++)
745+
{
746+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2);
747+
Assert.Equal(TestRemoteRefs.ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1);
748+
}
749+
}
750+
727751
[Fact]
728752
public void CanListRemoteReferencesWithDetachedRemoteHead()
729753
{

LibGit2Sharp/CertificateSsh.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ protected CertificateSsh()
2525
/// </summary>
2626
public readonly byte[] HashSHA1;
2727

28+
/// <summary>
29+
/// The SHA256 hash of the host. Meaningful if <see cref="HasSHA256"/> is true
30+
/// </summary>
31+
public readonly byte[] HashSHA256;
32+
2833
/// <summary>
2934
/// True if we have the MD5 hostkey hash from the server
3035
/// </summary>
@@ -35,11 +40,17 @@ protected CertificateSsh()
3540
/// </summary>
3641
public readonly bool HasSHA1;
3742

43+
/// <summary>
44+
/// True if we have the SHA256 hostkey hash from the server
45+
/// </summary>
46+
public readonly bool HasSHA256;
47+
3848
internal unsafe CertificateSsh(git_certificate_ssh* cert)
3949
{
4050

4151
HasMD5 = cert->type.HasFlag(GitCertificateSshType.MD5);
4252
HasSHA1 = cert->type.HasFlag(GitCertificateSshType.SHA1);
53+
HasSHA256 = cert->type.HasFlag(GitCertificateSshType.SHA256);
4354

4455
HashMD5 = new byte[16];
4556
for (var i = 0; i < HashMD5.Length; i++)
@@ -52,6 +63,12 @@ internal unsafe CertificateSsh(git_certificate_ssh* cert)
5263
{
5364
HashSHA1[i] = cert->HashSHA1[i];
5465
}
66+
67+
HashSHA256 = new byte[32];
68+
for (var i = 0; i < HashSHA256.Length; i++)
69+
{
70+
HashSHA256[i] = cert->HashSHA256[i];
71+
}
5572
}
5673

5774
internal unsafe IntPtr ToPointer()
@@ -65,6 +82,10 @@ internal unsafe IntPtr ToPointer()
6582
{
6683
sshCertType |= GitCertificateSshType.SHA1;
6784
}
85+
if (HasSHA256)
86+
{
87+
sshCertType |= GitCertificateSshType.SHA256;
88+
}
6889

6990
var gitCert = new git_certificate_ssh()
7091
{
@@ -88,6 +109,14 @@ internal unsafe IntPtr ToPointer()
88109
}
89110
}
90111

112+
fixed (byte* p = &HashSHA256[0])
113+
{
114+
for (var i = 0; i < HashSHA256.Length; i++)
115+
{
116+
gitCert.HashSHA256[i] = p[i];
117+
}
118+
}
119+
91120
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(gitCert));
92121
Marshal.StructureToPtr(gitCert, ptr, false);
93122

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,24 @@ private static IntPtr ResolveDll(string libraryName, Assembly assembly, DllImpor
132132
[DllImport("libdl", EntryPoint = "dlopen")]
133133
private static extern IntPtr LoadUnixLibrary(string path, int flags);
134134

135-
[DllImport("kernel32", EntryPoint = "LoadLibrary")]
136-
private static extern IntPtr LoadWindowsLibrary(string path);
135+
[DllImport("kernel32", EntryPoint = "AddDllDirectory", CharSet = CharSet.Unicode)]
136+
private static extern IntPtr AddDllDirectory(string path);
137+
138+
[DllImport("kernel32", EntryPoint = "LoadLibraryExW", CharSet = CharSet.Unicode)]
139+
private static extern IntPtr LoadWindowsLibraryEx(string path, IntPtr hFile, uint flags);
140+
141+
private const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000;
142+
143+
private static IntPtr LoadWindowsLibrary(string path)
144+
{
145+
var directory = Path.GetDirectoryName(path);
146+
if (directory != null)
147+
{
148+
AddDllDirectory(directory);
149+
}
150+
151+
return LoadWindowsLibraryEx(path, IntPtr.Zero, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
152+
}
137153

138154
// Avoid inlining this method because otherwise mono's JITter may try
139155
// to load the library _before_ we've configured the path.

LibGit2Sharp/Core/SshExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using LibGit2Sharp.Core;
2+
using System;
3+
using System.Runtime.InteropServices;
4+
5+
namespace LibGit2Sharp.Ssh
6+
{
7+
internal static class NativeMethods
8+
{
9+
private const string libgit2 = NativeDllName.Name;
10+
11+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
12+
internal static extern int git_cred_ssh_key_new(
13+
out IntPtr cred,
14+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string username,
15+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string publickey,
16+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string privatekey,
17+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string passphrase);
18+
19+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
20+
internal static extern int git_cred_ssh_key_memory_new(
21+
out IntPtr cred,
22+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string username,
23+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string publickey,
24+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string privatekey,
25+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string passphrase);
26+
}
27+
}

LibGit2Sharp/LibGit2Sharp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
</PropertyGroup>
3131

3232
<ItemGroup>
33-
<PackageReference Include="Octopus.LibGit2Sharp.NativeBinaries" Version="2.0.323-octopus.1" PrivateAssets="none" />
33+
<PackageReference Include="Octopus.LibGit2Sharp.NativeBinaries" Version="2.0.323-octopus.2" PrivateAssets="none" />
3434
<PackageReference Include="MinVer" Version="6.0.0" PrivateAssets="all" />
3535
</ItemGroup>
3636

LibGit2Sharp/ListRemoteOptions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using LibGit2Sharp.Handlers;
2+
3+
namespace LibGit2Sharp;
4+
5+
/// <summary>
6+
/// Options controlling ListRemote behavior.
7+
/// </summary>
8+
public sealed class ListRemoteOptions
9+
{
10+
/// <summary>
11+
/// Handler to generate <see cref="LibGit2Sharp.Credentials"/> for authentication.
12+
/// </summary>
13+
public CredentialsHandler CredentialsProvider { get; set; }
14+
15+
/// <summary>
16+
/// This handler will be called to let the user make a decision on whether to allow
17+
/// the connection to proceed based on the certificate presented by the server.
18+
/// </summary>
19+
public CertificateCheckHandler CertificateCheck { get; set; }
20+
21+
22+
/// <summary>
23+
/// Options for connecting through a proxy.
24+
/// </summary>
25+
public ProxyOptions ProxyOptions { get; set; } = new();
26+
}

0 commit comments

Comments
 (0)