diff --git a/shard.lock b/shard.lock index d071c03..0e4f42f 100644 --- a/shard.lock +++ b/shard.lock @@ -1,5 +1,9 @@ version: 2.0 shards: + athena-routing: + git: https://github.com/athena-framework/routing.git + version: 0.1.6 + db: git: https://github.com/crystal-lang/crystal-db.git version: 0.11.0 diff --git a/shard.yml b/shard.yml index d351ad7..39167f7 100644 --- a/shard.yml +++ b/shard.yml @@ -23,6 +23,9 @@ dependencies: branch: master http-session: github: straight-shoota/http-session + athena-routing: + github: athena-framework/routing + version: ~> 0.1.0 crystal: 1.6.2 diff --git a/src/crystal-gauntlet.cr b/src/crystal-gauntlet.cr index 76823cc..dbc69a7 100644 --- a/src/crystal-gauntlet.cr +++ b/src/crystal-gauntlet.cr @@ -9,6 +9,7 @@ require "colorize" require "option_parser" require "migrate" +require "./server" require "./enums" require "./lib/hash" require "./lib/format" @@ -64,7 +65,10 @@ module CrystalGauntlet DATA_FOLDER = Path.new("data") @@endpoints = Hash(String, (HTTP::Server::Context -> String)).new - @@template_endpoints = Hash(String, (HTTP::Server::Context -> Nil)).new + @@template_endpoints = Hash( + NamedTuple(name: String, path: String, methods: Enumerable(String)), + Proc(HTTP::Server::Context, Hash(String, String?), Nil) + ).new @@up_at = nil @@ -132,90 +136,6 @@ module CrystalGauntlet end end - class GDHandler - include HTTP::Handler - - def call(context : HTTP::Server::Context) - # expunge trailing slashes - path = context.request.path.chomp("/") - - path = path.sub(config_get("general.append_path").as(String | Nil) || "", "") - - body = context.request.body - - if CrystalGauntlet.endpoints.has_key?(path) && context.request.method == "POST" && body - func = CrystalGauntlet.endpoints[path] - begin - value = func.call(context) - rescue err - LOG.error { "error while handling #{path.colorize(:white)}:" } - LOG.error { err.to_s } - is_relevant = true - err.backtrace.each do |str| - # this is a hack. Oh well - if str.starts_with?("src/crystal-gauntlet.cr") || (!is_relevant) - is_relevant = false - else - LOG.error {" #{str}"} - end - end - context.response.content_type = "text/plain" - context.response.respond_with_status(500, "-1") - else - max_size = 2048 - - value_displayed = value - if value.size > max_size - value_displayed = value[0..max_size] + ("…".colorize(:dark_gray).to_s) - end - LOG.debug { "-> ".colorize(:green).to_s + value_displayed } - - context.response.content_type = "text/plain" - # to let endpoints manually write to IO - if value != "" - context.response.print value - end - end - else - call_next(context) - end - end - end - - class TemplateHandler - include HTTP::Handler - - def call(context : HTTP::Server::Context) - # expunge trailing slashes - path = context.request.path.chomp("/") - - body = context.request.body - - if CrystalGauntlet.template_endpoints.has_key?(path) - func = CrystalGauntlet.template_endpoints[path] - begin - func.call(context) - rescue err - LOG.error { "error while handling #{path.colorize(:white)}:" } - LOG.error { err.to_s } - is_relevant = true - err.backtrace.each do |str| - # this is a hack. Oh well - if str.starts_with?("src/crystal-gauntlet.cr") || (!is_relevant) - is_relevant = false - else - LOG.error {" #{str}"} - end - end - context.response.content_type = "text/html" - context.response.respond_with_status(500, "Internal server error occurred, sorry about that") - end - else - call_next(context) - end - end - end - def self.run() Log.setup_from_env(backend: Log::IOBackend.new(formatter: CrystalGauntletFormat)) @@ -294,31 +214,7 @@ module CrystalGauntlet Dir.mkdir_p(DATA_FOLDER / v) } - server = HTTP::Server.new([ - HTTP::LogHandler.new, - HTTP::StaticFileHandler.new("public/", fallthrough: true, directory_listing: false), - HTTP::StaticFileHandler.new((DATA_FOLDER / "songs").to_s, fallthrough: true, directory_listing: false), - CrystalGauntlet::GDHandler.new, - CrystalGauntlet::TemplateHandler.new - ]) - - listen_on = URI.parse(ENV["LISTEN_ON"]? || "http://localhost:8080").normalize - - case listen_on.scheme - when "http" - server.bind_tcp(listen_on.hostname.not_nil!, listen_on.port.not_nil!) - when "unix" - server.bind_unix(listen_on.to_s.sub("unix://","")) - end - - check_server_length(false) - - Reupload.init() - Ranks.init() - - @@up_at = Time.utc - LOG.notice { "Listening on #{listen_on.to_s.colorize(:white)}" } - server.listen + Server.run() end end diff --git a/src/server.cr b/src/server.cr new file mode 100644 index 0000000..1ecf8c9 --- /dev/null +++ b/src/server.cr @@ -0,0 +1,93 @@ +require "athena-routing" + +module CrystalGauntlet::Server + class GDHandler + include HTTP::Handler + + def call(context : HTTP::Server::Context) + # expunge trailing slashes + path = context.request.path.chomp("/") + + path = path.sub(config_get("general.append_path").as(String | Nil) || "", "") + + body = context.request.body + + if CrystalGauntlet.endpoints.has_key?(path) && context.request.method == "POST" && body + func = CrystalGauntlet.endpoints[path] + begin + value = func.call(context) + rescue err + LOG.error { "error while handling #{path.colorize(:white)}:" } + LOG.error { err.to_s } + is_relevant = true + err.backtrace.each do |str| + # this is a hack. Oh well + if str.starts_with?("src/crystal-gauntlet.cr") || (!is_relevant) + is_relevant = false + else + LOG.error {" #{str}"} + end + end + context.response.content_type = "text/plain" + context.response.respond_with_status(500, "-1") + else + max_size = 2048 + + value_displayed = value + if value.size > max_size + value_displayed = value[0..max_size] + ("…".colorize(:dark_gray).to_s) + end + LOG.debug { "-> ".colorize(:green).to_s + value_displayed } + + context.response.content_type = "text/plain" + # to let endpoints manually write to IO + if value != "" + context.response.print value + end + end + else + call_next(context) + end + end + end + + def self.run() + template_handler = ART::RoutingHandler.new + + CrystalGauntlet.template_endpoints.each do |key, handler| + template_handler.add( + key[:name], + ART::Route.new( + key[:path], + methods: key[:methods] + ) + ) { |ctx, params| handler.call(ctx, params) } + end + + server = HTTP::Server.new([ + HTTP::LogHandler.new, + HTTP::StaticFileHandler.new("public/", fallthrough: true, directory_listing: false), + HTTP::StaticFileHandler.new((DATA_FOLDER / "songs").to_s, fallthrough: true, directory_listing: false), + GDHandler.new, + template_handler.compile + ]) + + listen_on = URI.parse(ENV["LISTEN_ON"]? || "http://localhost:8080").normalize + + case listen_on.scheme + when "http" + server.bind_tcp(listen_on.hostname.not_nil!, listen_on.port.not_nil!) + when "unix" + server.bind_unix(listen_on.to_s.sub("unix://","")) + end + + check_server_length(false) + + Reupload.init() + Ranks.init() + + @@up_at = Time.utc + LOG.notice { "Listening on #{listen_on.to_s.colorize(:white)}" } + server.listen + end +end diff --git a/src/template_endpoints/account_management.cr b/src/template_endpoints/account_management.cr index 800c47d..fe4f819 100644 --- a/src/template_endpoints/account_management.cr +++ b/src/template_endpoints/account_management.cr @@ -1,11 +1,19 @@ include CrystalGauntlet -CrystalGauntlet.template_endpoints["/#{config_get("general.append_path").as(String | Nil) || ""}accounts/accountManagement.php"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "account_management_redirect", + path: "/#{config_get("general.append_path").as(String | Nil) || ""}accounts/accountManagement.php", + methods: ["get"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.headers.add("Location", "/accounts/") context.response.status = HTTP::Status::MOVED_PERMANENTLY } -CrystalGauntlet.template_endpoints["/accounts"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "account_management", + path: "/accounts", + methods: ["get"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" account_id = nil diff --git a/src/template_endpoints/account_settings.cr b/src/template_endpoints/account_settings.cr index 41d5d50..683c0bb 100644 --- a/src/template_endpoints/account_settings.cr +++ b/src/template_endpoints/account_settings.cr @@ -2,7 +2,12 @@ require "uri" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/accounts/settings"] = ->(context : HTTP::Server::Context) { + +CrystalGauntlet.template_endpoints[{ + name: "account_settings", + path: "/accounts/settings", + methods: ["get", "post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" account_id = nil diff --git a/src/template_endpoints/create_session.cr b/src/template_endpoints/create_session.cr index 407cd6f..1f7bb9e 100644 --- a/src/template_endpoints/create_session.cr +++ b/src/template_endpoints/create_session.cr @@ -3,7 +3,11 @@ require "compress/gzip" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/tools/create_session"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "create_session", + path: "/tools/create_session", + methods: ["get", "post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { disabled = !config_get("sessions.allow").as(Bool | Nil) result = nil body = context.request.body diff --git a/src/template_endpoints/index.cr b/src/template_endpoints/index.cr index 25e038c..e0a1738 100644 --- a/src/template_endpoints/index.cr +++ b/src/template_endpoints/index.cr @@ -2,12 +2,21 @@ require "ecr" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/tools"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "tools_redirect", + path: "/tools", + methods: ["get"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.headers.add("Location", "/") context.response.status = HTTP::Status::TEMPORARY_REDIRECT } -CrystalGauntlet.template_endpoints[""] = ->(context : HTTP::Server::Context) { + +CrystalGauntlet.template_endpoints[{ + name: "index", + path: "/", + methods: ["get"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" ECR.embed("./public/template/index.ecr", context.response) } diff --git a/src/template_endpoints/levels.cr b/src/template_endpoints/levels.cr index 3b21726..40913b8 100644 --- a/src/template_endpoints/levels.cr +++ b/src/template_endpoints/levels.cr @@ -4,7 +4,11 @@ include CrystalGauntlet levels_per_page = 10 -CrystalGauntlet.template_endpoints["/tools/levels"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "list_levels", + path: "/tools/levels", + methods: ["get"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" page = (context.request.query_params["page"]? || "0").to_i? || 0 total_levels = DATABASE.scalar("select count(*) from levels").as(Int64) diff --git a/src/template_endpoints/login.cr b/src/template_endpoints/login.cr index 11d6b5f..9ba273b 100644 --- a/src/template_endpoints/login.cr +++ b/src/template_endpoints/login.cr @@ -3,7 +3,11 @@ require "http-session" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/login"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "login", + path: "/login", + methods: ["get", "post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { if session = CrystalGauntlet.sessions.get(context) logged_in = true account_id = session.account_id diff --git a/src/template_endpoints/logout.cr b/src/template_endpoints/logout.cr index 51b88ee..d830351 100644 --- a/src/template_endpoints/logout.cr +++ b/src/template_endpoints/logout.cr @@ -3,12 +3,11 @@ require "http-session" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/accounts/logout"] = ->(context : HTTP::Server::Context) { - if context.request.method != "POST" - context.response.respond_with_status 405 - return - end - +CrystalGauntlet.template_endpoints[{ + name: "logout", + path: "/accounts/logout", + methods: ["post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { CrystalGauntlet.sessions.delete(context) context.response.headers.add("Location", "/") diff --git a/src/template_endpoints/notifications.cr b/src/template_endpoints/notifications.cr index dc8ce9b..1ef0eb4 100644 --- a/src/template_endpoints/notifications.cr +++ b/src/template_endpoints/notifications.cr @@ -1,6 +1,10 @@ include CrystalGauntlet -CrystalGauntlet.template_endpoints["/accounts/notifications"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "account_notifications", + path: "/accounts/notifications", + methods: ["get"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" account_id = nil diff --git a/src/template_endpoints/reupload.cr b/src/template_endpoints/reupload.cr index 433d819..736ff64 100644 --- a/src/template_endpoints/reupload.cr +++ b/src/template_endpoints/reupload.cr @@ -3,7 +3,11 @@ require "xml" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/tools/reupload"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "reupload", + path: "/tools/reupload", + methods: ["get", "post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" disabled = !(config_get("reuploads.allowed").as?(Bool)) diff --git a/src/template_endpoints/song_search.cr b/src/template_endpoints/song_search.cr index 8503741..0a3d793 100644 --- a/src/template_endpoints/song_search.cr +++ b/src/template_endpoints/song_search.cr @@ -2,7 +2,11 @@ require "ecr" include CrystalGauntlet -CrystalGauntlet.template_endpoints["/tools/song_search"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "song_search", + path: "/tools/song_search", + methods: ["get", "post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" error = nil diff --git a/src/template_endpoints/song_upload.cr b/src/template_endpoints/song_upload.cr index c9a6df4..8098373 100644 --- a/src/template_endpoints/song_upload.cr +++ b/src/template_endpoints/song_upload.cr @@ -17,7 +17,11 @@ def get_next_song_id() : Int32 end end -CrystalGauntlet.template_endpoints["/tools/song_upload"] = ->(context : HTTP::Server::Context) { +CrystalGauntlet.template_endpoints[{ + name: "song_upload", + path: "/tools/song_upload", + methods: ["get", "post"] +}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) { context.response.content_type = "text/html" disabled = !(config_get("songs.allow_custom_songs").as?(Bool))