diff --git a/app/controllers/click.cr b/app/controllers/click.cr index 7ef8241..17a2126 100644 --- a/app/controllers/click.cr +++ b/app/controllers/click.cr @@ -4,52 +4,55 @@ UserAgent.load_regexes(File.read("data/uap_core_regexes.yaml")) IpLookup.load_mmdb("data/GeoLite2-Country.mmdb") module App::Controllers - class ClickController + struct ClickController include App::Models include App::Lib include App::Services - def initialize(@env : HTTP::Server::Context); end + def self.redirect_handler + ->(env : HTTP::Server::Context) { + slug = env.params.url["slug"] - def redirect - slug = @env.params.url["slug"] - - link_data = nil - Database.raw_query("SELECT id, url FROM links WHERE slug = (?) LIMIT 1", slug) do |result| - if result.move_next - link_data = {result.read(Int64), result.read(String)} + link_data = nil + Database.raw_query("SELECT id, url FROM links WHERE slug = (?) LIMIT 1", slug) do |result| + if result.move_next + link_data = {result.read(Int64), result.read(String)} + end end - end - raise App::NotFoundException.new(@env) unless link_data + raise App::NotFoundException.new(env) unless link_data - link_id, url = link_data - client_ip = IpLookup.ip_from_address(@env.request.headers["Cf-Connecting-Ip"]? || @env.request.remote_address.to_s) + link_id, url = link_data + client_ip = IpLookup.ip_from_address(env.request.headers["Cf-Connecting-Ip"]? || env.request.remote_address.to_s) - @env.response.status_code = 301 - @env.response.headers["Location"] = url - @env.response.headers["X-Forwarded-For"] = client_ip.to_s - @env.response.headers["Connection"] = "close" + spawn do + begin + user_agent_str = env.request.headers["User-Agent"]? + referer = env.request.headers["Referer"]?.try { |r| URI.parse(r).host rescue r } || env.params.query["utm_source"]? || "Direct" - spawn do - begin - user_agent_str = @env.request.headers["User-Agent"]? - referer = @env.request.headers["Referer"]?.try { |r| URI.parse(r).host rescue r } || @env.params.query["utm_source"]? || "Direct" + ua_parser = user_agent_str ? UserAgent.new(user_agent_str) : nil - ua_parser = user_agent_str ? UserAgent.new(user_agent_str) : nil + click = App::Models::Click.new + click.link_id = link_id + click.country = client_ip ? IpLookup.new(client_ip).try(&.country.try(&.code)) : nil + click.user_agent = ua_parser.to_s + click.browser = ua_parser.try(&.family) + click.os = ua_parser.try(&.os.try(&.family)) + click.referer = referer - click = App::Models::Click.new - click.link_id = link_id - click.country = client_ip ? IpLookup.new(client_ip).try(&.country.try(&.code)) : nil - click.user_agent = ua_parser.to_s - click.browser = ua_parser.try(&.family) - click.os = ua_parser.try(&.os.try(&.family)) - click.referer = referer - - Database.insert(click) - rescue ex - Log.error { "Click tracking error: #{ex.message}" } + Database.insert(click) + rescue ex + Log.error { "Click tracking error: #{ex.message}" } + end end - end + + env.response.status_code = 301 + env.response.headers.add("Location", url) + env.response.headers.add("X-Forwarded-For", client_ip.to_s) + env.response.print "" + env.response.close + + return + } end end end diff --git a/app/lib/errors.cr b/app/lib/errors.cr index 99dca4a..7f300a5 100644 --- a/app/lib/errors.cr +++ b/app/lib/errors.cr @@ -55,12 +55,3 @@ module App end end end - -error 500 do |env| - App::InternalServerErrorException.new(env) - "" -end - -error 404 do |env| - "" -end diff --git a/app/routes.cr b/app/routes.cr index 1c49b49..8544e95 100644 --- a/app/routes.cr +++ b/app/routes.cr @@ -1,26 +1,40 @@ require "./controllers/**" -module App - # CORS handling middleware - before_all do |env| - if env.request.path.starts_with?("/api/") - env.response.headers["Access-Control-Allow-Origin"] = "*" - env.response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" - env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, Accept, Origin, X-Api-Key" +require "kemal" + +class CORSHandler < Kemal::Handler + exclude ["/:slug"] + + def initialize( + @allow_origin = "*", + @allow_methods = "GET, POST, PUT, DELETE, OPTIONS", + @allow_headers = "Content-Type, Accept, Origin, X-Api-Key" + ) + end + + def call(context) + return call_next(context) if exclude_match?(context) + + context.response.headers["Access-Control-Allow-Origin"] = @allow_origin + context.response.headers["Access-Control-Allow-Methods"] = @allow_methods + context.response.headers["Access-Control-Allow-Headers"] = @allow_headers + + # If this is a preflight OPTIONS request, we return immediately with 200 + if context.request.method == "OPTIONS" + context.response.status_code = 200 + context.response.content_type = "text/plain" + context.response.print("") + return context end - end - # Error handling middleware - error 404 do |env| - {error: "Not Found"}.to_json - end - error 500 do |env| - {error: "Internal Server Error"}.to_json + call_next(context) end +end - get "/:slug" do |env| - Controllers::ClickController.new(env).redirect - end +add_handler CORSHandler.new + +module App + get "/:slug", &App::Controllers::ClickController.redirect_handler # Namespace /api get "/api/ping" do |env| @@ -50,4 +64,9 @@ module App delete "/api/links/:id" do |env| Controllers::LinkController.new(env).delete end + + error 500 do |env| + App::InternalServerErrorException.new(env) + "" + end end diff --git a/benchmark.cr b/benchmark.cr index 312e15f..859cead 100755 --- a/benchmark.cr +++ b/benchmark.cr @@ -10,12 +10,11 @@ API_URL = "#{SERVER_URL}/api/links" API_KEY = "secure_api_key_1" TIME = "59s" -RESOURCE_USAGE_INTERVAL = 1 CONTAINER_NAME = "bit" STATS_FILE = "resource_usage.txt" class ResourceMonitor - def initialize(@container_name : String, @interval : Float64) + def initialize(@container_name : String) @running = false @stats = [] of {timestamp: Time, cpu: Float64, memory: Float64} end @@ -36,7 +35,6 @@ class ResourceMonitor end @stats << stat end - sleep @interval.seconds end end end @@ -159,7 +157,7 @@ def run_benchmark sleep 2.seconds process = Process.new( "bombardier", - ["-d", TIME.to_s, "-c", "30", "-l", "--fasthttp", random_link], + ["-d", TIME.to_s, "-c", "30", "-l", "--disableKeepAlives", random_link], output: Process::Redirect::Inherit, error: Process::Redirect::Inherit ) @@ -245,7 +243,7 @@ def main check_dependencies setup_containers - monitor = ResourceMonitor.new(CONTAINER_NAME, RESOURCE_USAGE_INTERVAL) + monitor = ResourceMonitor.new(CONTAINER_NAME) monitor.start begin