支持流下载 git clone代理

This commit is contained in:
root 2024-12-08 22:38:54 +08:00
parent df3520272e
commit e945102012
16 changed files with 602 additions and 383 deletions

View file

@ -1,13 +1,66 @@
### 文件代理下载工具
# 项目概述
iFileProxy 是一个功能丰富的代理下载系统,支持多种下载方式和用户管理功能。主要用于解决网络访问受限环境下的文件下载需求。
#### 功能列表
- [x] 代理下载文件
- [x] 浏览器下载文件
- [x] 文件名黑名单
- [x] 目标Host黑名单
- [x] 文件大小限制
- [x] 基于IP查询提交的任务状态
- [x] 基于IP和路由的请求次数限制
- [x] 任务队列
- [x] 已经存在对应文件时候直接跳过代理下载 直接下载已经缓存的内容
- [ ] 捐赠
## 核心功能
- 文件下载系统
2.
- 离线下载
- 多线程下载支持
- 任务队列管理
- 下载进度跟踪
- 文件缓存管理
- 文件处理
- 流式传输
- 断点续传
- 文件哈希校验
- 自动清理过期缓存
1. 代理功能
- GitHub 代理
jsDelivr CDN 加速支持
- 仓库文件直接下载
- Git 克隆支持
- 通用流代理
- HTTP/HTTPS 资源代理
- 请求头透传
- 流量控制
1.- 支持
1. 用户系统
- 账户管理
- 用户注册/登录
- JWT 身份验证
- 设备指纹验证
- 权限控制
- 多级权限(用户/管理员/超级管理员)
- 基于角色的访问控制
- 用户操作日志
1. 安全特性
- 访问控制
- IP 访问限制
- 请求频率限制
- 黑名单系统
- 文件大小限制
安全防护
HTTPS 支持
- CORS 配置
- 请求验证
1. 管理功能
- 任务管理
- 任务创建/删除
- 任务状态监控
- 任务优先级调整
- 任务队列管理
- 系统管理
- 用户管理
系统配置
- 访问统计
- 系统日志
1. 技术特性
- ASP.NET Core 8.0
- MySQL 数据库
- JWT 认证
- 依赖注入
- 中间件管道
- 日志系统
- 连接池
静态文件服务

View file

@ -14,6 +14,12 @@ namespace iFileProxy.Config
AllowTrailingCommas = true // 允许尾随逗号
};
public void Save(string configPath = "iFileProxy.json")
{
var config = JsonSerializer.Serialize(this);
File.WriteAllText(configPath, config);
}
[JsonPropertyName("Download")]
public DownloadOptions DownloadOptions { get; set; } = new();
@ -23,8 +29,8 @@ namespace iFileProxy.Config
[JsonPropertyName("Database")]
public Database Database { get; set; }
[JsonPropertyName("GithubProxy")]
public GithubProxyOptions GithubProxyOptions { get; set; } = new();
[JsonPropertyName("StreamProxy")]
public StreamProxyOptions StreamProxyOptions { get; set; } = new();
public static AppConfig GetCurrConfig(string configPath = "iFileProxy.json")
{
@ -88,6 +94,7 @@ namespace iFileProxy.Config
public List<string> BlockedHost { get; set; } = [];
public List<string> BlockedFileName { get; set; } = [];
public List<string> BlockedClientIP { get; set; } = [];
public List<string> BlockedKeyword { get; set; } = [];
public List<string> RoutesToTrack { get; set; } = [];
public int DailyRequestLimitPerIP { get; set; } = -1;
public bool AllowDifferentIPsForDownload { get; set; } = true;
@ -146,21 +153,11 @@ namespace iFileProxy.Config
public string Description { get; set; }
}
public class GithubProxyOptions
public class StreamProxyOptions
{
/// <summary>
/// 文件大小限制(字节)
/// </summary>
public long SizeLimit { get; set; } = 1024L * 1024L * 1024L; // 默认1GB
/// <summary>
/// 黑名单列表
/// </summary>
public List<string> Blacklist { get; set; } = [];
/// <summary>
/// 是否启用 jsDelivr 加速
/// </summary>
public bool EnableJsDelivr { get; set; } = false;
public long SizeLimit { get; set; } = 1024L * 1024L * 1024L * 10; // 默认10GB
}
}

