diff --git a/museum_math.lua b/museum_math.lua new file mode 100644 index 0000000..3e055c7 --- /dev/null +++ b/museum_math.lua @@ -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 diff --git a/nginx.conf b/nginx.conf index 041dc2f..8b0cdf6 100644 --- a/nginx.conf +++ b/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;