Skip to content

Commit

Permalink
Implement separate tokens for control and status actions
Browse files Browse the repository at this point in the history
Prior to this commit ...

Authentication for control and status actions is implemented via the same token.

With this commit ...

Puma implements a 'status-token' limited to status actions in Puma::App::Status.

If 'control-token' is defined, it is required for any action.
If 'control-token' is undefined, no token is required for any action.
If 'status-token' is undefined, no status token is required for status actions.
  • Loading branch information
tkishel committed Dec 3, 2019
1 parent befe00a commit c0b2dd0
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 32 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,17 @@ $ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert&no_tlsv1=true'

### Control/Status Server

Puma has a built-in status and control app that can be used to query and control Puma.
Puma has a built-in control and status app that can be used to manage and query Puma.

```
$ puma --control-url tcp://127.0.0.1:9293 --control-token foo
```

Puma will start the control server on localhost port 9293. All requests to the control server will need to include control token (in this case, `token=foo`) as a query parameter. This allows for simple authentication. Check out [status.rb](https://github.com/puma/puma/blob/master/lib/puma/app/status.rb) to see what the status app has available.
Puma will start the control server on localhost port 9293.
All requests to the control server will need to include control token (in this case, `token=foo`) as a query parameter.
This allows for simple authentication.

Check out [status.rb](https://github.com/puma/puma/blob/master/lib/puma/app/status.rb) to see what actions are available.

You can also interact with the control server via `pumactl`. This command will restart Puma:

Expand All @@ -212,6 +216,9 @@ $ pumactl --control-url 'tcp://127.0.0.1:9293' --control-token foo restart

To see a list of `pumactl` options, use `pumactl --help`.

Note: To specify separate tokens for control and status actions, define both `control-token` and `status-token`.
For backwards compatibility, the control token is authorized to provide access to both control and status actions.

### Configuration File

You can also provide a configuration file with the `-C` (or `--config`) flag:
Expand Down
52 changes: 41 additions & 11 deletions lib/puma/app/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,70 +9,100 @@ module App
class Status
OK_STATUS = '{ "status": "ok" }'.freeze

def initialize(cli, token = nil)
def initialize(cli, control_auth_token = nil, status_auth_token = nil)
@cli = cli
@auth_token = token
@control_token = control_auth_token
@status_token = status_auth_token
end

def call(env)
unless authenticate(env)
return rack_response(403, 'Invalid auth token', 'text/plain')
end

case env['PATH_INFO']

# Actions requiring the control token, if specified.

when /\/stop$/
return invalid_token('stop') unless authenticate_control(env)
@cli.stop
rack_response(200, OK_STATUS)

when /\/halt$/
return invalid_token('halt') unless authenticate_control(env)
@cli.halt
rack_response(200, OK_STATUS)

when /\/restart$/
return invalid_token('restart') unless authenticate_control(env)
@cli.restart
rack_response(200, OK_STATUS)

when /\/phased-restart$/
return invalid_token('phased restart') unless authenticate_control(env)
if !@cli.phased_restart
rack_response(404, '{ "error": "phased restart not available" }')
rack_response(404, '{ "error": "phased_restart not available" }')
else
rack_response(200, OK_STATUS)
end

when /\/reload-worker-directory$/
return invalid_token('reload worker directory') unless authenticate_control(env)
if !@cli.send(:reload_worker_directory)
rack_response(404, '{ "error": "reload_worker_directory not available" }')
else
rack_response(200, OK_STATUS)
end

when /\/gc$/
return invalid_token('gc') unless authenticate_control(env)
GC.start
rack_response(200, OK_STATUS)

# Actions requiring the control or status tokens, if specified.

when /\/gc-stats$/
return invalid_token('access gc stats') unless authenticate_status(env)
rack_response(200, GC.stat.to_json)

when /\/stats$/
return invalid_token('access stats') unless authenticate_status(env)
rack_response(200, @cli.stats)

when /\/thread-backtraces$/
return invalid_token('access thread backtraces') unless authenticate_status(env)
backtraces = []
@cli.thread_status do |name, backtrace|
backtraces << { name: name, backtrace: backtrace }
end

rack_response(200, backtraces.to_json)

# Require the control token, if specified, when responding to unsupported actions.

else
return invalid_token unless authenticate_control(env)
rack_response 404, "Unsupported action", 'text/plain'
end
end

private

def authenticate(env)
return true unless @auth_token
env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
def authenticate(env, token)
return true unless token
env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{token}")
end

def authenticate_control(env)
authenticate(env, @control_token)
end

# The control token includes access to status actions.
# But when no status token is defined, no token is needed, so the control token is not checked.

def authenticate_status(env)
authenticate(env, @status_token) || authenticate(env, @control_token)
end

def invalid_token(action = '')
action = "to #{action}" if action
rack_response(403, "Invalid auth token#{action}", 'text/plain')
end

def rack_response(status, body, content_type='application/json')
Expand Down
7 changes: 6 additions & 1 deletion lib/puma/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ def setup_options

o.on "--control-token TOKEN",
"The token to use as authentication for the control server" do |arg|
@control_options[:auth_token] = arg
@control_options[:control_auth_token] = arg
end

o.on "--status-token TOKEN",
"The token to use as authentication for the status server" do |arg|
@control_options[:status_auth_token] = arg
end

o.on "-d", "--daemon", "Daemonize the server into the background" do
Expand Down
13 changes: 11 additions & 2 deletions lib/puma/control_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def initialize(argv, stdout=STDOUT, stderr=STDERR)
@pid = nil
@control_url = nil
@control_auth_token = nil
@status_auth_token = nil
@config_file = nil
@command = nil
@environment = ENV['RACK_ENV'] || ENV['RAILS_ENV']
Expand Down Expand Up @@ -57,6 +58,10 @@ def initialize(argv, stdout=STDOUT, stderr=STDERR)
@control_auth_token = arg
end

o.on "-S", "--status-token TOKEN", "The token to use as authentication for the status server" do |arg|
@status_auth_token = arg
end

o.on "-F", "--config-file PATH", "Puma config script" do |arg|
@config_file = arg
end
Expand Down Expand Up @@ -97,6 +102,7 @@ def initialize(argv, stdout=STDOUT, stderr=STDERR)
@state ||= config.options[:state]
@control_url ||= config.options[:control_url]
@control_auth_token ||= config.options[:control_auth_token]
@status_auth_token ||= config.options[:status_auth_token]
@pidfile ||= config.options[:pidfile]
end
end
Expand Down Expand Up @@ -130,6 +136,7 @@ def prepare_configuration

@control_url = sf.control_url
@control_auth_token = sf.control_auth_token
@status_auth_token = sf.status_auth_token
@pid = sf.pid
elsif @pidfile
# get pid from pid_file
Expand Down Expand Up @@ -161,7 +168,9 @@ def send_request
else
url = "/#{@command}"

if @control_auth_token
if PRINTABLE_COMMANDS.include?(@command) && @status_auth_token
url = url + "?token=#{@status_auth_token}"
elsif @control_auth_token
url = url + "?token=#{@control_auth_token}"
end

Expand All @@ -179,7 +188,7 @@ def send_request

(@http,@code,@message) = response.first.split(" ",3)

if @code == "403"
if @code == "401" || @code == "403"
raise "Unauthorized access to server (wrong auth token)"
elsif @code == "404"
raise "Command error: #{response.last}"
Expand Down
13 changes: 9 additions & 4 deletions lib/puma/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,18 @@ def activate_control_app(url="auto", opts={})
# symbols as option values.
#
# See: https://github.com/puma/puma/issues/1193#issuecomment-305995488
auth_token = 'none'
control_auth_token = 'none'
status_auth_token = 'none'
else
auth_token = opts[:auth_token]
auth_token ||= Configuration.random_token
control_auth_token = opts[:control_auth_token]
control_auth_token ||= Configuration.random_token
status_auth_token = opts[:status_auth_token]
status_auth_token ||= Configuration.random_token
end

@options[:control_auth_token] = auth_token
@options[:control_auth_token] = control_auth_token
@options[:status_auth_token] = status_auth_token

@options[:control_url_umask] = opts[:umask] if opts[:umask]
end

Expand Down
1 change: 1 addition & 0 deletions lib/puma/launcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def write_state
sf.pid = Process.pid
sf.control_url = @options[:control_url]
sf.control_auth_token = @options[:control_auth_token]
sf.status_auth_token = @options[:status_auth_token]

sf.save path
end
Expand Down
10 changes: 7 additions & 3 deletions lib/puma/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,15 @@ def start_control

uri = URI.parse str

if token = @options[:control_auth_token]
token = nil if token.empty? || token == 'none'
if control_auth_token = @options[:control_auth_token]
control_auth_token = nil if control_auth_token.empty? || control_auth_token == 'none'
end

app = Puma::App::Status.new @launcher, token
if status_auth_token = @options[:status_auth_token]
status_auth_token = nil if status_auth_token.empty? || status_auth_token == 'none'
end

app = Puma::App::Status.new @launcher, control_auth_token, status_auth_token

control = Puma::Server.new app, @launcher.events
control.min_threads = 0
Expand Down
2 changes: 1 addition & 1 deletion lib/puma/state_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def load(path)
@options = YAML.load File.read(path)
end

FIELDS = %w!control_url control_auth_token pid!
FIELDS = %w!control_url control_auth_token status_auth_token pid!

FIELDS.each do |f|
define_method f do
Expand Down

0 comments on commit c0b2dd0

Please sign in to comment.