diff --git a/LibGit2Sharp.Tests/CloneFixture.cs b/LibGit2Sharp.Tests/CloneFixture.cs index 00dc9fee6..976ef9322 100644 --- a/LibGit2Sharp.Tests/CloneFixture.cs +++ b/LibGit2Sharp.Tests/CloneFixture.cs @@ -557,5 +557,54 @@ public void CanCancelRecursiveClone() } } + + [Fact] + public void CannotCloneWithForbiddenCustomHeaders() + { + var scd = BuildSelfCleaningDirectory(); + + const string url = "https://github.com/libgit2/TestGitRepository"; + + const string knownHeader = "User-Agent: mygit-201"; + var cloneOptions = new CloneOptions() + { + FetchOptions = new FetchOptions { CustomHeaders = new String[] { knownHeader } } + }; + + Assert.Throws(() => Repository.Clone(url, scd.DirectoryPath, cloneOptions)); + } + + [Fact] + public void CannotCloneWithMalformedCustomHeaders() + { + var scd = BuildSelfCleaningDirectory(); + + const string url = "https://github.com/libgit2/TestGitRepository"; + + const string knownHeader = "hello world"; + var cloneOptions = new CloneOptions() + { + FetchOptions = new FetchOptions { CustomHeaders = new String[] { knownHeader } } + }; + + Assert.Throws(() => Repository.Clone(url, scd.DirectoryPath, cloneOptions)); + } + + [Fact] + public void CanCloneWithCustomHeaders() + { + var scd = BuildSelfCleaningDirectory(); + + const string url = "https://github.com/libgit2/TestGitRepository"; + + const string knownHeader = "X-Hello: world"; + var cloneOptions = new CloneOptions() + { + FetchOptions = new FetchOptions { CustomHeaders = new String[] { knownHeader } } + }; + + var clonedRepoPath = Repository.Clone(url, scd.DirectoryPath, cloneOptions); + Assert.True(Directory.Exists(clonedRepoPath)); + } } } diff --git a/LibGit2Sharp.Tests/FetchFixture.cs b/LibGit2Sharp.Tests/FetchFixture.cs index dfe958fc0..170b64d61 100644 --- a/LibGit2Sharp.Tests/FetchFixture.cs +++ b/LibGit2Sharp.Tests/FetchFixture.cs @@ -239,5 +239,57 @@ public void FetchHonorsTheFetchPruneConfigurationEntry() Assert.Equal(4, clonedRepo.Branches.Count(b => b.IsRemote)); } } + + [Fact] + public void CannotFetchWithForbiddenCustomHeaders() + { + var scd = BuildSelfCleaningDirectory(); + + const string url = "https://github.com/libgit2/TestGitRepository"; + + string clonedRepoPath = Repository.Clone(url, scd.DirectoryPath); + + const string knownHeader = "User-Agent: mygit-201"; + var options = new FetchOptions { CustomHeaders = new String[] { knownHeader } }; + using (var repo = new Repository(clonedRepoPath)) + { + Assert.Throws(() => Commands.Fetch(repo, "origin", new string[0], options, null)); + } + } + + [Fact] + public void CanFetchWithCustomHeaders() + { + var scd = BuildSelfCleaningDirectory(); + + const string url = "https://github.com/libgit2/TestGitRepository"; + + string clonedRepoPath = Repository.Clone(url, scd.DirectoryPath); + + const string knownHeader = "X-Hello: mygit-201"; + var options = new FetchOptions { CustomHeaders = new String[] { knownHeader } }; + using (var repo = new Repository(clonedRepoPath)) + { + Commands.Fetch(repo, "origin", new string[0], options, null); + } + } + + [Fact] + public void CannotFetchWithMalformedCustomHeaders() + { + var scd = BuildSelfCleaningDirectory(); + + const string url = "https://github.com/libgit2/TestGitRepository"; + + string clonedRepoPath = Repository.Clone(url, scd.DirectoryPath); + + const string knownHeader = "Hello world"; + var options = new FetchOptions { CustomHeaders = new String[] { knownHeader } }; + using (var repo = new Repository(clonedRepoPath)) + { + Assert.Throws(() => Commands.Fetch(repo, "origin", new string[0], options, null)); + } + } + } } diff --git a/LibGit2Sharp/CloneOptions.cs b/LibGit2Sharp/CloneOptions.cs index 8d260db74..f88ff58d7 100644 --- a/LibGit2Sharp/CloneOptions.cs +++ b/LibGit2Sharp/CloneOptions.cs @@ -43,6 +43,11 @@ public CloneOptions() /// public CheckoutProgressHandler OnCheckoutProgress { get; set; } + /// + /// Gets or sets the fetch options. + /// + public FetchOptions FetchOptions { get; set; } + #region IConvertableToGitCheckoutOpts CheckoutCallbacks IConvertableToGitCheckoutOpts.GenerateCallbacks() diff --git a/LibGit2Sharp/Commands/Fetch.cs b/LibGit2Sharp/Commands/Fetch.cs index ed8ca66c0..d61fca5a5 100644 --- a/LibGit2Sharp/Commands/Fetch.cs +++ b/LibGit2Sharp/Commands/Fetch.cs @@ -38,6 +38,7 @@ public static void Fetch(Repository repository, string remote, IEnumerable 0) + { + fetchOptions.CustomHeaders = GitStrArrayManaged.BuildFrom(options.CustomHeaders); + } + fetchOptions.ProxyOptions = new GitProxyOptions { Version = 1 }; Proxy.git_remote_fetch(remoteHandle, refspecs, fetchOptions, logMessage); diff --git a/LibGit2Sharp/Core/GitFetchOptions.cs b/LibGit2Sharp/Core/GitFetchOptions.cs index 298c5ae33..3f0baa2c2 100644 --- a/LibGit2Sharp/Core/GitFetchOptions.cs +++ b/LibGit2Sharp/Core/GitFetchOptions.cs @@ -11,6 +11,6 @@ internal class GitFetchOptions public bool UpdateFetchHead = true; public TagFetchMode download_tags; public GitProxyOptions ProxyOptions; - public GitStrArrayManaged custom_headers; + public GitStrArrayManaged CustomHeaders; } } diff --git a/LibGit2Sharp/Core/GitFetchOptionsWrapper.cs b/LibGit2Sharp/Core/GitFetchOptionsWrapper.cs new file mode 100644 index 000000000..351947bbe --- /dev/null +++ b/LibGit2Sharp/Core/GitFetchOptionsWrapper.cs @@ -0,0 +1,36 @@ +using System; + +namespace LibGit2Sharp.Core +{ + /// + /// Git fetch options wrapper. Disposable wrapper for GitFetchOptions + /// + internal class GitFetchOptionsWrapper : IDisposable + { + public GitFetchOptionsWrapper() : this(new GitFetchOptions()) { } + + public GitFetchOptionsWrapper(GitFetchOptions fetchOptions) + { + this.Options = fetchOptions; + } + + public GitFetchOptions Options { get; private set; } + + #region IDisposable + private bool disposedValue = false; // To detect redundant calls + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + this.Options.CustomHeaders.Dispose(); + disposedValue = true; + } + + public void Dispose() + { + Dispose(true); + } + #endregion + } +} diff --git a/LibGit2Sharp/FetchOptions.cs b/LibGit2Sharp/FetchOptions.cs index a9d2fb2c7..487baed97 100644 --- a/LibGit2Sharp/FetchOptions.cs +++ b/LibGit2Sharp/FetchOptions.cs @@ -25,5 +25,26 @@ public sealed class FetchOptions : FetchOptionsBase /// /// public bool? Prune { get; set; } + + /// + /// Get/Set the custom headers. + /// + /// + /// This allows you to set custom headers (e.g. X-Forwarded-For, + /// X-Request-Id, etc), + /// + /// + /// + /// Libgit2 sets some headers for HTTP requests (User-Agent, Host, + /// Accept, Content-Type, Transfer-Encoding, Content-Length, Accept) that + /// cannot be overriden. + /// + /// + /// var fetchOptions - new FetchOptions() { + /// CustomHeaders = new String[] {"X-Request-Id: 12345"} + /// }; + /// + /// The custom headers string array + public string[] CustomHeaders { get; set; } } } diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index 84110f409..f49e2fcfc 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -703,21 +703,26 @@ public static string Clone(string sourceUrl, string workdirPath, throw new UserCancelledException("Clone cancelled by the user."); } - using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options)) + using (var checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options)) + using (var fetchOptionsWrapper = new GitFetchOptionsWrapper()) { var gitCheckoutOptions = checkoutOptionsWrapper.Options; - var remoteCallbacks = new RemoteCallbacks(options); - var gitRemoteCallbacks = remoteCallbacks.GenerateCallbacks(); - - var gitProxyOptions = new GitProxyOptions { Version = 1 }; + var gitFetchOptions = fetchOptionsWrapper.Options; + gitFetchOptions.ProxyOptions = new GitProxyOptions { Version = 1 }; + gitFetchOptions.RemoteCallbacks = new RemoteCallbacks(options).GenerateCallbacks(); + if (options.FetchOptions != null && options.FetchOptions.CustomHeaders != null) + { + gitFetchOptions.CustomHeaders = + GitStrArrayManaged.BuildFrom(options.FetchOptions.CustomHeaders); + } var cloneOpts = new GitCloneOptions { Version = 1, Bare = options.IsBare ? 1 : 0, CheckoutOpts = gitCheckoutOptions, - FetchOpts = new GitFetchOptions { ProxyOptions = gitProxyOptions, RemoteCallbacks = gitRemoteCallbacks }, + FetchOpts = gitFetchOptions, }; string clonedRepoPath;