View file

@ -1,87 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using iFileProxy.Services;
using iFileProxy.Models;
namespace iFileProxy.Controllers
{
[Route("[controller]")]
[ApiController]
public class GithubProxyController : ControllerBase
{
private readonly ILogger<GithubProxyController> _logger;
private readonly GithubProxyService _proxyService;
public GithubProxyController(
ILogger<GithubProxyController> logger,
GithubProxyService proxyService)
{
_logger = logger;
_proxyService = proxyService;
}
[HttpGet("{**url}")]
public async Task<IActionResult> ProxyDownload(string url)
{
try
{
// 1. URL 格式化
url = url.StartsWith("http") ? url : $"https://{url}";
// 2. 验证 URL
var (isValid, author, repo) = _proxyService.ValidateUrl(url);
if (!isValid || author == null || repo == null)
{
return BadRequest(new CommonRsp
{
Retcode = 1,
Message = "Invalid GitHub URL"
});
}
// 3. 检查黑名单
if (_proxyService.IsBlocked(author, repo))
{
return Forbid();
}
// 4. 处理URLCDN或Raw
url = _proxyService.ProcessUrl(url);
// 5. 代理请求
var response = await _proxyService.ProxyRequestAsync(url, Request.Headers);
if (!response.Success)
{
return StatusCode(response.StatusCode, new CommonRsp
{
Retcode = 1,
Message = response.Message
});
}
// 6. 设置响应头
if (response.Headers != null)
{
foreach (var header in response.Headers)
{
Response.Headers[header.Key] = header.Value;
}
}
// 7. 返回流式响应
return new FileStreamResult(
response.Stream!,
response.ContentType ?? "application/octet-stream"
);
}
catch (Exception ex)
{
_logger.LogError(ex, "Proxy download failed");
return StatusCode(500, new CommonRsp
{
Retcode = 1,
Message = "Internal server error"
});
}
}
}
}

View file

