unxf.git  about / heads / tags
Rack middleware to remove/demangle X-Forwarded-* headers
blob 1f6a06625ecec3c577eaf0b98529944eacb531e4 2691 bytes (raw)
$ git show HEAD:lib/unxf.rb	# shows this blob on the CLI

 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
 
# -*- encoding: binary -*-
# -*- frozen_string_literal: true -*-
require 'rpatricia'

# Rack middleware to remove "HTTP_X_FORWARDED_FOR" in the Rack environment and
# replace "REMOTE_ADDR" with the value of the original client address.
class UnXF

  # local LAN addresses described in RFC 1918
  RFC_1918 = %w(10.0.0.0/8 172.16.0.0/12 192.168.0.0/16)

  # IPv4 localhost addresses (127.0.0.0/8)
  LOCALHOST = %w(127.0.0.0/8)

  # IPv6 localhost address (::1/128)
  LOCALHOST6 = %w(::1/128)

  # In your Rack config.ru:
  #
  #   use UnXF
  #
  # If you do not want to trust any hosts other than "0.6.6.6",
  # you may only specify one host to trust:
  #
  #   use UnXF, "0.6.6.6"
  #
  # If you want to trust "0.6.6.6" in addition to the default set of hosts:
  #
  #   use UnXF, [ :RFC_1918, :LOCALHOST, "0.6.6.6" ]
  #
  def initialize(app, trusted = [:RFC_1918, :LOCALHOST, :LOCALHOST6])
    @app = app
    @trusted = Patricia.new
    @trusted6 = Patricia.new(:AF_INET6)
    Array(trusted).each do |mask|
      mask = UnXF.const_get(mask) if Symbol === mask
      Array(mask).each do |prefix|
        (/:/ =~ prefix ? @trusted6 : @trusted).add(prefix, true)
      end
    end
  end

  # Rack entry point
  def call(env) # :nodoc:
    unxf!(env) || @app.call(env)
  end

  # returns +nil+ on success and a Rack response triplet on failure
  # This allows existing applications to use UnXF without putting it
  # into the middleware stack (to avoid increasing stack depth and GC time)
  def unxf!(env)
    if xff_str = env.delete('HTTP_X_FORWARDED_FOR'.freeze)
      env['unxf.for'] = xff_str
      xff = xff_str.split(/\s*,\s*/)
      addr = env['REMOTE_ADDR']
      begin
        while (/:/ =~ addr ? @trusted6 : @trusted).include?(addr) &&
              tmp = xff.pop
          addr = tmp
        end
      rescue ArgumentError
        return on_broken_addr(env, xff_str)
      end

      # it's stupid to have https at any point other than the first
      # proxy in the chain, so we don't support that
      if xff.empty?
        env['REMOTE_ADDR'] = addr
        if xfp = env.delete('HTTP_X_FORWARDED_PROTO'.freeze)
          env['unxf.proto'] = xfp
          /\Ahttps\b/ =~ xfp and env['rack.url_scheme'] = 'https'.freeze
        end
      else
        return on_untrusted_addr(env, xff_str)
      end
    end
    nil
  end

  # Our default action on a broken address is to just fall back to calling
  # the app without modifying the env
  def on_broken_addr(env, xff_str)
    @app.call(env)
  end

  # Our default action on an untrusted address is to just fall back to calling
  # the app without modifying the env
  def on_untrusted_addr(env, xff_str)
    @app.call(env)
  end
end

git clone https://yhbt.net/unxf.git