aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2016-07-16 22:00:42 -0400
committerPaul Duncan <pabs@pablotron.org>2016-07-16 22:00:42 -0400
commit1e7ed83cbd2ddd58e8e60a4384eab011e23c22a6 (patch)
tree2cb5be0ac1c2bd522d7a2ba59916ddcc75dc9095 /src
parent49a82a69d1cef4d067dcd6d4074dd77f86cfc332 (diff)
downloadguff-1e7ed83cbd2ddd58e8e60a4384eab011e23c22a6.tar.bz2
guff-1e7ed83cbd2ddd58e8e60a4384eab011e23c22a6.zip
add file api handler, enable admin page downloads
Diffstat (limited to 'src')
-rw-r--r--src/guff/handlers.cr98
-rw-r--r--src/guff/models/file.cr2
2 files changed, 97 insertions, 3 deletions
diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr
index 3e51169..3ff58d0 100644
--- a/src/guff/handlers.cr
+++ b/src/guff/handlers.cr
@@ -576,9 +576,7 @@ module Guff::Handlers
if context.request.method == "GET"
# send body for GET requests
File.open(abs_path) do |fh|
- STDERR.puts "sending #{abs_path}"
IO.copy(fh, context.response)
- STDERR.puts "done sending #{abs_path}"
end
end
end
@@ -634,6 +632,95 @@ module Guff::Handlers
end
end
+ # FIXME: this is very similar to FilesHandler and AssetsHandler, so
+ # maybe combine them?
+ class FileAPIHandler < AuthenticatedHandler
+ def initialize(context : Context)
+ super(context, %w{admin editor})
+ @magic = Magic.new
+ end
+
+ def authenticated_call(context : HTTP::Server::Context)
+ req_path = context.request.path.not_nil!
+
+ if matching_request?(context.request.method, req_path) &&
+ valid_origin_headers?(context.request.headers)
+ # get expanded path to file
+ if abs_path = expand_path(req_path)
+ # get file digest
+ etag = get_file_etag(abs_path)
+
+ # check for cache header
+ if context.request.headers["if-none-match"]? == etag
+ # cached, send 304 not modified
+ context.response.status_code = 304
+ else
+ # not cached, set code and send headers
+ context.response.headers["x-frame-options"] = "SAMEORIGIN"
+ context.response.status_code = 200
+ context.response.content_type = get_mime_type(abs_path)
+ context.response.content_length = File.size(abs_path)
+ context.response.headers["etag"] = etag
+ context.response.headers["content-disposition"] = "attachment"
+
+ if context.request.method == "GET"
+ # send body for GET requests
+ File.open(abs_path) do |fh|
+ IO.copy(fh, context.response)
+ end
+ end
+ end
+ else
+ # expanded path does not exist
+ call_next(context)
+ end
+ else
+ # not a matching request
+ call_next(context)
+ end
+ end
+
+ VALID_METHODS = %w{GET HEAD}
+ PATH_RE = %r{^/guff/api/file/download/}
+
+ private def matching_request?(method, path)
+ VALID_METHODS.includes?(method) && PATH_RE.match(path)
+ end
+
+ private def expand_path(req_path : String) : String?
+ # unescape path, check for nil byte
+ path = URI.unescape(req_path)
+ return nil if path.includes?('\0')
+
+ # build absolute path
+ r = File.join(
+ @context.config.data_dir,
+ "files",
+ File.expand_path(path.gsub(PATH_RE, ""), "/")
+ )
+
+ # return path if file exists, or nil otherwise
+ File.file?(r) ? r : nil
+ end
+
+ private def get_file_etag(path : String) : String
+ st = File.stat(path)
+
+ # FIXME: rather than a hash this should be an HMAC
+ d = OpenSSL::Digest.new("SHA1")
+
+ # FIXME: should this be inode rather than path?
+ d << "%s-%d-%d" % [path, st.size, st.mtime.epoch_ms]
+
+ # return digest
+ d.hexdigest
+ end
+
+ private def get_mime_type(path : String) : String
+ @magic.file(path)
+ end
+ end
+
HANDLERS = [{
:dev => true,
@@ -672,6 +759,11 @@ module Guff::Handlers
:dev => false,
:id => :session,
}, {
+ # NOTE: special handler for api/file, should load
+ # before api handler
+ :dev => false,
+ :id => :file_api,
+ }, {
:dev => false,
:id => :api,
}, {
@@ -705,6 +797,8 @@ module Guff::Handlers
HTTP::DeflateHandler.new
when :session
SessionHandler.new(context)
+ when :file_api
+ FileAPIHandler.new(context)
when :api
APIHandler.new(context)
when :assets
diff --git a/src/guff/models/file.cr b/src/guff/models/file.cr
index bcd6db2..b001c2f 100644
--- a/src/guff/models/file.cr
+++ b/src/guff/models/file.cr
@@ -12,7 +12,7 @@ class Guff::Models::FileModel < Guff::Models::Model
# build base file path
base_path = File.expand_path(path)
- base_url = File.join("/files", base_path)
+ base_url = File.join("/guff/api/file/download", base_path)
Dir.entries(abs_path).select { |file|
# exclude hidden files