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" 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 code = params[:code] status if code.between? 100, 599 end