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.",
|
|
|
|
|
|
"I’m 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
|