diff --git a/src/handlers/auth_handler.cr b/src/handlers/auth_handler.cr index a64c83d..1482ec9 100644 --- a/src/handlers/auth_handler.cr +++ b/src/handlers/auth_handler.cr @@ -3,25 +3,97 @@ require "../storage" require "../util" class AuthHandler < Kemal::Handler + # Some of the code is copied form kemalcr/kemal-basic-auth on GitHub + + BASIC = "Basic" + AUTH = "Authorization" + AUTH_MESSAGE = "Could not verify your access level for that URL.\n" \ + "You have to login with proper credentials" + HEADER_LOGIN_REQUIRED = "Basic realm=\"Login Required\"" + def initialize(@storage : Storage) end - def call(env) - return call_next(env) if request_path_startswith env, ["/login", "/logout"] + def require_basic_auth(env) + headers = HTTP::Headers.new + env.response.status_code = 401 + env.response.headers["WWW-Authenticate"] = HEADER_LOGIN_REQUIRED + env.response.print AUTH_MESSAGE + call_next env + end + def validate_cookie_token(env) cookie = env.request.cookies.find do |c| c.name == "token-#{Config.current.port}" end - if cookie.nil? || !@storage.verify_token cookie.value + !cookie.nil? && @storage.verify_token cookie.value + end + + def validate_cookie_token_admin(env) + cookie = env.request.cookies.find do |c| + c.name == "token-#{Config.current.port}" + end + !cookie.nil? && @storage.verify_admin cookie.value + end + + def validate_auth_header(env) + if env.request.headers[AUTH]? + if value = env.request.headers[AUTH] + if value.size > 0 && value.starts_with?(BASIC) + token = verify_user value + return false if token.nil? + + # TODO use port number in token key + cookie = HTTP::Cookie.new "token", token + cookie.path = Config.current.base_url + cookie.expires = Time.local.shift years: 1 + env.response.cookies << cookie + + return true + end + end + end + false + end + + def verify_user(value) + username, password = Base64.decode_string(value[BASIC.size + 1..-1]) + .split(":") + @storage.verify_user username, password + end + + def handle_opds_auth(env) + if validate_cookie_token(env) || validate_auth_header(env) + return call_next env + else + headers = HTTP::Headers.new + env.response.status_code = 401 + env.response.headers["WWW-Authenticate"] = HEADER_LOGIN_REQUIRED + env.response.print AUTH_MESSAGE + end + end + + def handle_auth(env) + return call_next(env) if request_path_startswith env, ["/login", "/logout"] + + unless validate_cookie_token env return redirect env, "/login" end if request_path_startswith env, ["/admin", "/api/admin", "/download"] - unless @storage.verify_admin cookie.value + unless validate_cookie_token_admin env env.response.status_code = 403 end end 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/routes/opds.cr b/src/routes/opds.cr new file mode 100644 index 0000000..648bcac --- /dev/null +++ b/src/routes/opds.cr @@ -0,0 +1,32 @@ +require "./router" + +class OPDSRouter < Router + def initialize + get "/opds" do |env| + titles = @context.library.titles + render_xml "src/views/opds/index.ecr" + end + + get "/opds/book/:title_id" do |env| + begin + title = @context.library.get_title(env.params.url["title_id"]).not_nil! + render_xml "src/views/opds/title.ecr" + rescue e + @context.error e + env.response.status_code = 404 + end + end + + get "/opds/download/:title/:entry" do |env| + begin + title = (@context.library.get_title env.params.url["title"]).not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! + + send_attachment env, entry.zip_path + rescue e + @context.error e + env.response.status_code = 404 + end + end + end +end diff --git a/src/server.cr b/src/server.cr index 8412d0e..f12d470 100644 --- a/src/server.cr +++ b/src/server.cr @@ -53,6 +53,7 @@ class Server AdminRouter.new ReaderRouter.new APIRouter.new + OPDSRouter.new Kemal.config.logging = false add_handler LogHandler.new diff --git a/src/util.cr b/src/util.cr index 5146fde..ff22c09 100644 --- a/src/util.cr +++ b/src/util.cr @@ -39,6 +39,12 @@ def send_json(env, json) env.response.print json end +def send_attachment(env, path) + MIME.register ".cbz", "application/vnd.comicbook+zip" + MIME.register ".cbr", "application/vnd.comicbook-rar" + send_file env, path, filename: File.basename(path), disposition: "attachment" +end + def hash_to_query(hash) hash.map { |k, v| "#{k}=#{v}" }.join("&") end @@ -125,3 +131,7 @@ def validate_password(password) raise "password should contain ASCII characters only" end end + +macro render_xml(path) + send_file env, ECR.render({{path}}).to_slice, "application/xml" +end diff --git a/src/views/opds/index.ecr b/src/views/opds/index.ecr new file mode 100644 index 0000000..3bcf68f --- /dev/null +++ b/src/views/opds/index.ecr @@ -0,0 +1,24 @@ + + + + + urn:mango:index + + + + + Library + + + Mango + https://github.com/hkalexling/Mango + + + <% titles.each do |t| %> + + <%= t.display_name %> + urn:mango:<%= t.id %> + + + <% end %> + diff --git a/src/views/opds/title.ecr b/src/views/opds/title.ecr new file mode 100644 index 0000000..bd4567c --- /dev/null +++ b/src/views/opds/title.ecr @@ -0,0 +1,38 @@ + + + urn:mango:<%= title.id %> + + + + + <%= title.display_name %> + + + Mango + https://github.com/hkalexling/Mango + + + <% title.titles.each do |t| %> + + <%= t.display_name %> + urn:mango:<%= t.id %> + + + <% end %> + + <% title.entries.each do |e| %> + + <%= e.display_name %> + urn:mango:<%= e.id %> + + + + + + + + + + <% end %> + +