yahns Ruby server user/dev discussion
 help / color / mirror / code / Atom feed
blob 7144903ebad71c5e8a6f52817df6f8e935dd7188 5030 bytes (raw)
name: lib/yahns/rack_proxy.rb 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
 
# -*- encoding: binary -*-
# Copyright (C) 2017 all contributors <yahns-public@yhbt.net>
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
# frozen_string_literal: true
require_relative 'rack'
require_relative 'proxy_pass'

# Basically, a lazy way to setup ProxyPass to hand off some (or all)
# requests to any HTTP server backend (e.g. varnish, etc)
class Yahns::RackProxy < Yahns::Rack # :nodoc:

  # the key is the destination returned by the top-level config.ru
  # and the value is a splattable array for spawning another process
  # via Process.exec
  # {
  #   # [ key, backend URL, ]  => %w(splattable array for Process.exec),
  #   [:pass, 'http://127.0.0.1:9292/' ] => %w(rackup /path/to/config.ru)
  #   [:lsock, 'unix:/path/to/sock' ] => %w(bleh -l /path/to/sock ...)
  #
  #   # Users of Ruby 2.3+ can shorten their config when
  #   # running systemd-aware daemons which will bind to
  #   # a random TCP port:
  #   :pri => %w(blah -c conf.rb config.ru),
  #   :alt => %w(blah -c /path/to/alt.conf.rb alt.ru),
  #   :psgi => %w(blah foo.psgi),
  #   ...
  # }

  # By default, proxy all requests by using the :pass return value
  # Users can selectively process requests for non-buggy code in
  # the core yahns processes.
  PROXY_ALL = lambda { |env| :pass } # :nodoc:
  attr_reader :submasters # :nodoc: see http_context.rb /submasters

  # every declaration of this in yahns_config is unique:
  def self.instance_key(*args)
    args.object_id
  end

  def initialize(mapping = { :pass => %w(true) }, ru = PROXY_ALL, opts = {})
    sd_env = {
     'LISTEN_FDS' => '1',
     'LISTEN_PID' => lambda { "#$$" }
    }
    @submasters = []
    case mapping
    when Hash # multiple HTTP backends running different commands
      # nothing to do  { key: splattable array for Process.spawn }
    when Array # only one backend
      mapping = { :pass => mapping }
    else
      raise ArgumentError, "#{mapping.inspect} must be an Array or Hash"
    end

    @proxy_pass_map = {}
    mapping.each do |key, cmd|
      case key
      when Array
        key, addr, ppopts = key
        ppopts ||= {}
      when Symbol # OK
        ppopts = {}
      else
        raise ArgumentError, "#{key.inspect} is not a symbol"
      end
      Array === cmd or raise ArgumentError,
                "#{cmd.inspect} must be a splattable array for Process.exec"
      @proxy_pass_map[key] and raise ArgumentError,
                "#{key.inspect} may not be repeated in mapping"

      cmd = cmd.dup
      if addr
        env = {}
        rdr = {}
      else
        if RUBY_VERSION.to_f < 2.3 && @submasters.empty? # only warn once
           warn "Ruby < 2.3 may crash when emulating systemd to pass FDs\n",
" http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/69895\n"
        end

        # nope, no UNIXServer support, maybe not worth it to deal
        # with FS perms in containers.
        # TODO: try TCP Fast Open (Linux)
        srv = random_tcp_listener(ppopts)
        addr = srv.addr
        addr = "http://#{addr[3]}:#{addr[1]}/"
        env = sd_env
        rdr = { 3 => srv }
      end

      # never pass YAHNS_FD to children, they do not inherit what we use
      # for SIGUSR2 upgrades
      env['YAHNS_FD'] = nil
      case cmd[0]
      when Hash
        cmd[0] = cmd[0].merge(env)
      else
        cmd.unshift(env)
      end

      rdr[:close_others] = true
      case cmd[-1]
      when Hash
        cmd[-1] = cmd[-1].merge(rdr)
      else
        cmd << rdr
      end

      @submasters << Yahns::Submaster.new(key, cmd)
      @proxy_pass_map[key] = Yahns::ProxyPass.new(addr, ppopts)
    end
    super(ru, opts) # Yahns::Rack#initialize
  end

  def build_app!
    super # Yahns::Rack#build_app!
    proxy_app = @app

    # wrap the (possibly-)user-supplied app
    @app = lambda do |env|
      res = proxy_app.call(env)

      # standard Rack responses may be handled in yahns proper:
      Array === res and return res

      # the response is :pass or another Symbol, not a proper Rack response!
      # shove the env over to the appropriate Yahns::ProxyPass which
      # talks to a backend HTTP process:
      ppass = @proxy_pass_map[res] and return ppass.call(env)

      # oops, user screwed up :<
      logger = env['rack.logger'] and
        logger.error("bad response from user-supplied proxy: #{res.inspect}")

      [ 500, [ %w(Content-Type text/plain) ], [] ]
    end
  end

  def random_tcp_listener(opts) # TODO: should we support options?
    srv = TCPServer.new('127.0.0.1', 0) # 0: bind random port
    srv.close_on_exec = true
    srv.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
    srv.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1)

    # Deferring accepts slows down core yahns, but it's useful for
    # less-sophisticated upstream (backend) servers:
    Socket.const_defined?(:TCP_DEFER_ACCEPT) and
      srv.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, 1)

    srv.listen(1024)
    srv
  end
end

# register ourselves
Yahns::Config::APP_CLASS[:rack_proxy] = Yahns::RackProxy

debug log:

solving 7144903 ...
found 7144903 in https://yhbt.net/yahns-public/20170407034215.25377-1-yahns-public@yhbt.net/

applying [1/1] https://yhbt.net/yahns-public/20170407034215.25377-1-yahns-public@yhbt.net/
diff --git a/lib/yahns/rack_proxy.rb b/lib/yahns/rack_proxy.rb
new file mode 100644
index 0000000..7144903

Checking patch lib/yahns/rack_proxy.rb...
Applied patch lib/yahns/rack_proxy.rb cleanly.

index at:
100644 7144903ebad71c5e8a6f52817df6f8e935dd7188	lib/yahns/rack_proxy.rb

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this public inbox

	https://yhbt.net/yahns.git/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).