1. 程式人生 > >ASP.NET Core 開源GitServer 實現自己的GitHub

ASP.NET Core 開源GitServer 實現自己的GitHub

date pes name spn isp des odin control validate

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 都可以。

ASP.NET Core 開源GitServer 實現自己的GitHub