diff --git a/packages/ruby/server/.rubocop.yml b/packages/ruby/server/.rubocop.yml new file mode 100644 index 0000000..a4fea3e --- /dev/null +++ b/packages/ruby/server/.rubocop.yml @@ -0,0 +1,40 @@ +## +# Plugins +require: + - standard + +## +# Defaults: standard-rb +inherit_gem: + standard: config/base.yml + +## +# All cops +AllCops: + TargetRubyVersion: 3.2 + Include: + - lib/*.rb + - lib/**/*.rb + - test/*_test.rb + +## +# Enabled +Style/FrozenStringLiteralComment: + Enabled: true + +## +# Disabled +Layout/ArgumentAlignment: + Enabled: false +Layout/MultilineMethodCallIndentation: + Enabled: false +Layout/EmptyLineBetweenDefs: + Enabled: false +Style/TrivialAccessors: + Enabled: false +Lint/NestedMethodDefinition: + Exclude: + - test/server_dir_test.rb +Style/SingleLineMethods: + Exclude: + - test/server_dir_test.rb diff --git a/packages/ruby/server/lib/server.rb b/packages/ruby/server/lib/server.rb index 543d35d..d97bd64 100644 --- a/packages/ruby/server/lib/server.rb +++ b/packages/ruby/server/lib/server.rb @@ -3,6 +3,7 @@ class Server require "rack" require_relative "server/puma" + require_relative "server/gzip" require_relative "server/dir" def self.app(path) diff --git a/packages/ruby/server/lib/server/dir.rb b/packages/ruby/server/lib/server/dir.rb index d64ef48..cb1fe20 100644 --- a/packages/ruby/server/lib/server/dir.rb +++ b/packages/ruby/server/lib/server/dir.rb @@ -1,19 +1,17 @@ # frozen_string_literal: true +## +# A rack application that serves the contents +# of a directory over HTTP. class Server::Dir - MIME_TYPES = { - ".ttf" => "font/ttf" - }.freeze + prepend Server::Gzip def initialize(root) @root = File.realpath(root) end def call(env) - req = Rack::Request.new(env) - headers = Rack::Headers.new(env) - h, body = read(local_path(req.path)) - [200, headers.merge!(h), body] + finish Rack::Request.new(env) rescue Errno::EPERM, Errno::EACCES body = "Permission denied" [403, {"content-length" => body.bytesize, "content-type" => "text/plain"}, [body]] @@ -29,18 +27,24 @@ class Server::Dir attr_reader :root - def read(path) + def finish(request) + path = find_path(request) body = File.binread(path) extn = File.extname(path) [ - {"content-type" => MIME_TYPES[extn] || Rack::Mime.mime_type(extn), + 200, + {"content-type" => mime_types[extn] || Rack::Mime.mime_type(extn), "content-length" => body.bytesize}, body.each_line ] end - def local_path(req_path) - lpath = File.join root, File.expand_path(req_path) - File.directory?(lpath) ? File.join(lpath, "index.html") : lpath + def find_path(request) + path = File.join root, File.expand_path(request.path) + File.directory?(path) ? File.join(path, "index.html") : path + end + + def mime_types + {".ttf" => "font/ttf"}.freeze end end diff --git a/packages/ruby/server/lib/server/gzip.rb b/packages/ruby/server/lib/server/gzip.rb new file mode 100644 index 0000000..bfdeed2 --- /dev/null +++ b/packages/ruby/server/lib/server/gzip.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +## +# A mixin module that serves a compressed version of +# a file when the file is found to exist on disk, +# and has a ".gz" file extension. +module Server::Gzip + def finish(request) + path = gzip_path(request) + if path + body = File.binread(path) + extn = File.extname(path[0..-4]) + [ + 200, + {"content-type" => mime_types[extn] || Rack::Mime.mime_type(extn), + "content-encoding" => "gzip", + "content-length" => body.bytesize}, + body.each_line + ] + else + super + end + end + + private + + def gzip_path(request) + return unless request.get_header("accept-encoding") + &.include?("gzip") + path = "#{find_path(request)}.gz" + File.exist?(path) ? path : nil + end +end diff --git a/packages/ruby/server/server.rb.gemspec b/packages/ruby/server/server.rb.gemspec index 6f3d259..2b58bd6 100644 --- a/packages/ruby/server/server.rb.gemspec +++ b/packages/ruby/server/server.rb.gemspec @@ -10,6 +10,15 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.summary = "A static file web server" gem.description = gem.summary + + ## + # Default gems + # Pinned to specific versions for OpenBSD support + gem.add_runtime_dependency "json", "= 2.6.1" + gem.add_runtime_dependency "racc", "= 1.6.0" + gem.add_runtime_dependency "stringio", "= 3.0.1" + gem.add_runtime_dependency "set", "= 1.0.2" + gem.add_runtime_dependency "puma", "~> 6.3" gem.add_runtime_dependency "rack", "~> 3.0" gem.add_development_dependency "standard", "~> 1.24" diff --git a/packages/ruby/server/test/server_dir_test.rb b/packages/ruby/server/test/server_dir_test.rb index f09c799..c9de9a9 100644 --- a/packages/ruby/server/test/server_dir_test.rb +++ b/packages/ruby/server/test/server_dir_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "setup" require "rack/test" @@ -44,7 +46,7 @@ class ServerDirTest < Test::Unit::TestCase end def test_internal_server_error - def app.read(path) raise RuntimeError, "test" end + def app.finish(request) raise "test" end get "/" assert_equal 500, last_response.status assert_equal "text/plain", last_response.content_type @@ -62,7 +64,7 @@ class ServerDirTest < Test::Unit::TestCase assert_equal "Permission denied".bytesize, last_response.content_length assert_equal "Permission denied", last_response.body ensure - File.chmod 0440, "./test/webroot/permission_denied.html" + File.chmod 0o440, "./test/webroot/permission_denied.html" end def test_page_not_found