require "uri" require "../handler" require "../mime-type" require "../views/html/page" class Guff::Handlers::GuffStuffHandler < Guff::Handler def initialize(models) super(models) @digests = {} of String => String end def call(context : HTTP::Server::Context) path = context.request.path.not_nil! if matching_request?(context.request.method, path) reply(context) else call_next(context) end end private def reply(context : HTTP::Server::Context) # get expanded path to file if path = expand_path(context.request.path.not_nil!) # get file digest file_digest = digest(path) # check for cache header if context.request.headers["if-none-match"]? == file_digest # cached, send 304 not modified context.response.status_code = 304 else # not cached, set code and send headers context.response.status_code = 200 context.response.content_type = Guff::MimeType.mime_type(path) context.response.content_length = File.size(path) context.response.headers["etag"] = file_digest if context.request.method == "GET" # send body File.open(path) do |fh| IO.copy(fh, context.response) end end end else # file not found context.response.status_code = 404 end end VALID_METHODS = %w{GET HEAD} PATH_RE = %r{^/guff-stuff/} 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( @models.config["stuff"], File.expand_path(path.gsub(PATH_RE, ""), "/") ) # return path if file exists, or nil otherwise File.file?(r) ? r : nil end private def digest(path : String) : String @digests[path] ||= OpenSSL::Digest.new("SHA1").file(path).hexdigest end end