kipunji/app.rb

375 lines
6.3 KiB
Ruby

require "bundler/setup"
require "sinatra"
require "sinatra/json"
require "sinatra/cookies"
require "sinatra/multi_route"
require "time"
require "fileutils"
require "json"
require "securerandom"
require "random/formatter"
require "ulid"
require "anyflake"
require "jwt"
SESSION_SECRET_HEX_LENGTH = 64
set :session_secret, ENV.fetch("SESSION_SECRET") { SecureRandom.hex(SESSION_SECRET_HEX_LENGTH) }
CLK_TCK = 100
PID_FILE_PATH = "/run/app/pid".freeze
PROC_UPTIME_PATH = "/proc/uptime".freeze
SECONDS_PER_YEAR = 31_556_952
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 3_600
SECONDS_PER_DAY = 86_400
SECONDS_PER_WEEK = 604_800
SECONDS_PER_MONTH = SECONDS_PER_YEAR / 12
DURATION_PARTS = [
[SECONDS_PER_YEAR, "year", "y"],
[SECONDS_PER_MONTH, "month", "m"],
[SECONDS_PER_WEEK, "week", "w"],
[SECONDS_PER_DAY, "day", "d"],
[SECONDS_PER_HOUR, "hour", "h"],
[SECONDS_PER_MINUTE, "minute", "m"],
[1, "second", "s"]
].freeze
JWT_SECRET = SecureRandom.bytes(64).freeze
module Sinatra
module RequestHeadersHelper
def req_headers
hash = request.env.select { |k, _| k.start_with? "HTTP_" }
.collect { |k, v| [k.gsub(/^HTTP_/, "").tr("_", "-").downcase, v] }
.sort
h = Rack::Headers.new
h.merge hash
end
end
helpers RequestHeadersHelper
end
module State
def enable
FileUtils.touch(@file)
end
def disable
File.delete @file if File.file? @file
end
def enabled?
File.file? @file
end
def toggle
return enable unless enabled?
disable
end
end
module UpDown
def up
enable
end
def down
disable
end
def to_s
enabled? ? "up" : "down"
end
def to_json(*_args)
return unless enabled?
JSON.generate({"status" => "ok"})
end
end
class TickTock
def initialize
@pid = master_pid
@procfs_f = format "/proc/%s/stat", @pid
puts @pid
end
def uptime
x = ""
File.open @procfs_f do |f|
f.each_line do |l|
x = l.strip.split
end
end
system_uptime - (Integer(x[21]) / CLK_TCK)
end
def started_at
Time.now - uptime
end
end
class Health
include Singleton
include State
include UpDown
def initialize
@file = "./healthy"
end
def healthy?
enabled?
end
end
class Ready
include Singleton
include State
include UpDown
def initialize
@file = "./ready"
end
def ready?
enabled?
end
end
class Sleep
include Singleton
include State
def initialize
@file = "./sleep"
end
def asleep?
enabled?
end
def wake
disable
end
def sleep
enable
end
end
def master_pid
pid_s = File.read PID_FILE_PATH
Integer pid_s.strip
end
def system_uptime
uptime_s = File.read PROC_UPTIME_PATH
Float uptime_s.strip.split[0]
end
##
# Convert to human friendly time interval
#
# @param [Integer] time in seconds
def human_time(seconds, delim = " ")
s = []
DURATION_PARTS.each do |divisor, unit, abbrev|
q = seconds / divisor
r = seconds % divisor
s.push "#{q}#{abbrev}" if q.positive?
seconds = r
end
s.join delim
end
Health.instance.up
Ready.instance.up
Sleep.instance.wake
enable :sessions
configure do
mime_type :json, "application/json"
end
before do
# content_type 'text/plain'
sleep(1) while Sleep.instance.asleep? && request.path_info != "/livez/sleep"
content_type :text if request.path_info.start_with? "/_cat"
request.session_options[:skip] = !request.path_info.start_with?("/session")
end
get "/" do
"hello there!\n"
end
get "/env", provides: "json" do
content_type :json
return JSON.pretty_generate ENV.sort.to_h if params.key? "pretty"
JSON.generate ENV.sort.to_h
end
get "/headers", provides: "json" do
h = req_headers
return JSON.pretty_generate h if params.key? "pretty"
JSON.generate h
end
get "/livez" do
error 503 unless Health.instance.healthy?
return Health.instance.to_json if request.env["HTTP_ACCEPT"] == "application/json"
Health.instance.to_s
end
get "/livez/uptime" do
tt = TickTock.new
x = {started_at: tt.started_at, seconds: tt.uptime.to_i, human: human_time(tt.uptime.to_i)}
json x
end
post "/livez/toggle" do
Health.instance.toggle
"ok\n"
end
post "/livez/sleep" do
Sleep.instance.toggle
"ok\n"
end
get "/readyz" do
error 503 unless Ready.instance.ready?
return Ready.instance.to_json if request.env["HTTP_ACCEPT"] == "application/json"
Ready.instance.to_s
end
get "/uuid" do
n = params.fetch(:n, 1).to_i
stream do |out|
n.times do |_|
out << format("%s\n", SecureRandom.uuid)
end
end
end
get "/ulid" do
n = params.fetch(:n, 1).to_i
stream do |out|
n.times do |_|
out << format("%s\n", ULID.generate)
end
end
end
get "/snowflake" do
n = params.fetch(:n, 1).to_i
epoch = Time.new(2016, 7, 1, 0, 0, 0).strftime("%s%L").to_i
node_id = 1
af = AnyFlake.new(epoch, node_id)
stream do |out|
n.times do |_|
out << format("%s\n", af.next_id)
end
end
end
post "/quit" do
Process.kill("TERM", master_pid)
nil
end
post "/halt" do
Process.kill("QUIT", master_pid)
nil
end
get "/pid" do
JSON.generate({puma: master_pid, pid: Process.pid})
end
get "/token" do
exp = Time.now.to_i + SECONDS_PER_MINUTE * 2
payload = {name: "anonymous", exp: exp, jti: Random.uuid}
expires_at = Time.at(exp).to_datetime
token = JWT.encode payload, JWT_SECRET, "HS256"
x = {token: token, expires_at: expires_at}
json x
end
get "/token/validate" do
token = req_headers["authorization"].split[1]
payload = JWT.decode token, JWT_SECRET, true, algorithm: "HS256"
json payload
end
post "/session" do
session.merge! params
json session.to_hash
end
get "/session" do
j = session.to_hash
j[:hostname] = ENV["HOSTNAME"]
json j
end
get "/cookies" do
json response.headers
end
get "/_cat/headers" do
stream do |out|
req_headers.each do |k, v|
out << "#{k}: #{v.inspect}\n"
end
end
end
get "/_cat/env" do
stream do |out|
ENV.sort.each do |k, v|
out << "#{k}=#{v}\n"
end
end
end
get "/_cat/cookies" do
stream do |out|
cookies.each do |k, v|
out << "#{k}=#{v}\n"
end
end
end
route :delete, :get, :patch, :post, :put, "/status/:code" do
# hello
code = Integer(params[:code])
status code if code.between? 100, 599
end
get "/chunked/:delay" do
delay = Float(params[:delay])
stream do |out|
out << "Hello, world!\n"
sleep delay
out << "Hello, world!\n"
end
end