ASP.NET Core 开源GitServer 实现自己的GitHub

ASP.NET Core 2.0 开源Git HTTP Server,实现类似 GitHub、GitLab。

GitHub:https://github.com/linezero/GitServer

设置

  "GitSettings": {
    "BasePath": "D:\\Git",
    "GitPath": "git"
  }

需要先安装Git,并确保git 命令可以执行,GitPath 可以是 git 的绝对路径。

目前实现的功能

  • 创建仓库
  • 浏览仓库
  • git客户端push pull
  • 数据库支持 SQLite、MSSQL、MySQL
  • 支持用户管理仓库

更多功能可以查看readme,也欢迎大家贡献支持。

Git交互

LibGit2Sharp 用于操作Git库,实现创建读取仓库信息及删除仓库。

以下是主要代码:

        public Repository CreateRepository(string name)
        {
            string path = Path.Combine(Settings.BasePath, name);
            Repository repo = new Repository(Repository.Init(path, true));
            return repo;
        }

        public Repository CreateRepository(string name, string remoteUrl)
        {
            var path = Path.Combine(Settings.BasePath, name);
            try
            {
                using (var repo = new Repository(Repository.Init(path, true)))
                {
                    repo.Config.Set("core.logallrefupdates", true);
                    repo.Network.Remotes.Add("origin", remoteUrl, "+refs/*:refs/*");
                    var logMessage = "";
                    foreach (var remote in repo.Network.Remotes)
                    {
                        IEnumerable<string> refSpecs = remote.FetchRefSpecs.Select(x => x.Specification);
                        Commands.Fetch(repo, remote.Name, refSpecs, null, logMessage);
                    }
                    return repo;
                }                
            }
            catch
            {
                try
                {
                    Directory.Delete(path, true);
                }
                catch { }
                return null;
            }
        }

        public void DeleteRepository(string name)
        {
            Exception e = null;
            for(int i = 0; i < 3; i++)
            {
                try
                {
                    string path = Path.Combine(Settings.BasePath, name);
                    Directory.Delete(path, true);

                }
                catch(Exception ex) { e = ex; }
            }

            if (e != null)
                throw new GitException("Failed to delete repository", e);
        }

 

执行Git命令

git-upload-pack 

git-receive-pack

主要代码 GitCommandResult 实现IActionResult

