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 }
知识兔