Skip to content

Commit

Permalink
Add dsl method supported_http_methods (#3106)
Browse files Browse the repository at this point in the history
* Add config method `supported_http_methods`, calling it overrides SUPPORTED_HTTP_METHODS

Co-authored-by: francois-ferrandis <francois@ferrandis.cool>

* Define Const::IANA_HTTP_METHODS, allow all methods via DSL#supported_http_methods

Co-authored-by: Patrik Ragnarsson <patrik@starkast.net>

* dsl.rb - fix typo - elemnts -> elements

* Updates - Use `;any` to allow any request method  to be passed to app

---------

Co-authored-by: francois-ferrandis <francois@ferrandis.cool>
Co-authored-by: Patrik Ragnarsson <patrik@starkast.net>
  • Loading branch information
3 people committed May 29, 2023
1 parent a434005 commit dfd33df
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 6 deletions.
6 changes: 5 additions & 1 deletion 6.0-Upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ Check the following list to see if you're depending on any of these behaviors:
1. We've removed the following constants: `Puma::StateFile::FIELDS`, `Puma::CLI::KEYS_NOT_TO_PERSIST_IN_STATE` and `Puma::Launcher::KEYS_NOT_TO_PERSIST_IN_STATE`, and `Puma::ControlCLI::COMMANDS`.
1. We no longer support Ruby 2.2, 2.3, or JRuby on Java 1.7 or below.
1. The behavior of `remote_addr` has changed. When using the set_remote_address header: "header_name" functionality, if the header is not passed, REMOTE_ADDR is now set to the physical peeraddr instead of always being set to 127.0.0.1. When an error occurs preventing the physical peeraddr from being fetched, REMOTE_ADDR is now set to the unspecified source address ('0.0.0.0') instead of to '127.0.0.1'
1. Previously, Puma supported anything as an HTTP method and passed it to the app. We now only accept the 8 HTTP methods defined in RFC 9110.
1. Previously, Puma supported anything as an HTTP method and passed it to the app. We now only accept the following 8 HTTP methods, based on [RFC 9110, section 9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1). The [IANA HTTP Method Registry](https://www.iana.org/assignments/http-methods/http-methods.xhtml) contains a full list of HTTP methods.
```
HEAD GET POST PUT DELETE OPTIONS TRACE PATCH
```
As of Puma 6.2, these can be overridden by `supported_http_methods` in your config file, see `Puma::DSL#supported_http_methods`.

Then, update your Gemfile:

Expand Down
48 changes: 48 additions & 0 deletions lib/puma/const.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,55 @@ module Const

REQUEST_METHOD = "REQUEST_METHOD"
HEAD = "HEAD"

# based on https://www.rfc-editor.org/rfc/rfc9110.html#name-overview,
# with CONNECT removed, and PATCH added
SUPPORTED_HTTP_METHODS = %w[HEAD GET POST PUT DELETE OPTIONS TRACE PATCH].freeze

# list from https://www.iana.org/assignments/http-methods/http-methods.xhtml
# as of 04-May-23
IANA_HTTP_METHODS = %w[
ACL
BASELINE-CONTROL
BIND
CHECKIN
CHECKOUT
CONNECT
COPY
DELETE
GET
HEAD
LABEL
LINK
LOCK
MERGE
MKACTIVITY
MKCALENDAR
MKCOL
MKREDIRECTREF
MKWORKSPACE
MOVE
OPTIONS
ORDERPATCH
PATCH
POST
PRI
PROPFIND
PROPPATCH
PUT
REBIND
REPORT
SEARCH
TRACE
UNBIND
UNCHECKOUT
UNLINK
UNLOCK
UPDATE
UPDATEREDIRECTREF
VERSION-CONTROL
].freeze

# ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32)
LINE_END = "\r\n"
REMOTE_ADDR = "REMOTE_ADDR"
Expand Down
32 changes: 32 additions & 0 deletions lib/puma/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,38 @@ def http_content_length_limit(limit)
@options[:http_content_length_limit] = limit
end

# Supported http methods, which will replace `Puma::Const::SUPPORTED_HTTP_METHODS`.
# The value of `:any` will allows all methods, otherwise, the value must be
# an array of strings. Note that methods are all uppercase.
#
# `Puma::Const::SUPPORTED_HTTP_METHODS` is conservative, if you want a
# complete set of methods, the methods defined by the
# [IANA Method Registry](https://www.iana.org/assignments/http-methods/http-methods.xhtml)
# are pre-defined as the constant `Puma::Const::IANA_HTTP_METHODS`.
#
# @note If the `methods` value is `:any`, no method check with be performed,
# similar to Puma v5 and earlier.
#
# @example Adds 'PROPFIND' to existing supported methods
# supported_http_methods(Puma::Const::SUPPORTED_HTTP_METHODS + ['PROPFIND'])
# @example Restricts methods to the array elements
# supported_http_methods %w[HEAD GET POST PUT DELETE OPTIONS PROPFIND]
# @example Restricts methods to the methods in the IANA Registry
# supported_http_methods Puma::Const::IANA_HTTP_METHODS
# @example Allows any method
# supported_http_methods :any
#
def supported_http_methods(methods)
if methods == :any
@options[:supported_http_methods] = :any
elsif Array === methods && methods == (ary = methods.grep(String).uniq) &&
!ary.empty?
@options[:supported_http_methods] = ary
else
raise "supported_http_methods must be ':any' or a unique array of strings"
end
end

private

# To avoid adding cert_pem and key_pem as URI params, we store them on the
Expand Down
2 changes: 1 addition & 1 deletion lib/puma/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def handle_request(client, requests)
env[RACK_AFTER_REPLY] ||= []

begin
if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
status, headers, app_body = @thread_pool.with_force_shutdown do
@app.call(env)
end
Expand Down
12 changes: 12 additions & 0 deletions lib/puma/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ def initialize(app, events = nil, options = {})
@io_selector_backend = @options[:io_selector_backend]
@http_content_length_limit = @options[:http_content_length_limit]

# make this a hash, since we prefer `key?` over `include?`
@supported_http_methods =
if @options[:supported_http_methods] == :any
:any
else
if (ary = @options[:supported_http_methods])
ary
else
SUPPORTED_HTTP_METHODS
end.sort.product([nil]).to_h.freeze
end

temp = !!(@options[:environment] =~ /\A(development|test)\z/)
@leak_stack_on_error = @options[:environment] ? temp : true

Expand Down
37 changes: 37 additions & 0 deletions test/test_puma_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1642,6 +1642,43 @@ def test_form_data_encoding_windows
assert_equal out_r.read.bytesize, req_body.bytesize
end

def test_supported_http_methods_match
server_run(supported_http_methods: ['PROPFIND', 'PROPPATCH']) do |env|
body = [env['REQUEST_METHOD']]
[200, {}, body]
end
resp = send_http_and_read "PROPFIND / HTTP/1.0\r\n\r\n"
assert_match 'PROPFIND', resp
end

def test_supported_http_methods_no_match
server_run(supported_http_methods: ['PROPFIND', 'PROPPATCH']) do |env|
body = [env['REQUEST_METHOD']]
[200, {}, body]
end
resp = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
assert_match 'Not Implemented', resp
end

def test_supported_http_methods_accept_all
server_run(supported_http_methods: :any) do |env|
body = [env['REQUEST_METHOD']]
[200, {}, body]
end
resp = send_http_and_read "YOUR_SPECIAL_METHOD / HTTP/1.0\r\n\r\n"
assert_match 'YOUR_SPECIAL_METHOD', resp
end

def test_supported_http_methods_empty
server_run(supported_http_methods: []) do |env|
body = [env['REQUEST_METHOD']]
[200, {}, body]
end
resp = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
assert_match(/\AHTTP\/1\.0 501 Not Implemented/, resp)
end


def spawn_cmd(env = {}, cmd)
opts = {}

Expand Down
8 changes: 4 additions & 4 deletions test/test_web_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ def test_file_streamed_request
socket.close
end

def test_unsupported_method
socket = do_test("CONNECT www.zedshaw.com:443 HTTP/1.1\r\nConnection: close\r\n\r\n", 100)
def test_supported_http_method
socket = do_test("PATCH www.zedshaw.com:443 HTTP/1.1\r\nConnection: close\r\n\r\n", 100)
response = socket.read
assert_match "Not Implemented", response
assert_match "hello", response
socket.close
end

def test_nonexistent_method
def test_nonexistent_http_method
socket = do_test("FOOBARBAZ www.zedshaw.com:443 HTTP/1.1\r\nConnection: close\r\n\r\n", 100)
response = socket.read
assert_match "Not Implemented", response
Expand Down

0 comments on commit dfd33df

Please sign in to comment.