File: http.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (208 lines) | stat: -rw-r--r-- 6,687 bytes parent folder | download
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# frozen_string_literal: true

module HTTPX
  module Plugins
    module Proxy
      module HTTP
        class << self
          def extra_options(options)
            options.merge(supported_proxy_protocols: options.supported_proxy_protocols + %w[http])
          end
        end

        module InstanceMethods
          def with_proxy_basic_auth(opts)
            with(proxy: opts.merge(scheme: "basic"))
          end

          def with_proxy_digest_auth(opts)
            with(proxy: opts.merge(scheme: "digest"))
          end

          def with_proxy_ntlm_auth(opts)
            with(proxy: opts.merge(scheme: "ntlm"))
          end

          def fetch_response(request, selector, options)
            response = super

            if response &&
               response.is_a?(Response) &&
               response.status == 407 &&
               !request.headers.key?("proxy-authorization") &&
               response.headers.key?("proxy-authenticate") && options.proxy.can_authenticate?(response.headers["proxy-authenticate"])
              request.transition(:idle)
              request.headers["proxy-authorization"] =
                options.proxy.authenticate(request, response.headers["proxy-authenticate"])
              send_request(request, selector, options)
              return
            end

            response
          end
        end

        module ConnectionMethods
          def force_close(*)
            if @state == :connecting
              # proxy connect related requests should not be reenqueed
              @parser.reset!
              @inflight -= @parser.pending.size
              @parser.pending.clear
            end

            super
          end

          private

          def handle_transition(nextstate)
            return super unless @options.proxy && @options.proxy.uri.scheme == "http"

            case nextstate
            when :connecting
              return unless @state == :idle

              @io.connect
              return unless @io.connected?

              @parser || begin
                @parser = parser_type(@io.protocol).new(@write_buffer, @options.merge(max_concurrent_requests: 1))
                parser = @parser
                parser.extend(ProxyParser)
                parser.on(:response, &method(:__http_on_connect))
                parser.on(:close) do
                  next unless @parser

                  reset
                  disconnect
                end
                parser.on(:reset) do
                  if parser.empty?
                    reset
                  else
                    enqueue_pending_requests_from_parser(parser)

                    initial_state = @state

                    reset

                    if @pending.empty?
                      @parser = nil
                      next
                    end
                    # keep parser state around due to proxy auth protocol;
                    # intermediate authenticated request is already inside
                    # the parser
                    parser = nil

                    if initial_state == :connecting
                      parser = @parser
                      @parser.reset
                    end

                    idling

                    @parser = parser

                    transition(:connecting)
                  end
                end
                __http_proxy_connect(parser)
              end
              return if @state == :connected
            when :connected
              return unless @state == :idle || @state == :connecting

              case @state
              when :connecting
                parser = @parser
                @parser = nil
                parser.close
              when :idle
                @parser.callbacks.clear
                set_parser_callbacks(@parser)
              end
            end
            super
          end

          def __http_proxy_connect(parser)
            req = @pending.first
            if req && req.uri.scheme == "https"
              # if the first request after CONNECT is to an https address, it is assumed that
              # all requests in the queue are not only ALL HTTPS, but they also share the certificate,
              # and therefore, will share the connection.
              #
              connect_request = ConnectRequest.new(req.uri, @options)
              @inflight += 1
              parser.send(connect_request)
            else
              handle_transition(:connected)
            end
          end

          def __http_on_connect(request, response)
            @inflight -= 1
            if response.is_a?(Response) && response.status == 200
              req = @pending.first
              request_uri = req.uri
              @io = ProxySSL.new(@io, request_uri, @options)
              transition(:connected)
              throw(:called)
            elsif response.is_a?(Response) &&
                  response.status == 407 &&
                  !request.headers.key?("proxy-authorization") &&
                  @options.proxy.can_authenticate?(response.headers["proxy-authenticate"])

              request.transition(:idle)
              request.headers["proxy-authorization"] = @options.proxy.authenticate(request, response.headers["proxy-authenticate"])
              @parser.send(request)
              @inflight += 1
            else
              pending = @pending + @parser.pending
              while (req = pending.shift)
                response.finish!
                req.response = response
                req.emit(:response, response)
              end
              reset
            end
          end
        end

        module ProxyParser
          def join_headline(request)
            return super if request.verb == "CONNECT"

            "#{request.verb} #{request.uri} HTTP/#{@version.join(".")}"
          end

          def set_protocol_headers(request)
            extra_headers = super

            proxy_params = @options.proxy
            if proxy_params.scheme == "basic"
              # opt for basic auth
              extra_headers["proxy-authorization"] = proxy_params.authenticate(extra_headers)
            end
            extra_headers["proxy-connection"] = extra_headers.delete("connection") if extra_headers.key?("connection")
            extra_headers
          end
        end

        class ConnectRequest < Request
          def initialize(uri, options)
            super("CONNECT", uri, options)
            @headers.delete("accept")
          end

          def path
            "#{@uri.hostname}:#{@uri.port}"
          end
        end
      end
    end
    register_plugin :"proxy/http", Proxy::HTTP
  end
end