179 lines
7.1 KiB
Lua
179 lines
7.1 KiB
Lua
-- ****************************************************
|
|
-- *** Nginx/LUA stream module "CAPTCHA" for Telnet ***
|
|
-- ****************************************************
|
|
|
|
-- Steal the socket to the connecting client
|
|
local sock = assert(ngx.req.socket(true))
|
|
-- Get ourselves some randomness, use the system time as a rudimentary seed
|
|
-- (otherwise we'd be asking the same question every time we restart nginx).
|
|
math.randomseed(os.time())
|
|
-- Generate the actual math quiz..
|
|
local num1 = math.random(1,8)
|
|
local num2 = math.random(1,9-num1)
|
|
-- ...we should probably know the answer, too..
|
|
local ans = num1+num2
|
|
|
|
local numtext = {'first', 'second', 'third', 'fourth', 'fifth', 'sixth',
|
|
'seventh', 'eighth', 'ninth'}
|
|
|
|
-- IMPORTANT: This number tells the script how many active connections to
|
|
-- allow; it *must* be the same as the sum of max_conn settings for the
|
|
-- upstreams defined in nginx.conf!
|
|
local max_conns = 2
|
|
|
|
-- Access the shared DICT for the active user counter, for later use
|
|
local users = ngx.shared.users
|
|
|
|
-- Use 'ngx.say()' to talk to the connecting client. This function will add
|
|
-- a newline ('\n') to the end of each line, but we need to add the carriage
|
|
-- return as well, since, well, DOS and Windows and whatnot. Start with a
|
|
-- couple blank lines to make sure the text is easy to read.
|
|
ngx.say("\r\n\r")
|
|
ngx.say("Hello!\r\n\r")
|
|
|
|
-- Now we need to determine if we can even accept this visitor; if we can
|
|
-- not, try to be polite about it. First we fetch the current number of
|
|
-- active users..
|
|
local usercount = users:get("active")
|
|
-- ..then we compare to the max_conns variable we set above:
|
|
if usercount and usercount >= max_conns then
|
|
-- Give a reasonable error message..
|
|
ngx.say(" Too many active users, sorry! Try again later!\r\n\r")
|
|
ngx.say(" (While you wait, check out http://floppy.museum/guests.htm !)\r\n\r")
|
|
-- Sleep a couple of sconds in case the user's terminal clears the
|
|
-- screen on disconnect (or to tie up resources if there's a scanner
|
|
-- or bot attempting to connect) - then exit:
|
|
ngx.sleep(3)
|
|
ngx.exit(400)
|
|
end
|
|
|
|
-- If we made it here, all is presumably good (it can still go wrong if we
|
|
-- have other guests arriving while the challenge is being handled, but
|
|
-- we'll worry about that after the challenge.)
|
|
ngx.say(" ===================================================== \r")
|
|
ngx.say(" If you experience trouble, let me know at \r")
|
|
ngx.say(" @ltning@pleroma.anduin.net or ltning AT anduin.net.\r")
|
|
ngx.say(" ===================================================== \r")
|
|
ngx.say("\r")
|
|
ngx.say("To make sure you're human, please answer this simple question:\r")
|
|
ngx.say("\r")
|
|
ngx.say(" A fever of ", num1, " 90mm and 5.25\" Floppies crossed the road.\r")
|
|
ngx.say(" Why? To get to the Floppy Bar, of course!\r")
|
|
ngx.say(" Grandpa Eight-Inch was already on his ", numtext[num2], " beer,\r")
|
|
ngx.say(" but being the rich bugger he is he had bought enough for all the\r")
|
|
ngx.say(" younglings to have at least one each.\r")
|
|
ngx.say("\r")
|
|
ngx.say("How many beers has Grandpa Eight-Inch already paid for?\r")
|
|
ngx.say("\r")
|
|
-- Use 'ngx.print()' this time, as we specifically do *not* want a newline
|
|
-- after the prompt..
|
|
ngx.print("(Count the beverages and hit ENTER): ")
|
|
|
|
-- Read a single line of data from the client, presumably the answer to our
|
|
-- quiz.
|
|
-- local data = sock:receive(1)
|
|
local data
|
|
local reader = sock:receiveuntil("\n")
|
|
local _data, err, partial = reader(1)
|
|
if err then
|
|
ngx.say("failed to read the data stream: ", err)
|
|
else
|
|
local strval = string.byte(_data)
|
|
if strval >= 128 then
|
|
local junk, err, partial = reader(5)
|
|
if err then
|
|
ngx.say("failed to read the data stream: ", err)
|
|
end
|
|
_data, err, partial = reader(1)
|
|
strval = string.byte(_data)
|
|
--
|
|
-- ngx.say("first read chunk 1: [", _data, ",", strval, "]\r")
|
|
end
|
|
|
|
while true do
|
|
if not _data then
|
|
if err then
|
|
ngx.say("failed to read the data stream: ", err)
|
|
break
|
|
end
|
|
--
|
|
-- ngx.say("read done 1")
|
|
break
|
|
end
|
|
strval = string.byte(_data)
|
|
if strval == 13 or strval == 0 or strval == nil then
|
|
--
|
|
-- ngx.say("read done 2")
|
|
break
|
|
else
|
|
ngx.print(_data)
|
|
data = _data
|
|
end
|
|
--
|
|
-- ngx.say("read chunk 2: [", string.byte(data), ",", strval, "]\r")
|
|
_data, err, partial = reader(1)
|
|
end
|
|
end
|
|
|
|
-- Pick any consecutive number of digits from the given answer.
|
|
-- string.find(): %d+ represents 'digits, one or more'.
|
|
-- Wrapping that in (..) captures the first instance into the 'res'
|
|
-- variable (the two '_' variables are throwaways).
|
|
local res, _
|
|
if data ~= nil then
|
|
-- Only do all this if there are actual data to be read.
|
|
_,_,res = string.find(data, "(%d+)")
|
|
else
|
|
-- Not sure if we should be polite here; it'll usually either be
|
|
-- network scans (benign or not) or script kiddies hitting..
|
|
ngx.say("\r\n\rSomething went wrong and no data was received.\r\n\r")
|
|
ngx.exit(400)
|
|
end
|
|
|
|
-- Compare the given answer to our precomputed one. Make sure the answer is
|
|
-- cast into a number, otherwise the comparison will fail.
|
|
if tonumber(res) == ans then
|
|
-- Check again whether we have capacity for this user (in case someone
|
|
-- else connected in the meantime)
|
|
local usercount = users:get("active")
|
|
if usercount and usercount >= max_conns then
|
|
ngx.say("\r\n\rSorry, but someone arrived before you could answer, and now all nodes are busy.\r")
|
|
ngx.say("Please try again soon!\r\n\r")
|
|
ngx.say("(While you wait, check out http://floppy.museum/bbs.htm !)\r\n\r")
|
|
ngx.exit(429)
|
|
else
|
|
-- Increase the shared counter (see nginx.conf)
|
|
local newval, err = users:incr("active", 1, 0)
|
|
if err then
|
|
ngx.log(ngx.ERR, "Failed to increase user count: ", err)
|
|
ngx.exit(500)
|
|
end
|
|
|
|
-- Confirm to the user
|
|
ngx.say("\r\n\rYou lucky duck, you! Now step into my office..\r\n\r")
|
|
-- Set the nginx variable (which is connection-local) indicating
|
|
-- the user might be human
|
|
ngx.var.museum_challenge_passed = 1
|
|
-- then continue to end of script.
|
|
end
|
|
|
|
-- Here we're checking if the answer is actually a number, and if it is..
|
|
elseif tonumber(res) then
|
|
-- ..we gently mock the math skills of our guest.
|
|
ngx.say("\r\n\rBLEEP! ", tostring(res), " ?? Try again after brushing up on your maths..\r\n\r")
|
|
-- Wait for a few seconds (to slow down bots that hammer us)
|
|
ngx.sleep(3)
|
|
-- Exit with 403 (which is meaningless here, but the ngx.exit() function
|
|
-- needs an exit code)
|
|
ngx.exit(403)
|
|
|
|
-- The answer did not contain a number, so we're assuming it is nonsensical.
|
|
else
|
|
-- Tell the user to give us a sensible answer next time.
|
|
ngx.say("\r\n\rBLEEP! ", tostring(res), " (", res, ") ?? Try entering an actual number next time!\r\n\r")
|
|
-- Wait for a few seconds (to slow down bots that hammer us)
|
|
ngx.sleep(3)
|
|
-- Exit with 403 (which is meaningless here, but the ngx.exit() function
|
|
-- needs an exit code)
|
|
ngx.exit(403)
|
|
end
|