mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
102 lines
3.1 KiB
Lua
102 lines
3.1 KiB
Lua
local cjson = require "cjson.safe"
|
|
|
|
local pow_challenges = ngx.shared.openflare_pow_challenges
|
|
local pow_sessions = ngx.shared.openflare_pow_sessions
|
|
|
|
local args = ngx.req.get_uri_args()
|
|
local challenge_id = args["id"] or ""
|
|
local response = args["response"] or ""
|
|
local nonce_str = args["nonce"] or ""
|
|
local redir = args["redir"] or ""
|
|
local elapsed = args["elapsedTime"] or ""
|
|
|
|
if challenge_id == "" or response == "" or nonce_str == "" then
|
|
ngx.status = 400
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({error = "missing parameters"}))
|
|
return
|
|
end
|
|
|
|
local nonce = tonumber(nonce_str)
|
|
if not nonce then
|
|
ngx.status = 400
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({error = "invalid nonce"}))
|
|
return
|
|
end
|
|
|
|
-- Get stored challenge
|
|
local challenge_raw = pow_challenges:get(challenge_id)
|
|
if not challenge_raw then
|
|
ngx.status = 410
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({error = "challenge expired or not found"}))
|
|
return
|
|
end
|
|
|
|
local ok, challenge_info = pcall(cjson.decode, challenge_raw)
|
|
if not ok or not challenge_info then
|
|
ngx.status = 500
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({error = "invalid challenge data"}))
|
|
return
|
|
end
|
|
|
|
local challenge_data = challenge_info.data or ""
|
|
local difficulty = challenge_info.difficulty or 4
|
|
local host = challenge_info.host or ngx.var.host or ""
|
|
local session_ttl = challenge_info.session_ttl or 600
|
|
|
|
-- Compute SHA-256(challenge_data + nonce)
|
|
local calc_string = challenge_data .. tostring(math.floor(nonce))
|
|
local calculated = ngx.sha1_bin ~= nil and "" or ""
|
|
|
|
-- Use resty.sha256 for proper SHA-256
|
|
local sha256 = require "resty.sha256"
|
|
local str = require "resty.string"
|
|
local hasher = sha256:new()
|
|
hasher:update(calc_string)
|
|
local hash_bytes = hasher:final()
|
|
local hash_hex = str.to_hex(hash_bytes)
|
|
|
|
-- Verify hash matches response
|
|
if hash_hex ~= string.lower(response) then
|
|
ngx.status = 403
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({error = "hash mismatch"}))
|
|
return
|
|
end
|
|
|
|
-- Verify difficulty (leading zeros in hex)
|
|
local prefix = string.rep("0", difficulty)
|
|
if string.sub(hash_hex, 1, difficulty) ~= prefix then
|
|
ngx.status = 403
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({error = "insufficient difficulty"}))
|
|
return
|
|
end
|
|
|
|
-- Invalidate challenge (prevent replay)
|
|
pow_challenges:delete(challenge_id)
|
|
|
|
-- Generate session token
|
|
local session_token = str.to_hex(ngx.sha1_bin(challenge_id .. ngx.now() .. tostring(ngx.worker.pid())))
|
|
|
|
-- Store session
|
|
pow_sessions:set(host .. ":" .. session_token, "1", session_ttl)
|
|
|
|
-- Set cookie. Secure cookies are not sent over HTTP, so only add Secure when
|
|
-- the current request itself is HTTPS.
|
|
local cookie = "__openflare_pow=" .. session_token .. "; Path=/; HttpOnly; SameSite=Lax; Max-Age=" .. tostring(session_ttl)
|
|
if ngx.var.scheme == "https" then
|
|
cookie = cookie .. "; Secure"
|
|
end
|
|
ngx.header["Set-Cookie"] = cookie
|
|
|
|
if redir ~= "" then
|
|
return ngx.redirect(redir)
|
|
end
|
|
|
|
ngx.header.content_type = "application/json"
|
|
ngx.say(cjson.encode({ok = true}))
|