From aa0138ed35b2e6baf208be0e5008171b79e7fc45 Mon Sep 17 00:00:00 2001 From: Kay Posmyk Date: Mon, 1 Jan 2018 21:37:12 +0100 Subject: [PATCH 1/4] Reimplement the Audit Feature and Git Pack Parsing Fixes #767 --- .../Hooks/AfterPushAuditHandler.cs | 92 +++++ .../Application/Hooks/GitBranchPushData.cs | 39 +++ .../Application/Hooks/GitTagPushData.cs | 29 ++ .../Application/Hooks/IAfterGitPushHandler.cs | 18 + Bonobo.Git.Server/Bonobo.Git.Server.csproj | 28 +- .../Configuration/AppSettings.cs | 19 - Bonobo.Git.Server/Git/GitProtocolCommand.cs | 10 + Bonobo.Git.Server/Git/GitReceiveCommand.cs | 64 ++++ Bonobo.Git.Server/Git/GitRefType.cs | 10 + .../GitService/GitHandlerInvocationService.cs | 200 +++++++++++ .../Git/GitService/GitServiceResultParser.cs | 34 -- .../AutoCreateMissingRecoveryDirectories.cs | 48 --- .../Durability/DurableGitServiceResult.cs | 47 --- .../Durability/IRecoveryFilePathBuilder.cs | 16 - .../Durability/NamedArguments.cs | 32 -- .../OneFolderRecoveryFilePathBuilder.cs | 50 --- .../Durability/ReceivePackRecovery.cs | 94 ----- .../ReceivePackHook/GIT_OBJ_TYPE.cs | 17 - .../Hooks/AuditPusherToGitNotes.cs | 69 ---- .../Hooks/NullReceivePackHook.cs | 20 -- .../ReceivePackHook/IHookReceivePack.cs | 18 - .../ReceivePackHook/ParsedReceivePack.cs | 32 -- .../ReceivePackHook/ReceivePackCommit.cs | 28 -- .../ReceivePackCommitSignature.cs | 23 -- .../ReceivePackHook/ReceivePackParser.cs | 328 ------------------ .../ReceivePackHook/ReceivePackPktLine.cs | 20 -- .../Git/GitService/ReplicatingStream.cs | 84 ----- .../Git/ReceivePackInspectStream.cs | 318 +++++++++++++++++ Bonobo.Git.Server/Global.asax.cs | 82 +---- .../Views/Repository/Detail.cshtml | 24 +- .../Views/Repository/Edit.cshtml | 14 +- Bonobo.Git.Server/web.config | 1 - 32 files changed, 815 insertions(+), 1093 deletions(-) create mode 100644 Bonobo.Git.Server/Application/Hooks/AfterPushAuditHandler.cs create mode 100644 Bonobo.Git.Server/Application/Hooks/GitBranchPushData.cs create mode 100644 Bonobo.Git.Server/Application/Hooks/GitTagPushData.cs create mode 100644 Bonobo.Git.Server/Application/Hooks/IAfterGitPushHandler.cs delete mode 100644 Bonobo.Git.Server/Configuration/AppSettings.cs create mode 100644 Bonobo.Git.Server/Git/GitProtocolCommand.cs create mode 100644 Bonobo.Git.Server/Git/GitReceiveCommand.cs create mode 100644 Bonobo.Git.Server/Git/GitRefType.cs create mode 100644 Bonobo.Git.Server/Git/GitService/GitHandlerInvocationService.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/GitServiceResultParser.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/AutoCreateMissingRecoveryDirectories.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/DurableGitServiceResult.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/IRecoveryFilePathBuilder.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/NamedArguments.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/OneFolderRecoveryFilePathBuilder.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/ReceivePackRecovery.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/GIT_OBJ_TYPE.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/AuditPusherToGitNotes.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/NullReceivePackHook.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/IHookReceivePack.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/ParsedReceivePack.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommit.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommitSignature.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackParser.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackPktLine.cs delete mode 100644 Bonobo.Git.Server/Git/GitService/ReplicatingStream.cs create mode 100644 Bonobo.Git.Server/Git/ReceivePackInspectStream.cs diff --git a/Bonobo.Git.Server/Application/Hooks/AfterPushAuditHandler.cs b/Bonobo.Git.Server/Application/Hooks/AfterPushAuditHandler.cs new file mode 100644 index 000000000..7ab5df632 --- /dev/null +++ b/Bonobo.Git.Server/Application/Hooks/AfterPushAuditHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Bonobo.Git.Server.Data; +using Bonobo.Git.Server.Security; +using LibGit2Sharp; + +namespace Bonobo.Git.Server.Application.Hooks { + /// + /// Adds Bonobo user specific identity information to all newly pushed commits by appending a + /// Git note to them. + /// + public class AfterPushAuditHandler: IAfterGitPushHandler + { + /// + /// Fallback username if no database user is known for the pushing client. + /// + private const string EmptyUser = "anonymous"; + private const string EmptyEmail = "unknown"; + + private readonly IRepositoryRepository _repoConfig; + private readonly IMembershipService _bonoboUsers; + + public AfterPushAuditHandler(IRepositoryRepository repoConfig, IMembershipService bonoboUsers) + { + if (repoConfig == null) throw new ArgumentNullException(nameof(repoConfig)); + if (bonoboUsers == null) throw new ArgumentNullException(nameof(bonoboUsers)); + + _repoConfig = repoConfig; + _bonoboUsers = bonoboUsers; + } + + public void OnBranchCreated(HttpContext httpContext, GitBranchPushData branchData) + { + HandleAnyNewCommits(httpContext, branchData); + } + + public void OnBranchModified(HttpContext httpContext, GitBranchPushData branchData, bool isFastForward) + { + HandleAnyNewCommits(httpContext, branchData); + } + + private void HandleAnyNewCommits(HttpContext httpContext, GitBranchPushData branchData) + { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (!IsRepoAuditEnabled(branchData.RepositoryName)) + return; + + string bonoboUserName = HttpContext.Current.User.Username(); + AddCommitNotes(bonoboUserName, branchData); + } + + private bool IsRepoAuditEnabled(string repositoryName) + { + var repo = _repoConfig.GetRepository(repositoryName); + return repo.AuditPushUser; + } + + private void AddCommitNotes(string bonoboUserName, GitBranchPushData branchData) + { + string email = null; + if (string.IsNullOrEmpty(bonoboUserName)) + { + bonoboUserName = EmptyUser; + } else { + var bonoboUser = _bonoboUsers.GetUserModel(bonoboUserName); + if (bonoboUser != null) + email = bonoboUser.Email; + } + + if (string.IsNullOrWhiteSpace(email)) + email = EmptyEmail; + + foreach (var commit in branchData.AddedCommits) + { + branchData.Repository.Notes.Add( + commit.Id, + bonoboUserName, + new Signature(bonoboUserName, email, DateTimeOffset.Now), + new Signature(bonoboUserName, email, DateTimeOffset.Now), + "pusher"); + } + } + + public void OnBranchDeleted(HttpContext httpContext, GitBranchPushData branchData) {} + + public void OnTagCreated(HttpContext httpContext, GitTagPushData tagData) {} + + public void OnTagDeleted(HttpContext httpContext, GitTagPushData tagData) {} + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Application/Hooks/GitBranchPushData.cs b/Bonobo.Git.Server/Application/Hooks/GitBranchPushData.cs new file mode 100644 index 000000000..5118ad895 --- /dev/null +++ b/Bonobo.Git.Server/Application/Hooks/GitBranchPushData.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using LibGit2Sharp; + +namespace Bonobo.Git.Server.Application.Hooks +{ + public struct GitBranchPushData { + public string RepositoryName { get; set; } + public Repository Repository { get; set; } + + /// + /// Full name of the ref, e.g. "refs/heads/master" + /// + public string RefName { get; set; } + + /// + /// Branch name as it was extracted from + /// e.g. "master" or "feature/foo" + /// + public string BranchName { get; set; } + + /// + /// The commit referenced by the branch. + /// + /// + /// The 40 characters long SHA1 hash in hex. + /// + public string ReferenceCommit { get; set; } + + /// + /// Commits which were not previously referenced by the branch. + /// + /// + /// All commits of a newly pushed branch (including the commit the + /// branch originated from) or modified commits of an existing branch. + /// null if branch got deleted. + /// + public IEnumerable AddedCommits { get; set; } + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Application/Hooks/GitTagPushData.cs b/Bonobo.Git.Server/Application/Hooks/GitTagPushData.cs new file mode 100644 index 000000000..71062a9d5 --- /dev/null +++ b/Bonobo.Git.Server/Application/Hooks/GitTagPushData.cs @@ -0,0 +1,29 @@ +using LibGit2Sharp; + +namespace Bonobo.Git.Server.Application.Hooks +{ + public struct GitTagPushData + { + public string RepositoryName { get; set; } + public Repository Repository { get; set; } + + /// + /// Full name of the ref, e.g. "refs/tags/v.1.0" + /// + public string RefName { get; set; } + + /// + /// Tag name as it was extracted from + /// e.g. "v.1.0" or "foo-bar" + /// + public string TagName { get; set; } + + /// + /// The commit referenced by the tag. + /// + /// + /// The 40 characters long SHA1 hash in hex. + /// + public string ReferenceCommitSha { get; set; } + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Application/Hooks/IAfterGitPushHandler.cs b/Bonobo.Git.Server/Application/Hooks/IAfterGitPushHandler.cs new file mode 100644 index 000000000..4db625946 --- /dev/null +++ b/Bonobo.Git.Server/Application/Hooks/IAfterGitPushHandler.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using System.Web; +using LibGit2Sharp; + +namespace Bonobo.Git.Server.Application.Hooks { + public interface IAfterGitPushHandler { + void OnBranchCreated(HttpContext httpContext, GitBranchPushData branchData); + void OnBranchDeleted(HttpContext httpContext, GitBranchPushData branchData); + + /// The current . + /// Data related to the updated branch and repository. + /// true if push was fast forward, false implies force push. + void OnBranchModified(HttpContext httpContext, GitBranchPushData branchData, bool isFastForward); + + void OnTagCreated(HttpContext httpContext, GitTagPushData tagData); + void OnTagDeleted(HttpContext httpContext, GitTagPushData tagData); + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Bonobo.Git.Server.csproj b/Bonobo.Git.Server/Bonobo.Git.Server.csproj index ff55b58b6..5f6a6bd52 100644 --- a/Bonobo.Git.Server/Bonobo.Git.Server.csproj +++ b/Bonobo.Git.Server/Bonobo.Git.Server.csproj @@ -272,6 +272,11 @@ + + + + + @@ -302,21 +307,13 @@ - - - - - - - + + + + + - - - - - - @@ -385,11 +382,6 @@ - - - - - diff --git a/Bonobo.Git.Server/Configuration/AppSettings.cs b/Bonobo.Git.Server/Configuration/AppSettings.cs deleted file mode 100644 index b0ab7da4a..000000000 --- a/Bonobo.Git.Server/Configuration/AppSettings.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Configuration -{ - public static class AppSettings - { - public static bool IsPushAuditEnabled - { - get - { - return bool.Parse(ConfigurationManager.AppSettings["IsPushAuditEnabled"] ?? "false"); - } - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitProtocolCommand.cs b/Bonobo.Git.Server/Git/GitProtocolCommand.cs new file mode 100644 index 000000000..79e4041ce --- /dev/null +++ b/Bonobo.Git.Server/Git/GitProtocolCommand.cs @@ -0,0 +1,10 @@ +namespace Bonobo.Git.Server.Git { + /// + /// Commands which can be represented by git pkt-lines. + /// + public enum GitProtocolCommand { + Create, + Delete, + Modify + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitReceiveCommand.cs b/Bonobo.Git.Server/Git/GitReceiveCommand.cs new file mode 100644 index 000000000..a4f6f0889 --- /dev/null +++ b/Bonobo.Git.Server/Git/GitReceiveCommand.cs @@ -0,0 +1,64 @@ +namespace Bonobo.Git.Server.Git { + public struct GitReceiveCommand { + public static GitReceiveCommand Invalid = default(GitReceiveCommand); + + /// + /// Full name of the ref, e.g. "refs/heads/master" + /// + public string FullRefName { get; set; } + + public GitRefType RefType { get; set; } + + /// + /// Branch name as it was extracted from + /// e.g. "master" or "feature/foo" + /// + public string RefName { get; set; } + + public GitProtocolCommand CommandType { get; set; } + + /// + /// SHA1 identifier of the old object as hex string. + /// + /// + /// 40 characters long SHA1 hash in hex. + /// Will be zero if CommandType is CommandType.Create. + /// + public string OldSha1 { get; set; } + + /// + /// SHA1 identifier of the new object as hex string. + /// + /// + /// 40 characters long SHA1 hash in hex. + /// Will be zero if CommandType is CommandType.Delete. + /// + public string NewSha1 { get; set; } + + public GitReceiveCommand(string fullRefName, string oldSha1, string newSha1) { + this.OldSha1 = oldSha1; + this.NewSha1 = newSha1; + + const string zeroId = "0000000000000000000000000000000000000000"; + if (oldSha1 == zeroId) + this.CommandType = GitProtocolCommand.Create; + else if (newSha1 == zeroId) + this.CommandType = GitProtocolCommand.Delete; + else + this.CommandType = GitProtocolCommand.Modify; + + this.FullRefName = fullRefName; + int firstSlashPos = fullRefName.IndexOf('/'); + int secondSlashPos = fullRefName.IndexOf('/', firstSlashPos + 1); + var refTypeRaw = fullRefName.Substring(firstSlashPos + 1, secondSlashPos - firstSlashPos - 1); + this.RefName = fullRefName.Substring(secondSlashPos + 1); + + if (refTypeRaw == "heads") + this.RefType = GitRefType.Branch; + else if (refTypeRaw == "tags") + this.RefType = GitRefType.Tag; + else + this.RefType = GitRefType.Unknown; + } + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitRefType.cs b/Bonobo.Git.Server/Git/GitRefType.cs new file mode 100644 index 000000000..bbaf15d27 --- /dev/null +++ b/Bonobo.Git.Server/Git/GitRefType.cs @@ -0,0 +1,10 @@ +namespace Bonobo.Git.Server.Git { + /// + /// Interesting ref types found in refnames of command_pkt. + /// + public enum GitRefType { + Unknown, + Tag, + Branch, + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/GitHandlerInvocationService.cs b/Bonobo.Git.Server/Git/GitService/GitHandlerInvocationService.cs new file mode 100644 index 000000000..8c1335f0e --- /dev/null +++ b/Bonobo.Git.Server/Git/GitService/GitHandlerInvocationService.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web; +using System.Diagnostics; +using Bonobo.Git.Server.Application.Hooks; +using LibGit2Sharp; +using Log = Serilog.Log; + +namespace Bonobo.Git.Server.Git.GitService +{ + /// + /// Invokes according to transmitted Git protocol data. + /// + /// + /// This service must be registered before , i.e. it has to wrap it + /// directly or indirectly. + /// + public class GitHandlerInvocationService: IGitService + { + private readonly IGitService _next; + private readonly IAfterGitPushHandler _afterPushHandler; + private readonly IGitRepositoryLocator _repoLocator; + + public GitHandlerInvocationService(IGitService next, IAfterGitPushHandler afterPushHandler, IGitRepositoryLocator repoLocator) + { + if (next == null) throw new ArgumentNullException(nameof(next)); + if (afterPushHandler == null) throw new ArgumentNullException(nameof(afterPushHandler)); + if (repoLocator == null) throw new ArgumentNullException(nameof(repoLocator)); + + _next = next; + _afterPushHandler = afterPushHandler; + _repoLocator = repoLocator; + } + + public void ExecuteServiceByName(string correlationId, string repositoryName, string serviceName, ExecutionOptions options, Stream inStream, Stream outStream) + { + if (serviceName != "receive-pack") + { + _next.ExecuteServiceByName(correlationId, repositoryName, serviceName, options, inStream, outStream); + return; + } + + var inspectStream = new ReceivePackInspectStream(inStream); + + // this should actually run the git process and the push will be complete once this method returns + _next.ExecuteServiceByName(correlationId, repositoryName, serviceName, options, inspectStream, outStream); + + // because the Git process has successfully finished, the request really shouldn't fail due to any exceptions thrown from here + try + { + Debug.WriteLine( + $"{nameof(GitHandlerInvocationService)} has found {inspectStream.PackObjectCount} objects in the receive-pack stream."); + + DirectoryInfo repoDirectory = _repoLocator.GetRepositoryDirectoryPath(repositoryName); + Debug.Assert(repoDirectory.Exists); + + using (Repository repository = new Repository(repoDirectory.FullName)) + InvokeHandler(repositoryName, repository, inspectStream.PeekedCommands); + } + catch (Exception ex) + { + Log.Error($"Git after push handlers could not be invoked due to this exception:\n{ex}"); + } + } + + private void InvokeHandler(string repositoryName, Repository repository, IEnumerable commands) + { + Debug.Assert(repositoryName != null); + Debug.Assert(repositoryName.Length > 0); + Debug.Assert(repository != null); + Debug.Assert(commands != null); + + foreach (var command in commands) + { + try + { + if (command.RefType == GitRefType.Branch) + InvokeAccordingToBranchChange(repositoryName, repository, command); + else if (command.RefType == GitRefType.Tag) + InvokeAccordingToTagChange(repositoryName, repository, command); + } + catch (Exception ex) + { + Log.Error($"{nameof(IAfterGitPushHandler)} implementation has thrown an exception:\n{ex}"); + } + } + } + + private void InvokeAccordingToBranchChange(string repositoryName, Repository repository, GitReceiveCommand command) + { + Debug.Assert(repositoryName != null); + Debug.Assert(repository != null); + + switch (command.CommandType) { + case GitProtocolCommand.Create: + { + var eventData = new GitBranchPushData + { + RepositoryName = repositoryName, + Repository = repository, + BranchName = command.RefName, + RefName = command.FullRefName, + ReferenceCommit = command.NewSha1, + AddedCommits = BranchCommits(repository, command.RefName).ToList() + }; + _afterPushHandler.OnBranchCreated(HttpContext.Current, eventData); + + break; + } + case GitProtocolCommand.Delete: + { + var evenData = new GitBranchPushData + { + RepositoryName = repositoryName, + Repository = repository, + BranchName = command.RefName, + RefName = command.FullRefName, + ReferenceCommit = command.OldSha1 + }; + _afterPushHandler.OnBranchDeleted(HttpContext.Current, evenData); + + break; + } + case GitProtocolCommand.Modify: { + Branch branch = repository.Branches[command.RefName]; + bool isFastForward = branch.Commits.Any(c => c.Sha == command.OldSha1); + + IEnumerable addedCommits = BranchCommits(repository, command.RefName) + .TakeWhile(c => c.Sha != command.OldSha1).ToList(); + + var eventData = new GitBranchPushData + { + RepositoryName = repositoryName, + Repository = repository, + BranchName = command.RefName, + RefName = command.FullRefName, + ReferenceCommit = command.NewSha1, + AddedCommits = addedCommits + }; + _afterPushHandler.OnBranchModified(HttpContext.Current, eventData, isFastForward); + + break; + } + } + } + + private void InvokeAccordingToTagChange(string repositoryName, Repository repository, GitReceiveCommand command) { + Debug.Assert(repositoryName != null); + Debug.Assert(repository != null); + + switch (command.CommandType) { + case GitProtocolCommand.Create: + { + var eventData = new GitTagPushData + { + RepositoryName = repositoryName, + Repository = repository, + TagName = command.RefName, + RefName = command.FullRefName, + ReferenceCommitSha = command.NewSha1 + }; + _afterPushHandler.OnTagCreated(HttpContext.Current, eventData); + + break; + } + case GitProtocolCommand.Delete: + { + var eventData = new GitTagPushData + { + RepositoryName = repositoryName, + Repository = repository, + TagName = command.RefName, + RefName = command.FullRefName, + ReferenceCommitSha = command.OldSha1 + }; + _afterPushHandler.OnTagDeleted(HttpContext.Current, eventData); + + break; + } + } + } + + private static IEnumerable BranchCommits(Repository repository, string branchName) + { + var filter = new CommitFilter + { + IncludeReachableFrom = branchName, + SortBy = CommitSortStrategies.Topological, + FirstParentOnly = true + }; + + return repository.Commits + .QueryBy(filter) + .Intersect(repository.Branches[branchName].Commits); + } + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/GitServiceResultParser.cs b/Bonobo.Git.Server/Git/GitService/GitServiceResultParser.cs deleted file mode 100644 index 7388d2d13..000000000 --- a/Bonobo.Git.Server/Git/GitService/GitServiceResultParser.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService -{ - public class GitServiceResultParser - { - public GitExecutionResult ParseResult(System.IO.Stream outputStream) - { - bool hasError = true; - if (outputStream.Length >= 10) - { - var buff5 = new byte[5]; - - if (outputStream.Read(buff5, 0, buff5.Length) != buff5.Length) - { - throw new Exception("Unexpected number of bytes read"); - } - if (outputStream.Read(buff5, 0, buff5.Length) != buff5.Length) - { - throw new Exception("Unexpected number of bytes read"); - } - - var firstChars = Encoding.ASCII.GetString(buff5); - hasError = firstChars == "error"; - } - - return new GitExecutionResult(hasError); - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/AutoCreateMissingRecoveryDirectories.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/AutoCreateMissingRecoveryDirectories.cs deleted file mode 100644 index 3eb2767a4..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/AutoCreateMissingRecoveryDirectories.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability -{ - /// - /// Ensures directories for generated recovery paths exist - /// - public class AutoCreateMissingRecoveryDirectories : IRecoveryFilePathBuilder - { - private readonly IRecoveryFilePathBuilder pathBuilder; - - public AutoCreateMissingRecoveryDirectories(IRecoveryFilePathBuilder pathBuilder) - { - this.pathBuilder = pathBuilder; - } - - public string CreateDirectoryForFile(string filePath) - { - var dirPath = Path.GetDirectoryName(filePath); - Directory.CreateDirectory(dirPath); - return filePath; - } - - public string GetPathToResultFile(string correlationId, string repositoryName, string serviceName) - { - return CreateDirectoryForFile(pathBuilder.GetPathToResultFile(correlationId, repositoryName, serviceName)); - } - - public string GetPathToPackFile(ParsedReceivePack receivePack) - { - return CreateDirectoryForFile(pathBuilder.GetPathToPackFile(receivePack)); - } - - public string[] GetPathToPackDirectory() - { - var dirs = pathBuilder.GetPathToPackDirectory(); - foreach(var dir in dirs) - { - Directory.CreateDirectory(dir); - } - return dirs; - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/DurableGitServiceResult.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/DurableGitServiceResult.cs deleted file mode 100644 index f02ea6820..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/DurableGitServiceResult.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.ComTypes; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability -{ - /// - /// provides durability for result of git command execution - /// by writing result of git command to a file - /// - public class DurableGitServiceResult : IGitService - { - private readonly IGitService gitService; - private readonly IRecoveryFilePathBuilder resultFilePathBuilder; - - public DurableGitServiceResult(IGitService gitService, IRecoveryFilePathBuilder resultFilePathBuilder) - { - this.gitService = gitService; - this.resultFilePathBuilder = resultFilePathBuilder; - } - - public void ExecuteServiceByName(string correlationId, string repositoryName, string serviceName, ExecutionOptions options, System.IO.Stream inStream, System.IO.Stream outStream) - { - if (serviceName == "receive-pack") - { - var resultFilePath = resultFilePathBuilder.GetPathToResultFile(correlationId, repositoryName, serviceName); - using (var resultFileStream = File.OpenWrite(resultFilePath)) - { - this.gitService.ExecuteServiceByName(correlationId, repositoryName, serviceName, options, inStream, new ReplicatingStream(outStream, resultFileStream)); - } - - // only on successful execution remove the result file - if (File.Exists(resultFilePath)) - { - File.Delete(resultFilePath); - } - } - else - { - this.gitService.ExecuteServiceByName(correlationId, repositoryName, serviceName, options, inStream, outStream); - } - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/IRecoveryFilePathBuilder.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/IRecoveryFilePathBuilder.cs deleted file mode 100644 index c5a722110..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/IRecoveryFilePathBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability -{ - public interface IRecoveryFilePathBuilder - { - string GetPathToResultFile(string correlationId, string repositoryName, string serviceName); - - string GetPathToPackFile(ParsedReceivePack receivePack); - - string[] GetPathToPackDirectory(); - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/NamedArguments.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/NamedArguments.cs deleted file mode 100644 index 1ceced5e1..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/NamedArguments.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability -{ - /// - /// Perhaps there's a better way to handle wiring up simple types in Unity but i haven't found it - /// - public class NamedArguments - { - public class FailedPackWaitTimeBeforeExecution - { - public FailedPackWaitTimeBeforeExecution(TimeSpan timeSpan) - { - this.Value = timeSpan; - } - public TimeSpan Value { get; private set; } - } - - public class ReceivePackRecoveryDirectory - { - public ReceivePackRecoveryDirectory(string receivePackRecoveryDirectory) - { - this.Value = receivePackRecoveryDirectory; - } - - public string Value { get; private set; } - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/OneFolderRecoveryFilePathBuilder.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/OneFolderRecoveryFilePathBuilder.cs deleted file mode 100644 index bf2a9e7ff..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/OneFolderRecoveryFilePathBuilder.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability -{ - /// - /// Generates paths all going into one configured folder - /// - public class OneFolderRecoveryFilePathBuilder : IRecoveryFilePathBuilder - { - private static Regex illegalChars = new Regex("([/\\:*?\"<>|])"); - private readonly string receivePackRecoveryDirectory; - - public OneFolderRecoveryFilePathBuilder(NamedArguments.ReceivePackRecoveryDirectory receivePackRecoveryDirectory) - { - this.receivePackRecoveryDirectory = receivePackRecoveryDirectory.Value; - } - - public string StripIllegalChars(string input) - { - return illegalChars.Replace(input, ""); - } - - public string GetPathToResultFile(string correlationId, string repositoryName, string serviceName) - { - var path = string.Format("{0}.{1}.{2}.result", repositoryName, serviceName, correlationId); - - return Path.Combine(receivePackRecoveryDirectory, StripIllegalChars(path)); - } - - - public string GetPathToPackFile(ParsedReceivePack receivePack) - { - return Path.Combine( - receivePackRecoveryDirectory, - "ReceivePack", - StripIllegalChars(string.Format("{0}.{1}.pack", receivePack.RepositoryName, receivePack.PackId))); - } - - - public string[] GetPathToPackDirectory() - { - return new string [] { Path.Combine(receivePackRecoveryDirectory, "ReceivePack") }; - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/ReceivePackRecovery.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/ReceivePackRecovery.cs deleted file mode 100644 index 73de9d208..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Durability/ReceivePackRecovery.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Bonobo.Git.Server.Data; -using Bonobo.Git.Server.Git.GitService.ReceivePackHook; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability -{ - /// - /// Provides at least once execution guarantee to PostPackReceive hook method - /// - public class ReceivePackRecovery : IHookReceivePack - { - private readonly TimeSpan failedPackWaitTimeBeforeExecution; - private readonly IHookReceivePack next; - private readonly IRecoveryFilePathBuilder recoveryFilePathBuilder; - private readonly GitServiceResultParser resultFileParser; - - public ReceivePackRecovery( - IHookReceivePack next, - NamedArguments.FailedPackWaitTimeBeforeExecution failedPackWaitTimeBeforeExecution, - IRecoveryFilePathBuilder recoveryFilePathBuilder, - GitServiceResultParser resultFileParser) - { - this.next = next; - this.failedPackWaitTimeBeforeExecution = failedPackWaitTimeBeforeExecution.Value; - this.recoveryFilePathBuilder = recoveryFilePathBuilder; - this.resultFileParser = resultFileParser; - } - - public void PrePackReceive(ParsedReceivePack receivePack) - { - File.WriteAllText(recoveryFilePathBuilder.GetPathToPackFile(receivePack), JsonConvert.SerializeObject(receivePack)); - next.PrePackReceive(receivePack); - } - - public void PostPackReceive(ParsedReceivePack receivePack, GitExecutionResult result) - { - ProcessOnePack(receivePack, result); - RecoverAll(); - } - - private void ProcessOnePack(ParsedReceivePack receivePack, GitExecutionResult result) - { - next.PostPackReceive(receivePack, result); - - var packFilePath = recoveryFilePathBuilder.GetPathToPackFile(receivePack); - if (File.Exists(packFilePath)) - { - File.Delete(packFilePath); - } - } - - public void RecoverAll() - { - var waitingReceivePacks = new List(); - - foreach (var packDir in recoveryFilePathBuilder.GetPathToPackDirectory()) - { - foreach (var packFilePath in Directory.GetFiles(packDir)) - { - using (var fileReader = new StreamReader(packFilePath)) - { - var packFileData = fileReader.ReadToEnd(); - waitingReceivePacks.Add(JsonConvert.DeserializeObject(packFileData)); - } - } - } - - foreach (var pack in waitingReceivePacks.OrderBy(p => p.Timestamp)) - { - // execute if the pack has been waiting for X amount of time - if ((DateTime.Now - pack.Timestamp) >= failedPackWaitTimeBeforeExecution) - { - // re-parse result file and execute "post" hooks - // if result file is no longer there then move on - var failedPackResultFilePath = recoveryFilePathBuilder.GetPathToResultFile(pack.PackId, pack.RepositoryName, "receive-pack"); - if (File.Exists(failedPackResultFilePath)) - { - using (var resultFileStream = File.OpenRead(failedPackResultFilePath)) - { - var failedPackResult = resultFileParser.ParseResult(resultFileStream); - ProcessOnePack(pack, failedPackResult); - } - File.Delete(failedPackResultFilePath); - } - } - } - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/GIT_OBJ_TYPE.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/GIT_OBJ_TYPE.cs deleted file mode 100644 index 098baf62d..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/GIT_OBJ_TYPE.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - public enum GIT_OBJ_TYPE - { - OBJ_COMMIT = 1, - OBJ_TREE = 2, - OBJ_BLOB = 3, - OBJ_TAG = 4, - OBJ_OFS_DELTA = 6, - OBJ_REF_DELTA = 7 - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/AuditPusherToGitNotes.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/AuditPusherToGitNotes.cs deleted file mode 100644 index 0b56f990c..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/AuditPusherToGitNotes.cs +++ /dev/null @@ -1,69 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using LibGit2Sharp; -using Bonobo.Git.Server.Security; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Hooks -{ - public class AuditPusherToGitNotes : IHookReceivePack - { - public const string EMPTY_USER = "anonymous"; - private IGitRepositoryLocator repoLocator; - private IHookReceivePack next; - private Bonobo.Git.Server.Data.IRepositoryRepository repoConfig; - private readonly IMembershipService userRepo; - - public AuditPusherToGitNotes(IHookReceivePack next, IGitRepositoryLocator repoLocator, Bonobo.Git.Server.Data.IRepositoryRepository repoConfig, IMembershipService userRepo) - { - this.next = next; - this.repoLocator = repoLocator; - this.repoConfig = repoConfig; - this.userRepo = userRepo; - } - - public void PostPackReceive(ParsedReceivePack receivePack, GitExecutionResult result) - { - next.PostPackReceive(receivePack, result); - - if (result.HasError) - { - return; - } - - var repo = repoConfig.GetRepository(receivePack.RepositoryName); - if (repo.AuditPushUser == true) - { - var user = receivePack.PushedByUser; - var email = ""; - if (string.IsNullOrEmpty(user)) - { - user = EMPTY_USER; - } else { - var userData = userRepo.GetUserModel(user); - if(userData != null) { - email = userData.Email; - } - } - - var gitRepo = new Repository(repoLocator.GetRepositoryDirectoryPath(receivePack.RepositoryName).FullName); - foreach (var commit in receivePack.Commits) - { - gitRepo.Notes.Add( - new ObjectId(commit.Id), - user, - new Signature(user, email, DateTimeOffset.Now), - new Signature(user, email, DateTimeOffset.Now), - "pusher"); - } - } - } - - public void PrePackReceive(ParsedReceivePack receivePack) - { - next.PrePackReceive(receivePack); - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/NullReceivePackHook.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/NullReceivePackHook.cs deleted file mode 100644 index 240690d30..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/Hooks/NullReceivePackHook.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook.Hooks -{ - public class NullReceivePackHook : IHookReceivePack - { - public void PrePackReceive(ParsedReceivePack receivePack) - { - // do nothing - } - - public void PostPackReceive(ParsedReceivePack receivePack, GitExecutionResult result) - { - // do nothing - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/IHookReceivePack.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/IHookReceivePack.cs deleted file mode 100644 index 5890c0d27..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/IHookReceivePack.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - /// - /// Implement this interface to receive notifications when a pack is recieved - /// and perform any relevant pre/post-processing operations. - /// - public interface IHookReceivePack - { - void PrePackReceive(ParsedReceivePack receivePack); - - void PostPackReceive(ParsedReceivePack receivePack, GitExecutionResult result); - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ParsedReceivePack.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ParsedReceivePack.cs deleted file mode 100644 index 2afc3a59f..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ParsedReceivePack.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - public class ParsedReceivePack - { - public ParsedReceivePack (string packId, string repositoryName, IEnumerable pktLines, string pushedByUser, DateTime timestamp, IEnumerable commits) - { - this.PackId = packId; - this.PktLines = pktLines; - this.PushedByUser = pushedByUser; - this.Timestamp = timestamp; - this.RepositoryName = repositoryName; - this.Commits = commits; - } - - public string PackId { get; private set; } - - public IEnumerable PktLines { get; private set; } - - public IEnumerable Commits { get; private set; } - - public string PushedByUser { get; private set; } - - public DateTime Timestamp { get; private set; } - - public string RepositoryName { get; private set; } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommit.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommit.cs deleted file mode 100644 index 10bfe9132..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommit.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - public class ReceivePackCommit - { - public ReceivePackCommit(string id, string tree, IEnumerable parents, - ReceivePackCommitSignature author, ReceivePackCommitSignature committer, string message) - { - this.Id = id; - this.Tree = tree; - this.Parents = parents; - this.Author = author; - this.Committer = committer; - this.Message = message; - } - - public string Id { get; private set; } - public string Tree { get; private set; } - public IEnumerable Parents { get; private set; } - public ReceivePackCommitSignature Author { get; private set; } - public ReceivePackCommitSignature Committer { get; private set; } - public string Message { get; private set; } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommitSignature.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommitSignature.cs deleted file mode 100644 index cfa59168f..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackCommitSignature.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - public class ReceivePackCommitSignature - { - public ReceivePackCommitSignature(string name, string email, DateTimeOffset timestamp) - { - this.Name = name; - this.Email = email; - this.Timestamp = timestamp; - } - - public string Name { get; private set; } - - public string Email { get; private set; } - - public DateTimeOffset Timestamp { get; private set; } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackParser.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackParser.cs deleted file mode 100644 index 2ffd894cf..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackParser.cs +++ /dev/null @@ -1,328 +0,0 @@ -using Ionic.Zlib; -using LibGit2Sharp; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Text; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - public class ReceivePackParser : IGitService - { - private readonly IGitService gitService; - private readonly IHookReceivePack receivePackHandler; - private readonly GitServiceResultParser resultParser; - - public ReceivePackParser(IGitService gitService, IHookReceivePack receivePackHandler, GitServiceResultParser resultParser) - { - this.gitService = gitService; - this.receivePackHandler = receivePackHandler; - this.resultParser = resultParser; - } - - public void ExecuteServiceByName(string correlationId, string repositoryName, string serviceName, ExecutionOptions options, System.IO.Stream inStream, System.IO.Stream outStream) - { - ParsedReceivePack receivedPack = null; - - if (serviceName == "receive-pack" && inStream.Length > 0) - { - // PARSING RECEIVE-PACK THAT IS OF THE FOLLOWING FORMAT: - // (NEW LINES added for ease of reading) - // (LLLL is length of the line (expressed in HEX) until next LLLL value) - // - // LLLL------ REF LINE -----------\0------- OHTER DATA ----------- - // LLLL------ REF LINE ---------------- - // ... - // ... - // 0000PACK------- REST OF PACKAGE -------- - // - - var pktLines = new List(); - - var buff1 = new byte[1]; - var buff4 = new byte[4]; - var buff20 = new byte[20]; - var buff16K = new byte[1024 * 16]; - - while (true) - { - ReadStream(inStream, buff4); - var len = Convert.ToInt32(Encoding.UTF8.GetString(buff4), 16); - if (len == 0) - { - break; - } - len = len - buff4.Length; - - var accum = new LinkedList(); - - while (len > 0) - { - len -= 1; - ReadStream(inStream, buff1); - if (buff1[0] == 0) - { - break; - } - accum.AddLast(buff1[0]); - } - if (len > 0) - { - inStream.Seek(len, SeekOrigin.Current); - } - var pktLine = Encoding.UTF8.GetString(accum.ToArray()); - var pktLineItems = pktLine.Split(' '); - - var fromCommit = pktLineItems[0]; - var toCommit = pktLineItems[1]; - var refName = pktLineItems[2]; - - pktLines.Add(new ReceivePackPktLine(fromCommit, toCommit, refName)); - } - - // parse PACK contents - var packCommits = new List(); - - // PACK format - // https://www.kernel.org/pub/software/scm/git/docs/technical/pack-format.html - // http://schacon.github.io/gitbook/7_the_packfile.html - - if (inStream.Position < inStream.Length) - { - ReadStream(inStream, buff4); - if (Encoding.UTF8.GetString(buff4) != "PACK") - { - throw new Exception("Unexpected receive-pack 'PACK' content."); - } - ReadStream(inStream, buff4); - Array.Reverse(buff4); - var versionNum = BitConverter.ToInt32(buff4, 0); - - ReadStream(inStream, buff4); - Array.Reverse(buff4); - var numObjects = BitConverter.ToInt32(buff4, 0); - - while (numObjects > 0) - { - numObjects -= 1; - - ReadStream(inStream, buff1); - var type = (GIT_OBJ_TYPE)((buff1[0] >> 4) & 7); - long len = buff1[0] & 15; - - var shiftAmount = 4; - while ((buff1[0] >> 7) == 1) - { - ReadStream(inStream, buff1); - len = len | ((long)(buff1[0] & 127) << shiftAmount); - - shiftAmount += 7; - } - - if (type == GIT_OBJ_TYPE.OBJ_REF_DELTA) - { - // read ref name - ReadStream(inStream, buff20); - } - if (type == GIT_OBJ_TYPE.OBJ_OFS_DELTA) - { - // read negative offset - ReadStream(inStream, buff1); - while ((buff1[0] >> 7) == 1) - { - ReadStream(inStream, buff1); - } - } - - var origPosition = inStream.Position; - long offsetVal = 0; - - using (var zlibStream = new ZlibStream(inStream, CompressionMode.Decompress, true)) - { - // read compressed data max 16KB at a time - var readRemaining = len; - do - { - var bytesUncompressed = zlibStream.Read(buff16K, 0, buff16K.Length); - readRemaining -= bytesUncompressed; - } while (readRemaining > 0); - - if (type == GIT_OBJ_TYPE.OBJ_COMMIT) - { - var parsedCommit = ParseCommitDetails(buff16K, len); - packCommits.Add(parsedCommit); - } - offsetVal = zlibStream.TotalIn; - } - // move back position a bit because ZLibStream reads more than needed for inflating - inStream.Seek(origPosition + offsetVal, SeekOrigin.Begin); - } - } - // ------------------- - - var user = HttpContext.Current.User.Username(); - receivedPack = new ParsedReceivePack(correlationId, repositoryName, pktLines, user, DateTime.Now, packCommits); - - inStream.Seek(0, SeekOrigin.Begin); - - receivePackHandler.PrePackReceive(receivedPack); - } - - GitExecutionResult execResult = null; - using (var capturedOutputStream = new MemoryStream()) - { - gitService.ExecuteServiceByName(correlationId, repositoryName, serviceName, options, inStream, new ReplicatingStream(outStream, capturedOutputStream)); - - // parse captured output - capturedOutputStream.Seek(0, SeekOrigin.Begin); - execResult = resultParser.ParseResult(capturedOutputStream); - } - - if(receivedPack != null) - { - receivePackHandler.PostPackReceive(receivedPack, execResult); - } - } - - [MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - public ReceivePackCommit ParseCommitDetails(byte[] buff, long commitMsgLengthLong) - { - if (commitMsgLengthLong > buff.Length) - { - // buff at the moment is 16KB, should be enough for commit messages - // but break just in case this ever does happen so it could be addressed then - throw new Exception("Encountered unexpectedly large commit message"); - } - int commitMsgLength = (int)commitMsgLengthLong; // guaranteed no truncation because of above guard clause - - var commitMsg = Encoding.UTF8.GetString(buff, 0, commitMsgLength); - string treeHash = null; - var parentHashes = new List(); - ReceivePackCommitSignature author = null; - ReceivePackCommitSignature committer = null; - - var commitLines = commitMsg.Split('\n'); - - var commitHeadersEndIndex = 0; - foreach (var commitLine in commitLines) - { - commitHeadersEndIndex += 1; - - // Make sure we have safe default values in case the string is empty. - var commitHeaderType = ""; - var commitHeaderData = ""; - - // Find the index of the first space. - var firstSpace = commitLine.IndexOf(' '); - if (firstSpace < 0) - { - // Ensure that we always have a valid length for the type. - firstSpace = commitLine.Length; - } - - // Take everything up to the first space as the type. - commitHeaderType = commitLine.Substring(0, firstSpace); - - // Data starts immediately following the space (if there is any). - var dataStart = firstSpace + 1; - if (dataStart < commitLine.Length) - { - commitHeaderData = commitLine.Substring(dataStart); - } - - if (commitHeaderType == "tree") - { - treeHash = commitHeaderData; - } - else if (commitHeaderType == "parent") - { - parentHashes.Add(commitHeaderData); - } - else if (commitHeaderType == "author") - { - author = ParseSignature(commitHeaderData); - } - else if (commitHeaderType == "committer") - { - committer = ParseSignature(commitHeaderData); - } - else if (commitHeaderType == "") - { - // The first empty type indicates the end of the headers. - break; - } - else - { - // unrecognized header encountered, skip over it - } - } - - var commitComment = string.Join("\n", commitLines.Skip(commitHeadersEndIndex).ToArray()).TrimEnd('\n'); - - - // Compute commit hash - using (var sha1 = new SHA1CryptoServiceProvider()) - { - var commitHashHeader = Encoding.UTF8.GetBytes(string.Format("commit {0}\0", commitMsgLength)); - - sha1.TransformBlock(commitHashHeader, 0, commitHashHeader.Length, commitHashHeader, 0); - sha1.TransformFinalBlock(buff, 0, commitMsgLength); - - var commitHashBytes = sha1.Hash; - - var sb = new StringBuilder(); - foreach (byte b in commitHashBytes) - { - var hex = b.ToString("x2"); - sb.Append(hex); - } - var commitHash = sb.ToString(); - - return new ReceivePackCommit(commitHash, treeHash, parentHashes, - author, committer, commitComment); - } - } - - [MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - public ReceivePackCommitSignature ParseSignature(string commitHeaderData) - { - // Find the start and end markers of the email address. - var emailStart = commitHeaderData.IndexOf('<'); - var emailEnd = commitHeaderData.IndexOf('>'); - - // Leave out the trailing space. - var nameLength = emailStart - 1; - - // Leave out the starting bracket. - var emailLength = emailEnd - emailStart - 1; - - // Parse the name and email values. - var name = commitHeaderData.Substring(0, nameLength); - var email = commitHeaderData.Substring(emailStart + 1, emailLength); - - // The rest of the string is the timestamp, it may include a timezone. - var timestampString = commitHeaderData.Substring(emailEnd + 2); - var timestampComponents = timestampString.Split(' '); - - // Start with epoch in UTC, add the timestamp seconds. - var timestamp = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - timestamp = timestamp.AddSeconds(long.Parse(timestampComponents[0])); - - return new ReceivePackCommitSignature(name, email, timestamp); - } - - [MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - public void ReadStream(Stream s, byte[] buff) - { - var readBytes = s.Read(buff, 0, buff.Length); - if (readBytes != buff.Length) - { - throw new Exception(string.Format("Expected to read {0} bytes, got {1}", buff.Length, readBytes)); - } - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackPktLine.cs b/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackPktLine.cs deleted file mode 100644 index bb19b5931..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReceivePackHook/ReceivePackPktLine.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService.ReceivePackHook -{ - public class ReceivePackPktLine - { - public ReceivePackPktLine(string fromCommit, string toCommit, string refName) - { - this.FromCommit = fromCommit; - this.ToCommit = toCommit; - this.RefName = refName; - } - public string FromCommit { get; private set; } - public string ToCommit { get; private set; } - public string RefName { get; private set; } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/GitService/ReplicatingStream.cs b/Bonobo.Git.Server/Git/GitService/ReplicatingStream.cs deleted file mode 100644 index 4820f38d0..000000000 --- a/Bonobo.Git.Server/Git/GitService/ReplicatingStream.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Web; - -namespace Bonobo.Git.Server.Git.GitService -{ - public class ReplicatingStream : Stream - { - private readonly Stream source; - private readonly Stream target; - - public ReplicatingStream(Stream source, Stream target) - { - this.source = source; - this.target = target; - } - - public override bool CanRead - { - get { return source.CanRead; } - } - - public override bool CanSeek - { - get { return source.CanSeek; } - } - - public override bool CanWrite - { - get { return source.CanWrite; } - } - - public override void Flush() - { - source.Flush(); - target.Flush(); - } - - public override long Length - { - get { return source.Length; } - } - - public override long Position - { - get - { - return source.Position; - } - set - { - source.Position = value; - target.Position = value; - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - target.Read(buffer, offset, count); - return source.Read(buffer, offset, count); - } - - public override long Seek(long offset, SeekOrigin origin) - { - target.Seek(offset, origin); - return source.Seek(offset, origin); - } - - public override void SetLength(long value) - { - target.SetLength(value); - source.SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - target.Write(buffer, offset, count); - target.Flush(); - source.Write(buffer, offset, count); - } - } -} \ No newline at end of file diff --git a/Bonobo.Git.Server/Git/ReceivePackInspectStream.cs b/Bonobo.Git.Server/Git/ReceivePackInspectStream.cs new file mode 100644 index 000000000..a6467cffb --- /dev/null +++ b/Bonobo.Git.Server/Git/ReceivePackInspectStream.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Bonobo.Git.Server.Git { + /// + /// Reads data from the wrapped stream analyzing / parsing the initial parts of the communicated Git http protocol + /// and aggregates important metadata about what commands are performed. + /// This stream does not modify or transform the data. + /// + /// + /// + /// A recive pack request can be gigabytes in size and is made of a command list and a PACK file. + /// + /// + /// + /// This stream will peek on the (usually much smaller) command list portion of the request. This portion + /// contains refs (i.e. tags and branch heads) along with their old and new SHA1 identifiers so that Git can update them. + /// This information is sufficient for us to tell whether a branch / tag will be updated, created or deleted etc. + /// We may aswell tell which commits Git is going to add, if we check all refs between an old head pointer and + /// the new one after Git itself has received and parsed the PACK contents that far. + /// + /// + /// + /// The PACK contents could be parsed aswell, but this is going to cause a significant overhead because the + /// anatomy of the PACK format forces us to deflate all compressed objects in it. We can not just skip over certain + /// objects, because we don't know their inflated (current) size in the stream. + /// + /// + /// + /// + /// + /// + public class ReceivePackInspectStream: Stream { + // version, number of objects. (the 4 byte PACK signature is not included.) + private const int PackHeaderSize = 4 + 4; + private const int PktLineLengthSize = 4; + + private readonly Stream _wrappedStream; + + /// + /// While we follow the protocol, this will be the "phase" where we're currently in + /// + private ProtocolState _state; + + /// + /// The remaining amount of bytes we have to process until we're done with what we're currently reading. + /// + private int _bytesNeeded; + + // It's rather unlikely that we make use of our buffer at all, given that the initial Read buffer + // is of a decent size and the command list part of the request isn't unusually large. + private readonly Lazy _dataFromPreviousRead = new Lazy(); + + private readonly List _caughtOperations; + + private readonly Action _commandListReceived; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + + public override long Length { + get { throw new NotSupportedException(); } + } + + public override long Position { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// The Git commands which have been peeked from the wrapped stream so far. + /// + public ReadOnlyCollection PeekedCommands { get; } + + /// + /// The version number of the PACK. + /// + public int PackVersion { get; private set; } + + /// + /// The amount of objects in the Git pack file. + /// + public int PackObjectCount { get; private set; } + + /// + /// Initializes a new instance of this class. + /// + /// + /// Use to preprocess the Git commands right after they were + /// received. You may reject the whole pack then by throwing an . + /// + /// The origin stream to read from. + /// Called once the command list has been completely retrieved. + public ReceivePackInspectStream(Stream wrappedStream, Action commandListReceived = null) { + if (wrappedStream == null) throw new ArgumentNullException(nameof(wrappedStream)); + + _wrappedStream = wrappedStream; + _caughtOperations = new List(); + _commandListReceived = commandListReceived; + PeekedCommands = new ReadOnlyCollection(_caughtOperations); + + SetPktLineLengthState(); + } + + public override int Read(byte[] buffer, int offset, int count) { + if (buffer == null) throw new ArgumentNullException(nameof(buffer)); + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); + if (offset + count > buffer.Length) throw new ArgumentException(nameof(buffer)); + + int bytesRead = _wrappedStream.Read(buffer, offset, count); + + byte[] bufferToProcess = BufferWithPreviousDataPrepended(buffer, offset, ref bytesRead); + Continue(bytesRead, bufferToProcess, offset); + + return bytesRead; + } + + public override void Flush() { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) { + throw new NotImplementedException(); + } + + public override void SetLength(long value) { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) { + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing && _dataFromPreviousRead.IsValueCreated) + _dataFromPreviousRead.Value.Dispose(); + } + + private byte[] BufferWithPreviousDataPrepended(byte[] buffer, int offset, ref int bytesRead) + { + Debug.Assert(buffer != null); + Debug.Assert(offset >= 0); + + byte[] bufferToWorkWith; + if (_dataFromPreviousRead.IsValueCreated && _dataFromPreviousRead.Value.Length > 0) + { + var dataFromPreviousReadStream = _dataFromPreviousRead.Value; + + dataFromPreviousReadStream.Write(buffer, offset, bytesRead); + bufferToWorkWith = dataFromPreviousReadStream.ToArray(); + bytesRead = Convert.ToInt32(dataFromPreviousReadStream.Length); + + dataFromPreviousReadStream.SetLength(0); + } + else + bufferToWorkWith = buffer; + + return bufferToWorkWith; + } + + /// + /// Processes the data according to the current state of the protocol, if enough data is available. + /// Will recurse until either the whole command list has been processed or the buffer has run out of data. + /// + private void Continue(int bytesAvailable, byte[] buffer, int offset) { + Debug.Assert(bytesAvailable >= 0); + Debug.Assert(buffer != null); + Debug.Assert(offset >= 0); + + if (_bytesNeeded == 0) + return; + if (bytesAvailable < _bytesNeeded) { + _dataFromPreviousRead.Value.Write(buffer, offset, bytesAvailable); + return; + } + + switch (_state) { + case ProtocolState.PktLineLengthOrPackString: { + int peekByte = buffer[offset]; + if (peekByte != 'P') { + int pktLineLength = FourHexCharsToInt(buffer, offset); + + bool isFlushPkt = pktLineLength == 0; + if (isFlushPkt) + SetPackHeaderState(); // all commands have been processed + else if (pktLineLength == PktLineLengthSize) // empty line + SetPktLineLengthState(); + else + SetPktLinePayloadState(pktLineLength - PktLineLengthSize); + } else { + SetPackHeaderState(); // it was a pack string + } + + Continue(bytesAvailable - PktLineLengthSize, buffer, offset + PktLineLengthSize); + return; + } + case ProtocolState.PktLinePayload: { + int payloadLength = _bytesNeeded; + string pktLinePayload = Encoding.UTF8.GetString(buffer, offset, payloadLength); + ProcessCommandPktLine(pktLinePayload); + + SetPktLineLengthState(); + Continue(bytesAvailable - payloadLength, buffer, offset + payloadLength); + return; + } + case ProtocolState.PackHeader: { + PackVersion = FourNetByteToInt(buffer, offset); + PackObjectCount = FourNetByteToInt(buffer, offset + 4); + + _commandListReceived?.Invoke(this); + + SetPackObjectsState(); + Continue(bytesAvailable - PackHeaderSize, buffer, offset + PackHeaderSize); + return; + } + case ProtocolState.PackObjects: // won't parse this, skip + return; + } + } + + // command-pkt line format: \n? + // first command-pkt also appends a \0 followed by a + private void ProcessCommandPktLine(string payload) { + Debug.Assert(payload != null); + + try { + var oldSha1 = payload.Substring(0, 40); + var newSha1 = payload.Substring(41, 40); + + int refPathPos = 82; + int newlineOrTerminatorPos = payload.IndexOfAny(new[] {'\n', '\0'}, refPathPos); + + string refPath; + if (newlineOrTerminatorPos != -1) + refPath = payload.Substring(refPathPos, newlineOrTerminatorPos - refPathPos); + else + refPath = payload; + + _caughtOperations.Add(new GitReceiveCommand(refPath, oldSha1, newSha1)); + } catch (ArgumentException) { + throw new IOException("Unexpected pkt-line payload: " + payload); + } + } + + private void SetPktLineLengthState() { + _state = ProtocolState.PktLineLengthOrPackString; + _bytesNeeded = PktLineLengthSize; + } + + private void SetPktLinePayloadState(int payloadLength) { + Debug.Assert(payloadLength > PktLineLengthSize); + + _state = ProtocolState.PktLinePayload; + _bytesNeeded = payloadLength; + } + + private void SetPackHeaderState() { + _state = ProtocolState.PackHeader; + _bytesNeeded = PackHeaderSize; + } + + private void SetPackObjectsState() { + _state = ProtocolState.PackObjects; + _bytesNeeded = 0; + } + + // "0032" => 50; "00a0" => 160 etc. + private static int FourHexCharsToInt(byte[] buffer, int offset) { + Debug.Assert(buffer != null); + Debug.Assert(offset >= 0); + + int result = HexCharToInt(buffer[offset]) * 16 * 16 * 16; + result += HexCharToInt(buffer[offset + 1]) * 16 * 16; + result += HexCharToInt(buffer[offset + 2]) * 16; + result += HexCharToInt(buffer[offset + 3]); + return result; + } + + private static int HexCharToInt(int chr) { + Debug.Assert((chr >= '0' && chr <= '9') || (chr >= 'a' && chr <= 'f')); + + if (chr < 'a') + return chr - '0'; + else + return chr - 'a' + 10; + } + + /// + /// Converts a network byte-order int32 to a normal integer. + /// + private static int FourNetByteToInt(byte[] buffer, int offset) { + Debug.Assert(buffer != null); + Debug.Assert(offset >= 0); + + return (buffer[offset] << 24) | + (buffer[offset + 1] << 16) | + (buffer[offset + 2] << 8) | + (buffer[offset + 3] << 0); + } + + private enum ProtocolState { + PktLineLengthOrPackString, + PktLinePayload, + PackHeader, + PackObjects, + } + } +} \ No newline at end of file diff --git a/Bonobo.Git.Server/Global.asax.cs b/Bonobo.Git.Server/Global.asax.cs index 025442966..b93911a4c 100644 --- a/Bonobo.Git.Server/Global.asax.cs +++ b/Bonobo.Git.Server/Global.asax.cs @@ -17,9 +17,6 @@ using Bonobo.Git.Server.Data.Update; using Bonobo.Git.Server.Git; using Bonobo.Git.Server.Git.GitService; -using Bonobo.Git.Server.Git.GitService.ReceivePackHook; -using Bonobo.Git.Server.Git.GitService.ReceivePackHook.Durability; -using Bonobo.Git.Server.Git.GitService.ReceivePackHook.Hooks; using Bonobo.Git.Server.Security; using Microsoft.Practices.Unity; using System.Runtime.Caching; @@ -29,6 +26,7 @@ using System.Security.Claims; using System.Web.Helpers; using System.Web.Hosting; +using Bonobo.Git.Server.Application.Hooks; using Serilog; namespace Bonobo.Git.Server @@ -129,16 +127,6 @@ private static void RegisterDependencyResolver() { var container = new UnityContainer(); - /* - The UnityDecoratorContainerExtension breaks resolving named type registrations, like: - - container.RegisterType("ActiveDirectory"); - container.RegisterType("Internal"); - IMembershipService membershipService = container.Resolve(AuthenticationSettings.MembershipService); - - Until this issue is resolved, the following two switch hacks will have to do - */ - switch (AuthenticationSettings.MembershipService.ToLowerInvariant()) { case "activedirectory": @@ -179,7 +167,7 @@ private static void RegisterDependencyResolver() return new ConfigurationBasedRepositoryLocator(UserConfiguration.Current.Repositories); }) ); - + container.RegisterInstance( new GitServiceExecutorParams() { @@ -190,12 +178,14 @@ private static void RegisterDependencyResolver() container.RegisterType(); - if (AppSettings.IsPushAuditEnabled) - { - EnablePushAuditAnalysis(container); - } + container.RegisterType("Executor"); + container.RegisterType("AuditHandler"); - container.RegisterType(); + container.RegisterType( + new InjectionConstructor( + new ResolvedParameter("Executor"), + new ResolvedParameter("AuditHandler"), + new ResolvedParameter())); DependencyResolver.SetResolver(new UnityDependencyResolver(container)); @@ -206,60 +196,6 @@ private static void RegisterDependencyResolver() FilterProviders.Providers.Add(provider); } - private static void EnablePushAuditAnalysis(IUnityContainer container) - { - var isReceivePackRecoveryProcessEnabled = !string.IsNullOrEmpty(ConfigurationManager.AppSettings["RecoveryDataPath"]); - - if (isReceivePackRecoveryProcessEnabled) - { - // git service execution durability registrations to enable receive-pack hook execution after failures - container.RegisterType(); - container.RegisterType(); - container.RegisterType(); - container.RegisterType(); - container.RegisterInstance(new NamedArguments.FailedPackWaitTimeBeforeExecution(TimeSpan.FromSeconds(5 * 60))); - - container.RegisterInstance(new NamedArguments.ReceivePackRecoveryDirectory( - Path.IsPathRooted(ConfigurationManager.AppSettings["RecoveryDataPath"]) ? - ConfigurationManager.AppSettings["RecoveryDataPath"] : - HttpContext.Current.Server.MapPath(ConfigurationManager.AppSettings["RecoveryDataPath"]))); - } - - // base git service executor - container.RegisterType(); - container.RegisterType(); - - // receive pack hooks - container.RegisterType(); - container.RegisterType(); - - // run receive-pack recovery if possible - if (isReceivePackRecoveryProcessEnabled) - { - var recoveryProcess = container.Resolve( - new ParameterOverride( - "failedPackWaitTimeBeforeExecution", - new NamedArguments.FailedPackWaitTimeBeforeExecution(TimeSpan.FromSeconds(0)))); // on start up set time to wait = 0 so that recovery for all waiting packs is attempted - - try - { - recoveryProcess.RecoverAll(); - } - catch - { - // don't let a failed recovery attempt stop start-up process - } - finally - { - if (recoveryProcess != null) - { - container.Teardown(recoveryProcess); - } - } - } - } - - protected void Application_Error(object sender, EventArgs e) { Exception exception = Server.GetLastError(); diff --git a/Bonobo.Git.Server/Views/Repository/Detail.cshtml b/Bonobo.Git.Server/Views/Repository/Detail.cshtml index bece77225..242d5dee3 100644 --- a/Bonobo.Git.Server/Views/Repository/Detail.cshtml +++ b/Bonobo.Git.Server/Views/Repository/Detail.cshtml @@ -75,19 +75,17 @@ @Html.LabelFor(m => m.LinksUrl) @Html.TextBoxFor(m => m.LinksUrl, new { @readonly=""}) - @if (AppSettings.IsPushAuditEnabled) { -
- @Html.LabelFor(m => m.AuditPushUser) - @if (Model.AuditPushUser) - { - @Resources.Repository_Detail_Yes - } - else - { - @Resources.Repository_Detail_No - } -
- } +
+ @Html.LabelFor(m => m.AuditPushUser) + @if (Model.AuditPushUser) + { + @Resources.Repository_Detail_Yes + } + else + { + @Resources.Repository_Detail_No + } +
@Html.LabelFor(m => m.Users) @for (int i = 0; i < Model.Users.Length; i++) diff --git a/Bonobo.Git.Server/Views/Repository/Edit.cshtml b/Bonobo.Git.Server/Views/Repository/Edit.cshtml index 2c3d32337..2cc28c108 100644 --- a/Bonobo.Git.Server/Views/Repository/Edit.cshtml +++ b/Bonobo.Git.Server/Views/Repository/Edit.cshtml @@ -73,15 +73,11 @@ else @Html.EnumDropDownListFor(m => m.AllowAnonymousPush, attribs)
- - @if (AppSettings.IsPushAuditEnabled) - { -
- @Html.LabelFor(m => m.AuditPushUser) - @Html.CheckBoxFor(m => m.AuditPushUser) - -
- } +
+ @Html.LabelFor(m => m.AuditPushUser) + @Html.CheckBoxFor(m => m.AuditPushUser) + +
@Html.LabelFor(m => m.LinksUseGlobal) @Html.CheckBoxFor(m => m.LinksUseGlobal, new { @onClick = "document.getElementById('LinksRegex').disabled = this.checked; document.getElementById('LinksUrl').disabled = this.checked" }) diff --git a/Bonobo.Git.Server/web.config b/Bonobo.Git.Server/web.config index c41892d6a..882ee3ebe 100644 --- a/Bonobo.Git.Server/web.config +++ b/Bonobo.Git.Server/web.config @@ -12,7 +12,6 @@ -