diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs index 0f1bd2c..7d9d677 100644 --- a/BililiveRecorder.Cli/Program.cs +++ b/BililiveRecorder.Cli/Program.cs @@ -39,7 +39,8 @@ namespace BililiveRecorder.Cli var cmd_run = new Command("run", "Run BililiveRecorder in standard mode") { new Option(new []{ "--http-bind", "--bind", "-b" }, () => null, "Bind address for http service"), - // new Option(new []{ "--http-port", "--port", "-p" }, () => null, "Port number for http service"), + new Option(new []{ "--http-basic-user" }, () => null, "Web interface username"), + new Option(new []{ "--http-basic-pass" }, () => null, "Web interface password"), new Option(new []{ "--loglevel", "--log", "-l" }, () => LogEventLevel.Information, "Minimal log level output to console"), new Option(new []{ "--logfilelevel", "--flog" }, () => LogEventLevel.Debug, "Minimal log level output to file"), new Option(new []{ "--cert-pem-path", "--pem" }, "Path of the certificate pem file"), @@ -55,7 +56,8 @@ namespace BililiveRecorder.Cli var cmd_portable = new Command("portable", "Run BililiveRecorder in config-less mode") { new Option(new []{ "--http-bind", "--bind", "-b" }, () => null, "Bind address for http service"), - // new Option(new []{ "--http-port", "--port", "-p" }, () => null, "Port number for http service"), + new Option(new []{ "--http-basic-user" }, () => null, "Web interface username"), + new Option(new []{ "--http-basic-pass" }, () => null, "Web interface password"), new Option(new []{ "--loglevel", "--log", "-l" }, () => LogEventLevel.Information, "Minimal log level output to console"), new Option(new []{ "--logfilelevel", "--flog" }, () => LogEventLevel.Debug, "Minimal log level output to file"), new Option(new []{ "--cert-pem-path", "--pem" }, "Path of the certificate pem file"), @@ -170,6 +172,11 @@ namespace BililiveRecorder.Cli .ConfigureServices(services => { services.AddSingleton(recorderAccessProxy); + + if (sharedArguments.HttpBasicUser is not null || sharedArguments.HttpBasicPass is not null) + { + services.AddSingleton(new BasicAuthCredential(sharedArguments.HttpBasicUser ?? string.Empty, sharedArguments.HttpBasicPass ?? string.Empty)); + } }) .ConfigureWebHost(webBuilder => { @@ -419,7 +426,9 @@ namespace BililiveRecorder.Cli public string? HttpBind { get; set; } = null; - // public int? HttpPort { get; set; } = null; + public string? HttpBasicUser { get; set; } = null; + + public string? HttpBasicPass { get; set; } = null; public string? CertPemPath { get; set; } = null; diff --git a/BililiveRecorder.Web/BasicAuthCredential.cs b/BililiveRecorder.Web/BasicAuthCredential.cs new file mode 100644 index 0000000..e312b1f --- /dev/null +++ b/BililiveRecorder.Web/BasicAuthCredential.cs @@ -0,0 +1,19 @@ +using System; +using System.Text; + +namespace BililiveRecorder.Web +{ + public class BasicAuthCredential + { + public BasicAuthCredential(string username, string password) + { + this.Username = username ?? throw new ArgumentNullException(nameof(username)); + this.Password = password ?? throw new ArgumentNullException(nameof(password)); + this.EncoededValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + } + + public string Username { get; } + public string Password { get; } + public string EncoededValue { get; } + } +} diff --git a/BililiveRecorder.Web/BasicAuthMiddleware.cs b/BililiveRecorder.Web/BasicAuthMiddleware.cs new file mode 100644 index 0000000..fef69c9 --- /dev/null +++ b/BililiveRecorder.Web/BasicAuthMiddleware.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; +using Microsoft.Net.Http.Headers; + +namespace BililiveRecorder.Web +{ + public class BasicAuthMiddleware + { + private readonly RequestDelegate next; + private readonly ManifestEmbeddedFileProvider fileProvider; + private readonly BasicAuthCredential? credential; + private const string BasicAndSpace = "Basic "; + + private static string? Html401Page; + + public BasicAuthMiddleware(RequestDelegate next, ManifestEmbeddedFileProvider fileProvider, BasicAuthCredential? credential) + { + this.next = next ?? throw new ArgumentNullException(nameof(next)); + this.fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); + this.credential = credential; + } + + public Task InvokeAsync(HttpContext context) + { + if (this.credential is null) + { + // 没有启用身份验证 + return this.next(context); + } + + string headerValue = context.Request.Headers["Authorization"]; + if (string.IsNullOrEmpty(headerValue) || + !headerValue.StartsWith(BasicAndSpace, StringComparison.OrdinalIgnoreCase)) + { + return this.ResponseWith401Async(context); + } + + var requestCredential = headerValue[BasicAndSpace.Length..].Trim(); + + if (string.IsNullOrEmpty(requestCredential)) + { + return this.ResponseWith401Async(context); + } + + if (this.credential.EncoededValue.Equals(requestCredential, StringComparison.Ordinal)) + { + return this.next(context); + } + else + { + return this.ResponseWith401Async(context); + } + } + + private async Task ResponseWith401Async(HttpContext context) + { + context.Response.StatusCode = 401; + context.Response.ContentType = "text/html"; + context.Response.Headers.Append(HeaderNames.WWWAuthenticate, $"{BasicAndSpace}realm=\"BililiveRecorder {GitVersionInformation.FullSemVer}\""); + + if (Html401Page is null) + { + using var file = this.fileProvider.GetFileInfo("/401.html").CreateReadStream(); + using var reader = new StreamReader(file); + var str = await reader.ReadToEndAsync().ConfigureAwait(false); + Html401Page = str.Replace("__VERSION__", GitVersionInformation.FullSemVer).Replace("__FULL_VERSION__", GitVersionInformation.InformationalVersion); + } + + await context.Response.WriteAsync(Html401Page, System.Text.Encoding.UTF8).ConfigureAwait(false); + } + } +} diff --git a/BililiveRecorder.Web/Startup.cs b/BililiveRecorder.Web/Startup.cs index d222955..81d3c23 100644 --- a/BililiveRecorder.Web/Startup.cs +++ b/BililiveRecorder.Web/Startup.cs @@ -122,6 +122,8 @@ namespace BililiveRecorder.Web { const string PAGE404 = "/404.html"; + app.UseMiddleware(); + app.UseCors().UseWebSockets(); app.Use(static next => async context => diff --git a/BililiveRecorder.Web/embeded/401.html b/BililiveRecorder.Web/embeded/401.html new file mode 100644 index 0000000..d7a7cc9 --- /dev/null +++ b/BililiveRecorder.Web/embeded/401.html @@ -0,0 +1,19 @@ + + + + + + + + 401 未登录 - B站录播姬 __VERSION__ + + + + +

HTTP 401

+

+ B站录播姬 __VERSION__ +

+ + +