修复部分GitHub url不携带协议头的时候报错的问题

This commit is contained in:
root 2024-12-09 23:26:01 +08:00
parent 26d01669fd
commit 00db7a863b
6 changed files with 96 additions and 82 deletions

View file

@ -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<StreamProxyController>();
// 匹配文件代理请求
@ -23,14 +23,19 @@ namespace iFileProxy.Controllers
[HttpPatch("{*proxyUrl}")]
public async Task<IActionResult> 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<KeyValuePair<string, string>> failedHeaders = new();
List<KeyValuePair<string, string>> 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)
{

View file

@ -213,5 +213,19 @@ namespace iFileProxy.Helpers
return debugInfo;
}
// 定义正则表达式
private static readonly Regex exp1 = new Regex(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:releases|archive)/.*$", RegexOptions.IgnoreCase);
private static readonly Regex exp2 = new Regex(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:blob|raw)/.*$", RegexOptions.IgnoreCase);
private static readonly Regex exp3 = new Regex(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:info|git-).*$", RegexOptions.IgnoreCase);
private static readonly Regex exp4 = new Regex(@"^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?<author>.+?)/(?<repo>.+?)/.+?/.+$", RegexOptions.IgnoreCase);
private static readonly Regex exp5 = new Regex(@"^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?<author>.+?)/.+?/.+$", 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);
}
}
}

View file

@ -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<ErrorHandlerMiddleware>();
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
private readonly static ILogger _logger = Log.Logger.ForContext<ErrorHandlerMiddleware>();
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;

View file

@ -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<DatabaseGateService>();
builder.Services.AddSingleton<TaskManager>();
builder.Services.AddSingleton<Dictionary<string, Dictionary<string, uint>>>();
// 添加验证服务
@ -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<ErrorHandlerMiddleware>();
// 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<ErrorHandlerMiddleware>();
// 5. 请求日志记录在Routing之前
app.UseSerilogRequestLogging(options =>
{

View file

@ -154,7 +154,7 @@ namespace iFileProxy.Services
try
{
var conn = new MySqlConnection(builder.ConnectionString);
var conn = new MySqlConnection(builder.ConnectionString);
conn.Open();
return conn;
}

View file

@ -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;
}