@ -12,7 +12,7 @@ namespace iFileProxy.Controllers
[Authorize(UserMask.Admin,UserMask.SuperAdmin)]
[Route("[controller]")]
[ApiController]
public class ManagementController(TaskManager taskManager, DatabaseGateService dbGateService) : ControllerBase
public class ManagementController(TaskManager taskManager, DatabaseGateService dbGateService, Dictionary<string, Dictionary<string, uint>> ipAccessLimitData) : ControllerBase
{
public TaskManager _taskManager = taskManager;
public DatabaseGateService _dbGateService = dbGateService;
@ -593,6 +593,12 @@ namespace iFileProxy.Controllers
}
}
[HttpGet("GetIPAccessLimitData")]
public ActionResult<CommonRsp> GetIPAccessLimitData()
{
return Ok(new CommonRsp { Retcode = 0, Data = ipAccessLimitData, Message = "succ" });
}
public class AddUserRequest
{
public string Username { get; set; } = string.Empty;

View file

@ -0,0 +1,184 @@
using iFileProxy.Config;
using iFileProxy.Models;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using iFileProxy.Helpers;
using System.Web;
using System.Net; // 用于 URL 解码
namespace iFileProxy.Controllers
{
[Route("/")]
public class StreamProxyController(IHttpClientFactory httpClientFactory, AppConfig appConfig) : Controller
{
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
private long SizeLimit = appConfig.StreamProxyOptions.SizeLimit;
private readonly static Serilog.ILogger _logger = Log.Logger.ForContext<StreamProxyController>();
// 匹配文件代理请求
[HttpGet("{*proxyUrl}")]
[HttpPost("{*proxyUrl}")]
[HttpPut("{*proxyUrl}")]
[HttpDelete("{*proxyUrl}")]
[HttpPatch("{*proxyUrl}")]
public async Task<IActionResult> ProxyGitRequest(string proxyUrl)
{
DownloadFileInfo downloadFileInfo;
try
{
foreach (var keywords in appConfig.SecurityOptions.BlockedKeyword)
{
if (proxyUrl.IndexOf(keywords) != -1)
return StatusCode((int)HttpStatusCode.Forbidden, "Keyword::Forbidden");
}
var t = new Uri(proxyUrl);
downloadFileInfo = FileDownloadHelper.GetDownloadFileInfo(proxyUrl);
if (downloadFileInfo != null)
{
if (appConfig.SecurityOptions.BlockedFileName.IndexOf(downloadFileInfo.FileName) != -1)
{
return StatusCode((int)HttpStatusCode.Forbidden, "This Filename is Blocked");
}
if (appConfig.SecurityOptions.BlockedHost.IndexOf(t.Host) != -1)
{
return StatusCode((int)HttpStatusCode.Forbidden, "Target Host is Blocked");
}
}
else
return NotFound();
}
catch (Exception)
{
return NotFound();
}
if (string.IsNullOrWhiteSpace(proxyUrl))
{
return BadRequest("URL cannot be empty.");
}
// URL 解码,确保完整的 URL 被正确处理
proxyUrl = HttpUtility.UrlDecode(proxyUrl);
// 如果 URL 没有带协议http:// 或 https://),默认加上 https://
if (!proxyUrl.StartsWith("http://") && !proxyUrl.StartsWith("https://"))
{
proxyUrl = "https://" + proxyUrl; // 默认为 https
}
// 如果目标 host 是 github.com强制使用 https 协议
var targetUri = new Uri(proxyUrl);
if (targetUri.Host == "github.com" && !proxyUrl.StartsWith("https://"))
{
proxyUrl = "https://" + targetUri.Host + targetUri.PathAndQuery;
}
// 获取原始请求的查询字符串并添加到目标 URL 中
string queryString = Request.QueryString.HasValue ? Request.QueryString.Value : string.Empty;
// 拼接完整的 URL包括查询字符串
string targetUrl = proxyUrl + queryString;
try
{
var client = _httpClientFactory.CreateClient();
// 创建请求头(转发客户端的请求头,除了 'Host'
var requestHeaders = Request.Headers
.Where(h => h.Key != "Host") // 排除 'Host' 头部
.ToDictionary(h => h.Key, h => h.Value.ToString());
List<KeyValuePair<string, string>> failedHeaders = new();
client.DefaultRequestHeaders.Clear();
// 设置目标主机名
client.DefaultRequestHeaders.Host = targetUri.Host;
foreach (var header in requestHeaders)
{
// 排除反代理的转发头
if (header.Key.StartsWith("X-Forwarded") || header.Key.StartsWith("X-Real"))
continue;
try
{
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
catch (Exception ex)
{
// 添加失败的头放到后面处理
failedHeaders.Add(new(header.Key, header.Value));
_logger.Debug($"Http头添加失败: Key: {header.Key} Value: {header.Value} {ex.Message}");
}
}
foreach (var header in client.DefaultRequestHeaders)
{
_logger.Debug($"[Request Header] Key: {header.Key} Value: {string.Join(",", header.Value)}");
}
// 动态获取客户端的 HTTP 请求方法
var method = Request.Method;
// 根据客户端请求方法动态代理请求
HttpRequestMessage requestMessage = new HttpRequestMessage(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);
}
requestMessage.Content = content;
}
// 发送请求并获取响应
using (var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead))
{
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(); // 控制器返回空结果,响应已经在流中完成
}
}
catch (Exception ex)
{
// 捕获异常并返回错误
return StatusCode(500, $"An error occurred while proxying the request: {ex.Message}");
}
}
}
}

View file

