From e2b0cf97941b02b87ae40d3c69a5bdec61566d1a Mon Sep 17 00:00:00 2001 From: Ryan Cavicchioni Date: Tue, 2 Jul 2024 15:22:32 -0500 Subject: [PATCH] first commit --- .gitignore | 5 + Dockerfile | 48 +++++++ Gemfile | 20 +++ Gemfile.lock | 103 ++++++++++++++ app.rb | 331 +++++++++++++++++++++++++++++++++++++++++++++ config.ru | 6 + config/puma.rb | 3 + docker-compose.yml | 15 ++ sig/app.rbs | 63 +++++++++ 9 files changed, 594 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 app.rb create mode 100644 config.ru create mode 100644 config/puma.rb create mode 100644 docker-compose.yml create mode 100644 sig/app.rbs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0145dfb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.bundle +.cache +.local +.ruby-lsp +.ash_history diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b7fde32 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +FROM ruby:alpine as base + +WORKDIR /app + +RUN < 0.0.1) + nanoid (2.0.0) + nio4r (2.7.3) + parallel (1.25.1) + parser (3.3.3.0) + ast (~> 2.4.1) + racc + prism (0.30.0) + puma (6.4.2) + nio4r (~> 2.0) + racc (1.8.0) + rack (3.1.3) + rack-protection (4.0.0) + base64 (>= 0.1.0) + rack (>= 3.0.0, < 4) + rack-session (2.0.0) + rack (>= 3.0.0) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rainbow (3.1.1) + rbs (3.5.1) + logger + regexp_parser (2.9.2) + rexml (3.3.0) + strscan + rubocop (1.64.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) + ruby-lsp (0.17.3) + language_server-protocol (~> 3.17.0) + prism (>= 0.29.0, < 0.31) + rbs (>= 3, < 4) + sorbet-runtime (>= 0.5.10782) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + sinatra (4.0.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.0.0) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + sinatra-contrib (4.0.0) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.0.0) + sinatra (= 4.0.0) + tilt (~> 2.0) + sorbet-runtime (0.5.11435) + strscan (3.1.0) + tilt (2.3.0) + ulid (1.4.0) + unicode-display_width (2.5.0) + uuid7 (0.2.0) + zeitwerk (~> 2.4) + webrick (1.8.1) + zeitwerk (2.6.15) + +PLATFORMS + ruby + x86_64-linux-musl + +DEPENDENCIES + anyflake + jwt + ksuid + nanoid + puma + rackup + rbs + rubocop + ruby-lsp + sinatra + sinatra-contrib + ulid + uuid7 + +BUNDLED WITH + 2.5.13 diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..8ee3853 --- /dev/null +++ b/app.rb @@ -0,0 +1,331 @@ +require "bundler/setup" +require "sinatra" +require "sinatra/json" +require "sinatra/cookies" +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/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_/, "").gsub(/_/, "-").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? unless request.path_info == "/livez/sleep" +end + +get "/" do + "hello there!\n" +end + +get "/env" do + stream do |out| + ENV.sort.each do |k, v| + out << "#{k}=#{v}\n" + end + end +end + +get "/headers", provides: "json" do + h = req_headers + return JSON.pretty_generate h if params.key? "pretty" + + JSON.generate h +end + +get "/headers" do + stream do |out| + req_headers.each do |k, v| + out << "#{k}: #{v.inspect}\n" + end + end +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 "/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" + token + 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 + json session.to_hash +end + +get "/cookies" do + json response.headers +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..cbc11d2 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +require 'bundler/setup' +require 'sinatra' + +require './app' + +run Sinatra::Application \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..314478e --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,3 @@ +# workers 3 +pidfile '/run/pid' +preload_app! diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ec1aaa9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + web: + build: + context: . + target: dev + ports: + - "4567:4567" + volumes: + - .:/app + environment: + {} + # WEB_CONCURRENCY: 3 + command: + - sleep + - infinity diff --git a/sig/app.rbs b/sig/app.rbs new file mode 100644 index 0000000..d945585 --- /dev/null +++ b/sig/app.rbs @@ -0,0 +1,63 @@ +module State + def enable: () -> untyped + + def disable: () -> (untyped | nil) + + def enabled?: () -> bool + + def toggle: () -> untyped +end + +module UpDown + def up: () -> untyped + + def down: () -> untyped + + def to_s: () -> ("up" | "down") + + def to_json: (*untyped _args) -> (nil | untyped) +end + +class Health + @file: untyped + + include Singleton + + include State + + include UpDown + + def initialize: () -> void + + def healthy?: () -> bool +end + +class Ready + @file: untyped + + include Singleton + + include State + + include UpDown + + def initialize: () -> void + + def ready?: () -> bool +end + +class Sleep + @file: untyped + + include Singleton + + include State + + def initialize: () -> void + + def asleep?: () -> bool + + def wake: () -> untyped + + def sleep: () -> untyped +end