no-as-a-service/app.rb

143 lines
4 KiB
Ruby
Raw Normal View History

2025-05-13 07:58:31 +02:00
require 'sinatra'
require 'json'
require 'rack/accept'
require 'time'
use Rack::Accept
NO_RESPONSES = {
"en" => [
"No.", "Nope.", "Nah.", "Nuh-uh.", "Negative.", "Nay.",
"Hell no.", "Not in a million years.", "Get lost.",
"Not happening, buddy.", "Why don't you try asking someone else?",
"Are you kidding me?", "Absolutely not, go away.",
"Im terribly sorry, but no.", "I regret to inform you that I cannot comply.",
"Unfortunately, that will not be possible at this time.",
"I must respectfully decline your request.", "I wish I could, but sadly I cannot.",
"It would be my pleasure to help, but I must regretfully decline.",
"With the greatest respect, I must say no.",
"I apologize, but I must kindly refuse your request."
],
"de" => ["Nein.", "Keineswegs."],
"fr" => ["Non.", "Jamais."],
"ja" => ["いいえ", "ダメです。"],
"ru" => ["Нет", "Никак нет"],
"pt" => ["Não.", "De jeito nenhum."],
"es" => ["No.", "Para nada."],
"pl" => ["Nie.", "W żadnym wypadku."],
"fi" => ["Ei.", "Ei todellakaan."],
"tr" => ["Hayır.", "Bu değil."],
"he" => ["לא"],
"hi" => ["नहीं", "बिलकुल नहीं"],
"no" => ["Nei.", "Absolutt ikke.", "Ikke tale om."],
"sv" => ["Nej.", "Inte alls."],
"da" => ["Nej.", "Ikke en chance."],
"it" => ["No.", "Assolutamente no."],
"nl" => ["Nee.", "Absoluut niet."],
"cs" => ["Ne.", "Rozhodně ne."],
"ko" => ["아니요.", "절대 안 돼요."],
"zh" => ["不。", "绝对不行。"]
}
RATE_LIMIT = {
window: 60, # seconds
limit: 60 # requests per IP per window
}
$requests = {}
helpers do
def client_ip
request.env['HTTP_X_FORWARDED_FOR'] || request.ip
end
def log_to_journald(message)
IO.popen(["/usr/bin/systemd-cat", "--identifier=noaas"], "w") { |io| io.puts message }
rescue
puts message # fallback to stdout
end
def log_structured(lang:, format:, status:, duration_ms:)
log_entry = {
timestamp: Time.now.utc.iso8601,
ip: client_ip,
method: request.request_method,
path: request.path,
user_agent: request.user_agent,
language: lang,
format: format,
status: status,
duration_ms: duration_ms
}
log_to_journald(JSON.generate(log_entry))
end
def rate_limited?
ip = client_ip
now = Time.now.to_i
$requests[ip] ||= []
$requests[ip].reject! { |timestamp| timestamp < now - RATE_LIMIT[:window] }
if $requests[ip].size >= RATE_LIMIT[:limit]
true
else
$requests[ip] << now
false
end
end
def preferred_language
params["lang"] || request.env["HTTP_ACCEPT_LANGUAGE"].to_s.scan(/[a-z]{2}/).find do |lang|
NO_RESPONSES.key?(lang)
end || "en"
end
def no_message(lang)
NO_RESPONSES[lang].sample
end
end
before do
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
after do
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time) * 1000).round(2)
lang = preferred_language
format = request.preferred_type&.to_s || request.accept.first.to_s
log_structured(lang: lang, format: format, status: response.status, duration_ms: duration)
end
get '/' do
if rate_limited?
2025-05-13 20:00:18 +02:00
response = [ { status: 402, message: "I'm not paid enough for this." },
{ status: 418, message: "I'm just a teapot."},
{ status: 429, message: "Not so fast!"} ].sample
status response[:status]
2025-05-13 07:58:31 +02:00
content_type :json
2025-05-13 20:00:18 +02:00
return { error: response[:message] }.to_json
2025-05-13 07:58:31 +02:00
end
lang = preferred_language
message = no_message(lang)
if request.preferred_type.to_s == 'application/json' || request.accept.include?('application/json')
content_type :json
status 200
{ message: message, language: lang }.to_json
else
content_type :html
status 200
"<!DOCTYPE html>
<html lang='#{lang}'>
<head>
<meta charset='UTF-8'>
<title>No as a Service</title>
</head>
<body style='font-family: sans-serif; text-align: center; margin-top: 20%; font-size: 2em;'>
<p>#{message}</p>
</body>
</html>"
end
end