为了方便做日志和限流,给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