From 00db7a863b86a0cc8a80b870b770e72e1982c259 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Dec 2024 23:26:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86GitHub=20ur?= =?UTF-8?q?l=E4=B8=8D=E6=90=BA=E5=B8=A6=E5=8D=8F=E8=AE=AE=E5=A4=B4?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=E6=8A=A5=E9=94=99=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controllers/StreamProxyController.cs | 97 ++++++++++++------------ src/Helpers/MasterHelper.cs | 14 ++++ src/Middleware/ErrorHandlerMiddleware.cs | 34 +++++---- src/Program.cs | 29 +++---- src/Services/DatabaseGateService.cs | 2 +- src/Services/TaskManager.cs | 2 +- 6 files changed, 96 insertions(+), 82 deletions(-) diff --git a/src/Controllers/StreamProxyController.cs b/src/Controllers/StreamProxyController.cs index 44e5483..a501eb3 100644 --- a/src/Controllers/StreamProxyController.cs +++ b/src/Controllers/StreamProxyController.cs @@ -2,9 +2,9 @@ using iFileProxy.Config; using iFileProxy.Models; using Microsoft.AspNetCore.Mvc; using Serilog; -using iFileProxy.Helpers; using System.Web; -using System.Net; // 用于 URL 解码 +using System.Net; +using iFileProxy.Helpers; // 用于 URL 解码 namespace iFileProxy.Controllers { @@ -12,7 +12,7 @@ namespace iFileProxy.Controllers public class StreamProxyController(IHttpClientFactory httpClientFactory, AppConfig appConfig) : ControllerBase { private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; - private long SizeLimit = appConfig.StreamProxyOptions.SizeLimit; + private long SizeLimit = appConfig.StreamProxyOptions.SizeLimit; private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); // 匹配文件代理请求 @@ -23,14 +23,19 @@ namespace iFileProxy.Controllers [HttpPatch("{*proxyUrl}")] public async Task ProxyGitRequest(string proxyUrl) { - DownloadFileInfo? downloadFileInfo; try { - if (proxyUrl.StartsWith("github.com")) - proxyUrl = "https://" + proxyUrl; + if (MasterHelper.IsGithubUrl(proxyUrl)) + { + if (!proxyUrl.StartsWith("http://") && !proxyUrl.StartsWith("https://")) + proxyUrl = "http://" + proxyUrl; // 默认选择Http协议 如果支持HTTPs应该正常应该会被重定向(除非网站管理员没设置) + } + else + if (!proxyUrl.StartsWith("http://") && !proxyUrl.StartsWith("https://")) // 不带协议头 直接扔进垃圾桶 + return StatusCode(400, "非法Url! 除GitHubUrl外其他代理请求必须携带协议头!"); foreach (var keywords in appConfig.SecurityOptions.BlockedKeyword) { - if (proxyUrl.IndexOf(keywords) != -1) + if (proxyUrl.Contains(keywords, StringComparison.CurrentCulture)) return StatusCode((int)HttpStatusCode.Forbidden, "Keyword::Forbidden"); } var t = new Uri(proxyUrl); @@ -38,7 +43,7 @@ namespace iFileProxy.Controllers catch (Exception ex) { _logger.Error("[Stream] 解析下载文件时出现问题: {ex}", ex); - return StatusCode((int)HttpStatusCode.InternalServerError,"服务故障 请联系开发者反馈"); + return StatusCode((int)HttpStatusCode.InternalServerError, "服务故障 请联系开发者反馈"); } if (string.IsNullOrWhiteSpace(proxyUrl)) @@ -77,7 +82,7 @@ namespace iFileProxy.Controllers .Where(h => h.Key != "Host") // 排除 'Host' 头部 .ToDictionary(h => h.Key, h => h.Value.ToString()); - List> failedHeaders = new(); + List> failedHeaders = []; client.DefaultRequestHeaders.Clear(); @@ -109,12 +114,12 @@ namespace iFileProxy.Controllers var method = Request.Method; // 根据客户端请求方法动态代理请求 - HttpRequestMessage requestMessage = new HttpRequestMessage(new HttpMethod(method), targetUrl); + HttpRequestMessage requestMessage = new(new HttpMethod(method), targetUrl); if (method == HttpMethods.Post || method == HttpMethods.Put || method == HttpMethods.Patch) { var content = new StreamContent(Request.Body); // 仅对于 POST/PUT/PATCH 请求,转发请求体 - // 处理之前添加失败的请求头 + // 处理之前添加失败的请求头 foreach (var header in failedHeaders) { content.Headers.Add(header.Key, header.Value); @@ -123,44 +128,42 @@ namespace iFileProxy.Controllers } // 发送请求并获取响应 - using (var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead)) + using var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); + foreach (var header in response.Headers) { - foreach (var header in response.Headers) - { - _logger.Debug($"[Response Header] Key: {header.Key} Value: {string.Join(",", header.Value)}"); - } - - // 确保响应成功 - if (!response.IsSuccessStatusCode) - { - return StatusCode((int)response.StatusCode, await response.Content.ReadAsStringAsync()); - } - - // 获取服务器响应的文件大小 - var contentLength = response.Content.Headers.ContentLength; - - // 如果文件超过大小限制,返回错误 - if (contentLength.HasValue && contentLength.Value > SizeLimit) - { - return StatusCode(413, "File is too large to download."); - } - - // 设置响应头:告诉浏览器这是一个文件下载 - var fileName = Path.GetFileName(new Uri(targetUrl).LocalPath); // 从 URL 获取文件名 - Response.Headers.Add("Content-Disposition", $"attachment; filename={fileName}"); - Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream"; - - if (contentLength > 0) - Response.ContentLength = contentLength; - - // 流式转发文件 - using (var stream = await response.Content.ReadAsStreamAsync()) - { - await stream.CopyToAsync(Response.Body); - } - - return new EmptyResult(); // 控制器返回空结果,响应已经在流中完成 + _logger.Debug($"[Response Header] Key: {header.Key} Value: {string.Join(",", header.Value)}"); } + + // 确保响应成功 + if (!response.IsSuccessStatusCode) + { + return StatusCode((int)response.StatusCode, await response.Content.ReadAsStringAsync()); + } + + // 获取服务器响应的文件大小 + var contentLength = response.Content.Headers.ContentLength; + + // 如果文件超过大小限制,返回错误 + if (contentLength.HasValue && contentLength.Value > SizeLimit) + { + return StatusCode(413, "File is too large to download."); + } + + // 设置响应头:告诉浏览器这是一个文件下载 + var fileName = HttpUtility.UrlEncode(Path.GetFileName(new Uri(targetUrl).LocalPath)); // 对文件名进行编码 防止报错 + Response.Headers.Add("Content-Disposition", $"attachment; filename={fileName}"); + Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream"; + + if (contentLength > 0) + Response.ContentLength = contentLength; + + // 流式转发文件 + using (var stream = await response.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(Response.Body); + } + + return new EmptyResult(); // 控制器返回空结果,响应已经在流中完成 } catch (Exception ex) { diff --git a/src/Helpers/MasterHelper.cs b/src/Helpers/MasterHelper.cs index 3412caf..b6c109a 100644 --- a/src/Helpers/MasterHelper.cs +++ b/src/Helpers/MasterHelper.cs @@ -213,5 +213,19 @@ namespace iFileProxy.Helpers return debugInfo; } + + // 定义正则表达式 + private static readonly Regex exp1 = new Regex(@"^(?:https?://)?github\.com/(?.+?)/(?.+?)/(?:releases|archive)/.*$", RegexOptions.IgnoreCase); + private static readonly Regex exp2 = new Regex(@"^(?:https?://)?github\.com/(?.+?)/(?.+?)/(?:blob|raw)/.*$", RegexOptions.IgnoreCase); + private static readonly Regex exp3 = new Regex(@"^(?:https?://)?github\.com/(?.+?)/(?.+?)/(?:info|git-).*$", RegexOptions.IgnoreCase); + private static readonly Regex exp4 = new Regex(@"^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?.+?)/(?.+?)/.+?/.+$", RegexOptions.IgnoreCase); + private static readonly Regex exp5 = new Regex(@"^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?.+?)/.+?/.+$", RegexOptions.IgnoreCase); + + public static bool IsGithubUrl(string url) + { + // 判断url是否匹配任何一个正则表达式 + return exp1.IsMatch(url) || exp2.IsMatch(url) || exp3.IsMatch(url) || exp4.IsMatch(url) || exp5.IsMatch(url); + } + } } diff --git a/src/Middleware/ErrorHandlerMiddleware.cs b/src/Middleware/ErrorHandlerMiddleware.cs index 997222a..93843b0 100644 --- a/src/Middleware/ErrorHandlerMiddleware.cs +++ b/src/Middleware/ErrorHandlerMiddleware.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Components; -namespace iFileProxy +namespace iFileProxy.Middleware { using iFileProxy.Helpers; using iFileProxy.Models; @@ -11,28 +11,30 @@ namespace iFileProxy using System.Text.Json; using System.Threading.Tasks; - public class ErrorHandlerMiddleware + public class ErrorHandlerMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next = next; - private readonly static Serilog.ILogger _logger = Log.Logger.ForContext(); - - - public ErrorHandlerMiddleware(RequestDelegate next) - { - _next = next; - } + private readonly static ILogger _logger = Log.Logger.ForContext(); public async Task InvokeAsync(HttpContext context) { try { await _next(context); + if (context.Response.HasStarted) + { + _logger.Debug($"响应已经开始 错误处理中间件无法再修改其响应内容"); + return; + } if (context.Response.StatusCode == 404) - { - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync( JsonSerializer.Serialize(new CommonRsp { Retcode = 404, Message = "this route not exists." })); - + { + context.Response.OnStarting(async () => + { + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 404, Message = "你正在请求的资源不存在!" })); + } + ); } } catch (Exception ex) @@ -44,10 +46,10 @@ namespace iFileProxy private static Task HandleExceptionAsync(HttpContext context, Exception exception) { var code = HttpStatusCode.InternalServerError; // 500 if unexpected - _logger.Error("Crash Data: {exception}\nContext: {context}", exception, JsonSerializer.Serialize (MasterHelper.ExtractDebugInfo(context))); + _logger.Fatal("崩溃数据: {exception}\n上下文信息: {context}", exception, JsonSerializer.Serialize(MasterHelper.ExtractDebugInfo(context))); switch (exception) { - + case NotImplementedException: code = HttpStatusCode.NotImplemented; // 501 break; diff --git a/src/Program.cs b/src/Program.cs index f1f5959..8aa948b 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,10 +1,10 @@ using iFileProxy.Config; -using iFileProxy.Helpers; using iFileProxy.Middleware; +using iFileProxy.Helpers; using iFileProxy.Services; -using Serilog; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using Serilog; using System.Text; namespace iFileProxy @@ -19,6 +19,7 @@ namespace iFileProxy Console.Write(" "); // 补全日志第一行开头的空白 var builder = WebApplication.CreateBuilder(args); + // CORS配置 builder.Services.AddCors(options => { @@ -26,18 +27,15 @@ namespace iFileProxy builder => { builder - .WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn", "https://github.linxi.info", "http://github.linxi.info/", "http://localhost:4173" ) + .WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn", "https://github.linxi.info", "http://github.linxi.info/", "http://localhost:4173") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); }); - - // Add services to the container. - + // Add services to the container builder.Services.AddControllers(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -45,11 +43,8 @@ namespace iFileProxy // 注入依赖 builder.Services.AddSingleton(AppConfig.GetCurrConfig()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton>>(); // 添加验证服务 @@ -75,24 +70,24 @@ namespace iFileProxy var app = builder.Build(); // 初始化缓存管理服务 - LocalCacheManager localCacheManager = new (app.Services); + LocalCacheManager localCacheManager = new(app.Services); - // 1. 配置CORS(要在Routing之前) + // 1. 错误处理(放在请求管道的最前面) + app.UseMiddleware(); + + // 2. 配置CORS(要在Routing之前) app.UseCors("AllowFrontend"); - // 2. 配置静态文件(要在Routing之前) + // 3. 配置静态文件(要在Routing之前) var defaultFilesOptions = new DefaultFilesOptions(); defaultFilesOptions.DefaultFileNames.Clear(); defaultFilesOptions.DefaultFileNames.Add("index.html"); app.UseDefaultFiles(defaultFilesOptions); app.UseStaticFiles(); - // 3. 强制使用HTTPS(要在Routing之前) + // 4. 强制使用HTTPS(要在Routing之前) app.UseHttpsRedirection(); - // 4. 错误处理(在RequestLogging之前) - app.UseMiddleware(); - // 5. 请求日志记录(在Routing之前) app.UseSerilogRequestLogging(options => { diff --git a/src/Services/DatabaseGateService.cs b/src/Services/DatabaseGateService.cs index 73461d5..53eab9d 100644 --- a/src/Services/DatabaseGateService.cs +++ b/src/Services/DatabaseGateService.cs @@ -154,7 +154,7 @@ namespace iFileProxy.Services try { - var conn = new MySqlConnection(builder.ConnectionString); + var conn = new MySqlConnection(builder.ConnectionString); conn.Open(); return conn; } diff --git a/src/Services/TaskManager.cs b/src/Services/TaskManager.cs index 1ff04ae..496fd3d 100644 --- a/src/Services/TaskManager.cs +++ b/src/Services/TaskManager.cs @@ -95,7 +95,7 @@ namespace iFileProxy.Services string? t_url = c.Request.Query["url"].FirstOrDefault() ?? c.Request.Form["url"].FirstOrDefault(); foreach (var keywords in _appConfig.SecurityOptions.BlockedKeyword) { - if (t_url.IndexOf(keywords) != -1) + if (t_url.Contains(keywords, StringComparison.CurrentCulture)) return TaskAddState.ErrKeywordForbidden; }