支持流下载 git clone代理
This commit is contained in:
parent
df3520272e
commit
e945102012
16 changed files with 602 additions and 383 deletions
77
README.md
77
README.md
|
@ -1,13 +1,66 @@
|
||||||
### 文件代理下载工具
|
# 项目概述
|
||||||
|
iFileProxy 是一个功能丰富的代理下载系统,支持多种下载方式和用户管理功能。主要用于解决网络访问受限环境下的文件下载需求。
|
||||||
|
|
||||||
#### 功能列表
|
## 核心功能
|
||||||
- [x] 代理下载文件
|
- 文件下载系统
|
||||||
- [x] 浏览器下载文件
|
2.
|
||||||
- [x] 文件名黑名单
|
- 离线下载
|
||||||
- [x] 目标Host黑名单
|
- 多线程下载支持
|
||||||
- [x] 文件大小限制
|
- 任务队列管理
|
||||||
- [x] 基于IP查询提交的任务状态
|
- 下载进度跟踪
|
||||||
- [x] 基于IP和路由的请求次数限制
|
- 文件缓存管理
|
||||||
- [x] 任务队列
|
- 文件处理
|
||||||
- [x] 已经存在对应文件时候直接跳过代理下载 直接下载已经缓存的内容
|
- 流式传输
|
||||||
- [ ] 捐赠
|
- 断点续传
|
||||||
|
- 文件哈希校验
|
||||||
|
- 自动清理过期缓存
|
||||||
|
1. 代理功能
|
||||||
|
- GitHub 代理
|
||||||
|
jsDelivr CDN 加速支持
|
||||||
|
- 仓库文件直接下载
|
||||||
|
- Git 克隆支持
|
||||||
|
- 通用流代理
|
||||||
|
- HTTP/HTTPS 资源代理
|
||||||
|
- 请求头透传
|
||||||
|
- 流量控制
|
||||||
|
1.- 支持
|
||||||
|
1. 用户系统
|
||||||
|
- 账户管理
|
||||||
|
- 用户注册/登录
|
||||||
|
- JWT 身份验证
|
||||||
|
- 设备指纹验证
|
||||||
|
- 权限控制
|
||||||
|
- 多级权限(用户/管理员/超级管理员)
|
||||||
|
- 基于角色的访问控制
|
||||||
|
- 用户操作日志
|
||||||
|
1. 安全特性
|
||||||
|
- 访问控制
|
||||||
|
- IP 访问限制
|
||||||
|
- 请求频率限制
|
||||||
|
- 黑名单系统
|
||||||
|
- 文件大小限制
|
||||||
|
安全防护
|
||||||
|
HTTPS 支持
|
||||||
|
- CORS 配置
|
||||||
|
- 请求验证
|
||||||
|
1. 管理功能
|
||||||
|
- 任务管理
|
||||||
|
- 任务创建/删除
|
||||||
|
- 任务状态监控
|
||||||
|
- 任务优先级调整
|
||||||
|
- 任务队列管理
|
||||||
|
- 系统管理
|
||||||
|
- 用户管理
|
||||||
|
系统配置
|
||||||
|
- 访问统计
|
||||||
|
- 系统日志
|
||||||
|
|
||||||
|
1. 技术特性
|
||||||
|
- ASP.NET Core 8.0
|
||||||
|
- MySQL 数据库
|
||||||
|
- JWT 认证
|
||||||
|
- 依赖注入
|
||||||
|
- 中间件管道
|
||||||
|
- 日志系统
|
||||||
|
- 连接池
|
||||||
|
静态文件服务
|
|
@ -14,6 +14,12 @@ namespace iFileProxy.Config
|
||||||
AllowTrailingCommas = true // 允许尾随逗号
|
AllowTrailingCommas = true // 允许尾随逗号
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public void Save(string configPath = "iFileProxy.json")
|
||||||
|
{
|
||||||
|
var config = JsonSerializer.Serialize(this);
|
||||||
|
File.WriteAllText(configPath, config);
|
||||||
|
}
|
||||||
|
|
||||||
[JsonPropertyName("Download")]
|
[JsonPropertyName("Download")]
|
||||||
public DownloadOptions DownloadOptions { get; set; } = new();
|
public DownloadOptions DownloadOptions { get; set; } = new();
|
||||||
|
|
||||||
|
@ -23,8 +29,8 @@ namespace iFileProxy.Config
|
||||||
[JsonPropertyName("Database")]
|
[JsonPropertyName("Database")]
|
||||||
public Database Database { get; set; }
|
public Database Database { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("GithubProxy")]
|
[JsonPropertyName("StreamProxy")]
|
||||||
public GithubProxyOptions GithubProxyOptions { get; set; } = new();
|
public StreamProxyOptions StreamProxyOptions { get; set; } = new();
|
||||||
|
|
||||||
public static AppConfig GetCurrConfig(string configPath = "iFileProxy.json")
|
public static AppConfig GetCurrConfig(string configPath = "iFileProxy.json")
|
||||||
{
|
{
|
||||||
|
@ -88,6 +94,7 @@ namespace iFileProxy.Config
|
||||||
public List<string> BlockedHost { get; set; } = [];
|
public List<string> BlockedHost { get; set; } = [];
|
||||||
public List<string> BlockedFileName { get; set; } = [];
|
public List<string> BlockedFileName { get; set; } = [];
|
||||||
public List<string> BlockedClientIP { get; set; } = [];
|
public List<string> BlockedClientIP { get; set; } = [];
|
||||||
|
public List<string> BlockedKeyword { get; set; } = [];
|
||||||
public List<string> RoutesToTrack { get; set; } = [];
|
public List<string> RoutesToTrack { get; set; } = [];
|
||||||
public int DailyRequestLimitPerIP { get; set; } = -1;
|
public int DailyRequestLimitPerIP { get; set; } = -1;
|
||||||
public bool AllowDifferentIPsForDownload { get; set; } = true;
|
public bool AllowDifferentIPsForDownload { get; set; } = true;
|
||||||
|
@ -146,21 +153,11 @@ namespace iFileProxy.Config
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GithubProxyOptions
|
public class StreamProxyOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件大小限制(字节)
|
/// 文件大小限制(字节)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long SizeLimit { get; set; } = 1024L * 1024L * 1024L; // 默认1GB
|
public long SizeLimit { get; set; } = 1024L * 1024L * 1024L * 10; // 默认10GB
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 黑名单列表
|
|
||||||
/// </summary>
|
|
||||||
public List<string> Blacklist { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否启用 jsDelivr 加速
|
|
||||||
/// </summary>
|
|
||||||
public bool EnableJsDelivr { get; set; } = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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. 处理URL(CDN或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"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ namespace iFileProxy.Controllers
|
||||||
[Authorize(UserMask.Admin,UserMask.SuperAdmin)]
|
[Authorize(UserMask.Admin,UserMask.SuperAdmin)]
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
[ApiController]
|
[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 TaskManager _taskManager = taskManager;
|
||||||
public DatabaseGateService _dbGateService = dbGateService;
|
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 class AddUserRequest
|
||||||
{
|
{
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
|
|
184
src/Controllers/StreamProxyController.cs
Normal file
184
src/Controllers/StreamProxyController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ namespace iFileProxy.Controllers
|
||||||
TaskAddState.ErrQueueLengthLimit => (ActionResult<CommonRsp>)Ok(new CommonRsp() { Retcode = (int)TaskAddState.ErrQueueLengthLimit, Message = "服务器任务队列已满 请稍候重试!" }),
|
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.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.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" }),
|
_ => (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 d = _taskManager.GetTaskListByIpAddr(HttpContext);
|
||||||
var taskInfo = _taskManager.GetTaskInfo(taskID);
|
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 (_appConfig.SecurityOptions.AllowDifferentIPsForDownload)
|
||||||
{
|
{
|
||||||
|
if (taskInfo == null || taskInfo.Count == 0)
|
||||||
|
{
|
||||||
|
return Ok(new CommonRsp() { Message = "task not exists", Retcode = -1 });
|
||||||
|
}
|
||||||
fileName = taskInfo[0].FileName;
|
fileName = taskInfo[0].FileName;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -53,7 +53,7 @@ namespace iFileProxy.Helpers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="c">HttpContext</param>
|
/// <param name="c">HttpContext</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string? GetClientIPAddr(HttpContext c)
|
public static string GetClientIPAddr(HttpContext c)
|
||||||
{
|
{
|
||||||
// 尝试从 X-Forwarded-For 请求头获取客户端IP地址
|
// 尝试从 X-Forwarded-For 请求头获取客户端IP地址
|
||||||
string? clientIp = c.Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
string? clientIp = c.Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||||
|
@ -65,7 +65,7 @@ namespace iFileProxy.Helpers
|
||||||
IPAddress? ipAddress = null;
|
IPAddress? ipAddress = null;
|
||||||
if (IPAddress.TryParse(clientIp, out ipAddress))
|
if (IPAddress.TryParse(clientIp, out ipAddress))
|
||||||
return ipAddress.ToString();
|
return ipAddress.ToString();
|
||||||
return null;
|
return "127.0.0.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -9,24 +9,22 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
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 RequestDelegate _next = next;
|
||||||
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict;
|
private readonly Dictionary<string, Dictionary<string, uint>> _IPAccessCountDict = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
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)
|
foreach (var p in routesToTrack)
|
||||||
|
@ -52,11 +50,11 @@
|
||||||
{
|
{
|
||||||
if (_IPAccessCountDict[dateStr].ContainsKey(ipStr))
|
if (_IPAccessCountDict[dateStr].ContainsKey(ipStr))
|
||||||
{
|
{
|
||||||
if (_IPAccessCountDict[dateStr][ipStr] >= _dailyRequestLimitPerIP)
|
if (_IPAccessCountDict[dateStr][ipStr] >= appConfig.SecurityOptions.DailyRequestLimitPerIP)
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = 200;
|
context.Response.StatusCode = 200;
|
||||||
context.Response.ContentType = "application/json";
|
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;
|
return;
|
||||||
}
|
}
|
||||||
_IPAccessCountDict[dateStr][ipStr]++;
|
_IPAccessCountDict[dateStr][ipStr]++;
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
namespace iFileProxy.Models
|
using Newtonsoft.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace iFileProxy.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 通用Http Response
|
/// 通用Http Response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommonRsp
|
public class CommonRsp
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
[JsonProperty("message")]
|
||||||
public string Message { get; set; } = "def msg";
|
public string Message { get; set; } = "def msg";
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
[JsonProperty("data")]
|
||||||
public object Data { get; set; } = null;
|
public object Data { get; set; } = null;
|
||||||
|
[JsonPropertyName("retcode")]
|
||||||
|
[JsonProperty("retcode")]
|
||||||
public int Retcode { get; set; }
|
public int Retcode { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,10 @@
|
||||||
/// 文件不允许0大小或者流式(动态大小)传输
|
/// 文件不允许0大小或者流式(动态大小)传输
|
||||||
/// </summary>
|
/// </summary>
|
||||||
ErrDisabledStreamTransferOrZeroSize = 14,
|
ErrDisabledStreamTransferOrZeroSize = 14,
|
||||||
|
/// <summary>
|
||||||
|
/// 触发关键词
|
||||||
|
/// </summary>
|
||||||
|
ErrKeywordForbidden = 15,
|
||||||
}
|
}
|
||||||
public class DownloadFileInfo {
|
public class DownloadFileInfo {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -43,17 +43,15 @@ namespace iFileProxy
|
||||||
|
|
||||||
builder.Host.UseSerilog(logger: Log.Logger);
|
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(AppConfig.GetCurrConfig());
|
||||||
|
|
||||||
builder.Services.AddSingleton<DatabaseGateService>();
|
builder.Services.AddSingleton<DatabaseGateService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<TaskManager>();
|
builder.Services.AddSingleton<TaskManager>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<Dictionary<string, Dictionary<string, uint>>>();
|
||||||
|
|
||||||
// 添加验证服务
|
// 添加验证服务
|
||||||
builder.Services.AddScoped<AuthService>();
|
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>();
|
app.UseMiddleware<ErrorHandlerMiddleware>();
|
||||||
|
|
||||||
|
// 5. 请求日志记录(在Routing之前)
|
||||||
app.UseSerilogRequestLogging(options =>
|
app.UseSerilogRequestLogging(options =>
|
||||||
{
|
{
|
||||||
options.EnrichDiagnosticContext = (diagCtx, httpCtx) =>
|
options.EnrichDiagnosticContext = (diagCtx, httpCtx) =>
|
||||||
|
@ -88,44 +104,23 @@ namespace iFileProxy
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// 6. 路由配置
|
||||||
if (app.Environment.IsDevelopment())
|
app.UseRouting();
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查自定义配置
|
// 7. 限制访问(IP访问限制,通常在认证之前)
|
||||||
AppConfig.CheckAppConfig(app.Services);
|
app.UseMiddleware<IPAccessLimitMiddleware>();
|
||||||
|
|
||||||
// 初始化缓存管理器
|
|
||||||
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();
|
|
||||||
|
|
||||||
|
// 8. 身份验证与授权
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseMiddleware<IPAccessLimitMiddleware>(
|
// 9. 配置JWT验证
|
||||||
app.Services.GetRequiredService<Dictionary<string, Dictionary<string, uint>>>(),
|
|
||||||
AppConfig.GetCurrConfig().SecurityOptions.DailyRequestLimitPerIP);
|
|
||||||
|
|
||||||
// JWT中间件
|
|
||||||
app.UseMiddleware<JwtMiddleware>();
|
app.UseMiddleware<JwtMiddleware>();
|
||||||
|
|
||||||
|
// 10. 映射控制器
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// 11. 启动应用
|
||||||
var dbGateService = app.Services.GetRequiredService<DatabaseGateService>();
|
var dbGateService = app.Services.GetRequiredService<DatabaseGateService>();
|
||||||
SerilogConfig.CreateLogger(dbGateService);
|
SerilogConfig.CreateLogger(dbGateService);
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -94,6 +94,11 @@ namespace iFileProxy.Services
|
||||||
string? clientIp = MasterHelper.GetClientIPAddr(c);
|
string? clientIp = MasterHelper.GetClientIPAddr(c);
|
||||||
string? t_url = c.Request.Query["url"].FirstOrDefault() ?? c.Request.Form["url"].FirstOrDefault();
|
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;
|
bool queue_task = false;
|
||||||
|
|
||||||
// 如果当前并行任务量已经达到设定并行任务和列队上限
|
// 如果当前并行任务量已经达到设定并行任务和列队上限
|
||||||
|
|
|
@ -10,13 +10,5 @@
|
||||||
"Key": "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security",
|
"Key": "iFileProxy-JWT-Secret-Key-2024-Very-Long-Secret-Key-For-Security",
|
||||||
"Issuer": "iFileProxy",
|
"Issuer": "iFileProxy",
|
||||||
"Audience": "iFileProxy.Client"
|
"Audience": "iFileProxy.Client"
|
||||||
},
|
|
||||||
"GithubProxy": {
|
|
||||||
"SizeLimit": 1073741824, // 1GB in bytes
|
|
||||||
"Blacklist": [
|
|
||||||
"blockedUser1",
|
|
||||||
"blockedUser2/blockedRepo",
|
|
||||||
"blockedOrg/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"Download": {
|
"Download": {
|
||||||
"SavePath": "./download/", // 临时文件保存位置
|
"SavePath": "./download/", // 临时文件保存位置
|
||||||
"ThreadNum": 4, // 下载线程数
|
"ThreadNum": 4, // 下载线程数
|
||||||
"MaxAllowedFileSize": 1000000000, // 允许代理的最大文件尺寸
|
"MaxAllowedFileSize": 1000000000, // 允许代理的最大文件尺寸 单位: byte
|
||||||
"MaxParallelTasks": 4, // 同一时间最大并行任务数
|
"MaxParallelTasks": 4, // 同一时间最大并行任务数
|
||||||
"MaxQueueLength": 60, // 最大等待队列长度
|
"MaxQueueLength": 60, // 最大等待队列长度
|
||||||
"Aria2cPath": "./lib/aria2c",
|
"Aria2cPath": "./lib/aria2c",
|
||||||
|
@ -32,6 +32,8 @@
|
||||||
"BlockedClientIP": [ // 禁止使用服务的客户端IP
|
"BlockedClientIP": [ // 禁止使用服务的客户端IP
|
||||||
"127.0.0.1"
|
"127.0.0.1"
|
||||||
],
|
],
|
||||||
|
"BlockedKeyword": [ // 关键字黑名单 检测url是否包含
|
||||||
|
],
|
||||||
"DailyRequestLimitPerIP": 200, // 单个IP每日最大请求次数限制
|
"DailyRequestLimitPerIP": 200, // 单个IP每日最大请求次数限制
|
||||||
"RoutesToTrack": [ // 参加IP请求数统计的路径名单
|
"RoutesToTrack": [ // 参加IP请求数统计的路径名单
|
||||||
"/Download",
|
"/Download",
|
||||||
|
@ -41,13 +43,7 @@
|
||||||
"AllowStreamTransferOrZeroSize": true, // 是否允许0大小文件或者动态大小文件下载
|
"AllowStreamTransferOrZeroSize": true, // 是否允许0大小文件或者动态大小文件下载
|
||||||
"EnableUserRegistration": true // 是否开放用户注册
|
"EnableUserRegistration": true // 是否开放用户注册
|
||||||
},
|
},
|
||||||
"GithubProxy": {
|
"StreamProxy": { // 流代理设置 支持仓库clone
|
||||||
"SizeLimit": 10000000000,
|
"SizeLimit": 10000000000
|
||||||
"Blacklist": [
|
|
||||||
"blockedUser1",
|
|
||||||
"blockedUser2/blockedRepo",
|
|
||||||
"blockedOrg/*"
|
|
||||||
],
|
|
||||||
"EnableJsDelivr": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,95 +4,287 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Github文件下载加速</title>
|
<title>iFileProxy - 文件下载加速服务</title>
|
||||||
<!-- 引入 Bootstrap 5 CSS -->
|
<link href="static/css/bootstarp/5/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="static/css/bootstarp/5/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
<style>
|
||||||
<link href="static/css/custom/Common.css" rel="stylesheet" crossorigin="anonymous">
|
.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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div id="redirectModal">
|
||||||
<div class="container mt-5">
|
<h5>提示</h5>
|
||||||
<div class="row justify-content-center">
|
<p>发现新版本界面,是否跳转?</p>
|
||||||
<div id="loading-mask" style="display: none;">
|
<div class="modal-buttons">
|
||||||
<div class="loading-spinner"></div>
|
<button class="btn btn-secondary" onclick="closeModal()">否</button>
|
||||||
<p class="loading-text">数据提交中,请稍等...</p>
|
<button class="btn btn-primary" onclick="redirectToNewVersion()">是</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 下载区域 -->
|
||||||
|
<div class="row mb-4" id="download">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div id="from_data" class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title text-center">Github文件下载加速</h5>
|
<h5 class="card-title">文件下载</h5>
|
||||||
|
|
||||||
<!-- URL 输入框 -->
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="url_ipt" class="form-label">目标文件URL</label>
|
<label class="form-label">下载方式</label>
|
||||||
<input type="text" required="required" class="form-control" id="url_ipt"
|
<select class="form-select mb-3" id="downloadType">
|
||||||
placeholder="请输入要下载的文件链接">
|
<option value="direct">直接代理</option>
|
||||||
|
<option value="offline">离线下载</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" class="form-control" id="urlInput"
|
||||||
|
placeholder="输入文件URL或GitHub链接">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
<!-- 验证码 输入框 -->
|
<button class="btn btn-primary" id="submitBtn">提交</button>
|
||||||
<!-- <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>
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<p>Tips: <span id="tips">任务列表每个IP相互隔离,不必担心任务信息泄露</span></p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 优先加载jq一类的三方库 -->
|
<!-- 功能说明 -->
|
||||||
<script src="static/js/bootstarp/5/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
<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>
|
||||||
|
|
||||||
<script src="static/js/jquery/2.1.4/jquery.min.js"></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>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
console.log("document ready")
|
// 检查是否已经显示过弹窗
|
||||||
sub_btn.addEventListener('click', function (param) {
|
if (!localStorage.getItem('redirectModalShown')) {
|
||||||
showLoadingMask();
|
// 显示弹窗
|
||||||
$.ajax({
|
$('#redirectModal').show();
|
||||||
type: "POST",
|
// 标记已显示
|
||||||
url: "/AddOfflineTask",
|
localStorage.setItem('redirectModalShown', new Date().toISOString());
|
||||||
data: {
|
}
|
||||||
url: url_ipt.value
|
|
||||||
},
|
// 加载服务器状态
|
||||||
dataType: "json",
|
function loadServerStatus() {
|
||||||
success: function (response) {
|
$.get("/GetServerLoad", function(response) {
|
||||||
hideLoadingMask();
|
if (response.retcode === 0) {
|
||||||
if (response.retcode == 0)
|
$("#runningCount").text(response.data.running);
|
||||||
alert("任务提交成功! 请稍后点击页面下方的 \"查询文件下载任务状态\" 超链接查询任务状态!");
|
$("#queuingCount").text(response.data.queuing);
|
||||||
else
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交下载请求
|
||||||
|
$("#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);
|
alert(response.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
// 加载服务器负载信息
|
|
||||||
$.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue