Skip to content

Commit

Permalink
Binder refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
MSP-Greg authored and nateberkopec committed Sep 26, 2019
1 parent 9402051 commit cc03819
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 135 deletions.
3 changes: 2 additions & 1 deletion History.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Your feature goes here (#Github Number)

* Bugfixes
* Your bugfix goes here (#Github Number)
* Fix an issue where SSL binds to port 0 were not reported correctly (#1989)
* Fix an issue where binding to one protocol and then another would raise an exception (#1994, #1986)


## 4.2.0 / 2019-09-23
Expand Down
181 changes: 66 additions & 115 deletions lib/puma/binder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

require 'uri'
require 'socket'
require 'forwardable'

require 'puma/const'
require 'puma/util'
require 'puma/binding'

module Puma
class Binder
Expand All @@ -14,42 +16,47 @@ class Binder

def initialize(events)
@events = events
@listeners = []
@bindings = []
@inherited_fds = {}
@activated_sockets = {}
@unix_paths = []

@proto_env = {
"rack.version".freeze => RACK_VERSION,
"rack.errors".freeze => events.stderr,
"rack.multithread".freeze => true,
"rack.multiprocess".freeze => false,
"rack.run_once".freeze => false,
"SCRIPT_NAME".freeze => ENV['SCRIPT_NAME'] || "",

# I'd like to set a default CONTENT_TYPE here but some things
# depend on their not being a default set and inferring
# it from the content. And so if i set it here, it won't
# infer properly.

"QUERY_STRING".freeze => "",
SERVER_PROTOCOL => HTTP_11,
SERVER_SOFTWARE => PUMA_SERVER_STRING,
GATEWAY_INTERFACE => CGI_VER
}

@envs = {}
@ios = []
end

attr_reader :ios
PROTO_ENV = {
"rack.version".freeze => RACK_VERSION,
"rack.multithread".freeze => true,
"rack.multiprocess".freeze => false,
"rack.run_once".freeze => false,
"SCRIPT_NAME".freeze => ENV['SCRIPT_NAME'] || "",

def env(sock)
@envs.fetch(sock, @proto_env)
end
# I'd like to set a default CONTENT_TYPE here but some things
# depend on their not being a default set and inferring
# it from the content. And so if i set it here, it won't
# infer properly.

"QUERY_STRING".freeze => "",
SERVER_PROTOCOL => HTTP_11,
SERVER_SOFTWARE => PUMA_SERVER_STRING,
GATEWAY_INTERFACE => CGI_VER
}

attr_reader :bindings

def close
@ios.each { |i| i.close }
@bindings.each(&:close)
end

def bound_servers
@bindings.map(&:server)
end

def env_for_server(server)
@env_cache ||= Hash.new do |h, key|
binding_env = @bindings.find { |b| b.server == key }.env
env = PROTO_ENV.dup
env.merge!(binding_env)["rack.errors".freeze] = @events.stderr
h[key] = env
end
@env_cache[server]
end

def import_from_env
Expand Down Expand Up @@ -91,39 +98,27 @@ def parse(binds, logger)
case uri.scheme
when "tcp"
if fd = @inherited_fds.delete(str)
io = inherit_tcp_listener uri.host, uri.port, fd
inherit_tcp_listener uri.host, uri.port, fd
logger.log "* Inherited #{str}"
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
io = inherit_tcp_listener uri.host, uri.port, sock
inherit_tcp_listener uri.host, uri.port, sock
logger.log "* Activated #{str}"
else
params = Util.parse_query uri.query

opt = params.key?('low_latency')
bak = params.fetch('backlog', 1024).to_i

io = add_tcp_listener uri.host, uri.port, opt, bak

@ios.each do |i|
addr = if i.local_address.ipv6?
"[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
else
i.local_address.ip_unpack.join(':')
end

logger.log "* Listening on tcp://#{addr}"
end
add_tcp_listener uri.host, uri.port, opt, bak
end

@listeners << [str, io] if io
when "unix"
path = "#{uri.host}#{uri.path}".gsub("%20", " ")

if fd = @inherited_fds.delete(str)
io = inherit_unix_listener path, fd
inherit_unix_listener path, fd
logger.log "* Inherited #{str}"
elsif sock = @activated_sockets.delete([ :unix, path ])
io = inherit_unix_listener path, sock
inherit_unix_listener path, sock
logger.log "* Activated #{str}"
else
umask = nil
Expand All @@ -146,11 +141,8 @@ def parse(binds, logger)
end
end

io = add_unix_listener path, umask, mode, backlog
logger.log "* Listening on #{str}"
add_unix_listener path, umask, mode, backlog
end

@listeners << [str, io]
when "ssl"
params = Util.parse_query uri.query
require 'puma/minissl'
Expand Down Expand Up @@ -214,21 +206,21 @@ def parse(binds, logger)

if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
io = inherit_ssl_listener fd, ctx
inherit_ssl_listener fd, ctx
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
io = inherit_ssl_listener sock, ctx
inherit_ssl_listener sock, ctx
logger.log "* Activated #{str}"
else
io = add_ssl_listener uri.host, uri.port, ctx
logger.log "* Listening on #{str}"
add_ssl_listener uri.host, uri.port, ctx
end

@listeners << [str, io] if io
else
logger.error "Invalid URI: #{str}"
end
end

#TODO: say Inherited/Activated instead of listening as appropriate
@bindings.each { |b| logger.log "* Listening on #{b}" }

# If we inherited fds but didn't use them (because of a
# configuration change), then be sure to close them.
@inherited_fds.each do |str, fd|
Expand Down Expand Up @@ -289,21 +281,14 @@ def add_tcp_listener(host, port, optimize_for_latency=true, backlog=1024)
s.listen backlog
@connected_port = s.addr[1]

@ios << s
s
@bindings << Binding.new(s)
end

attr_reader :connected_port

def inherit_tcp_listener(host, port, fd)
if fd.kind_of? TCPServer
s = fd
else
s = TCPServer.for_fd(fd)
end

@ios << s
s
s = fd.kind_of?(TCPServer) ? fd : TCPServer.for_fd(fd)
@bindings << Binding.new(s)
end

def add_ssl_listener(host, port, ctx,
Expand All @@ -327,41 +312,24 @@ def add_ssl_listener(host, port, ctx,
s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
s.listen backlog


ssl = MiniSSL::Server.new s, ctx
env = @proto_env.dup
env[HTTPS_KEY] = HTTPS
@envs[ssl] = env

@ios << ssl
s
@bindings << Binding.new(ssl)
end

def inherit_ssl_listener(fd, ctx)
require 'puma/minissl'
MiniSSL.check

if fd.kind_of? TCPServer
s = fd
else
s = TCPServer.for_fd(fd)
end
s = fd.kind_of?(TCPServer) ? fd : TCPServer.for_fd(fd)
ssl = MiniSSL::Server.new(s, ctx)

env = @proto_env.dup
env[HTTPS_KEY] = HTTPS
@envs[ssl] = env

@ios << ssl

s
@bindings << Binding.new(ssl)
end

# Tell the server to listen on +path+ as a UNIX domain socket.
#
def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
@unix_paths << path

# Let anyone connect by default
umask ||= 0

Expand All @@ -381,57 +349,40 @@ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)

s = UNIXServer.new(path)
s.listen backlog
@ios << s
@bindings << Binding.new(s)
ensure
File.umask old_mask
end

if mode
File.chmod mode, path
end

env = @proto_env.dup
env[REMOTE_ADDR] = "127.0.0.1"
@envs[s] = env

s
end

def inherit_unix_listener(path, fd)
@unix_paths << path

if fd.kind_of? TCPServer
s = fd
else
s = UNIXServer.for_fd fd
end
@ios << s

env = @proto_env.dup
env[REMOTE_ADDR] = "127.0.0.1"
@envs[s] = env
# TODO: bug? Should this read kind_of? UNIXServer?
s = fd.kind_of?(TCPServer) ? fd : UNIXServer.for_fd(fd)

s
@bindings << Binding.new(s)
end

def close_listeners
@listeners.each do |l, io|
io.close
uri = URI.parse(l)
next unless uri.scheme == 'unix'
File.unlink("#{uri.host}#{uri.path}")
@bindings.each do |binder|
binder.close
binder.unlink_fd if binder.unix?
end
end

def close_unix_paths
@unix_paths.each { |up| File.unlink(up) if File.exist? up }
@bindings.select(&:unix?).each(&:unlink_fd)
end

def redirects_for_restart
redirects = {:close_others => true}
@listeners.each_with_index do |(l, io), i|
ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
redirects[io.to_i] = io.to_i
@bindings.each_with_index do |binder, i|
server_integer = binder.server.to_i
ENV["PUMA_INHERIT_#{i}"] = "#{server_integer}:#{binder}"
redirects[server_integer] = server_integer
end
redirects
end
Expand Down
67 changes: 67 additions & 0 deletions lib/puma/binding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module Puma
class Binding
include Puma::Const
extend Forwardable

def initialize(server)
@server = server
@path = server.path if unix?
end

attr_reader :server

def to_s
"#{protocol}://#{addrinfo_to_uri}"
end

def protocol
if ssl?
"ssl"
elsif tcp?
"tcp"
elsif unix?
"unix"
end
end

def tcp?
TCPServer === server
end

def ssl?
defined?(MiniSSL::Server) && MiniSSL::Server === server
end

def unix?
defined?(UNIXServer) && UNIXServer === server
end

def unlink_fd
File.unlink(@path) if File.exist?(@path)
end

def env
if unix?
{ REMOTE_ADDR => "127.0.0.1" }
elsif ssl?
{ HTTPS_KEY => HTTPS }
else
{}
end
end

def_delegators :@server, :close, :local_address, :no_tlsv1, :no_tlsv1_1

private

def addrinfo_to_uri
if local_address.ipv6?
"[#{local_address.ip_address}]:#{local_address.ip_port}"
elsif local_address.ipv4?
local_address.ip_unpack.join(':')
elsif local_address.unix?
local_address.unix_path
end
end
end
end

0 comments on commit cc03819

Please sign in to comment.