Class: Arachni::HTTP::ProxyServer::Connection

Inherits:
Reactor::Connection
  • Object
show all
Includes:
UI::Output
Defined in:
lib/arachni/http/proxy_server/connection.rb

Direct Known Subclasses

SSLInterceptor

Constant Summary collapse

SKIP_HEADERS =
%w(transfer-encoding connection proxy-connection keep-alive
content-encoding te trailers accept-encoding accept-ranges vary)

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from UI::Output

#debug?, #debug_level_1?, #debug_level_2?, #debug_level_3?, #debug_level_4?, #debug_off, #debug_on, #disable_only_positives, #included, #mute, #muted?, #only_positives, #only_positives?, #print_bad, #print_debug, #print_debug_backtrace, #print_debug_level_1, #print_debug_level_2, #print_debug_level_3, #print_debug_level_4, #print_error, #print_error_backtrace, #print_exception, #print_info, #print_line, #print_ok, #print_status, #print_verbose, #reroute_to_file, #reroute_to_file?, reset_output_options, #unmute, #verbose?, #verbose_on

Constructor Details

#initialize(options = {}) ⇒ Connection

Returns a new instance of Connection.



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
# File 'lib/arachni/http/proxy_server/connection.rb', line 23

def initialize( options = {} )
    @options = options
    @parent  = options[:parent]

    @body     = ''
    @parser   = ::HTTP::Parser.new
    @raw_request = ''

    @parser.on_message_begin = proc do
        if @reused
            print_debug_level_3 "Reusing connection: #{object_id}"
        else
            print_debug_level_3 "Starting new connection: #{object_id}"
        end

        @reused = true

        print_debug_level_3 'Incoming request.'
        @parent.mark_connection_active self
    end

    @parser.on_body = proc do |chunk|
        print_debug_level_3 "Got #{chunk.size} bytes."
        @body << chunk
    end

    @parser.on_message_complete = proc do
        method  = @parser.http_method.downcase.to_sym
        headers = cleanup_request_headers( @parser.headers )

        print_debug_level_3 "Request received: #{@parser.http_method} #{@parser.request_url}"

        if headers['upgrade']
            handle_upgrade( headers )
            next
        end

        if method == :connect
            handle_connect( headers )
            next
        end

        if !@parent.has_available_request_tokens?
            print_debug_level_3 'Waiting for a request token.'
        end

        @parent.get_request_token do |token|
            print_debug_level_3 "Got request token ##{token}."

            if closed?
                print_debug_level_3 'Connection closed while waiting for a request token.'
                @parent.return_request_token( token )
                print_debug_level_3 "Returned request token ##{token}."

                next
            end

            Thread.new do
                begin
                    @request = Arachni::HTTP::Request.new(
                        http_opts.merge(
                            url:     sanitize_url( @parser.request_url, headers ),
                            method:  method,
                            body:    @body,
                            headers: Arachni::HTTP::Client.headers.to_h.merge( headers )
                        )
                    )

                    handle_request( @request )
                rescue => e
                    close e
                ensure
                    @parent.return_request_token( token )
                    print_debug_level_3 "Returned request token ##{token}."
                end
            end
        end
    end
end

Instance Attribute Details

#parentObject (readonly)

Returns the value of attribute parent.



20
21
22
# File 'lib/arachni/http/proxy_server/connection.rb', line 20

def parent
  @parent
end

#requestObject (readonly)

Returns the value of attribute request.



21
22
23
# File 'lib/arachni/http/proxy_server/connection.rb', line 21

def request
  @request
end

Instance Method Details

#cleanup_request_headers(headers) ⇒ Object



287
288
289
290
291
292
293
294
295
# File 'lib/arachni/http/proxy_server/connection.rb', line 287

def cleanup_request_headers( headers )
    headers = Arachni::HTTP::Headers.new( headers )

    SKIP_HEADERS.each do |name|
        headers.delete name
    end

    headers.to_h
end

#cleanup_response_headers(headers) ⇒ Object



297
298
299
300
301
302
# File 'lib/arachni/http/proxy_server/connection.rb', line 297

def cleanup_response_headers( headers )
    SKIP_HEADERS.each do |name|
        headers.delete name
    end
    headers
end

#handle_connect(headers) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
# File 'lib/arachni/http/proxy_server/connection.rb', line 116

def handle_connect( headers )
    print_debug_level_3 'Preparing to intercept.'

    host = (headers['Host'] || @parser.request_url).split( ':', 2 ).first
    start_interceptor( host )

    # This is our last HTTP message, from this point on we'll only be
    # tunnelling to the interceptor.
    @last_http = true
    write "HTTP/#{http_version} 200\r\n\r\n"
end

#handle_request(request) ⇒ Object



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
# File 'lib/arachni/http/proxy_server/connection.rb', line 128

def handle_request( request )
    print_debug_level_3 'Processing request.'

    if @options[:request_handler]
        print_debug_level_3 "-- Has special handler: #{@options[:request_handler]}"

        # Provisional empty, response in case the request_handler wants us to
        # skip performing the request.
        response = Response.new( url: request.url )
        response.request = request

        # If the handler returns false then don't perform the HTTP request.
        if @options[:request_handler].call( request, response )
            print_debug_level_3 '-- Handler approves, running...'

            response = request.run

            print_debug_level_3 "-- ...completed in #{response.time}: #{response.status_line}"
        else
            print_debug_level_3 '-- Handler did not approve, will not run.'
        end
    else
        print_debug_level_3 '-- Running...'

        response = request.run

        print_debug_level_3 "-- ...completed in #{response.time}: #{response.status_line}"
    end

    print_debug_level_3 'Processed request.'

    reactor.schedule { handle_response( response ) }
