diff --git a/src/config.cr b/src/config.cr index 6c31c5b..408b554 100644 --- a/src/config.cr +++ b/src/config.cr @@ -22,6 +22,7 @@ class Config property page_margin : Int32 = 30 property disable_login = false property default_username = "" + property auth_proxy_header_name = "" property mangadex = Hash(String, String | Int32).new @[YAML::Field(ignore: true)] diff --git a/src/handlers/auth_handler.cr b/src/handlers/auth_handler.cr index b6891d5..692fa8a 100644 --- a/src/handlers/auth_handler.cr +++ b/src/handlers/auth_handler.cr @@ -15,7 +15,11 @@ class AuthHandler < Kemal::Handler env.response.status_code = 401 env.response.headers["WWW-Authenticate"] = HEADER_LOGIN_REQUIRED env.response.print AUTH_MESSAGE - call_next env + end + + def require_auth(env) + env.session.string "callback", env.request.path + redirect env, "/login" end def validate_token(env) @@ -49,55 +53,50 @@ class AuthHandler < Kemal::Handler Storage.default.verify_user username, password end - def handle_opds_auth(env) - if validate_token(env) || validate_auth_header(env) - call_next env - else - env.response.status_code = 401 - env.response.headers["WWW-Authenticate"] = HEADER_LOGIN_REQUIRED - env.response.print AUTH_MESSAGE - end - end - - def handle_auth(env) + def call(env) + # Skip all authentication if requesting /login, /logout, or a static file if request_path_startswith(env, ["/login", "/logout"]) || requesting_static_file env return call_next(env) end - unless validate_token(env) || Config.current.disable_login - env.session.string "callback", env.request.path - return redirect env, "/login" + # Check user is logged in + if validate_token env + # Skip if the request has a valid token + elsif Config.current.disable_login + # Check default username if login is disabled + unless Storage.default.username_exists Config.current.default_username + Logger.warn "Default username #{Config.current.default_username} " \ + "does not exist" + return require_auth env + end + elsif !Config.current.auth_proxy_header_name.empty? + # Check auth proxy if present + username = env.request.headers[Config.current.auth_proxy_header_name]? + unless username && Storage.default.username_exists username + Logger.warn "Header #{Config.current.auth_proxy_header_name} unset " \ + "or is not a valid username" + return require_auth env + end + elsif request_path_startswith env, ["/opds"] + # Check auth header if requesting an opds page + unless validate_auth_header env + return require_basic_auth env + end + else + return require_auth env end - if request_path_startswith env, ["/admin", "/api/admin", "/download"] - # The token (if exists) takes precedence over the default user option. - # this is why we check the default username first before checking the - # token. - should_reject = true - if Config.current.disable_login && - Storage.default.username_is_admin Config.current.default_username - should_reject = false - end - if env.session.string? "token" - should_reject = !validate_token_admin(env) - end - if should_reject + # Check admin access when requesting an admin page + if request_path_startswith env, %w(/admin /api/admin /download) + unless is_admin? env env.response.status_code = 403 - send_error_page "HTTP 403: You are not authorized to visit " \ - "#{env.request.path}" - return + return send_error_page "HTTP 403: You are not authorized to visit " \ + "#{env.request.path}" end end + # Let the request go through if it passes the above checks call_next env end - - def call(env) - if request_path_startswith env, ["/opds"] - handle_opds_auth env - else - handle_auth env - end - end end diff --git a/src/storage.cr b/src/storage.cr index 2bc6daa..937200f 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -48,14 +48,6 @@ class Storage user_count = db.query_one "select count(*) from users", as: Int32 init_admin if init_user && user_count == 0 - - # Verifies that the default username in config is valid - if Config.current.disable_login - username = Config.current.default_username - unless username_exists username - raise "Default username #{username} does not exist" - end - end end unless @auto_close @db = DB.open "sqlite3://#{@path}" diff --git a/src/util/web.cr b/src/util/web.cr index 03c114d..67227c7 100644 --- a/src/util/web.cr +++ b/src/util/web.cr @@ -1,23 +1,23 @@ # Web related helper functions/macros -# This macro defines `is_admin` when used -macro check_admin_access +def is_admin?(env) : Bool is_admin = false - # The token (if exists) takes precedence over the default user option. - # this is why we check the default username first before checking the - # token. - if Config.current.disable_login - is_admin = Storage.default. - username_is_admin Config.current.default_username + if !Config.current.auth_proxy_header_name.empty? || + Config.current.disable_login + is_admin = Storage.default.username_is_admin get_username env end + + # The token (if exists) takes precedence over other authentication methods. if token = env.session.string? "token" is_admin = Storage.default.verify_admin token end + + is_admin end macro layout(name) base_url = Config.current.base_url - check_admin_access + is_admin = is_admin? env begin page = {{name}} render "src/views/#{{{name}}}.html.ecr", "src/views/layout.html.ecr" @@ -32,7 +32,7 @@ end macro send_error_page(msg) message = {{msg}} base_url = Config.current.base_url - check_admin_access + is_admin = is_admin? env page = "Error" html = render "src/views/message.html.ecr", "src/views/layout.html.ecr" send_file env, html.to_slice, "text/html" @@ -49,6 +49,8 @@ macro get_username(env) rescue e if Config.current.disable_login Config.current.default_username + elsif (header = Config.current.auth_proxy_header_name) && !header.empty? + env.request.headers[header] else raise e end