@ -45,6 +45,7 @@ namespace iFileProxy.Controllers
TaskAddState.ErrQueueLengthLimit => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrQueueLengthLimit, Message = "服务器任务队列已满 请稍候重试!" }),
TaskAddState.Pending => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.Pending, Message = "已经添加到任务队列!" }),
TaskAddState.ErrDisabledStreamTransferOrZeroSize => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrDisabledStreamTransferOrZeroSize, Message = "禁止0大小文件或者流式传输!" }),
TaskAddState.ErrKeywordForbidden => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrKeywordForbidden, Message = "禁止代理此url!" }),
_ => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.Success, Message = "succ default" }),
};
}
@ -83,10 +84,14 @@ namespace iFileProxy.Controllers
var d = _taskManager.GetTaskListByIpAddr(HttpContext);
var taskInfo = _taskManager.GetTaskInfo(taskID);
if ((!_appConfig.SecurityOptions.AllowDifferentIPsForDownload && d.Where(x => x.TaskId == taskID).Any()) || taskInfo != null)
if ((!_appConfig.SecurityOptions.AllowDifferentIPsForDownload && d.Where(x => x.TaskId == taskID).Any()) || taskInfo?.Count > 0)
{
if (_appConfig.SecurityOptions.AllowDifferentIPsForDownload)
{
if (taskInfo == null || taskInfo.Count == 0)
{
return Ok(new CommonRsp() { Message = "task not exists", Retcode = -1 });
}
fileName = taskInfo[0].FileName;
}
else

View file

@ -53,7 +53,7 @@ namespace iFileProxy.Helpers
/// </summary>
/// <param name="c">HttpContext</param>
/// <returns></returns>
public static string? GetClientIPAddr(HttpContext c)
public static string GetClientIPAddr(HttpContext c)
{
// 尝试从 X-Forwarded-For 请求头获取客户端IP地址
string? clientIp = c.Request.Headers["X-Forwarded-For"].FirstOrDefault();
@ -65,7 +65,7 @@ namespace iFileProxy.Helpers
IPAddress? ipAddress = null;
if (IPAddress.TryParse(clientIp, out ipAddress))
return ipAddress.ToString();
return null;
return "127.0.0.1";
}
/// <summary>

View file

@ -9,24 +9,22 @@
using System.Text.Json;
using System.Threading.Tasks;
public class IPAccessLimitMiddleware
public class IPAccessLimitMiddleware(RequestDelegate next, Dictionary<string, Dictionary<string, uint>> IPAccessCountDict, AppConfig appConfig)
{
private readonly RequestDelegate _next;
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict;
private readonly int _dailyRequestLimitPerIP;
private readonly AppConfig _appConfig = AppConfig.GetCurrConfig();
public IPAccessLimitMiddleware(RequestDelegate next, Dictionary<string, Dictionary<string, uint>> IPAccessCountDict, int dailyRequestLimitPerIP)
{
_next = next;
_IPAccessCountDict = IPAccessCountDict;
_dailyRequestLimitPerIP = dailyRequestLimitPerIP;
}
private readonly RequestDelegate _next = next;
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict = IPAccessCountDict;
public async Task InvokeAsync(HttpContext context)
{
if (appConfig.SecurityOptions.BlockedClientIP.IndexOf(MasterHelper.GetClientIPAddr(context)) != -1)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new CommonRsp { Retcode = 403, Message = "你的IP地址已经被管理员拉入黑名单!"});
return;
}
// 获取需要跟踪的路由列表
var routesToTrack = _appConfig.SecurityOptions.RoutesToTrack;
var routesToTrack = appConfig.SecurityOptions.RoutesToTrack;
// 检查当前请求的路径是否在需要跟踪的路由列表中
foreach (var p in routesToTrack)
@ -52,11 +50,11 @@
{
if (_IPAccessCountDict[dateStr].ContainsKey(ipStr))
{
if (_IPAccessCountDict[dateStr][ipStr] >= _dailyRequestLimitPerIP)
if (_IPAccessCountDict[dateStr][ipStr] >= appConfig.SecurityOptions.DailyRequestLimitPerIP)
{
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 403, Message = "请求次数超过限制!" }));
await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 403, Message = "请求次数超过限制! (GMT+8 时间00:00重置)" }));
return;
}
_IPAccessCountDict[dateStr][ipStr]++;

View file