end

#handle_response(response) ⇒ Object



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
209
210
211
212
213
# File 'lib/arachni/http/proxy_server/connection.rb', line 166

def handle_response( response )
    print_debug_level_3 'Preparing response.'

    # Connection was rudely closed before we had a chance to respond,
    # don't bother proceeding.
    if closed?
        print_debug_level_3 '-- Connection closed, will not respond.'
        return
    end

    if @options[:response_handler]
        print_debug_level_3 "-- Has special handler: #{@options[:response_handler]}"
        @options[:response_handler].call( response.request, response )
    end

    code = response.code
    if response.code == 0
        code = 504
    end

    res = "HTTP/#{http_version} #{code}\r\n"

    headers = cleanup_response_headers( response.headers )
    headers['Content-Length'] = response.body.bytesize

    if response.text? && headers.content_type
        headers['Content-Type'] =
            "#{headers.content_type.split( ';' ).first}; charset=utf-8"
    end

    headers.each do |k, v|
        if v.is_a?( Array )
            v.flatten.each do |h|
                res << "#{k}: #{h.gsub(/[\n\r]/, '')}\r\n"
            end

            next
        end

        res << "#{k}: #{v}\r\n"
    end

    res << "\r\n"

    print_debug_level_3 "Sending response for: #{@request.url}"

    write (res << response.body)
end

#handle_upgrade(headers) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/arachni/http/proxy_server/connection.rb', line 103

def handle_upgrade( headers )
    print_debug_level_3 'Preparing to upgrade.'

    host = (headers['Host'] || @parser.request_url).split( ':', 2 ).first

    @tunnel = reactor.connect( host, 80, Tunnel, @options.merge( client: self ) )

    # This is our last HTTP message, from this point on we'll only be
    # tunnelling to the origin server.
    @last_http = true
    @tunnel.write @raw_request
end

#http_opts(options = {}) ⇒ Object

Parameters:

  • options (Hash) (defaults to: {})

    Merges the given HTTP options with some default ones.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/arachni/http/proxy_server/connection.rb', line 319

def http_opts( options = {} )
    options.merge(
        performer:         self,

        # Don't follow redirects, the client should handle this.
        follow_location:   false,

        # Set the HTTP request timeout.
        timeout:           @options[:timeout],

        # Update the framework-wide cookie-jar with the transmitted cookies.
        update_cookies:    true,

        # We perform the request in blocking mode, parallelism is up to the
        # proxy client.
        mode:              :sync,

        # Don't limit the response size when using the proxy.
        response_max_size: -1
    )
end

#http_versionObject



162
163
164
# File 'lib/arachni/http/proxy_server/connection.rb', line 162

def http_version
    @parser.http_version.join('.')
end

#on_close(reason = nil) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/arachni/http/proxy_server/connection.rb', line 215

def on_close( reason = nil )
    print_debug_level_3 "Closed because: [#{reason.class}] #{reason}"

    @parent.mark_connection_inactive self

    if @ssl_interceptor
        @ssl_interceptor.close( reason )
        @ssl_interceptor = nil
    end

    if @tunnel
        @tunnel.close_without_callback
        @tunnel = nil
    end
end

#on_flushObject



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/arachni/http/proxy_server/connection.rb', line 231

def on_flush
    if !@tunnel || @last_http

        if @last_http
            print_debug_level_3 'Last response sent, switching to tunnel.'
        elsif @request
            print_debug_level_3 "Response sent for: #{@request.url}"
        end

        @last_http = false
    end

    @body        = ''
    @raw_request = ''
    @request     = nil

    @parser.reset!
    @parent.mark_connection_inactive self
end

#on_read(data) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/arachni/http/proxy_server/connection.rb', line 256

def on_read( data )
    # We need this in case we need to establish a tunnel for an "Upgrade".
    @raw_request << data

    if @tunnel
        @tunnel.write( data )
        return
    end

    # ap data
    @parser << data
rescue ::HTTP::Parser::Error => e
    close e
end

#sanitize_url(str, headers) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/arachni/http/proxy_server/connection.rb', line 304

def sanitize_url( str, headers )
    uri = Arachni::URI( str )
    return uri.to_s if uri.absolute?

    host, port = *headers['Host'].split( ':', 2 )

    uri.scheme = self.is_a?( SSLInterceptor ) ? 'https' : 'http'
    uri.host = host
    uri.port = port ? port.to_i : nil

    uri.to_s
end

#start_interceptor(origin_host) ⇒ Object



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/arachni/http/proxy_server/connection.rb', line 271

def start_interceptor( origin_host )
    @interceptor_port = Utilities.available_port

    print_debug_level_3 "Starting interceptor on port: #{@interceptor_port}"

    @ssl_interceptor = reactor.listen(
        @options[:address], @interceptor_port, SSLInterceptor,
        @options.merge( origin_host: origin_host )
    )

    @tunnel = reactor.connect(
        @options[:address], @interceptor_port, Tunnel,
        @options.merge( client: self )
    )
end

#write(data) ⇒ Object



251
252
253
254
# File 'lib/arachni/http/proxy_server/connection.rb', line 251

def write( data )
    return if closed?
    super data
end