public async Task ExecuteResultAsync(ActionContext context)
        {
            HttpResponse response = context.HttpContext.Response;
            Stream responseStream = GetOutputStream(context.HttpContext);

            string contentType = $"application/x-{Options.Service}";
            if (Options.AdvertiseRefs)
                contentType += "-advertisement";

            response.ContentType = contentType;

            response.Headers.Add("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
            response.Headers.Add("Pragma", "no-cache");
            response.Headers.Add("Cache-Control", "no-cache, max-age=0, must-revalidate");

            ProcessStartInfo info = new ProcessStartInfo(_gitPath, Options.ToString())
            {
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            };

            using (Process process = Process.Start(info))
            {
                GetInputStream(context.HttpContext).CopyTo(process.StandardInput.BaseStream);

                if (Options.EndStreamWithNull)
                    process.StandardInput.Write('\0');
                process.StandardInput.Dispose();

                using (StreamWriter writer = new StreamWriter(responseStream))
                {
                    if (Options.AdvertiseRefs)
                    {
                        string service = $"# service={Options.Service}\n";
                        writer.Write($"{service.Length + 4:x4}{service}0000");
                        writer.Flush();
                    }

                    process.StandardOutput.BaseStream.CopyTo(responseStream);
                }

                process.WaitForExit();
            }
        }

 

BasicAuthentication 基本认证实现

git http 默认的认证为Basic 基本认证,所以这里实现Basic 基本认证。

在ASP.NET Core 2.0 中 Authentication 变化很大之前1.0的一些代码是无法使用。

首先实现 AuthenticationHandler,然后实现  AuthenticationSchemeOptions,创建 BasicAuthenticationOptions。

最主要就是这两个类,下面两个类为辅助类,用于配置和中间件注册。

更多可以查看官方文档

身份验证

https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/

https://docs.microsoft.com/zh-cn/aspnet/core/migration/1x-to-2x/identity-2x

 1    public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
 2     {
 3         public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
 4             : base(options, logger, encoder, clock)
 5         { }
 6         protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
 7         {
 8             if (!Request.Headers.ContainsKey("Authorization"))
 9                 return AuthenticateResult.NoResult();
10 
11             string authHeader = Request.Headers["Authorization"];
12             if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
13                 return AuthenticateResult.NoResult();
14 
15             string token = authHeader.Substring("Basic ".Length).Trim();
16             string credentialString = Encoding.UTF8.GetString(Convert.FromBase64String(token));
17             string[] credentials = credentialString.Split(':');
18 
19             if (credentials.Length != 2)
20                 return AuthenticateResult.Fail("More than two strings seperated by colons found");
21 
22             ClaimsPrincipal principal = await Options.SignInAsync(credentials[0], credentials[1]);
23 
24             if (principal != null)
25             {
26                 AuthenticationTicket ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), BasicAuthenticationDefaults.AuthenticationScheme);
27                 return AuthenticateResult.Success(ticket);
28             }
29 
30             return AuthenticateResult.Fail("Wrong credentials supplied");
31         }
32         protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
33         {
34             Response.StatusCode = 403;
35             return base.HandleForbiddenAsync(properties);
36         }
37 
38         protected override Task HandleChallengeAsync(AuthenticationProperties properties)
39         {
40             Response.StatusCode = 401;
41             string headerValue = $"{BasicAuthenticationDefaults.AuthenticationScheme} realm=\"{Options.Realm}\"";
42             Response.Headers.Append(Microsoft.Net.Http.Headers.HeaderNames.WWWAuthenticate, headerValue);
43             return base.HandleChallengeAsync(properties);
44         }
45     }
46 
47     public class BasicAuthenticationOptions : AuthenticationSchemeOptions, IOptions<BasicAuthenticationOptions>
48     {
49         private string _realm;
50 
51         public IServiceCollection ServiceCollection { get; set; }
52         public BasicAuthenticationOptions Value => this;
53         public string Realm
54         {
55             get { return _realm; }
56             set
57             {
58                 _realm = value;
59             }
60         }
61 
62         public async Task<ClaimsPrincipal> SignInAsync(string userName, string password)
63         {
64             using (var serviceScope = ServiceCollection.BuildServiceProvider().CreateScope())
65             {
66                 var _user = serviceScope.ServiceProvider.GetService<IRepository<User>>();
67                 var user = _user.List(r => r.Name == userName && r.Password == password).FirstOrDefault();
68                 if (user == null)
69                     return null;
70                 var identity = new ClaimsIdentity(BasicAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
71                 identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
72                 var principal = new ClaimsPrincipal(identity);
73                 return principal;
74             }
75         }
76     }
77 
78     public static class BasicAuthenticationDefaults
79     {
80         public const string AuthenticationScheme = "Basic";
81     }
82     public static class BasicAuthenticationExtensions
83     {
84         public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder)
85             => builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme, _ => { _.ServiceCollection = builder.Services;_.Realm = "GitServer"; });
86 
87         public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action<BasicAuthenticationOptions> configureOptions)
88             => builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme, configureOptions);
89 
90         public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action<BasicAuthenticationOptions> configureOptions)
91             => builder.AddBasic(authenticationScheme, displayName: null, configureOptions: configureOptions);
92 
93         public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<BasicAuthenticationOptions> configureOptions)
94         {
95             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptions<BasicAuthenticationOptions>, BasicAuthenticationOptions>());
96             return builder.AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
97         }
98     }
View Code

 

CookieAuthentication Cookie认证

实现自定义登录,无需identity ,实现注册登录。

主要代码:

启用Cookie

https://github.com/linezero/GitServer/blob/master/GitServer/Startup.cs#L60

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            }).AddCookie(options=> {
                options.AccessDeniedPath = "/User/Login";
                options.LoginPath = "/User/Login";
            })

登录

https://github.com/linezero/GitServer/blob/master/GitServer/Controllers/UserController.cs#L34

                    var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
                    identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Name));
                    identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
                    identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
                    var principal = new ClaimsPrincipal(identity);
                    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);

官方文档介绍:https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/cookie?tabs=aspnetcore2x

部署说明

发布后配置数据库及git目录(可以为绝对地址和命令)、git 仓库目录。

{
  "ConnectionStrings": {
    "ConnectionType": "Sqlite", //Sqlite,MSSQL,MySQL
    "DefaultConnection": "Filename=gitserver.db"
  },
  "GitSettings": {
    "BasePath": "D:\\Git",
    "GitPath": "git"
  }
}

运行后注册账户,登录账户创建仓库,然后根据提示操作,随后git push、git pull 都可以。

 

posted @ 2017-10-28 11:35  LineZero  阅读(5635)  评论(6编辑  收藏  举报