├── .gitattributes ├── wget4Nginx.conf ├── README.md ├── README_en.md └── wget4Nginx.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /wget4Nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name wget4Nginx.example.com; 4 | return 301 https://$host$request_uri; 5 | } 6 | 7 | server { 8 | listen 443 ssl; 9 | ssl_certificate "/usr/local/openresty/nginx/ssl/fullchain.cer"; 10 | ssl_certificate_key "/usr/local/openresty/nginx/ssl/cert.key"; 11 | server_name wget4Nginx.example.com; 12 | 13 | location = /favicon.ico { 14 | access_log off; 15 | return 404; 16 | } 17 | 18 | location = / { 19 | return 404; 20 | } 21 | 22 | location / { 23 | # 加载wget4Nginx lua脚本 24 | content_by_lua_file /usr/local/openresty/nginx/lua/wget4Nginx.lua; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wget4Nginx - Nginx反向代理下载任意文件 2 | 3 | [简体中文](./README.md) | [English](./README_en.md) 4 | 5 | 通过 Nginx + Lua实现智能反向代理下载服务,支持多次自动跟随重定向、域名黑白名单、流式传输、断点续传、智能 DNS解析等功能。 6 | 7 | ## 🚀 功能特性 8 | 9 | - ✅ 自动跟随重定向(可配置最大次数) 10 | - ✅ 分块流式传输(内存优化) 11 | - ✅ 断点续传支持(Range头处理) 12 | - ✅ 智能 DNS解析(IPv4/IPv6双栈优先) 13 | - ✅ 域名访问控制(黑白名单机制) 14 | - ✅ 自定义 DNS服务器 15 | - ✅ SSL证书验证开关 16 | - ✅ 自定义 User-Agent 17 | - ✅ 连接超时控制 18 | - ✅ 支持 GET和 POST请求 19 | 20 | ## 📦 安装部署 21 | 22 | ### 环境要求 23 | - OpenResty(推荐) / Nginx (with lua-nginx-module) 24 | - LuaSocket 25 | 26 | ### 部署步骤 27 | 1. **安装 OpenResty** 28 | 请根据发行版参考官方文档安装: 29 | 📚 [OpenResty官方预编译包安装](https://openresty.org/cn/linux-packages.html) 30 | 31 | 2. **安装 LuaSocket** 32 | 建议通过 LuaRocks安装 33 | ```bash 34 | # 安装 LuaRocks 35 | sudo apt install luarocks 36 | # 安装 LuaSocket 37 | luarocks install luasocket 38 | ``` 39 | 40 | 3. **部署配置文件** 41 | ```bash 42 | # conf文件仅供参考,使用前请按需修改! 43 | # 关键在于使用 content_by_lua_file加载本项目的 lua脚本 44 | # 创建目录 45 | sudo mkdir -p /usr/local/openresty/nginx/lua 46 | 47 | # 复制配置文件 48 | cp wget4Nginx.lua /usr/local/openresty/nginx/lua/ 49 | cp wget4Nginx.conf /usr/local/openresty/nginx/conf/ 50 | ``` 51 | 52 | 4. **重载服务** 53 | ```bash 54 | sudo systemctl reload openresty 55 | ``` 56 | 57 | ## 🛠 配置详解 58 | 59 | ### Lua脚本配置 (`wget4Nginx.lua`) 60 | 61 | ```lua 62 | -- 网络协议优先级 63 | local IPV6_FIRST = false -- 启用 IPv6解析优先(默认为 IPV4优先) 64 | 65 | -- 重定向控制 66 | local MAX_REDIRECTS = 5 -- 最大重定向次数(防止死循环) 67 | 68 | -- 传输优化 69 | local CHUNK_SIZE = 8192 -- 分块传输大小(bytes) 70 | local ENABLE_RANGE = true -- 启用断点续传支持 71 | 72 | -- 超时设置 73 | local DNS_TIMEOUT = 5000 -- DNS查询超时(ms) 74 | local CONN_TIMEOUT = 5000 -- 后端连接超时(ms) 75 | 76 | -- 安全配置 77 | local ENABLE_SSL_VERIFY = true -- 开启 SSL证书验证 78 | local ACL_MODE = "none" -- 访问控制模式: 79 | -- "whitelist"|"blacklist"|"none" 80 | 81 | -- DNS服务器配置 82 | local DNS_SERVERS = { -- 自定义 DNS服务器池 83 | "1.1.1.1", 84 | "8.8.8.8" 85 | } 86 | 87 | -- 自定义 UA 88 | local DEFAULT_UA = "Mozilla/5.0..." -- 缺省 UA(默认使用客户端 UA,仅当客户端 UA为空时使用,如不需要可置空) 89 | ``` 90 | 91 | ### 域名访问控制 92 | 93 | ```lua 94 | -- 白名单配置(当ACL_MODE=whitelist时生效) 95 | local DOMAIN_WHITELIST = { 96 | "example.com", -- 精确匹配 97 | "*.example.com" -- 通配符匹配子域名(不包括example.com) 98 | } 99 | 100 | -- 黑名单配置(当ACL_MODE=blacklist时生效) 101 | local DOMAIN_BLACKLIST = { 102 | "example.cn", 103 | "*.example.cn" 104 | } 105 | ``` 106 | 107 | ## 🧰 使用示例 108 | 109 | ### 基础使用 110 | ```bash 111 | # 基本链接格式 112 | https://wget4Nginx.example.com/{file_url} 113 | 114 | # 下载示例 115 | wget https://wget4Nginx.example.com/https://github.com/example/repo.zip 116 | 117 | # 脚本将自动补充 http:// 118 | wget https://wget4Nginx.example.com/github.com/example/repo.zip 119 | 120 | # 脚本支持网址编码 121 | wget https://wget4Nginx.example.com/https%3A%2F%2Fgithub.com%2Fexample%2Frepo.zip 122 | ``` 123 | 124 | ### 断点续传 125 | ```bash 126 | wget --continue https://wget.example.com/http://example.org/bigfile.tar.gz 127 | ``` 128 | 129 | ### 指定下载范围 130 | ```bash 131 | curl -H "Range: bytes=100-200" https://wget.example.com/http://example.com/testfile 132 | ``` 133 | 134 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # wget4Nginx - Nginx Reverse Proxy for Downloading Any Files 2 | 3 | [English](./README_en.md) | [简体中文](./README.md) 4 | 5 | Implement smart reverse proxy download service using Nginx + Lua, supporting features like automatic redirect following, domain whitelist/blacklist, streaming transmission, resumable downloads, smart DNS resolution, etc. 6 | 7 | ## 🚀 Features 8 | 9 | - ✅ Auto redirect following (configurable max attempts) 10 | - ✅ Chunked streaming transmission (memory optimized) 11 | - ✅ Resumable downloads support (Range header handling) 12 | - ✅ Smart DNS resolution (IPv4/IPv6 dual-stack priority) 13 | - ✅ Domain access control (whitelist/blacklist mechanism) 14 | - ✅ Custom DNS servers 15 | - ✅ SSL certificate verification toggle 16 | - ✅ Custom User-Agent 17 | - ✅ Connection timeout control 18 | - ✅ GET/POST request support 19 | 20 | ## 📦 Installation 21 | 22 | ### Requirements 23 | - OpenResty (Recommended) / Nginx (with lua-nginx-module) 24 | - LuaSocket 25 | 26 | ### Deployment Steps 27 | 1. **Install OpenResty** 28 | Refer to official documentation for your distribution: 29 | 📚 [OpenResty Prebuilt Packages Installation](https://openresty.org/en/linux-packages.html) 30 | 31 | 2. **Install LuaSocket** 32 | Recommended via LuaRocks: 33 | ```bash 34 | # Install LuaRocks 35 | sudo apt install luarocks 36 | # Install LuaSocket 37 | luarocks install luasocket 38 | ``` 39 | 40 | 3. **Deploy Configuration Files** 41 | ```bash 42 | # Conf files are for reference only - modify as needed before use! 43 | # Key configuration: Use content_by_lua_file to load the Lua script 44 | # Create directory 45 | sudo mkdir -p /usr/local/openresty/nginx/lua 46 | 47 | # Copy config files 48 | cp wget4Nginx.lua /usr/local/openresty/nginx/lua/ 49 | cp wget4Nginx.conf /usr/local/openresty/nginx/conf/ 50 | ``` 51 | 52 | 4. **Reload Service** 53 | ```bash 54 | sudo systemctl reload openresty 55 | ``` 56 | 57 | ## 🛠 Configuration Guide 58 | 59 | ### Lua Script Configuration (`wget4Nginx.lua`) 60 | 61 | ```lua 62 | -- Network protocol priority 63 | local IPV6_FIRST = false -- Enable IPv6 priority (default: IPv4) 64 | 65 | -- Redirect control 66 | local MAX_REDIRECTS = 5 -- Max redirect attempts (prevent infinite loops) 67 | 68 | -- Transmission optimization 69 | local CHUNK_SIZE = 8192 -- Chunk size in bytes 70 | local ENABLE_RANGE = true -- Enable resumable downloads 71 | 72 | -- Timeout settings 73 | local DNS_TIMEOUT = 5000 -- DNS query timeout (ms) 74 | local CONN_TIMEOUT = 5000 -- Backend connection timeout (ms) 75 | 76 | -- Security 77 | local ENABLE_SSL_VERIFY = true -- Enable SSL certificate verification 78 | local ACL_MODE = "none" -- Access control mode: 79 | -- "whitelist"|"blacklist"|"none" 80 | 81 | -- DNS servers 82 | local DNS_SERVERS = { -- Custom DNS servers 83 | "1.1.1.1", 84 | "8.8.8.8" 85 | } 86 | 87 | -- Custom UA 88 | local DEFAULT_UA = "Mozilla/5.0..." -- Default UA (used when client UA is empty) 89 | ``` 90 | 91 | ### Domain Access Control 92 | 93 | ```lua 94 | -- Whitelist (effective when ACL_MODE=whitelist) 95 | local DOMAIN_WHITELIST = { 96 | "example.com", -- Exact match 97 | "*.example.com" -- Wildcard subdomains (excluding example.com) 98 | } 99 | 100 | -- Blacklist (effective when ACL_MODE=blacklist) 101 | local DOMAIN_BLACKLIST = { 102 | "example.cn", 103 | "*.example.cn" 104 | } 105 | ``` 106 | 107 | ## 🧰 Usage Examples 108 | 109 | ### Basic Usage 110 | ```bash 111 | # Basic URL format 112 | https://wget4Nginx.example.com/{file_url} 113 | 114 | # Download example 115 | wget https://wget4Nginx.example.com/https://github.com/example/repo.zip 116 | 117 | # Auto-prepend http:// 118 | wget https://wget4Nginx.example.com/github.com/example/repo.zip 119 | 120 | # Supports URL encoding 121 | wget https://wget4Nginx.example.com/https%3A%2F%2Fgithub.com%2Fexample%2Frepo.zip 122 | ``` 123 | 124 | ### Resumable Downloads 125 | ```bash 126 | wget --continue https://wget.example.com/http://example.org/bigfile.tar.gz 127 | ``` 128 | 129 | ### Partial Content Request 130 | ```bash 131 | curl -H "Range: bytes=100-200" https://wget.example.com/http://example.com/testfile 132 | ``` -------------------------------------------------------------------------------- /wget4Nginx.lua: -------------------------------------------------------------------------------- 1 | local http = require "resty.http" 2 | local url = require "socket.url" 3 | local dns = require "resty.dns.resolver" 4 | 5 | -- 配置区域 ------------------------------------------------- 6 | local IPV6_FIRST = false -- true=IPv6优先,false=IPv4优先 7 | local MAX_REDIRECTS = 5 -- 最大重定向次数 8 | local CHUNK_SIZE = 8192 -- 分块传输大小 9 | local ENABLE_RANGE = true -- 启用断点续传 10 | local DNS_TIMEOUT = 5000 -- DNS查询超时(ms) 11 | local CONN_TIMEOUT = 5000 -- 连接超时(ms) 12 | local ENABLE_SSL_VERIFY = true -- 启用SSL证书验证 13 | local DNS_SERVERS = { -- DNS服务器列表 14 | "1.1.1.1", "8.8.8.8" 15 | } 16 | local ACL_MODE = "none" -- 访问控制模式: "whitelist"/"blacklist"/"none" 17 | local DOMAIN_WHITELIST = { -- 白名单域名列表(ACL_MODE=whitelist时生效) 18 | "example.com", 19 | "*.example.com" 20 | } 21 | local DOMAIN_BLACKLIST = { -- 黑名单域名列表(ACL_MODE=blacklist时生效) 22 | "example.cn", 23 | "*.example.cn" 24 | } 25 | local DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" 26 | -- 结束配置 ------------------------------------------------- 27 | 28 | -- 连接管理 ------------------------------------------------- 29 | local current_httpc = nil 30 | 31 | local function close_httpc() 32 | if current_httpc then 33 | pcall(current_httpc.close, current_httpc) 34 | current_httpc = nil 35 | end 36 | end 37 | 38 | ngx.on_abort(close_httpc) 39 | 40 | local function error_response(status, message) 41 | close_httpc() 42 | ngx.status = status 43 | ngx.header["Content-Type"] = "text/plain" 44 | ngx.say(message) 45 | ngx.exit(status) 46 | end 47 | 48 | -- URL解析 ------------------------------------------------- 49 | local function parse_target_url(target_url) 50 | local parsed = url.parse(target_url) 51 | parsed.scheme = parsed.scheme or "http" 52 | parsed.port = parsed.port or (parsed.scheme == "https" and 443 or 80) 53 | parsed.host = parsed.host or "" 54 | parsed.path = parsed.path or "/" 55 | return parsed 56 | end 57 | 58 | -- 域名匹配 ------------------------------------------------- 59 | local function match_domain(pattern, domain) 60 | pattern = pattern:lower() 61 | domain = domain:lower():match("[^:]+") 62 | 63 | if pattern:sub(1,2) == "*." then 64 | local suffix = pattern:sub(3) 65 | -- 检查 domain 是否以 ".suffix" 结尾,确保匹配的是子域名而不是根域名 66 | return domain:match("%." .. suffix .. "$") ~= nil 67 | end 68 | return domain == pattern 69 | end 70 | 71 | -- ACL检查 -------------------------------------------------- 72 | local function check_domain_access(domain) 73 | if ACL_MODE:lower() == "whitelist" then 74 | for _, pattern in ipairs(DOMAIN_WHITELIST) do 75 | if match_domain(pattern, domain) then return true end 76 | end 77 | return false 78 | elseif ACL_MODE:lower() == "blacklist" then 79 | for _, pattern in ipairs(DOMAIN_BLACKLIST) do 80 | if match_domain(pattern, domain) then return false end 81 | end 82 | return true 83 | end 84 | return true 85 | end 86 | 87 | -- DNS解析 -------------------------------------------------- 88 | local function resolve_host(host) 89 | local resolver, err = dns:new{ 90 | nameservers = DNS_SERVERS, 91 | retrans = 3, 92 | timeout = DNS_TIMEOUT, 93 | } 94 | if not resolver then return nil, "DNS init failed: "..(err or "unknown error") end 95 | 96 | local function query(qtype) 97 | local answers, err = resolver:query(host, { qtype = qtype }) 98 | if not answers then return nil, err end 99 | if answers.errcode then return nil, answers.errstr end 100 | 101 | local results = {} 102 | for _, ans in ipairs(answers) do 103 | if ans.type == qtype and ans.address then 104 | table.insert(results, { 105 | address = (qtype == dns.TYPE_AAAA and "["..ans.address.."]" or ans.address), 106 | ttl = ans.ttl or 300 107 | }) 108 | end 109 | end 110 | return #results > 0 and results or nil 111 | end 112 | 113 | local addresses = {} 114 | local aaaa = query(dns.TYPE_AAAA) 115 | local a = query(dns.TYPE_A) 116 | 117 | if IPV6_FIRST then 118 | if aaaa then for _, v in ipairs(aaaa) do table.insert(addresses, v) end end 119 | if a then for _, v in ipairs(a) do table.insert(addresses, v) end end 120 | else 121 | if a then for _, v in ipairs(a) do table.insert(addresses, v) end end 122 | if aaaa then for _, v in ipairs(aaaa) do table.insert(addresses, v) end end 123 | end 124 | 125 | if #addresses == 0 then 126 | return nil, "No DNS records found for "..host 127 | end 128 | return addresses 129 | end 130 | 131 | -- 请求头处理 ----------------------------------------------- 132 | local function process_request_headers(parsed) 133 | local hop_headers = { 134 | ["connection"] = true, 135 | ["keep-alive"] = true, 136 | ["proxy-authenticate"] = true, 137 | ["proxy-authorization"] = true, 138 | ["te"] = true, 139 | ["trailers"] = true, 140 | ["transfer-encoding"] = true, 141 | ["upgrade"] = true, 142 | ["content-length"] = true, 143 | ["host"] = true 144 | } 145 | 146 | local headers = {} 147 | local req_headers = ngx.req.get_headers() 148 | 149 | for k, v in pairs(req_headers) do 150 | if not hop_headers[k:lower()] then 151 | headers[k] = type(v) == "table" and table.concat(v, ", ") or v 152 | end 153 | end 154 | 155 | headers.Host = parsed.host 156 | headers["User-Agent"] = headers["User-Agent"] or DEFAULT_UA 157 | headers.Accept = headers.Accept or "*/*" 158 | 159 | if not ENABLE_RANGE then 160 | headers.Range = nil 161 | end 162 | 163 | return headers 164 | end 165 | 166 | -- 建立后端连接 --------------------------------------------- 167 | local function connect_backend(parsed, addresses) 168 | for _, addr in ipairs(addresses) do 169 | current_httpc = http.new() 170 | current_httpc:set_timeout(CONN_TIMEOUT) 171 | 172 | local ok, err = current_httpc:connect{ 173 | host = addr.address, 174 | port = parsed.port, 175 | scheme = parsed.scheme, 176 | ssl_verify = false 177 | } 178 | 179 | if ok and parsed.scheme == "https" then 180 | ok, err = current_httpc:ssl_handshake(true, parsed.host, ENABLE_SSL_VERIFY) 181 | end 182 | 183 | if ok then return true end 184 | ngx.log(ngx.WARN, "Connection failed to "..addr.address..": "..(err or "unknown")) 185 | close_httpc() 186 | end 187 | return false, "All connection attempts failed" 188 | end 189 | 190 | -- 处理响应数据 --------------------------------------------- 191 | local function process_response(res) 192 | -- 需要跳过的响应头 193 | local exclude_headers = { 194 | ["transfer-encoding"] = true, 195 | ["connection"] = true, 196 | ["content-length"] = (res.status == 206) 197 | } 198 | 199 | -- 设置响应头 200 | for k, v in pairs(res.headers) do 201 | if not exclude_headers[k:lower()] then 202 | ngx.header[k] = v 203 | end 204 | end 205 | 206 | -- 流式传输 207 | local reader = res.body_reader 208 | repeat 209 | local chunk, err = reader(CHUNK_SIZE) 210 | if err then 211 | ngx.log(ngx.ERR, "Stream read error: ", err) 212 | break 213 | end 214 | if chunk then 215 | local ok, send_err = ngx.print(chunk) 216 | ngx.flush(true) 217 | if not ok then 218 | ngx.log(ngx.INFO, "Client disconnected: ", send_err) 219 | break 220 | end 221 | end 222 | until not chunk 223 | end 224 | 225 | -- 主流程 --------------------------------------------------- 226 | local function main() 227 | local redirect_count = 0 228 | local target_url = ngx.unescape_uri(ngx.var.request_uri:gsub("^/+", "")) 229 | target_url = target_url:find("^https?://") and target_url or "http://"..target_url 230 | 231 | while redirect_count <= MAX_REDIRECTS do 232 | -- host检查 233 | local parsed = parse_target_url(target_url) 234 | if parsed.host == "" then 235 | error_response(400, "Invalid URL: Missing hostname") 236 | end 237 | 238 | -- 域名检查 239 | if not check_domain_access(parsed.host) then 240 | error_response(403, "Access denied for domain: "..domain) 241 | end 242 | 243 | -- DNS解析 244 | local addresses, err = resolve_host(parsed.host) 245 | if not addresses then 246 | error_response(502, "DNS resolution failed for host: " .. parsed.host .. 247 | "\nError: " .. err .. 248 | "\nTarget URL: " .. target_url) 249 | end 250 | 251 | -- 建立连接 252 | local ok, conn_err = connect_backend(parsed, addresses) 253 | if not ok then 254 | error_response(502, "Connection failed: "..conn_err) 255 | end 256 | 257 | -- 构造请求 258 | local path = parsed.path 259 | if parsed.query then path = path.."?"..parsed.query end 260 | 261 | local res, req_err = current_httpc:request{ 262 | method = ngx.req.get_method(), 263 | path = path, 264 | headers = process_request_headers(parsed), 265 | body = ngx.req.get_method() == "POST" and ngx.req.get_body_data() or nil 266 | } 267 | 268 | if not res then 269 | error_response(502, "Backend request failed: "..req_err) 270 | end 271 | 272 | -- 处理重定向 273 | if res.status >= 300 and res.status < 400 then 274 | local location = res.headers.Location or res.headers.location 275 | if location then 276 | redirect_count = redirect_count + 1 277 | target_url = url.absolute(target_url, location) 278 | close_httpc() 279 | ngx.log(ngx.INFO, "Redirecting to: ", target_url) 280 | else 281 | error_response(502, "Redirect missing Location header") 282 | end 283 | else 284 | ngx.status = res.status 285 | process_response(res) 286 | close_httpc() 287 | return 288 | end 289 | end 290 | error_response(508, "Too many redirects (max "..MAX_REDIRECTS..")") 291 | end 292 | 293 | -- 启动执行 ------------------------------------------------- 294 | main() --------------------------------------------------------------------------------