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 %>
+
+