Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static file server #100

Merged
merged 3 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions lib/rage/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions lib/rage/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -246,6 +256,10 @@ def middlewares
end
end

class PublicFileServer
attr_accessor :enabled
end

# @private
class Internal
attr_accessor :rails_mode
Expand Down
132 changes: 132 additions & 0 deletions spec/integration/file_server_spec.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 2 additions & 11 deletions spec/integration/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions spec/integration/test_app/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Empty file.
1 change: 1 addition & 0 deletions spec/integration/test_app/public/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ABCDEFGHIJKLMNOPQRSTUVWXYZ
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -21,6 +22,7 @@
Iodine.patch_rack
end

config.include IntegrationHelper
config.include RequestHelper
config.include ControllerHelper
config.include ReactorHelper
Expand Down
20 changes: 20 additions & 0 deletions spec/support/integration_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Loading