Add telnet captcha for floppy museum guestbook

This commit is contained in:
ltning 2025-04-20 13:33:19 +00:00 committed by ltning
parent c13fe7f4e9
commit d6e4fa7deb
2 changed files with 208 additions and 1 deletions

179
museum_math.lua Normal file
View file

@ -0,0 +1,179 @@
-- ****************************************************
-- *** 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

View file

@ -48,6 +48,12 @@ stream {
server 192.88.99.15:8023 max_conns=2;
}
# Another upstream - the floppy.museum guestbook:
upstream museum {
server 192.88.99.42:23 max_conns=1;
# server 192.88.99.43:23 max_conns=1;
}
# Create a shared DICT (an in-memory database) to keep track of active
# users that have passed the challenge. This will be used to give a
# sensible response to connections made when all nodes are busy.
@ -112,6 +118,28 @@ stream {
# connection to "bbs" (defined in the "upstream" section above).
proxy_pass bbs;
}
server {
listen 8023;
set $museum_challenge_passed 0;
preread_by_lua_file museum_math.lua;
log_by_lua_block {
if tonumber(ngx.var.museum_challenge_passed) == 1 then
local users = ngx.shared.users
users:incr("active", -1, 1)
-- For debugging, uncomment the following line. The output
-- lands in nginx' error log file.
-- ngx.log(ngx.ERR, "Currently active users: ", users:get("active"))
end
}
proxy_connect_timeout 5s;
proxy_buffer_size 1;
proxy_timeout 60s;
proxy_protocol off;
proxy_pass museum;
}
}
http {
@ -121,7 +149,7 @@ http {
reset_timedout_connection off;
client_body_timeout 10s;
send_timeout 2s;
send_timeout 2s;
lingering_timeout 5s;
client_header_timeout 5s;
keepalive_timeout 65;