worker_processes 1; error_log stderr warn; pid /tmp/nginx.pid; events { worker_connections 1024; } http { include /usr/local/openresty/nginx/conf/mime.types; default_type application/json; access_log /dev/stdout; sendfile on; keepalive_timeout 65; client_max_body_size 10M; # Lua 共享字典用于缓存 lua_shared_dict data_cache 10m; server { listen 80; server_name _; # CORS 配置 add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Content-Type, X-Password, X-Token' always; # OPTIONS 请求 if ($request_method = 'OPTIONS') { return 204; } # 健康检查 location = /health { access_log off; return 200 '{"status":"ok"}'; add_header Content-Type application/json; } # 登录页面 location = /login { root /usr/local/openresty/nginx/html; try_files /login.html =404; } # 登录API location = /api/login { content_by_lua_block { local cjson = require "cjson" ngx.req.read_body() local body = ngx.req.get_body_data() local ok, data = pcall(cjson.decode, body) if ok and data.password == "admin123123123" then ngx.header["Set-Cookie"] = "auth=verified; Path=/; Max-Age=604800" ngx.status = 200 ngx.say(cjson.encode({success = true})) else ngx.status = 401 ngx.say(cjson.encode({error = "Invalid password"})) end } } # 静态文件(需要认证) location / { access_by_lua_block { local cookie = ngx.var.http_cookie if not cookie or not string.match(cookie, "auth=verified") then return ngx.redirect("/login") end } root /usr/local/openresty/nginx/html; index index.html; } # POST /api/data - 上传/更新数据 location = /api/data { content_by_lua_block { local cjson = require "cjson" local method = ngx.req.get_method() -- GET 请求:列出所有 tag if method == "GET" then -- 读取索引文件 local index_file = io.open("/data/index.json", "r") if not index_file then ngx.status = 200 ngx.say(cjson.encode({tags = {}})) return end local index_content = index_file:read("*all") index_file:close() local ok, index = pcall(cjson.decode, index_content) if not ok then ngx.status = 500 ngx.say(cjson.encode({error = "Invalid index file"})) return end -- 计算状态 local now = os.time() for tag, item in pairs(index.tags) do if item.expires_at then local expires_time = os.time({ year = tonumber(item.expires_at:sub(1, 4)), month = tonumber(item.expires_at:sub(6, 7)), day = tonumber(item.expires_at:sub(9, 10)), hour = tonumber(item.expires_at:sub(12, 13)), min = tonumber(item.expires_at:sub(15, 16)), sec = tonumber(item.expires_at:sub(18, 19)) }) if expires_time < now then item.status = "expired" elseif (expires_time - now) < 172800 then -- 2 days item.status = "expiring_soon" else item.status = "valid" end else item.status = "no_expiry" end end ngx.status = 200 ngx.say(cjson.encode(index)) return end -- POST 请求:上传数据 if method == "POST" then -- 读取请求体 ngx.req.read_body() local body = ngx.req.get_body_data() -- 如果 body 为空,可能是因为请求体被写入了临时文件 if not body then local body_file = ngx.req.get_body_file() if body_file then local file = io.open(body_file, "r") if file then body = file:read("*all") file:close() end end end if not body then ngx.status = 400 ngx.say(cjson.encode({error = "Empty body"})) return end -- 记录请求体大小,方便调试 ngx.log(ngx.INFO, "Received body size: ", string.len(body)) -- 调试:保存请求体到文件以便分析 local debug_file = io.open("/tmp/last_request_body.json", "w") if debug_file then debug_file:write(body) debug_file:close() ngx.log(ngx.INFO, "Request body saved to /tmp/last_request_body.json") end -- 解析 JSON local ok, data = pcall(cjson.decode, body) if not ok then -- 记录解析错误的详细信息 ngx.log(ngx.ERR, "JSON parse error: ", data) ngx.log(ngx.ERR, "Body preview (first 500 chars): ", string.sub(body, 1, 500)) -- 保存出错的位置附近的内容 local error_pos = tonumber(string.match(tostring(data), "character (%d+)")) if error_pos then local start_pos = math.max(1, error_pos - 100) local end_pos = math.min(string.len(body), error_pos + 100) ngx.log(ngx.ERR, "Error context (chars ", start_pos, "-", end_pos, "): ", string.sub(body, start_pos, end_pos)) end ngx.status = 400 ngx.say(cjson.encode({ error = "Invalid JSON", details = tostring(data), body_size = string.len(body), body_preview = string.sub(body, 1, 200), error_position = error_pos })) return end -- 验证密码 if data.password ~= "admin123123123" then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid password"})) return end -- 验证必填字段 if not data.tag or not data.type or not data.data then ngx.status = 400 ngx.say(cjson.encode({error = "Missing required fields: tag, type, data"})) return end -- 生成文件名(tag 中的特殊字符替换为 _) local filename = data.tag:gsub("[^%w%-]", "_") .. ".json" local filepath = "/data/" .. filename -- 添加时间戳 local timestamp = os.date("%Y-%m-%d %H:%M:%S") if not data.metadata then data.metadata = {} end if not data.metadata.created_at then data.metadata.created_at = timestamp end data.metadata.updated_at = timestamp -- 计算过期时间 if data.metadata.expires_in then local expires_at = os.time() + data.metadata.expires_in data.metadata.expires_at = os.date("%Y-%m-%d %H:%M:%S", expires_at) end -- 移除密码字段 data.password = nil -- 保存数据文件 local file = io.open(filepath, "w") if not file then ngx.status = 500 ngx.say(cjson.encode({error = "Failed to save file"})) return end file:write(cjson.encode(data)) file:close() -- 更新索引 local index_file = io.open("/data/index.json", "r") local index = {tags = {}} if index_file then local index_content = index_file:read("*all") index_file:close() local ok, parsed = pcall(cjson.decode, index_content) if ok then index = parsed end end -- 更新或添加 tag(tags是对象,直接赋值) if not index.tags then index.tags = {} end index.tags[data.tag] = { tag = data.tag, type = data.type, file = filename, created_at = index.tags[data.tag] and index.tags[data.tag].created_at or data.metadata.created_at, updated_at = timestamp, expires_at = data.metadata.expires_at } -- 保存索引 index_file = io.open("/data/index.json", "w") if index_file then index_file:write(cjson.encode(index)) index_file:close() end ngx.status = 200 ngx.say(cjson.encode({ success = true, message = "Data saved", tag = data.tag, timestamp = timestamp })) return end -- 其他方法不支持 ngx.status = 405 ngx.say(cjson.encode({error = "Method not allowed"})) } } # GET /api/data/copy/:tag - 一键复制格式 location ~ ^/api/data/copy/([^/]+)$ { content_by_lua_block { local cjson = require "cjson" local tag = ngx.var[1] -- 生成文件名 local filename = tag:gsub("[^%w%-]", "_") .. ".json" local filepath = "/data/" .. filename -- 读取数据 local file = io.open(filepath, "r") if not file then ngx.status = 404 ngx.say(cjson.encode({error = "Tag not found"})) return end local content = file:read("*all") file:close() local ok, data = pcall(cjson.decode, content) if not ok then ngx.status = 500 ngx.say(cjson.encode({error = "Invalid data file"})) return end -- 生成复制文本 local copy_config = { serverUrl = "http://47.122.126.244:5001", password = "admin123123123", tag = tag, type = data.type } local copy_text = cjson.encode(copy_config) ngx.status = 200 ngx.say(cjson.encode({ copy_text = copy_text, formatted = "```json\\n" .. copy_text .. "\\n```" })) } } # GET /api/data/:tag - 获取数据 # DELETE /api/data/:tag - 删除数据 location ~ ^/api/data/([^/]+)$ { content_by_lua_block { local cjson = require "cjson" local tag = ngx.var[1] local method = ngx.req.get_method() -- 生成文件名 local filename = tag:gsub("[^%w%-]", "_") .. ".json" local filepath = "/data/" .. filename -- DELETE 请求 if method == "DELETE" then -- 验证密码或 Token local password = ngx.var.http_x_password local token = ngx.var.http_x_token local authenticated = false -- Try token authentication first if token then local token_index_file = io.open("/data/tokens.json", "r") if token_index_file then local token_index_content = token_index_file:read("*all") token_index_file:close() local ok, token_index = pcall(cjson.decode, token_index_content) if ok and token_index.tokens and token_index.tokens[token] then local token_file = io.open("/data/" .. token_index.tokens[token].file, "r") if token_file then local token_content = token_file:read("*all") token_file:close() local ok, token_data = pcall(cjson.decode, token_content) if ok then if token_data.status == "disabled" then ngx.status = 401 ngx.say(cjson.encode({error = "Token disabled"})) return end if token_data.expires_at then local expires_time = os.time({ year = tonumber(token_data.expires_at:sub(1, 4)), month = tonumber(token_data.expires_at:sub(6, 7)), day = tonumber(token_data.expires_at:sub(9, 10)), hour = tonumber(token_data.expires_at:sub(12, 13)), min = tonumber(token_data.expires_at:sub(15, 16)), sec = tonumber(token_data.expires_at:sub(18, 19)) }) if expires_time < os.time() then ngx.status = 401 ngx.say(cjson.encode({error = "Token expired", expires_at = token_data.expires_at})) return end end local limit_key = "rate_limit:" .. token local dict = ngx.shared.data_cache local count = dict:get(limit_key) or 0 if count >= 60 then ngx.status = 429 ngx.say(cjson.encode({error = "Rate limit exceeded", message = "Maximum 60 requests per minute", retry_after = 60})) return end dict:incr(limit_key, 1, 0, 60) -- Check permission for DELETE (write operation) if token_data.permission == "readonly" then ngx.status = 403 ngx.say(cjson.encode({error = "Permission denied", message = "This token has readonly permission", required = "readwrite"})) return end authenticated = true -- Update stats token_data.stats = token_data.stats or {} token_data.stats.total_requests = (token_data.stats.total_requests or 0) + 1 token_data.stats.last_used_at = os.date("%Y-%m-%d %H:%M:%S") token_data.stats.last_ip = ngx.var.remote_addr token_data.stats.failed_attempts = 0 -- Add to access log token_data.access_log = token_data.access_log or {} table.insert(token_data.access_log, 1, { timestamp = os.date("%Y-%m-%d %H:%M:%S"), ip = ngx.var.remote_addr, method = "DELETE", path = ngx.var.uri, status = 200 }) while #token_data.access_log > 20 do table.remove(token_data.access_log) end -- Save updated token data local save_file = io.open("/data/" .. token_index.tokens[token].file, "w") if save_file then save_file:write(cjson.encode(token_data)) save_file:close() end end end end end if not authenticated then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid token"})) return end elseif password ~= "admin123123123" then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid password"})) return end -- 删除文件 os.remove(filepath) -- 更新索引 local index_file = io.open("/data/index.json", "r") if index_file then local index_content = index_file:read("*all") index_file:close() local ok, index = pcall(cjson.decode, index_content) if ok then -- tags 是对象,直接删除key if index.tags and index.tags[tag] then index.tags[tag] = nil end index_file = io.open("/data/index.json", "w") if index_file then index_file:write(cjson.encode(index)) index_file:close() end end end ngx.status = 200 ngx.say(cjson.encode({ success = true, message = "Data deleted", tag = tag })) return end -- GET 请求 if method == "GET" then -- 验证密码或 Token local password = ngx.var.http_x_password local token = ngx.var.http_x_token local authenticated = false -- Try token authentication first if token then local token_index_file = io.open("/data/tokens.json", "r") if token_index_file then local token_index_content = token_index_file:read("*all") token_index_file:close() local ok, token_index = pcall(cjson.decode, token_index_content) if ok and token_index.tokens and token_index.tokens[token] then local token_file = io.open("/data/" .. token_index.tokens[token].file, "r") if token_file then local token_content = token_file:read("*all") token_file:close() local ok, token_data = pcall(cjson.decode, token_content) if ok then if token_data.status == "disabled" then ngx.status = 401 ngx.say(cjson.encode({error = "Token disabled"})) return end if token_data.expires_at then local expires_time = os.time({ year = tonumber(token_data.expires_at:sub(1, 4)), month = tonumber(token_data.expires_at:sub(6, 7)), day = tonumber(token_data.expires_at:sub(9, 10)), hour = tonumber(token_data.expires_at:sub(12, 13)), min = tonumber(token_data.expires_at:sub(15, 16)), sec = tonumber(token_data.expires_at:sub(18, 19)) }) if expires_time < os.time() then ngx.status = 401 ngx.say(cjson.encode({error = "Token expired", expires_at = token_data.expires_at})) return end end local limit_key = "rate_limit:" .. token local dict = ngx.shared.data_cache local count = dict:get(limit_key) or 0 if count >= 60 then ngx.status = 429 ngx.say(cjson.encode({error = "Rate limit exceeded", message = "Maximum 60 requests per minute", retry_after = 60})) return end dict:incr(limit_key, 1, 0, 60) authenticated = true -- Update stats token_data.stats = token_data.stats or {} token_data.stats.total_requests = (token_data.stats.total_requests or 0) + 1 token_data.stats.last_used_at = os.date("%Y-%m-%d %H:%M:%S") token_data.stats.last_ip = ngx.var.remote_addr token_data.stats.failed_attempts = 0 -- Add to access log token_data.access_log = token_data.access_log or {} table.insert(token_data.access_log, 1, { timestamp = os.date("%Y-%m-%d %H:%M:%S"), ip = ngx.var.remote_addr, method = "GET", path = ngx.var.uri, status = 200 }) while #token_data.access_log > 20 do table.remove(token_data.access_log) end -- Save updated token data local save_file = io.open("/data/" .. token_index.tokens[token].file, "w") if save_file then save_file:write(cjson.encode(token_data)) save_file:close() end end end end end if not authenticated then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid token"})) return end elseif password ~= "admin123123123" then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid password"})) return end -- 读取数据 local file = io.open(filepath, "r") if not file then ngx.status = 404 ngx.say(cjson.encode({error = "Tag not found"})) return end local content = file:read("*all") file:close() ngx.status = 200 ngx.say(content) return end -- 其他方法不支持 ngx.status = 405 ngx.say(cjson.encode({error = "Method not allowed"})) } } # Token Management API location = /api/tokens { content_by_lua_block { local cjson = require "cjson" local method = ngx.req.get_method() -- Verify admin password local password = ngx.var.http_x_password if password ~= "admin123123123" then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid password"})) return end if method == "POST" then -- Parse request ngx.req.read_body() local body = ngx.req.get_body_data() local ok, data = pcall(cjson.decode, body) if not ok or not data.name or not data.permission then ngx.status = 400 ngx.say(cjson.encode({error = "Missing required fields: name, permission"})) return end -- Validate permission if data.permission ~= "readonly" and data.permission ~= "readwrite" then ngx.status = 400 ngx.say(cjson.encode({error = "Invalid permission, must be readonly or readwrite"})) return end -- Generate random token (dss_ + 32 hex chars) math.randomseed(os.time() * 1000 + os.clock() * 1000000) local charset = "0123456789abcdef" local token_suffix = "" for i = 1, 32 do local rand = math.random(1, #charset) token_suffix = token_suffix .. charset:sub(rand, rand) end local token = "dss_" .. token_suffix -- Create token data local timestamp = os.date("%Y-%m-%d %H:%M:%S") local expires_at = nil if data.expires_in and type(data.expires_in) == "number" and data.expires_in > 0 then expires_at = os.date("%Y-%m-%d %H:%M:%S", os.time() + data.expires_in) end local token_data = { token = token, name = data.name, permission = data.permission, status = "active", created_at = timestamp, expires_at = expires_at, stats = { total_requests = 0, last_used_at = nil, last_ip = nil, failed_attempts = 0 }, access_log = {} } -- Create tokens directory if needed os.execute("mkdir -p /data/tokens") -- Save token file local filename = "tokens/" .. token .. ".json" local filepath = "/data/" .. filename local file = io.open(filepath, "w") if not file then ngx.status = 500 ngx.say(cjson.encode({error = "Failed to save token"})) return end file:write(cjson.encode(token_data)) file:close() -- Update index local index_file = io.open("/data/tokens.json", "r") local index = {tokens = {}} if index_file then local content = index_file:read("*all") index_file:close() local ok, parsed = pcall(cjson.decode, content) if ok then index = parsed end end if not index.tokens then index.tokens = {} end index.tokens[token] = { name = data.name, permission = data.permission, status = "active", created_at = timestamp, expires_at = expires_at, file = filename } local index_file = io.open("/data/tokens.json", "w") if index_file then index_file:write(cjson.encode(index)) index_file:close() end ngx.status = 200 ngx.say(cjson.encode({ success = true, token = token, name = data.name, permission = data.permission, created_at = timestamp, expires_at = token_data.expires_at })) return end if method == "GET" then -- List tokens local index_file = io.open("/data/tokens.json", "r") if not index_file then ngx.status = 200 ngx.say(cjson.encode({tokens = {}})) return end local content = index_file:read("*all") index_file:close() local ok, index = pcall(cjson.decode, content) if not ok then ngx.status = 500 ngx.say(cjson.encode({error = "Invalid token index"})) return end local tokens_list = {} for token, info in pairs(index.tokens or {}) do local token_file = io.open("/data/" .. info.file, "r") if token_file then local token_content = token_file:read("*all") token_file:close() local ok, token_data = pcall(cjson.decode, token_content) if ok then table.insert(tokens_list, { token = token:sub(1, 12) .. "...", token_full = token, name = info.name, permission = info.permission, status = token_data.status, created_at = info.created_at, expires_at = info.expires_at, stats = token_data.stats }) end end end ngx.status = 200 ngx.say(cjson.encode({tokens = tokens_list})) return end ngx.status = 405 ngx.say(cjson.encode({error = "Method not allowed"})) } } # Token detail operations location ~ ^/api/tokens/([^/]+)$ { content_by_lua_block { local cjson = require "cjson" local token = ngx.var[1] local method = ngx.req.get_method() -- Verify admin password local password = ngx.var.http_x_password if password ~= "admin123123123" then ngx.status = 401 ngx.say(cjson.encode({error = "Invalid password"})) return end -- Load token index local index_file = io.open("/data/tokens.json", "r") if not index_file then ngx.status = 404 ngx.say(cjson.encode({error = "Token not found"})) return end local index_content = index_file:read("*all") index_file:close() local ok, index = pcall(cjson.decode, index_content) if not ok or not index.tokens or not index.tokens[token] then ngx.status = 404 ngx.say(cjson.encode({error = "Token not found"})) return end local token_filepath = "/data/" .. index.tokens[token].file if method == "GET" then -- Get token details local token_file = io.open(token_filepath, "r") if not token_file then ngx.status = 404 ngx.say(cjson.encode({error = "Token file not found"})) return end local content = token_file:read("*all") token_file:close() ngx.status = 200 ngx.say(content) return end if method == "PATCH" then -- Update token ngx.req.read_body() local body = ngx.req.get_body_data() local ok, update_data = pcall(cjson.decode, body) if not ok then ngx.status = 400 ngx.say(cjson.encode({error = "Invalid JSON"})) return end -- Load current token data local token_file = io.open(token_filepath, "r") if not token_file then ngx.status = 404 ngx.say(cjson.encode({error = "Token file not found"})) return end local content = token_file:read("*all") token_file:close() local ok, token_data = pcall(cjson.decode, content) if not ok then ngx.status = 500 ngx.say(cjson.encode({error = "Invalid token data"})) return end -- Update allowed fields if update_data.name then token_data.name = update_data.name index.tokens[token].name = update_data.name end if update_data.status then if update_data.status == "active" or update_data.status == "disabled" then token_data.status = update_data.status index.tokens[token].status = update_data.status -- Reset failed attempts when re-enabling if update_data.status == "active" and token_data.stats then token_data.stats.failed_attempts = 0 end end end -- Save updated token local file = io.open(token_filepath, "w") if file then file:write(cjson.encode(token_data)) file:close() end -- Save updated index local index_file = io.open("/data/tokens.json", "w") if index_file then index_file:write(cjson.encode(index)) index_file:close() end ngx.status = 200 ngx.say(cjson.encode({ success = true, token = token, status = token_data.status, name = token_data.name })) return end if method == "DELETE" then -- Delete token (requires password confirmation in body) ngx.req.read_body() local body = ngx.req.get_body_data() local ok, delete_data = pcall(cjson.decode, body) if not ok or not delete_data.confirm_password or delete_data.confirm_password ~= "admin123123123" then ngx.status = 400 ngx.say(cjson.encode({error = "Confirmation password required"})) return end -- Delete token file os.remove(token_filepath) -- Update index index.tokens[token] = nil local index_file = io.open("/data/tokens.json", "w") if index_file then index_file:write(cjson.encode(index)) index_file:close() end ngx.status = 200 ngx.say(cjson.encode({ success = true, message = "Token deleted", token = token })) return end ngx.status = 405 ngx.say(cjson.encode({error = "Method not allowed"})) } } } }