From af24b053884b80e75c5d161586e2b46361454403 Mon Sep 17 00:00:00 2001 From: Paul Sturgess Date: Wed, 25 Oct 2023 18:16:41 +0100 Subject: [PATCH 1/2] fix(rack): ensure OPTIONS preflight requests are handled --- Gemfile | 1 + examples/core_api/main_authenticator.rb | 8 ++--- lib/apia/rack.rb | 3 +- spec/specs/apia/rack_spec.rb | 45 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 3eaff64..24c32b9 100644 --- a/Gemfile +++ b/Gemfile @@ -11,5 +11,6 @@ gem 'rspec-mocks' gem 'solargraph' +gem 'pry' gem 'rake' gem 'rubocop' diff --git a/examples/core_api/main_authenticator.rb b/examples/core_api/main_authenticator.rb index f24e87e..212cdc0 100644 --- a/examples/core_api/main_authenticator.rb +++ b/examples/core_api/main_authenticator.rb @@ -18,15 +18,15 @@ def call cors.methods = %w[GET POST PUT PATCH DELETE OPTIONS] # Define a list of cors headers that are permitted for the request. - cors.headers = %w[X-Custom-Header] + cors.headers = %w[Authorization Content-Type] # or allow all with '*' # Define a the hostname to allow for CORS requests. cors.origin = '*' # or 'example.com' - cors.origin = 'krystal.uk' + + return if request.options? given_token = request.headers['authorization']&.sub(/\ABearer /, '') - case given_token - when 'example' + if given_token == 'example' request.identity = { name: 'Example User', id: 1234 } else raise_error 'CoreAPI/MainAuthenticator/InvalidToken', given_token: given_token.to_s diff --git a/lib/apia/rack.rb b/lib/apia/rack.rb index 485ccfd..b734afa 100644 --- a/lib/apia/rack.rb +++ b/lib/apia/rack.rb @@ -79,7 +79,8 @@ def handle_request(env, api_path) validate_api if development? - route = find_route(request_method, api_path) + access_control_request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']&.upcase + route = find_route((access_control_request_method || request_method), api_path) if route.nil? Apia::Notifications.notify(:request_route_not_found, notify_hash) raise RackError.new(404, 'route_not_found', "No route matches '#{api_path}' for #{request_method}") diff --git a/spec/specs/apia/rack_spec.rb b/spec/specs/apia/rack_spec.rb index 8710386..8b6372f 100644 --- a/spec/specs/apia/rack_spec.rb +++ b/spec/specs/apia/rack_spec.rb @@ -154,6 +154,51 @@ def call(_env) expect(Apia::Notifications).to have_received(:notify).with(:request_end, hash_including(path: 'test', method: 'GET', env: kind_of(Hash))).once end + it 'should handle OPTIONS requests' do + controller = Apia::Controller.create('Controller') do + endpoint :test do + action do + response.add_field :hello, 'world' + end + end + end + auth = Apia::Authenticator.create('Authenticator') do + action do + cors.methods = %w[GET OPTIONS] + cors.headers = %w[Authorization Content-Type] + cors.origin = 'example.com' + next if request.options? + + response.add_header 'x-executed', 123 + end + end + api = Apia::API.create('MyAPI') do + authenticator auth + + routes do + get 'test', controller: controller, endpoint: :test + end + end + rack = described_class.new(app, api, 'api/v1') + mock_request = Rack::MockRequest.env_for( + '/api/v1/test', + 'REQUEST_METHOD' => 'OPTIONS', + 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET' + ) + result = rack.call(mock_request) + expect(result).to be_a Array + expect(result[0]).to eq 200 + + headers = result[1] + expect(headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS' + expect(headers['Access-Control-Allow-Headers']).to eq 'Authorization, Content-Type' + expect(headers['Access-Control-Allow-Origin']).to eq 'example.com' + expect(headers['x-executed'].nil?).to be true + + # assert body is empty (does not contain the response from the test endpoint) + expect(result[2][0]).to eq('""') + end + it 'should catch rack errors and return an error triplet' do api = Apia::API.create('MyAPI') rack = described_class.new(app, api, 'api/v1', development: true) From 81738e47ba2f8b64bf1c4d1cde6d26cbc369384c Mon Sep 17 00:00:00 2001 From: Paul Sturgess Date: Thu, 9 Nov 2023 13:56:18 +0000 Subject: [PATCH 2/2] only check HTTP_ACCESS_CONTROL_REQUEST_METHOD for options requests --- .DS_Store | Bin 0 -> 6148 bytes lib/apia/rack.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..94e8e06d0b3c0eea52a799b60b44125cd5fb1383 GIT binary patch literal 6148 zcmeHKJ8Hu~5S?*U2;8_#xmWNF7NeX%7qG!0jV*_Sgp{gst{ly8K7<&_jUkPB12b=T zcHRoTLZcB8-F)oVA}bMT;fC^UVQO}6KCwk+6bQ#1uX2!QdH?L+hDr5&!niFsU$T?q zU;g1Vyu`ipL}sY~6`%rCfC^B7n-s9#3u~8wj8uRMP=Q|s?E6sQhBa{v^iKzZj{v|1 zX*aBWmH-w@0BhnHhzv}F3Jj{|h@nA8zGPiZ90P+cn!|_YlQkz4^{3kdX>d zflCD*V!N{b{{+7>|6h{0qXJamt`yLw>$?qJDSPYW<*e5h_!e$8KX5awor2))80hU7 g8*9gpUKDl3);O<;W1!QKcRG+i1Evd&3jDVMXN%qytN;K2 literal 0 HcmV?d00001 diff --git a/lib/apia/rack.rb b/lib/apia/rack.rb index b734afa..761617e 100644 --- a/lib/apia/rack.rb +++ b/lib/apia/rack.rb @@ -79,7 +79,7 @@ def handle_request(env, api_path) validate_api if development? - access_control_request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']&.upcase + access_control_request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']&.upcase if request_method == 'OPTIONS' route = find_route((access_control_request_method || request_method), api_path) if route.nil? Apia::Notifications.notify(:request_route_not_found, notify_hash)