diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs index 14f6d92..8727135 100644 --- a/src/Config/AppConfig.cs +++ b/src/Config/AppConfig.cs @@ -23,6 +23,9 @@ namespace iFileProxy.Config [JsonPropertyName("Database")] public Database Database { get; set; } + [JsonPropertyName("GithubProxy")] + public GithubProxyOptions GithubProxyOptions { get; set; } = new(); + public static AppConfig GetCurrConfig(string configPath = "iFileProxy.json") { if (File.Exists(configPath)) @@ -140,4 +143,22 @@ namespace iFileProxy.Config [JsonPropertyName("Description")] public string Description { get; set; } } + + public class GithubProxyOptions + { + /// + /// 文件大小限制(字节) + /// + public long SizeLimit { get; set; } = 1024L * 1024L * 1024L; // 默认1GB + + /// + /// 黑名单列表 + /// + public List Blacklist { get; set; } = []; + + /// + /// 是否启用 jsDelivr 加速 + /// + public bool EnableJsDelivr { get; set; } = false; + } } diff --git a/src/Controllers/GithubProxyController.cs b/src/Controllers/GithubProxyController.cs new file mode 100644 index 0000000..f0562a8 --- /dev/null +++ b/src/Controllers/GithubProxyController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc; +using iFileProxy.Services; +using iFileProxy.Models; + +namespace iFileProxy.Controllers +{ + [Route("[controller]")] + [ApiController] + public class GithubProxyController : ControllerBase + { + private readonly ILogger _logger; + private readonly GithubProxyService _proxyService; + + public GithubProxyController( + ILogger logger, + GithubProxyService proxyService) + { + _logger = logger; + _proxyService = proxyService; + } + + [HttpGet("{**url}")] + public async Task 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" + }); + } + } + } +} \ No newline at end of file diff --git a/src/Controllers/UserController.cs b/src/Controllers/UserController.cs index bbbc212..59915d4 100644 --- a/src/Controllers/UserController.cs +++ b/src/Controllers/UserController.cs @@ -50,7 +50,7 @@ namespace iFileProxy.Controllers { try { - var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + var ip = MasterHelper.GetClientIPAddr(HttpContext); var userAgent = HttpContext.Request.Headers["User-Agent"].FirstOrDefault() ?? "unknown"; var fingerprint = HttpContext.Request.Headers["X-Device-Fingerprint"].FirstOrDefault() ?? "unknown"; diff --git a/src/Middleware/JwtMiddleware.cs b/src/Middleware/JwtMiddleware.cs index 3aca506..1a3e75a 100644 --- a/src/Middleware/JwtMiddleware.cs +++ b/src/Middleware/JwtMiddleware.cs @@ -1,3 +1,4 @@ +using iFileProxy.Helpers; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; @@ -21,7 +22,7 @@ namespace iFileProxy.Middleware var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); var fingerprint = context.Request.Headers["X-Device-Fingerprint"].FirstOrDefault(); var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault(); - var ip = context.Connection.RemoteIpAddress?.ToString(); + var ip = MasterHelper.GetClientIPAddr(context); if (token != null) { diff --git a/src/Program.cs b/src/Program.cs index 6af721c..d75abf7 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -19,7 +19,6 @@ namespace iFileProxy Console.Write(" "); // 补全日志第一行开头的空白 var builder = WebApplication.CreateBuilder(args); - // CORS配置 builder.Services.AddCors(options => { @@ -27,7 +26,7 @@ namespace iFileProxy builder => { builder - .WithOrigins("http://localhost:3000", "http://admin.gitdl.cn", "https://admin.gitdl.cn","http://47.243.56.137:50050", "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(); diff --git a/src/Services/DatabaseGateService.cs b/src/Services/DatabaseGateService.cs index 33ba47b..28923ae 100644 --- a/src/Services/DatabaseGateService.cs +++ b/src/Services/DatabaseGateService.cs @@ -995,7 +995,7 @@ namespace iFileProxy.Services { "@level", log.Level }, { "@message", log.Message }, { "@exception", log.Exception }, - { "@properties", JsonConvert.SerializeObject(log.Properties) }, + { "@properties", log.Properties }, { "@timestamp", log.Timestamp } }; diff --git a/src/Services/GithubProxyService.cs b/src/Services/GithubProxyService.cs new file mode 100644 index 0000000..7b1aa7f --- /dev/null +++ b/src/Services/GithubProxyService.cs @@ -0,0 +1,130 @@ +using System.Text.RegularExpressions; +using iFileProxy.Config; +using iFileProxy.Models; + +namespace iFileProxy.Services +{ + public class GithubProxyService + { + private readonly ILogger _logger; + private readonly AppConfig _appConfig; + private readonly HttpClient _httpClient; + + private static readonly Regex[] URL_PATTERNS = + { + new(@"^(?:https?://)?github\.com/(?.+?)/(?.+?)/(?:releases|archive)/.*$"), + new(@"^(?:https?://)?github\.com/(?.+?)/(?.+?)/(?:blob|raw)/.*$"), + new(@"^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?.+?)/(?.+?)/.+?/.+$"), + new(@"^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?.+?)/.+?/.+$") + }; + + public GithubProxyService( + ILogger 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 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? Headers { get; set; } + public Stream? Stream { get; set; } + public string? ContentType { get; set; } + } +} \ No newline at end of file diff --git a/src/appsettings.json b/src/appsettings.json index 42f9c78..90a065a 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -10,5 +10,13 @@ "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/*" + ] } } diff --git a/src/document/iFileProxy.sql b/src/document/iFileProxy.sql index 9117ea6..f928e15 100644 --- a/src/document/iFileProxy.sql +++ b/src/document/iFileProxy.sql @@ -11,12 +11,28 @@ Target Server Version : 50743 File Encoding : 65001 - Date: 01/12/2024 01:28:13 + Date: 04/12/2024 00:13:59 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; +-- ---------------------------- +-- Table structure for t_system_logs +-- ---------------------------- +DROP TABLE IF EXISTS `t_system_logs`; +CREATE TABLE `t_system_logs` ( + `log_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `level` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `exception` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `properties` json NULL, + `timestamp` datetime NOT NULL, + PRIMARY KEY (`log_id`) USING BTREE, + INDEX `idx_timestamp`(`timestamp`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + -- ---------------------------- -- Table structure for t_tasks_info -- ---------------------------- @@ -35,7 +51,7 @@ CREATE TABLE `t_tasks_info` ( `tag` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标记', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `tid`(`tid`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 9130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 111135 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for t_user_events @@ -60,6 +76,7 @@ DROP TABLE IF EXISTS `t_users`; CREATE TABLE `t_users` ( `user_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称', + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电子邮箱', `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码哈希值', `mask` int(11) NOT NULL DEFAULT 0 COMMENT '权限掩码', @@ -68,7 +85,8 @@ CREATE TABLE `t_users` ( `last_login_time` datetime NULL DEFAULT NULL COMMENT '上次登录时间', `last_login_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '上次登录IP', PRIMARY KEY (`user_id`) USING BTREE, - UNIQUE INDEX `username`(`username`) USING BTREE + UNIQUE INDEX `username`(`username`) USING BTREE, + UNIQUE INDEX `email`(`email`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/iFileProxy.json b/src/iFileProxy.json index 36ee834..2b41bf4 100644 --- a/src/iFileProxy.json +++ b/src/iFileProxy.json @@ -39,5 +39,14 @@ "/AddOfflineTask" ], "AllowDifferentIPsForDownload": true // 下载者与任务数据提交者IP不同是否允许下载文件 + }, + "GithubProxy": { + "SizeLimit": 1073741824, + "Blacklist": [ + "blockedUser1", + "blockedUser2/blockedRepo", + "blockedOrg/*" + ], + "EnableJsDelivr": false } } \ No newline at end of file