@ -1,12 +1,21 @@
namespace iFileProxy.Models
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace iFileProxy.Models
{
/// <summary>
/// 通用Http Response
/// </summary>
public class CommonRsp
{
[JsonPropertyName("message")]
[JsonProperty("message")]
public string Message { get; set; } = "def msg";
[JsonPropertyName("data")]
[JsonProperty("data")]
public object Data { get; set; } = null;
[JsonPropertyName("retcode")]
[JsonProperty("retcode")]
public int Retcode { get; set; }
}
}

View file

@ -105,6 +105,10 @@
/// 文件不允许0大小或者流式(动态大小)传输
/// </summary>
ErrDisabledStreamTransferOrZeroSize = 14,
/// <summary>
/// 触发关键词
/// </summary>
ErrKeywordForbidden = 15,
}
public class DownloadFileInfo {
/// <summary>

View file

@ -43,17 +43,15 @@ namespace iFileProxy
builder.Host.UseSerilog(logger: Log.Logger);
builder.Services.AddSingleton<Dictionary<string, Dictionary<string, uint>>>(serviceProvider =>
{
return new Dictionary<string, Dictionary<string, uint>>();
});
// 注入依赖
builder.Services.AddSingleton(AppConfig.GetCurrConfig());
builder.Services.AddSingleton<DatabaseGateService>();
builder.Services.AddSingleton<TaskManager>();
builder.Services.AddSingleton<Dictionary<string, Dictionary<string, uint>>>();
// 添加验证服务
builder.Services.AddScoped<AuthService>();
@ -73,11 +71,29 @@ namespace iFileProxy
};
});
var app = builder.Build();
builder.Services.AddHttpClient();
// 全局错误处理中间件
var app = builder.Build();
// 初始化缓存管理服务
LocalCacheManager localCacheManager = new (app.Services);
// 1. 配置CORS要在Routing之前
app.UseCors("AllowFrontend");
// 2. 配置静态文件要在Routing之前
var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(defaultFilesOptions);
app.UseStaticFiles();
// 3. 强制使用HTTPS要在Routing之前
app.UseHttpsRedirection();
// 4. 错误处理在RequestLogging之前
app.UseMiddleware<ErrorHandlerMiddleware>();
// 5. 请求日志记录在Routing之前
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagCtx, httpCtx) =>
@ -88,44 +104,23 @@ namespace iFileProxy
};
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 6. 路由配置
app.UseRouting();
// 检查自定义配置
AppConfig.CheckAppConfig(app.Services);
// 初始化缓存管理器
LocalCacheManager localCacheManager = new(app.Services);
app.UseCors("AllowFrontend");
app.UseHttpsRedirection();
var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(defaultFilesOptions);
app.UseStaticFiles();
// 7. 限制访问IP访问限制通常在认证之前
app.UseMiddleware<IPAccessLimitMiddleware>();
// 8. 身份验证与授权
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<IPAccessLimitMiddleware>(
app.Services.GetRequiredService<Dictionary<string, Dictionary<string, uint>>>(),
AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP);
// JWT中间件
// 9. 配置JWT验证
app.UseMiddleware<JwtMiddleware>();
// 10. 映射控制器
app.MapControllers();
// 11. 启动应用
var dbGateService = app.Services.GetRequiredService<DatabaseGateService>();
SerilogConfig.CreateLogger(dbGateService);

View file

