From 27c111d273c734029346970d07ff03213ccdf991 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 1 Jun 2020 13:20:05 +0000 Subject: [PATCH 1/5] Handle basic auth for OPDS --- src/handlers/auth_handler.cr | 69 +++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/src/handlers/auth_handler.cr b/src/handlers/auth_handler.cr index afa17df..0c5bb28 100644 --- a/src/handlers/auth_handler.cr +++ b/src/handlers/auth_handler.cr @@ -3,23 +3,84 @@ 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) + 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 { |c| c.name == "token" } + !cookie.nil? && @storage.verify_token cookie.value + end + + def validate_cookie_token_admin(env) + cookie = env.request.cookies.find { |c| c.name == "token" } + !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) + return !verify_user(value).nil? + 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"] - cookie = env.request.cookies.find { |c| c.name == "token" } - if cookie.nil? || !@storage.verify_token cookie.value + 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 From 60100c51feb2870fa434c6a062969ca2b9ea7721 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 1 Jun 2020 13:21:10 +0000 Subject: [PATCH 2/5] Add `send_attachment` function for direct download --- src/util.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util.cr b/src/util.cr index 36a7791..e6008fb 100644 --- a/src/util.cr +++ b/src/util.cr @@ -35,6 +35,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 From 1493c3de906d3a3c9765df46d5a547422fb1c2e3 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 5 Jun 2020 14:19:49 +0000 Subject: [PATCH 3/5] Set token cookie after successful basic auth --- src/handlers/auth_handler.cr | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/handlers/auth_handler.cr b/src/handlers/auth_handler.cr index 0c5bb28..7b0f9f8 100644 --- a/src/handlers/auth_handler.cr +++ b/src/handlers/auth_handler.cr @@ -36,7 +36,16 @@ class AuthHandler < Kemal::Handler if env.request.headers[AUTH]? if value = env.request.headers[AUTH] if value.size > 0 && value.starts_with?(BASIC) - return !verify_user(value).nil? + 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 From 871a5fe755b468da6ad2ced5eb2f88587d964b12 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 5 Jun 2020 14:20:26 +0000 Subject: [PATCH 4/5] Add `render_xml` helper function --- src/util.cr | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util.cr b/src/util.cr index e6008fb..3aea0b7 100644 --- a/src/util.cr +++ b/src/util.cr @@ -127,3 +127,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 From 8a0e9250c8a63408e8fa8dbc5ce99817219d052d Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 1 Jun 2020 13:22:04 +0000 Subject: [PATCH 5/5] Finish OPDS --- src/routes/opds.cr | 32 ++++++++++++++++++++++++++++++++ src/server.cr | 1 + src/views/opds/index.ecr | 24 ++++++++++++++++++++++++ src/views/opds/title.ecr | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 src/routes/opds.cr create mode 100644 src/views/opds/index.ecr create mode 100644 src/views/opds/title.ecr 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/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 %> + +