diff --git a/lib/rage/cli.rb b/lib/rage/cli.rb index ec4175a0..5e24555b 100644 --- a/lib/rage/cli.rb +++ b/lib/rage/cli.rb @@ -29,12 +29,15 @@ def server app = ::Rack::Builder.parse_file(options[:config] || "config.ru") app = app[0] if app.is_a?(Array) - port = options[:port] || Rage.config.server.port - address = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost") - timeout = Rage.config.server.timeout - max_clients = Rage.config.server.max_clients + server_options = { service: :http, handler: app } - ::Iodine.listen service: :http, handler: app, port: port, address: address, timeout: timeout, max_clients: max_clients + server_options[:port] = options[:port] || Rage.config.server.port + server_options[:address] = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost") + server_options[:timeout] = Rage.config.server.timeout + server_options[:max_clients] = Rage.config.server.max_clients + server_options[:public] = Rage.config.public_file_server.enabled ? Rage.root.join("public").to_s : nil + + ::Iodine.listen(**server_options) ::Iodine.threads = Rage.config.server.threads_count ::Iodine.workers = Rage.config.server.workers_count diff --git a/lib/rage/configuration.rb b/lib/rage/configuration.rb index b12937ce..50f2c569 100644 --- a/lib/rage/configuration.rb +++ b/lib/rage/configuration.rb @@ -102,6 +102,12 @@ # # > Specifies connection timeout. # +# # Static file server +# +# • _config.public_file_server.enabled_ +# +# > Configures whether Rage should serve static files from the public directory. Defaults to `false`. +# # # Cable Configuration # # • _config.cable.protocol_ @@ -165,6 +171,10 @@ def cable @cable ||= Cable.new end + def public_file_server + @public_file_server ||= PublicFileServer.new + end + def internal @internal ||= Internal.new end @@ -246,6 +256,10 @@ def middlewares end end + class PublicFileServer + attr_accessor :enabled + end + # @private class Internal attr_accessor :rails_mode diff --git a/spec/integration/file_server_spec.rb b/spec/integration/file_server_spec.rb new file mode 100644 index 00000000..12ee9273 --- /dev/null +++ b/spec/integration/file_server_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require "http" + +RSpec.describe "File server" do + before :all do + skip("skipping file server tests") unless ENV["ENABLE_EXTERNAL_TESTS"] == "true" + end + + subject { http.get(url) } + let(:http) { HTTP } + let(:url) { "http://localhost:3000/test.txt" } + + context "with file server disabled" do + before :all do + launch_server + end + + after :all do + stop_server + end + + it "doesn't allow to access public assets" do + expect(subject.code).to eq(404) + end + end + + context "with file server enabled" do + before :all do + launch_server(env: { "ENABLE_FILE_SERVER" => "1" }) + end + + after :all do + stop_server + end + + it "allows to access public assets" do + expect(subject.code).to eq(200) + expect(subject.to_s).to eq("ABCDEFGHIJKLMNOPQRSTUVWXYZ\n") + end + + it "returns correct headers" do + expect(subject.headers["content-length"]).to eq("27") + expect(subject.headers["content-type"]).to eq("text/plain") + expect(subject.headers["etag"]).not_to be_empty + expect(subject.headers["last-modified"]).not_to be_empty + end + + it "fallbacks to application routes" do + response = HTTP.get("http://localhost:3000/get") + expect(response.code).to eq(200) + expect(response.to_s).to eq("i am a get response") + end + + context "with valid range" do + let(:http) { HTTP.headers(range: "bytes=5-9") } + + it "returns correct response" do + expect(subject.code).to eq(206) + expect(subject.to_s).to eq("FGHIJ") + end + + it "returns correct headers" do + expect(subject.headers["content-length"]).to eq("5") + expect(subject.headers["content-range"]).to eq("bytes 5-9/27") + end + end + + context "with invalid range" do + let(:http) { HTTP.headers(range: "bytes=5-100") } + + it "returns correct response" do + expect(subject.code).to eq(416) + end + + it "returns correct headers" do + expect(subject.headers["content-range"]).to eq("bytes */27") + end + end + + context "with If-None-Match" do + let(:http) { HTTP.headers(if_none_match: etag) } + + context "with valid etag" do + let(:etag) { HTTP.get(url).headers["etag"] } + + it "returns correct response" do + expect(subject.code).to eq(304) + end + end + + context "with invalid etag" do + let(:etag) { "invalid-etag" } + + it "returns correct response" do + expect(subject.code).to eq(200) + expect(subject.to_s).to eq("ABCDEFGHIJKLMNOPQRSTUVWXYZ\n") + end + end + end + + context "with If-Range" do + let(:http) { HTTP.headers(range: "bytes=5-9", if_range: etag) } + + context "with valid etag" do + let(:etag) { HTTP.get(url).headers["etag"] } + + it "returns correct response" do + expect(subject.code).to eq(206) + expect(subject.to_s).to eq("FGHIJ") + end + end + + context "with invalid etag" do + let(:etag) { "invalid-etag" } + + it "returns correct status code" do + expect(subject.code).to eq(200) + expect(subject.to_s).to eq("ABCDEFGHIJKLMNOPQRSTUVWXYZ\n") + end + end + end + + context "with URL outside public directory" do + let(:url) { "http://localhost:3000/../Gemfile" } + + it "returns correct status code" do + expect(subject.code).to eq(404) + end + end + end +end diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb index a6d9c86f..8e4f3ade 100644 --- a/spec/integration/integration_spec.rb +++ b/spec/integration/integration_spec.rb @@ -10,20 +10,11 @@ end before :all do - Bundler.with_unbundled_env do - system("gem build -o rage-local.gem && gem install rage-local.gem --no-document && bundle install") - @pid = spawn("bundle exec rage s", chdir: "spec/integration/test_app") - sleep(1) - end + launch_server end after :all do - if @pid - Process.kill(:SIGTERM, @pid) - Process.wait - system("rm spec/integration/test_app/Gemfile.lock") - system("rm spec/integration/test_app/log/development.log") - end + stop_server end it "correctly processes lambda requests" do diff --git a/spec/integration/test_app/config/application.rb b/spec/integration/test_app/config/application.rb index 97dd47ae..aabd0402 100644 --- a/spec/integration/test_app/config/application.rb +++ b/spec/integration/test_app/config/application.rb @@ -20,6 +20,7 @@ def call(env) Rage.configure do config.middleware.use TestMiddleware + config.public_file_server.enabled = !!ENV["ENABLE_FILE_SERVER"] end require "rage/setup" diff --git a/spec/integration/test_app/public/.keep b/spec/integration/test_app/public/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/integration/test_app/public/test.txt b/spec/integration/test_app/public/test.txt new file mode 100644 index 00000000..72d007b6 --- /dev/null +++ b/spec/integration/test_app/public/test.txt @@ -0,0 +1 @@ +ABCDEFGHIJKLMNOPQRSTUVWXYZ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a84aff3d..1816bd07 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "rage/all" +require_relative "support/integration_helper" require_relative "support/request_helper" require_relative "support/controller_helper" require_relative "support/reactor_helper" @@ -21,6 +22,7 @@ Iodine.patch_rack end + config.include IntegrationHelper config.include RequestHelper config.include ControllerHelper config.include ReactorHelper diff --git a/spec/support/integration_helper.rb b/spec/support/integration_helper.rb new file mode 100644 index 00000000..1ac12d55 --- /dev/null +++ b/spec/support/integration_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module IntegrationHelper + def launch_server(env: {}) + Bundler.with_unbundled_env do + system("gem build -o rage-local.gem && gem install rage-local.gem --no-document && bundle install") + @pid = spawn(env, "bundle exec rage s", chdir: "spec/integration/test_app") + sleep(1) + end + end + + def stop_server + if @pid + Process.kill(:SIGTERM, @pid) + Process.wait + system("rm spec/integration/test_app/Gemfile.lock") + system("rm spec/integration/test_app/log/development.log") + end + end +end