@ -1,130 +0,0 @@
using System.Text.RegularExpressions;
using iFileProxy.Config;
using iFileProxy.Models;
namespace iFileProxy.Services
{
public class GithubProxyService
{
private readonly ILogger<GithubProxyService> _logger;
private readonly AppConfig _appConfig;
private readonly HttpClient _httpClient;
private static readonly Regex[] URL_PATTERNS =
{
new(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:releases|archive)/.*$"),
new(@"^(?:https?://)?github\.com/(?<author>.+?)/(?<repo>.+?)/(?:blob|raw)/.*$"),
new(@"^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?<author>.+?)/(?<repo>.+?)/.+?/.+$"),
new(@"^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?<author>.+?)/.+?/.+$")
};
public GithubProxyService(
ILogger<GithubProxyService> logger,
AppConfig appConfig,
HttpClient httpClient)
{
_logger = logger;
_appConfig = appConfig;
_httpClient = httpClient;
}
public (bool isValid, string? author, string? repo) ValidateUrl(string url)
{
foreach (var pattern in URL_PATTERNS)
{
var match = pattern.Match(url);
if (match.Success)
{
return (true, match.Groups["author"].Value, match.Groups["repo"].Value);
}
}
return (false, null, null);
}
public bool IsBlocked(string author, string repo)
{
return _appConfig.GithubProxyOptions.Blacklist.Contains($"{author}/{repo}") ||
_appConfig.GithubProxyOptions.Blacklist.Contains(author);
}
public string ProcessUrl(string url)
{
if (_appConfig.GithubProxyOptions.EnableJsDelivr && url.Contains("/blob/"))
{
return url.Replace("/blob/", "@")
.Replace("github.com", "cdn.jsdelivr.net/gh");
}
else if (url.Contains("/blob/"))
{
return url.Replace("/blob/", "/raw/");
}
return url;
}
public async Task<ProxyResponse> ProxyRequestAsync(
string url,
IHeaderDictionary headers,
CancellationToken cancellationToken = default)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
// 复制请求头
foreach (var header in headers)
{
if (!header.Key.Equals("Host", StringComparison.OrdinalIgnoreCase))
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
}
}
var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);
// 检查文件大小
var contentLength = response.Content.Headers.ContentLength;
if (contentLength.HasValue && contentLength.Value > _appConfig.GithubProxyOptions.SizeLimit)
{
return new ProxyResponse
{
Success = false,
StatusCode = StatusCodes.Status413PayloadTooLarge,
Message = "File too large"
};
}
return new ProxyResponse
{
Success = true,
StatusCode = (int)response.StatusCode,
Headers = response.Headers.ToDictionary(h => h.Key, h => h.Value.ToArray()),
Stream = await response.Content.ReadAsStreamAsync(cancellationToken),
ContentType = response.Content.Headers.ContentType?.ToString()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Proxy request failed");
return new ProxyResponse
{
Success = false,
StatusCode = StatusCodes.Status500InternalServerError,
Message = "Internal server error"
};
}
}
}
public class ProxyResponse
{
public bool Success { get; set; }
public int StatusCode { get; set; }
public string? Message { get; set; }
public Dictionary<string, string[]>? Headers { get; set; }
public Stream? Stream { get; set; }
public string? ContentType { get; set; }
}
}

View file

@ -94,6 +94,11 @@ namespace iFileProxy.Services
string? clientIp = MasterHelper.GetClientIPAddr(c);
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)
return TaskAddState.ErrKeywordForbidden;
}
bool queue_task = false;
// 如果当前并行任务量已经达到设定并行任务和列队上限

View file

@ -10,13 +10,5 @@
"Key": "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security",
"Issuer": "iFileProxy",
"Audience": "iFileProxy.Client"
},
"GithubProxy": {
"SizeLimit": 1073741824, // 1GB in bytes
"Blacklist": [
"blockedUser1",
"blockedUser2/blockedRepo",
"blockedOrg/*"
]
}
}

View file

