mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
CLI: Support HTTPS
This commit is contained in:
parent
1cebee7c0e
commit
c054e8a3f5
|
@ -5,6 +5,9 @@ using System.CommandLine.Invocation;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -16,6 +19,7 @@ using BililiveRecorder.DependencyInjection;
|
|||
using BililiveRecorder.ToolBox;
|
||||
using BililiveRecorder.Web;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
|
@ -34,9 +38,15 @@ namespace BililiveRecorder.Cli
|
|||
|
||||
var cmd_run = new Command("run", "Run BililiveRecorder in standard mode")
|
||||
{
|
||||
new Option<string?>(new []{ "--web-bind", "--bind", "-b" }, () => null, "Bind address for web api"),
|
||||
new Option<string?>(new []{ "--http-bind", "--bind", "-b" }, () => null, "Bind address for http service"),
|
||||
// new Option<int?>(new []{ "--http-port", "--port", "-p" }, () => null, "Port number for http service"),
|
||||
new Option<LogEventLevel>(new []{ "--loglevel", "--log", "-l" }, () => LogEventLevel.Information, "Minimal log level output to console"),
|
||||
new Option<LogEventLevel>(new []{ "--logfilelevel", "--flog" }, () => LogEventLevel.Debug, "Minimal log level output to file"),
|
||||
new Option<string?>(new []{ "--cert-pem-path", "--pem" }, "Path of the certificate pem file"),
|
||||
new Option<string?>(new []{ "--cert-key-path", "--key" }, "Path of the certificate key file"),
|
||||
new Option<string?>(new []{ "--cert-pfx-path", "--pfx" }, "Path of the certificate pfx file"),
|
||||
new Option<string?>(new []{ "--cert-password"}, "Password of the certificate"),
|
||||
|
||||
new Argument<string>("path"),
|
||||
};
|
||||
cmd_run.AddAlias("r");
|
||||
|
@ -44,9 +54,15 @@ namespace BililiveRecorder.Cli
|
|||
|
||||
var cmd_portable = new Command("portable", "Run BililiveRecorder in config-less mode")
|
||||
{
|
||||
new Option<string?>(new []{ "--web-bind", "--bind", "-b" }, () => null, "Bind address for web api"),
|
||||
new Option<string?>(new []{ "--http-bind", "--bind", "-b" }, () => null, "Bind address for http service"),
|
||||
// new Option<int?>(new []{ "--http-port", "--port", "-p" }, () => null, "Port number for http service"),
|
||||
new Option<LogEventLevel>(new []{ "--loglevel", "--log", "-l" }, () => LogEventLevel.Information, "Minimal log level output to console"),
|
||||
new Option<LogEventLevel>(new []{ "--logfilelevel", "--flog" }, () => LogEventLevel.Debug, "Minimal log level output to file"),
|
||||
new Option<string?>(new []{ "--cert-pem-path", "--pem" }, "Path of the certificate pem file"),
|
||||
new Option<string?>(new []{ "--cert-key-path", "--key" }, "Path of the certificate key file"),
|
||||
new Option<string?>(new []{ "--cert-pfx-path", "--pfx" }, "Path of the certificate pfx file"),
|
||||
new Option<string?>(new []{ "--cert-password"}, "Password of the certificate"),
|
||||
|
||||
new Option<RecordMode>(new []{ "--record-mode", "--mode" }, () => RecordMode.Standard, "Recording mode"),
|
||||
new Option<string>(new []{ "--cookie", "-c" }, "Cookie string for api requests"),
|
||||
new Option<string>(new []{ "--filename", "-f" }, "File name format"),
|
||||
|
@ -89,7 +105,7 @@ namespace BililiveRecorder.Cli
|
|||
|
||||
var serviceProvider = BuildServiceProvider(config, logger);
|
||||
|
||||
return await RunRecorderAsync(serviceProvider, args.WebBind);
|
||||
return await RunRecorderAsync(serviceProvider, args);
|
||||
}
|
||||
|
||||
private static async Task<int> RunPortableModeAsync(PortableModeArguments args)
|
||||
|
@ -132,10 +148,10 @@ namespace BililiveRecorder.Cli
|
|||
|
||||
var serviceProvider = BuildServiceProvider(config, logger);
|
||||
|
||||
return await RunRecorderAsync(serviceProvider, args.WebBind);
|
||||
return await RunRecorderAsync(serviceProvider, args);
|
||||
}
|
||||
|
||||
private static async Task<int> RunRecorderAsync(IServiceProvider serviceProvider, string? webBind)
|
||||
private static async Task<int> RunRecorderAsync(IServiceProvider serviceProvider, SharedArguments sharedArguments)
|
||||
{
|
||||
var logger = serviceProvider.GetRequiredService<ILogger>();
|
||||
IRecorder recorderAccessProxy(IServiceProvider x) => serviceProvider.GetRequiredService<IRecorder>();
|
||||
|
@ -143,15 +159,12 @@ namespace BililiveRecorder.Cli
|
|||
// recorder setup done
|
||||
// check if web service required
|
||||
IHost? host = null;
|
||||
if (webBind is null)
|
||||
if (sharedArguments.HttpBind is null)
|
||||
{
|
||||
logger.Information("Web API not enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
var bind = FixBindUrl(webBind, logger);
|
||||
logger.Information("Creating web server on {BindAddress}", bind);
|
||||
|
||||
host = new HostBuilder()
|
||||
.UseSerilog(logger: logger)
|
||||
.ConfigureServices(services =>
|
||||
|
@ -161,10 +174,33 @@ namespace BililiveRecorder.Cli
|
|||
.ConfigureWebHost(webBuilder =>
|
||||
{
|
||||
webBuilder
|
||||
.UseUrls(urls: bind)
|
||||
.UseKestrel(option =>
|
||||
{
|
||||
(var scheme, var host, var port) = ParseBindArgument(sharedArguments.HttpBind, logger);
|
||||
|
||||
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
option.ListenLocalhost(port, ListenConfigure);
|
||||
}
|
||||
else if (IPAddress.TryParse(host, out var ip))
|
||||
{
|
||||
option.Listen(ip, port, ListenConfigure);
|
||||
}
|
||||
else
|
||||
{
|
||||
option.ListenAnyIP(port, ListenConfigure);
|
||||
}
|
||||
|
||||
void ListenConfigure(ListenOptions listenOptions)
|
||||
{
|
||||
if (scheme == "https")
|
||||
{
|
||||
listenOptions.UseHttps(LoadCertificate(sharedArguments, logger) ?? GenerateSelfSignedCertificate(logger), https =>
|
||||
{
|
||||
https.SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.UseStartup<Startup>();
|
||||
})
|
||||
|
@ -223,24 +259,104 @@ namespace BililiveRecorder.Cli
|
|||
return 0;
|
||||
}
|
||||
|
||||
private static string FixBindUrl(string bind, ILogger logger)
|
||||
private static X509Certificate2? LoadCertificate(SharedArguments arguments, ILogger logger)
|
||||
{
|
||||
if (Regex.IsMatch(bind, @"^\d+$"))
|
||||
if (arguments.CertPfxPath is not null)
|
||||
{
|
||||
var result = "http://localhost:" + bind;
|
||||
logger.Warning("标准的参数格式为 {Example} 而不是只有端口号,已自动修正为 {BindUrl}", @"http://{接口地址}:{端口号}", result);
|
||||
return result;
|
||||
if (arguments.CertPemPath is not null || arguments.CertKeyPath is not null)
|
||||
{
|
||||
logger.Warning("Both cert-pfx and cert-pem/cert-key are specified. Using cert-pfx.");
|
||||
}
|
||||
|
||||
if (!File.Exists(arguments.CertPfxPath))
|
||||
{
|
||||
logger.Error("Certificate file {Path} not found.", arguments.CertPfxPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new X509Certificate2(arguments.CertPfxPath, arguments.CertPassword);
|
||||
}
|
||||
else
|
||||
if (Regex.IsMatch(bind, @"https?:"))
|
||||
else if (arguments.CertPemPath is not null || arguments.CertKeyPath is not null)
|
||||
{
|
||||
return bind;
|
||||
if (arguments.CertPemPath is null)
|
||||
{
|
||||
logger.Error("Certificate PEM file not specified.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (arguments.CertKeyPath is null)
|
||||
{
|
||||
logger.Error("Certificate key file not specified.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(arguments.CertPemPath))
|
||||
{
|
||||
logger.Error("Certificate PEM file {Path} not found.", arguments.CertPemPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!File.Exists(arguments.CertKeyPath))
|
||||
{
|
||||
logger.Error("Certificate key file {Path} not found.", arguments.CertKeyPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
var cert = arguments.CertPassword is null
|
||||
? X509Certificate2.CreateFromPemFile(arguments.CertPemPath, arguments.CertKeyPath)
|
||||
: X509Certificate2.CreateFromEncryptedPemFile(arguments.CertPemPath, arguments.CertPassword, arguments.CertKeyPath);
|
||||
|
||||
return new X509Certificate2(cert.Export(X509ContentType.Pfx));
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = "http://" + bind;
|
||||
logger.Warning("标准的参数格式为 {Example} 而不是只有 IP 和端口号,已自动修正为 {BindUrl}", @"http://{接口地址}:{端口号}", result);
|
||||
return result;
|
||||
logger.Debug("No certificate specified.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate2 GenerateSelfSignedCertificate(ILogger logger)
|
||||
{
|
||||
logger.Warning("使用录播姬生成的自签名证书");
|
||||
|
||||
using var key = RSA.Create();
|
||||
var req = new CertificateRequest("CN=B站录播姬, OU=自签名证书,每次启动都会重新生成", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
var subjectAltName = new SubjectAlternativeNameBuilder();
|
||||
subjectAltName.AddDnsName("BililiveRecorder");
|
||||
subjectAltName.AddDnsName("localhost");
|
||||
subjectAltName.AddIpAddress(IPAddress.Loopback);
|
||||
subjectAltName.AddIpAddress(IPAddress.IPv6Loopback);
|
||||
subjectAltName.AddDnsName("*.nip.io");
|
||||
subjectAltName.AddDnsName("*.sslip.io");
|
||||
req.CertificateExtensions.Add(subjectAltName.Build());
|
||||
|
||||
var firstDayofCurrentYear = new DateTimeOffset(DateTime.Now.Year, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
using var cert = req.CreateSelfSigned(firstDayofCurrentYear, firstDayofCurrentYear.AddYears(10));
|
||||
var bytes = cert.Export(X509ContentType.Pfx);
|
||||
return new X509Certificate2(bytes);
|
||||
}
|
||||
|
||||
private static (string schema, string host, int port) ParseBindArgument(string bind, ILogger logger)
|
||||
{
|
||||
if (int.TryParse(bind, out var value))
|
||||
{
|
||||
// 只传入了一个端口号
|
||||
return ("http", "localhost", value);
|
||||
}
|
||||
|
||||
var match = Regex.Match(bind, @"^(?<schema>https?):\/\/(?<host>[^\:\/\?\#]+)(?:\:(?<port>\d+))?(?:\/.*)?$", RegexOptions.Singleline | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(5));
|
||||
if (match.Success)
|
||||
{
|
||||
var schema = match.Groups["schema"].Value.ToLower();
|
||||
var host = match.Groups["host"].Value;
|
||||
var port = match.Groups["port"].Success ? int.Parse(match.Groups["port"].Value) : 2356;
|
||||
return (schema, host, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warning("侦听参数解析失败,使用默认值 {DefaultBindLocation}", "http://localhost:2356");
|
||||
return ("http", "localhost", 2356);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,25 +387,32 @@ namespace BililiveRecorder.Cli
|
|||
.WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", restrictedToMinimumLevel: logFileLevel, shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
|
||||
.CreateLogger();
|
||||
|
||||
public class RunModeArguments
|
||||
public abstract class SharedArguments
|
||||
{
|
||||
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
|
||||
|
||||
public LogEventLevel LogFileLevel { get; set; } = LogEventLevel.Information;
|
||||
|
||||
public string? WebBind { get; set; } = null;
|
||||
public string? HttpBind { get; set; } = null;
|
||||
|
||||
// public int? HttpPort { get; set; } = null;
|
||||
|
||||
public string? CertPemPath { get; set; } = null;
|
||||
|
||||
public string? CertKeyPath { get; set; } = null;
|
||||
|
||||
public string? CertPfxPath { get; set; } = null;
|
||||
|
||||
public string? CertPassword { get; set; } = null;
|
||||
}
|
||||
|
||||
public sealed class RunModeArguments : SharedArguments
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PortableModeArguments
|
||||
public sealed class PortableModeArguments : SharedArguments
|
||||
{
|
||||
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
|
||||
|
||||
public LogEventLevel LogFileLevel { get; set; } = LogEventLevel.Debug;
|
||||
|
||||
public string? WebBind { get; set; } = null;
|
||||
|
||||
public RecordMode RecordMode { get; set; } = RecordMode.Standard;
|
||||
|
||||
public string OutputPath { get; set; } = string.Empty;
|
||||
|
|
Loading…
Reference in New Issue
Block a user