Add telnet captcha for floppy museum guestbook
This commit is contained in:
parent
c13fe7f4e9
commit
d6e4fa7deb
2 changed files with 208 additions and 1 deletions
179
museum_math.lua
Normal file
179
museum_math.lua
Normal 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
|
||||
30
nginx.conf
30
nginx.conf
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue