class Purr::Server
This class implements a Rack-based server with socket hijacking and proxying to a remote TCP endpoint.
The remote TCP endpoint selection is implemented by passing a block to the class instantiation. If any kind of error happens during the hijacking process a 404 error is returned to the requester.
Public Class Methods
new(&block)
click to toggle source
@yield [env] The block responsible for the remote TCP endpoint selection @yieldparam env [Hash] The environment hash returned by the Rack middleware @yieldreturn [Array[String, Integer]] The host:port pair as a two element array @raise [ArgumentError] If the passed block takes other number of arguments than one
# File lib/purr/server.rb, line 15 def initialize(&block) raise ArgumentError, 'The method requires a block with a single argument' unless block && block.arity == 1 @remote = block @proxy = SurroGate.new @transmitter = Thread.new do loop do @proxy.select(1000) @proxy.each_ready do |left, right| begin right.write_nonblock(left.read_nonblock(4096)) rescue => ex # FIXME: env is not available here, logging is probably bad in this way # logger(env, :info, "Connection #{left} <-> #{right} closed due to #{ex}") cleanup(left, right) end end end end @transmitter.abort_on_exception = true end
Public Instance Methods
call(env)
click to toggle source
Method required by the Rack API
@see rack.github.io/
# File lib/purr/server.rb, line 43 def call(env) upgrade = parse_headers(env) # Return with a 404 error if the upgrade header is not present return not_found unless %i(websocket purr).include?(upgrade) host, port = @remote.call(env) # Return with a 404 error if no host:port pair was determined if host.nil? || port.nil? logger(env, :error, "No matching endpoint found for request incoming from #{env['REMOTE_ADDR']}") return not_found end # Hijack the HTTP socket from the Rack middleware http = env['rack.hijack'].call # Write a proper HTTP response http.write(http_response(upgrade)) # Open the remote TCP socket sock = TCPSocket.new(host, port) # Start proxying @proxy.push(http, sock) logger(env, :info, "Redirecting incoming request from #{env['REMOTE_ADDR']} to [#{host}]:#{port}") # Rack requires this line below return [200, {}, []] rescue => ex logger(env, :error, "#{ex.class} happened for #{env['REMOTE_ADDR']} trying to access #{host}:#{port}") cleanup(http, sock) return not_found # Return with a 404 error end
Private Instance Methods
cleanup(*sockets)
click to toggle source
# File lib/purr/server.rb, line 104 def cleanup(*sockets) # Omit `nil`s from the array sockets.compact! # Close the opened sockets and remove them from the proxy sockets.each { |sock| sock.close unless sock.closed? } @proxy.pop(*sockets) if sockets.length > 1 end
http_response(upgrade)
click to toggle source
# File lib/purr/server.rb, line 90 def http_response(upgrade) <<~HEREDOC.sub(/\n$/, "\n\n").gsub(/ {2,}/, '').gsub("\n", "\r\n") HTTP/1.1 101 Switching Protocols Upgrade: #{upgrade} Purr-Version: #{Purr::VERSION} Purr-Request: MEOW Connection: Upgrade HEREDOC end
logger(env, level, message)
click to toggle source
# File lib/purr/server.rb, line 112 def logger(env, level, message) # Do logging only if Rack::Logger is loaded as a middleware env['rack.logger'].send(level, message) if env['rack.logger'] end
not_found()
click to toggle source
# File lib/purr/server.rb, line 100 def not_found [404, { 'Content-Type' => 'text/plain' }, ['Not found!']] end
parse_headers(env)
click to toggle source
# File lib/purr/server.rb, line 76 def parse_headers(env) case true when env['HTTP_PURR_REQUEST'] != 'MEOW' logger(env, :error, "Invalid request from #{env['REMOTE_ADDR']}") when !SUPPORT.include?(env['HTTP_PURR_VERSION']) logger(env, :error, "Unsupported client from #{env['REMOTE_ADDR']}") when %w(websocket purr).include?(env['HTTP_UPGRADE']) logger(env, :info, "Upgrading to #{env['HTTP_UPGRADE']}") return env['HTTP_UPGRADE'].to_sym else logger(env, :error, "Invalid upgrade request from #{env['REMOTE_ADDR']}") end end