为了方便做日志和限流,给RSSHub配个和好伙伴openresty(NGINX的一个支持lua的分支)写了一下反代的配置,只说一下自己写的功能吧:
请求头会说自己是SpringBoot应用,因为我最恶心的就是spring全家桶了。 请求会记录在rsshub代理等待了多久。 rsshub查询错误的话直接返回状态码,响应体直接放空,省点流量。 stat 给出一些简单的性能数据和热点IP 1http { 2 3 ## 4 # Basic Settings 5 ## 6 7 sendfile on; 8 tcp_nopush on; 9 tcp_nodelay on; 10 keepalive_timeout 65; 11 types_hash_max_size 2048; 12 server_tokens off; 13 14 # server_names_hash_bucket_size 64; 15 # server_name_in_redirect off; 16 17 include /etc/nginx/mime.types; 18 default_type application/octet-stream; 19 20 ## 21 # SSL Settings 22 ## 23 24 ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE 25 ssl_prefer_server_ciphers on; 26 27 ## 28 # Logging Settings 29 ## 30 31 access_log off; 32 error_log /var/log/nginx/error.log; 33 34 ## 35 # Gzip Settings 36 ## 37 38 gzip on; 39 40 gzip_vary on; 41 # gzip_proxied any; 42 gzip_comp_level 6; 43 # gzip_buffers 16 8k; 44 gzip_http_version 1.1; 45 gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 46 gzip_min_length 1024; 47 48 ## 49 # Virtual Host Configs 50 ## 51 52 # include /etc/nginx/conf.d/*.conf; 53 # include /etc/nginx/sites-enabled/*; 54 55 lua_shared_dict rss_limit_req_store 10m; 56 57 init_by_lua_block { 58 package.path = "/etc/openresty/lua/?.lua;" .. package.path 59 } 60 61 upstream rsshub { 62 server 127.0.0.1:1222 max_fails=3 fail_timeout=30s; 63 keepalive 8; 64 } 65 66 server { 67 listen 1200; 68 listen [::]:1200; 69 server_name ""; 70 71 access_by_lua_block { 72 local rtime = require "rtime" 73 rtime.start() 74 } 75 76 header_filter_by_lua_block { 77 ngx.header["Server"] = "spring-boot" 78 79 local rtime = require "rtime" 80 local process_time = rtime.get_duration_ms() 81 ngx.header["x-wait-for"] = process_time 82 } 83 84 log_by_lua_block { 85 local log_path = "/var/log/nginx/access.log" 86 87 local now = os.date("%Y-%m-%d %H:%M:%S") 88 local method = ngx.var.request_method or "-" 89 local uri = ngx.var.request_uri or "-" 90 local rt_ms = tonumber(ngx.var.request_time) * 1000 91 if not rt_ms then rt_ms = 0 end 92 93 local srcip = ngx.var.remote_addr or "-" 94 local dstip = ngx.var.server_addr or "-" 95 local bytes = tonumber(ngx.var.body_bytes_sent) or 0 96 97 -- 获取 User-Agent 98 local ua = ngx.var.http_user_agent or "-" 99 100 -- 构造日志 101 local log_lines = { 102 string.format("%s [%s] %s %.1fms", now, method, uri, rt_ms), 103 string.format("[%s] <=> [%s] Byte: %d", srcip, dstip, bytes), 104 ua, 105 "" -- 空行分隔 106 } 107 108 -- 安全写入 109 local fp, err = io.open(log_path, "a") 110 if not fp then 111 ngx.log(ngx.ERR, "Failed to open log file: ", err) 112 return 113 end 114 115 for _, line in ipairs(log_lines) do 116 local ok, err = fp:write(line, "\n") 117 if not ok then 118 ngx.log(ngx.ERR, "Failed to write log line: ", err) 119 break 120 end 121 end 122 fp:close() 123 } 124 125 location = /stat { 126 stub_status; 127 access_log off; 128 129 access_by_lua_block { 130 local addr = ngx.var.remote_addr 131 132 if not addr:match("^10%.126%.126%.%d+$") then 133 ngx.log(ngx.ERR, "IP not allowed: ", addr, " for /stat, redirect to @fallback") 134 ngx.exit(503) 135 end 136 } 137 138 error_page 503 = @fallback; 139 } 140 141 location ~ /(telegram|twitter)/ { 142 access_by_lua_block { 143 ngx.exit(503) 144 } 145 146 error_page 503 = @fallback_gfw; 147 } 148 149 location / { 150 151 access_by_lua_block { 152 local rtime = require "rtime" 153 rtime.start() 154 155 local limit_req = require "resty.limit.req" 156 local lim, err = limit_req.new("rss_limit_req_store", 10, 20) 157 if not lim then 158 ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err) 159 ngx.exit(500) 160 end 161 162 local key = ngx.var.binary_remote_addr 163 local delay, err = lim:incoming(key, true) 164 165 if not delay then 166 if err == "rejected" then 167 ngx.log(ngx.WARN, "rate limited, key: ", key) 168 ngx.exit(429) 169 end 170 ngx.log(ngx.ERR, "failed to limit req: ", err) 171 ngx.exit(500) 172 end 173 174 if delay > 0 then 175 ngx.sleep(delay) 176 end 177 } 178 179 proxy_pass http://rsshub; 180 # 代理统一用HTTP1.1 181 proxy_http_version 1.1; 182 183 # 传递真实客户端信息 184 proxy_set_header Host $host; 185 proxy_set_header X-Real-IP $remote_addr; 186 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 187 proxy_set_header X-Forwarded-Proto $scheme; 188 # proxy_set_header X-Forwarded-Host $host; 189 proxy_set_header X-Forwarded-Port $server_port; 190 191 # 支持 WebSocket 192 proxy_set_header Upgrade $http_upgrade; 193 proxy_set_header Connection "upgrade"; 194 195 # 超时设置 196 proxy_connect_timeout 60s; 197 proxy_send_timeout 60s; 198 proxy_read_timeout 60s; 199 200 # 错误处理 201 proxy_next_upstream error timeout; 202 proxy_intercept_errors on; 203 error_page 503 = @fallback; 204 } 205 206 location ~* \.(jpg|jpeg|gif|ico|css|js|woff|woff2|ttf|svg|eot)$ { 207 expires 1d; 208 add_header Cache-Control "public, immutable"; 209 210 # 仍代理到后端(如果你的服务自己处理静态资源) 211 proxy_pass http://rsshub; 212 proxy_set_header Host $host; 213 proxy_set_header X-Real-IP $remote_addr; 214 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 215 proxy_set_header X-Forwarded-Proto $scheme; 216 217 proxy_http_version 1.1; 218 proxy_pass_request_headers on; 219 } 220 221 location ~* \.(png)$ { 222 empty_gif; 223 } 224 225 location @fallback { 226 internal; 227 content_by_lua_block { 228 ngx.status = 503 229 ngx.print() 230 ngx.exit(ngx.status) 231 } 232 } 233 234 location @fallback_gfw { 235 internal; 236 content_by_lua_block { 237 ngx.status = 503 238 ngx.header["Content-Type"] = "text/plain" 239 ngx.print("we cannot access GFW site.") 240 ngx.exit(ngx.status) 241 } 242 } 243 } 244 245} 1-- ip_cidr.lua 2 3local _M = {} 4 5local bit = require "bit" 6 7local internal_ipset = { 8 "10.0.0.0/8", 9 "172.16.0.0/12", 10 "192.168.0.0/16" 11} 12 13local function ip_to_num(ip) 14 -- 将 IPv4 字符串(如 "192.168.1.1")转换为 32 位整数 15 local parts = {} 16 for part in ip:gmatch("%d+") do -- 提取所有数字部分(如 "192", "168") 17 table.insert(parts, tonumber(part)) 18 end 19 20 if #parts ~= 4 then 21 return nil, "invalid IPv4 address: 不是有效的 IPv4 格式" 22 end 23 24 for _, p in ipairs(parts) do 25 if p < 0 or p > 255 then 26 return nil, "invalid IPv4 address: 八位组超出 0-255 范围" 27 end 28 end 29 30 -- 计算 32 位整数(大端序) 31 -- 傻瓜也能听懂的讲解:也算是帮我复习了 32 -- IP地址每个位最大255 也就是 FF = 1111 1111 33 -- 192.168.1.1 经过上面转成parts就是 [192, 168, 1 ,1] 34 -- 现在你要把这些数字转成一个整数,当然你可以直接乘10的倍数变成19216811 35 -- 但是这样子网掩码计算就比较复杂,并且乘法在计算机底层上算不上很高效率的指令 36 -- 所以这里用左移,上面说了每个位最大255也就是8个1 37 -- 那么最前面的192是从右数第4位那就移动 8 * 4 = 24 38 -- 以此类推,最后这个32位二进制数,8位的16进制数就是ip地址 39 local part1 = bit.lshift(parts[1], 24) -- 左移 24 位 40 local part2 = bit.lshift(parts[2], 16) -- 左移 16 位 41 local part3 = bit.lshift(parts[3], 8) -- 左移 8 位 42 local part4 = parts[4] -- 无需移位 43 return bit.bor(part1, part2, part3, part4), nil -- 按位或合并 44end 45 46function _M.match(target_ip, cidr) 47 48 if target_ip == nil or type(target_ip) ~= "string" or target_ip == "" then 49 return false, nil 50 end 51 52 local cidr_ip_str, prefix_str = cidr:match("^([^/]+)/(%d+)$") 53 if not cidr_ip_str or not prefix_str then 54 return false, "invalid CIDR format: 错误 = " .. cidr 55 end 56 57 local prefix = tonumber(prefix_str) 58 if prefix < 0 or prefix > 32 then 59 return false, "invalid prefix: 前缀长度需为 0-32 的整数" 60 end 61 62 -- 步骤 2:将 CIDR 中的 IP 转换为整数 63 local cidr_ip_num, err = ip_to_num(cidr_ip_str) 64 if not cidr_ip_num then 65 return false, "CIDR IP error: " .. err 66 end 67 68 -- 生成掩码位 69 -- 傻瓜也能听懂的讲解: 70 -- 打个比方,最常用的24,32 - 24 = 8 71 -- 0xFFFFFFFF << 8 = 0xFFFFFFF00 72 -- 然后去掉高位,转回32位(这个是16进制,1位当4位算) 73 -- 0xFFFFFF00 按照子网掩码转成可读的方式 74 -- FF FF FF 00 = 255 255 255 0 75 -- 如果这还听不懂,那只能是根本不知道进制之间的转换是怎么算的 76 77 local mask = 0 78 79 if prefix ~= 0 then 80 mask = bit.lshift(0xFFFFFFFF, 32 - prefix) 81 mask = bit.band(mask, 0xFFFFFFFF) 82 end 83 84 -- 网段 85 local cidr_network = bit.band(cidr_ip_num, mask) 86 87 local target_ip_num, err = ip_to_num(target_ip) 88 89 if not target_ip_num then 90 return false, "Target IP error: " .. err 91 end 92 93 local target_ip_cidr = bit.band(target_ip_num, mask) 94 95 print(target_ip_cidr) 96 print(cidr_network) 97 print(mask) 98 99 return target_ip_cidr == cidr_network, nil 100end 101 102function _M.is_internal(target_ip) 103 104 for _, t in ipairs(internal_ipset) do 105 local ok, err = _M.match(target_ip, t) 106 if ok and err == nil then return ok, err end 107 end 108 109 return false, nil 110end 111 112return _M 1-- ngx_log.lua 2local _M = {} 3 4local cjson = require "cjson" 5 6function _M.req_info() 7 local log_path = "/var/log/nginx/access.log" 8 9 local now = os.date("%Y-%m-%d %H:%M:%S") 10 local method = ngx.var.request_method or "-" 11 local uri = ngx.var.request_uri or "-" 12 local rt_ms = tonumber(ngx.var.request_time) * 1000 13 if not rt_ms then rt_ms = 0 end 14 15 local srcip = ngx.var.remote_addr or "-" 16 local dstip = ngx.var.server_addr or "-" 17 local bytes = tonumber(ngx.var.body_bytes_sent) or 0 18 19 -- 获取 User-Agent 20 local ua = ngx.var.http_user_agent or "-" 21 22 -- 构造日志 23 local log_lines = { 24 string.format("%s [%s] %s %.1fms", now, method, uri, rt_ms), 25 string.format("[%s] <=> [%s] Byte: %d", srcip, dstip, bytes), 26 ua, 27 "" -- 空行分隔 28 } 29 30 -- 安全写入 31 local fp, err = io.open(log_path, "a") 32 if not fp then 33 ngx.log(ngx.ERR, "Failed to open log file: ", err) 34 return 35 end 36 37 local ok, err = pcall(function() 38 for _, line in ipairs(log_lines) do 39 local ok, err = fp:write(line, "\n") 40 if not ok then 41 ngx.log(ngx.ERR, "Failed to write log line: ", err) 42 break 43 end 44 end 45 end) 46 47 fp:close() 48 if not ok then ngx.log(ngx.ERR, "Logging error: ", err) end 49end 50 51return _M 1-- ngx_log.lua 2 3local _M = {} 4 5function _M.start() 6 ngx.ctx.start_time = ngx.now() 7end 8 9function _M.get_duration_ms() 10 local start = ngx.ctx.start_time 11 if not start then return 0 end 12 return string.format("%.1fms", (ngx.now() - start) * 1000) 13end 14 15return _M