@ -17,7 +17,7 @@
"Download": {
"SavePath": "./download/", //
"ThreadNum": 4, // 线
"MaxAllowedFileSize": 1000000000, //
"MaxAllowedFileSize": 1000000000, // : byte
"MaxParallelTasks": 4, //
"MaxQueueLength": 60, //
"Aria2cPath": "./lib/aria2c",
@ -32,6 +32,8 @@
"BlockedClientIP": [ // 使IP
"127.0.0.1"
],
"BlockedKeyword": [ // url
],
"DailyRequestLimitPerIP": 200, // IP
"RoutesToTrack": [ // IP
"/Download",
@ -41,13 +43,7 @@
"AllowStreamTransferOrZeroSize": true, // 0
"EnableUserRegistration": true //
},
"GithubProxy": {
"SizeLimit": 10000000000,
"Blacklist": [
"blockedUser1",
"blockedUser2/blockedRepo",
"blockedOrg/*"
],
"EnableJsDelivr": false
"StreamProxy": { // clone
"SizeLimit": 10000000000
}
}

View file

@ -4,95 +4,287 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Github文件下载加速</title>
<!-- 引入 Bootstrap 5 CSS -->
<link href="static/css/bootstarp/5/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="static/css/custom/Common.css" rel="stylesheet" crossorigin="anonymous">
<title>iFileProxy - 文件下载加速服务</title>
<link href="static/css/bootstarp/5/bootstrap.min.css" rel="stylesheet">
<style>
.loading-mask {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 9999;
}
.example-box {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
.feature-icon {
font-size: 1.5rem;
margin-right: 10px;
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
#redirectModal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 90%;
width: 400px;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
</style>
</head>
<body>
<div id="redirectModal">
<h5>提示</h5>
<p>发现新版本界面,是否跳转?</p>
<div class="modal-buttons">
<button class="btn btn-secondary" onclick="closeModal()"></button>
<button class="btn btn-primary" onclick="redirectToNewVersion()"></button>
</div>
</div>
<div class="container mt-5">
<div class="row justify-content-center">
<div id="loading-mask" style="display: none;">
<div class="loading-spinner"></div>
<p class="loading-text">数据提交中,请稍等...</p>
<div class="container mt-4">
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="#">iFileProxy</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#features">功能</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#download">下载</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#examples">示例</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/query_download_task.html">任务查询</a>
</li>
</ul>
</div>
</div>
<div class="col-md-6">
</nav>
<!-- 下载区域 -->
<div class="row mb-4" id="download">
<div class="col-md-8 mx-auto">
<div class="card">
<div id="from_data" class="card-body">
<h5 class="card-title text-center">Github文件下载加速</h5>
<!-- URL 输入框 -->
<div class="card-body">
<h5 class="card-title">文件下载</h5>
<div class="mb-3">
<label for="url_ipt" class="form-label">目标文件URL</label>
<input type="text" required="required" class="form-control" id="url_ipt"
placeholder="请输入要下载的文件链接">
<label class="form-label">下载方式</label>
<select class="form-select mb-3" id="downloadType">
<option value="direct">直接代理</option>
<option value="offline">离线下载</option>
</select>
<input type="text" class="form-control" id="urlInput"
placeholder="输入文件URL或GitHub链接">
</div>
<!-- 验证码 输入框 -->
<!-- <div class="mb-3 input-group">
<input type="text" class="form-control" id="vcode" placeholder="交易验证码">
<button type="button" id="send_vcode_btn" class="btn btn-primary">发送验证码</button>
</div> -->
<!-- 提交按钮 -->
<div class="d-grid gap-2">
<button type="button" id="sub_btn" class="btn btn-primary">提交</button>
<div class="d-grid">
<button class="btn btn-primary" id="submitBtn">提交</button>
</div>
<br />
<hr />
<p class="more-content">运行中任务: <span id="running_count">0</span> 排队中任务: <span
id="queuing_count">0</span></p>
<p class="more-content"><a target="_blank" href="/query_download_task.html">查询文件下载任务状态</a> | 捐赠 | <a target="_blank" href="/delete_my_info.html">删除我的访客信息</a></p>
</div>
</div>
<p>Tips: <span id="tips">任务列表每个IP相互隔离,不必担心任务信息泄露</span></p>
</div>
</div>
<!-- 功能说明 -->
<div class="row mb-4" id="features">
<div class="col-12">
<h4>主要功能</h4>
<div class="row">
<div class="col-md-4">
<div class="card mb-3">
<div class="card-body">
<h5>🚀 下载加速</h5>
<ul>
<li>多线程下载</li>
<li>断点续传</li>
<li>流式传输</li>
<li>CDN加速</li>
</ul>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-body">
<h5>🛡️ 安全特性</h5>
<ul>
<li>IP访问限制</li>
<li>文件大小限制</li>
<li>黑名单机制</li>
<li>HTTPS支持</li>
</ul>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-body">
<h5>📦 特殊支持</h5>
<ul>
<li>GitHub仓库克隆</li>
<li>文件缓存</li>
<li>队列管理</li>
<li>任务监控</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 使用示例 -->
<div class="row" id="examples">
<div class="col-12">
<h4>使用示例</h4>
<div class="example-box">
<h6>GitHub文件下载</h6>
<code>https://github.com/user/repo/blob/master/file.txt</code>
</div>
<div class="example-box">
<h6>仓库克隆</h6>
<code>https://github.com/user/repo.git</code>
</div>
<div class="example-box">
<h6>通用文件下载</h6>
<code>https://example.com/path/to/file.zip</code>
</div>
</div>
</div>
<!-- 服务器状态 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5>服务器状态</h5>
<p>运行中任务: <span id="runningCount">0</span> |
排队中任务: <span id="queuingCount">0</span></p>
</div>
</div>
</div>
</div>
</div>
<!-- Loading遮罩 -->
<div id="loadingMask" class="loading-mask">
<div class="d-flex justify-content-center align-items-center h-100">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<!-- 优先加载jq一类的三方库 -->
<script src="static/js/bootstarp/5/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="static/js/jquery/2.1.4/jquery.min.js"></script>
<script src="static/js/custom/Common.js"></script>
<script src="static/js/bootstarp/5/bootstrap.bundle.min.js"></script>
<script>
$(document).ready(function () {
console.log("document ready")
sub_btn.addEventListener('click', function (param) {
showLoadingMask();
$.ajax({
type: "POST",
url: "/AddOfflineTask",
data: {
url: url_ipt.value
},
dataType: "json",
success: function (response) {
hideLoadingMask();
if (response.retcode == 0)
alert("任务提交成功! 请稍后点击页面下方的 \"查询文件下载任务状态\" 超链接查询任务状态!");
else
alert(response.message);
$(document).ready(function() {
// 检查是否已经显示过弹窗
if (!localStorage.getItem('redirectModalShown')) {
// 显示弹窗
$('#redirectModal').show();
// 标记已显示
localStorage.setItem('redirectModalShown', new Date().toISOString());
}
// 加载服务器状态
function loadServerStatus() {
$.get("/GetServerLoad", function(response) {
if (response.retcode === 0) {
$("#runningCount").text(response.data.running);
$("#queuingCount").text(response.data.queuing);
}
});
});
// 加载服务器负载信息
$.ajax({
type: "GET",
url: "/GetServerLoad",
dataType: "json",
success: function (response) {
if (response.retcode == 0) {
running_count.textContent = response.data.running;
queuing_count.textContent = response.data.queuing;
}
}
// 提交下载请求
$("#submitBtn").click(function() {
const url = $("#urlInput").val();
const type = $("#downloadType").val();
if (!url) {
alert("请输入URL");
return;
}
$("#loadingMask").show();
if (type === "direct") {
window.location.href = `/StreamProxy/${encodeURIComponent(url)}`;
$("#loadingMask").hide();
} else {
$.post("/AddOfflineTask", { url: url }, function(response) {
$("#loadingMask").hide();
if (response.retcode === 0) {
alert("任务已提交,请在任务查询页面查看进度");
} else {
alert(response.message);
}
});
}
});
});
</script>
// 初始加载
loadServerStatus();
// 定期刷新状态
setInterval(loadServerStatus, 30000);
});
function closeModal() {
$('#redirectModal').hide();
}
function redirectToNewVersion() {
window.location.href = 'https://github.linxi.info/';
}
// 每24小时重置一次弹窗显示状态
function checkAndResetModalFlag() {
const lastShown = localStorage.getItem('redirectModalShown');
if (lastShown) {
const lastShownDate = new Date(lastShown);
const now = new Date();
const hoursDiff = (now - lastShownDate) / (1000 * 60 * 60);
if (hoursDiff >= 24) {
localStorage.removeItem('redirectModalShown');
}
}
}
// 页面加载时检查是否需要重置弹窗状态
checkAndResetModalFlag();
</script>
</body>
</html>