mirror of
https://gitea.invidious.io/iv-org/shard-kemal.git
synced 2024-08-15 00:53:36 +00:00
gzip static files options dir listing and etags
format
This commit is contained in:
parent
2ff5991c92
commit
a8cc4f4177
7 changed files with 224 additions and 10 deletions
|
@ -133,4 +133,21 @@ describe "Macros" do
|
||||||
Kemal.config.handlers.last.is_a?(HTTP::DeflateHandler).should eq false
|
Kemal.config.handlers.last.is_a?(HTTP::DeflateHandler).should eq false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#serve_static" do
|
||||||
|
it "should disble static file hosting" do
|
||||||
|
serve_static false
|
||||||
|
Kemal.config.serve_static.should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should disble enable gzip and dir_listing" do
|
||||||
|
serve_static({"gzip" => true, "dir_listing" => true})
|
||||||
|
conf = Kemal.config.serve_static
|
||||||
|
conf.is_a?(Hash).should eq true
|
||||||
|
if conf.is_a?(Hash)
|
||||||
|
conf["gzip"].should eq true
|
||||||
|
conf["dir_listing"].should eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
9
spec/static/dir/bigger.txt
Normal file
9
spec/static/dir/bigger.txt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse posuere cursus consectetur. Donec mauris lorem, sodales a eros a, ultricies convallis ante. Quisque elementum lacus purus, sagittis mollis justo dignissim ac. Suspendisse potenti. Cras non mauris accumsan mi porttitor congue. Quisque posuere aliquam tellus sit amet ultrices. Sed at tortor sed libero fringilla luctus vitae quis magna. In maximus congue felis, et porta tortor egestas sed. Phasellus orci eros, finibus sed ipsum eget, euismod bibendum nisl. Etiam ultrices facilisis diam in gravida. Praesent lobortis leo vitae aliquet volutpat. Praesent vel blandit risus. In suscipit eget nunc at ultrices. Proin dapibus feugiat diam ut tincidunt. Donec lectus diam, ornare ut consequat nec, gravida sit amet metus.
|
||||||
|
|
||||||
|
Nunc a viverra urna, quis ullamcorper augue. Morbi posuere auctor nibh, tempor luctus massa mollis laoreet. Pellentesque sagittis leo eu felis interdum finibus. Pellentesque porttitor lobortis arcu, eu mollis dui iaculis nec. Vestibulum sit amet sodales erat. Nullam quis mi massa. Suspendisse sit amet elit auctor, feugiat ipsum a, placerat metus. Vestibulum quis felis a lectus blandit aliquam. Nam consectetur iaculis nulla. Mauris sit amet condimentum erat, in vestibulum dui. Nullam nec mattis tortor, non viverra nunc. Proin eget congue augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed ut hendrerit nulla. Etiam cursus sagittis metus, et feugiat ligula molestie sit amet. Aliquam laoreet auctor sagittis.
|
||||||
|
|
||||||
|
Aliquam tempor urna non consectetur tincidunt. Maecenas porttitor augue diam, ac lobortis nulla suscipit eget. Ut quis lacus facilisis, euismod lacus non, ullamcorper urna. Cras pretium fringilla pharetra. Praesent sed nunc at elit vulputate elementum. Suspendisse ac molestie nunc, sit amet consectetur nunc. Cras placerat ligula tortor, non bibendum massa tempus ut. Etiam eros erat, gravida id felis eget, congue suscipit ipsum. Sed condimentum erat at facilisis dictum. Cras venenatis vitae turpis vitae sagittis. Proin id posuere est, non ornare sem. Donec vitae sollicitudin dolor, a pulvinar ex. Integer porta velit lectus, et imperdiet enim commodo a.
|
||||||
|
|
||||||
|
Donec sit amet ipsum tempus, tincidunt neque eget, luctus massa. Praesent vel nulla pretium, bibendum enim a, pulvinar enim. Vestibulum non libero eu est dignissim cursus. Nullam commodo tellus imperdiet feugiat placerat. Sed sed dolor ut nibh blandit maximus ac eget neque. Ut sit amet augue maximus, lacinia eros non, faucibus eros. Suspendisse ac bibendum libero, eu lobortis nulla. Mauris arcu nulla, tempus eu varius eu, bibendum at nibh. Donec id libero consequat, volutpat ex vitae, molestie velit. Aliquam aliquam sem ac arcu pellentesque, placerat bibendum enim dapibus. Duis consectetur ligula non placerat euismod.
|
||||||
|
|
||||||
|
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin commodo ullamcorper venenatis. Cras ac lorem sit amet augue varius convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris dolor nisi, efficitur id aliquet ut, ultricies sed elit. Proin ultricies turpis dolor, in auctor velit aliquet nec. Praesent vehicula aliquam viverra. Suspendisse potenti. Donec aliquet iaculis ultricies. Proin dignissim vitae nisl at rutrum.
|
2
spec/static/dir/test.txt
Normal file
2
spec/static/dir/test.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
hello
|
||||||
|
world
|
102
spec/static_file_handler.cr
Normal file
102
spec/static_file_handler.cr
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
require "./spec_helper"
|
||||||
|
|
||||||
|
private def handle(request, fallthrough = true)
|
||||||
|
io = MemoryIO.new
|
||||||
|
response = HTTP::Server::Response.new(io)
|
||||||
|
context = HTTP::Server::Context.new(request, response)
|
||||||
|
handler = Kemal::StaticFileHandler.new "#{__DIR__}/static", fallthrough
|
||||||
|
handler.call context
|
||||||
|
response.close
|
||||||
|
io.rewind
|
||||||
|
HTTP::Client::Response.from_io(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Kemal::StaticFileHandler do
|
||||||
|
file_text = File.read "#{__DIR__}/static/dir/test.txt"
|
||||||
|
|
||||||
|
it "should serve a file with content type and etag" do
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/test.txt")
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
response.headers["Content-Type"].should eq "text/plain"
|
||||||
|
response.headers["Etag"].should contain "W/\""
|
||||||
|
response.body.should eq(File.read("#{__DIR__}/static/dir/test.txt"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should respond with 304 if file has not changed" do
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/test.txt")
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
etag = response.headers["Etag"]
|
||||||
|
|
||||||
|
headers = HTTP::Headers{"If-None-Match" => etag}
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/test.txt", headers)
|
||||||
|
response.status_code.should eq(304)
|
||||||
|
response.body.should eq ""
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not list directory's entries" do
|
||||||
|
serve_static({"gzip" => true, "dir_listing" => false})
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/")
|
||||||
|
response.status_code.should eq(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should list directory's entries when config is set" do
|
||||||
|
serve_static({"gzip" => true, "dir_listing" => true})
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/")
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
response.body.should match(/test.txt/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should gzip a file if config is true, headers accept gzip and file is > 880 bytes" do
|
||||||
|
serve_static({"gzip" => true, "dir_listing" => true})
|
||||||
|
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers)
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
response.headers["Content-Encoding"].should eq "gzip"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not gzip a file if config is true, headers accept gzip and file is < 880 bytes" do
|
||||||
|
serve_static({"gzip" => true, "dir_listing" => true})
|
||||||
|
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/test.txt", headers)
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
response.headers["Content-Encoding"]?.should eq nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not gzip a file if config is false, headers accept gzip and file is > 880 bytes" do
|
||||||
|
serve_static({"gzip" => false, "dir_listing" => true})
|
||||||
|
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers)
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
response.headers["Content-Encoding"]?.should eq nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not serve a not found file" do
|
||||||
|
response = handle HTTP::Request.new("GET", "/not_found_file.txt")
|
||||||
|
response.status_code.should eq(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not serve a not found directory" do
|
||||||
|
response = handle HTTP::Request.new("GET", "/not_found_dir/")
|
||||||
|
response.status_code.should eq(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not serve a file as directory" do
|
||||||
|
response = handle HTTP::Request.new("GET", "/dir/test.txt/")
|
||||||
|
response.status_code.should eq(404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should handle only GET and HEAD method" do
|
||||||
|
%w(GET HEAD).each do |method|
|
||||||
|
response = handle HTTP::Request.new(method, "/dir/test.txt")
|
||||||
|
response.status_code.should eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(POST PUT DELETE).each do |method|
|
||||||
|
response = handle HTTP::Request.new(method, "/dir/test.txt")
|
||||||
|
response.status_code.should eq(404)
|
||||||
|
response = handle HTTP::Request.new(method, "/dir/test.txt"), false
|
||||||
|
response.status_code.should eq(405)
|
||||||
|
response.headers["Allow"].should eq("GET, HEAD")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,13 +11,13 @@ module Kemal
|
||||||
@ssl : OpenSSL::SSL::Context::Server?
|
@ssl : OpenSSL::SSL::Context::Server?
|
||||||
|
|
||||||
property host_binding, ssl, port, env, public_folder, logging,
|
property host_binding, ssl, port, env, public_folder, logging,
|
||||||
always_rescue, serve_static, server, extra_options
|
always_rescue, serve_static : (Bool | Hash(String, Bool)), server, extra_options
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@host_binding = "0.0.0.0"
|
@host_binding = "0.0.0.0"
|
||||||
@port = 3000
|
@port = 3000
|
||||||
@env = "development"
|
@env = "development"
|
||||||
@serve_static = true
|
@serve_static = {"dir_listing" => false, "gzip" => true}
|
||||||
@public_folder = "./public"
|
@public_folder = "./public"
|
||||||
@logging = true
|
@logging = true
|
||||||
@logger = nil
|
@logger = nil
|
||||||
|
@ -89,7 +89,7 @@ module Kemal
|
||||||
end
|
end
|
||||||
|
|
||||||
private def setup_static_file_handler
|
private def setup_static_file_handler
|
||||||
HANDLERS.insert(3, Kemal::StaticFileHandler.new(@public_folder)) if @serve_static
|
HANDLERS.insert(3, Kemal::StaticFileHandler.new(@public_folder)) if @serve_static.is_a?(Hash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ def logger(logger)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enables / Disables static file serving.
|
# Enables / Disables static file serving.
|
||||||
def serve_static(status)
|
def serve_static(status : (Bool | Hash))
|
||||||
Kemal.config.serve_static = status
|
Kemal.config.serve_static = status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,92 @@
|
||||||
|
{% if !flag?(:without_zlib) %}
|
||||||
|
require "zlib"
|
||||||
|
{% end %}
|
||||||
|
|
||||||
module Kemal
|
module Kemal
|
||||||
# Kemal::StaticFileHandler is used to serve static files(.js/.css/.png e.g).
|
|
||||||
# This handler is on by default and you can disable it like.
|
|
||||||
#
|
|
||||||
# serve_static false
|
|
||||||
#
|
|
||||||
class StaticFileHandler < HTTP::StaticFileHandler
|
class StaticFileHandler < HTTP::StaticFileHandler
|
||||||
def call(context)
|
def call(context)
|
||||||
return call_next(context) if context.request.path.not_nil! == "/"
|
return call_next(context) if context.request.path.not_nil! == "/"
|
||||||
super
|
|
||||||
|
unless context.request.method == "GET" || context.request.method == "HEAD"
|
||||||
|
if @fallthrough
|
||||||
|
call_next(context)
|
||||||
|
else
|
||||||
|
context.response.status_code = 405
|
||||||
|
context.response.headers.add("Allow", "GET, HEAD")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
config = Kemal.config.serve_static
|
||||||
|
original_path = context.request.path.not_nil!
|
||||||
|
is_dir_path = original_path.ends_with? "/"
|
||||||
|
request_path = URI.unescape(original_path)
|
||||||
|
|
||||||
|
# File path cannot contains '\0' (NUL) because all filesystem I know
|
||||||
|
# don't accept '\0' character as file name.
|
||||||
|
if request_path.includes? '\0'
|
||||||
|
context.response.status_code = 400
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
expanded_path = File.expand_path(request_path, "/")
|
||||||
|
if is_dir_path && !expanded_path.ends_with? "/"
|
||||||
|
expanded_path = "#{expanded_path}/"
|
||||||
|
end
|
||||||
|
is_dir_path = expanded_path.ends_with? "/"
|
||||||
|
|
||||||
|
file_path = File.join(@public_dir, expanded_path)
|
||||||
|
is_dir = Dir.exists? file_path
|
||||||
|
|
||||||
|
if request_path != expanded_path || is_dir && !is_dir_path
|
||||||
|
redirect_to context, "#{expanded_path}#{is_dir && !is_dir_path ? "/" : ""}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if Dir.exists?(file_path)
|
||||||
|
if config.is_a?(Hash) && config["dir_listing"] == true
|
||||||
|
context.response.content_type = "text/html"
|
||||||
|
directory_listing(context.response, request_path, file_path)
|
||||||
|
else
|
||||||
|
return call_next(context)
|
||||||
|
end
|
||||||
|
elsif File.exists?(file_path)
|
||||||
|
return if self.etag(context, file_path)
|
||||||
|
minsize = 860 # http://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits ??
|
||||||
|
context.response.content_type = mime_type(file_path)
|
||||||
|
request_headers = context.request.headers
|
||||||
|
filesize = File.size(file_path)
|
||||||
|
File.open(file_path) do |file|
|
||||||
|
if request_headers.includes_word?("Accept-Encoding", "gzip") && config.is_a?(Hash) && config["gzip"] == true && filesize > minsize && self.zip_types(file_path)
|
||||||
|
context.response.headers["Content-Encoding"] = "gzip"
|
||||||
|
Zlib::Deflate.gzip(context.response) do |deflate|
|
||||||
|
IO.copy(file, deflate)
|
||||||
|
end
|
||||||
|
elsif request_headers.includes_word?("Accept-Encoding", "deflate") && config.is_a?(Hash) && config["gzip"]? == true && filesize > minsize && self.zip_types(file_path)
|
||||||
|
context.response.headers["Content-Encoding"] = "deflate"
|
||||||
|
Zlib::Deflate.new(context.response) do |deflate|
|
||||||
|
IO.copy(file, deflate)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
context.response.content_length = filesize
|
||||||
|
IO.copy(file, context.response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
call_next(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def etag(context, file_path)
|
||||||
|
etag = %{W/"#{File.lstat(file_path).mtime.epoch.to_s}"}
|
||||||
|
context.response.headers["ETag"] = etag
|
||||||
|
return false if !context.request.headers["If-None-Match"]? || context.request.headers["If-None-Match"] != etag
|
||||||
|
context.response.content_length = 0
|
||||||
|
context.response.status_code = 304 # not modified
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
def zip_types(path) # https://github.com/h5bp/server-configs-nginx/blob/master/nginx.conf
|
||||||
|
[".htm", ".html", ".txt", ".css", ".js", ".svg", ".json", ".xml", ".otf", ".ttf", ".woff", ".woff2"].includes? File.extname(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mime_type(path)
|
def mime_type(path)
|
||||||
|
@ -20,6 +99,11 @@ module Kemal
|
||||||
when ".jpg", ".jpeg" then "image/jpeg"
|
when ".jpg", ".jpeg" then "image/jpeg"
|
||||||
when ".gif" then "image/gif"
|
when ".gif" then "image/gif"
|
||||||
when ".svg" then "image/svg+xml"
|
when ".svg" then "image/svg+xml"
|
||||||
|
when ".xml" then "application/xml"
|
||||||
|
when ".json" then "application/json"
|
||||||
|
when ".otf", ".ttf" then "application/font-sfnt"
|
||||||
|
when ".woff" then "application/font-woff"
|
||||||
|
when ".woff2" then "font/woff2"
|
||||||
else "application/octet-stream"
|
else "application/octet-stream"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue