為了方便做日誌和限流,給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