ssrf_proxy 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/LICENSE.md +1 -1
- data/README.md +87 -62
- data/bin/console +10 -8
- data/bin/ssrf-proxy +212 -99
- data/lib/ssrf_proxy.rb +4 -2
- data/lib/ssrf_proxy/banner.rb +15 -0
- data/lib/ssrf_proxy/http.rb +857 -436
- data/lib/ssrf_proxy/server.rb +98 -61
- data/lib/ssrf_proxy/version.rb +2 -10
- metadata +134 -92
data/lib/ssrf_proxy.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
#
|
3
|
-
# Copyright (c) 2015-
|
2
|
+
# Copyright (c) 2015-2017 Brendan Coles <bcoles@gmail.com>
|
4
3
|
# SSRF Proxy - https://github.com/bcoles/ssrf_proxy
|
5
4
|
# See the file 'LICENSE.md' for copying permission
|
6
5
|
#
|
@@ -30,7 +29,9 @@ require 'cgi'
|
|
30
29
|
require 'webrick'
|
31
30
|
require 'stringio'
|
32
31
|
require 'base64'
|
32
|
+
require 'stringio'
|
33
33
|
require 'htmlentities'
|
34
|
+
require 'mimemagic'
|
34
35
|
|
35
36
|
# client request url rules
|
36
37
|
require 'digest'
|
@@ -41,5 +42,6 @@ require 'ipaddress'
|
|
41
42
|
|
42
43
|
# SSRF Proxy gem libs
|
43
44
|
require 'ssrf_proxy/version'
|
45
|
+
require 'ssrf_proxy/banner'
|
44
46
|
require 'ssrf_proxy/http'
|
45
47
|
require 'ssrf_proxy/server'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2015-2017 Brendan Coles <bcoles@gmail.com>
|
3
|
+
# SSRF Proxy - https://github.com/bcoles/ssrf_proxy
|
4
|
+
# See the file 'LICENSE.md' for copying permission
|
5
|
+
#
|
6
|
+
|
7
|
+
module SSRFProxy
|
8
|
+
# Elite font ASCII art from: http://patorjk.com/software/taag/
|
9
|
+
BANNER = " \n" \
|
10
|
+
" .▄▄ · .▄▄ · ▄▄▄ ·▄▄▄ ▄▄▄·▄▄▄ ▐▄• ▄ ▄· ▄▌ \n" \
|
11
|
+
" ▐█ ▀. ▐█ ▀. ▀▄ █·▐▄▄· ▐█ ▄█▀▄ █·▪ █▌█▌▪▐█▪██▌ \n" \
|
12
|
+
" ▄▀▀▀█▄▄▀▀▀█▄▐▀▀▄ ██▪ ██▀·▐▀▀▄ ▄█▀▄ ·██· ▐█▌▐█▪ \n" \
|
13
|
+
" ▐█▄▪▐█▐█▄▪▐█▐█•█▌██▌. ▐█▪·•▐█•█▌▐█▌.▐▌▪▐█·█▌ ▐█▀·. \n" \
|
14
|
+
" ▀▀▀▀ ▀▀▀▀ .▀ ▀▀▀▀ .▀ .▀ ▀ ▀█▄▀▪•▀▀ ▀▀ ▀ • \n".freeze
|
15
|
+
end
|
data/lib/ssrf_proxy/http.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
#
|
3
|
-
# Copyright (c) 2015-
|
2
|
+
# Copyright (c) 2015-2017 Brendan Coles <bcoles@gmail.com>
|
4
3
|
# SSRF Proxy - https://github.com/bcoles/ssrf_proxy
|
5
4
|
# See the file 'LICENSE.md' for copying permission
|
6
5
|
#
|
@@ -8,11 +7,12 @@
|
|
8
7
|
module SSRFProxy
|
9
8
|
#
|
10
9
|
# SSRFProxy::HTTP object takes information required to connect
|
11
|
-
# to a HTTP server vulnerable to
|
12
|
-
# requests via the
|
10
|
+
# to a HTTP(S) server vulnerable to Server-Side Request Forgery
|
11
|
+
# (SSRF) and issue arbitrary HTTP requests via the vulnerable
|
12
|
+
# server.
|
13
13
|
#
|
14
|
-
# Once configured, the #send_uri
|
15
|
-
# HTTP requests through the server.
|
14
|
+
# Once configured, the #send_uri and #send_request methods can
|
15
|
+
# be used to tunnel HTTP requests through the vulnerable server.
|
16
16
|
#
|
17
17
|
# Several request modification options can be used to format
|
18
18
|
# the HTTP request appropriately for the SSRF vector and
|
@@ -25,444 +25,726 @@ module SSRFProxy
|
|
25
25
|
# HTTP request.
|
26
26
|
#
|
27
27
|
# Refer to the wiki for more information about configuring the
|
28
|
-
# SSRF, requestion modification,
|
28
|
+
# SSRF, requestion modification, response modification, and
|
29
|
+
# example configurations:
|
29
30
|
# https://github.com/bcoles/ssrf_proxy/wiki/Configuration
|
30
31
|
#
|
31
32
|
class HTTP
|
33
|
+
# @return [Logger] logger
|
34
|
+
attr_reader :logger
|
35
|
+
# @return [URI] SSRF URL
|
36
|
+
attr_reader :url
|
37
|
+
# @return [URI] upstream proxy
|
38
|
+
attr_reader :proxy
|
39
|
+
# @return [String] SSRF request HTTP method
|
40
|
+
attr_reader :method
|
41
|
+
# @return [Hash] SSRF request HTTP headers
|
42
|
+
attr_reader :headers
|
43
|
+
# @return [String] SSRF request HTTP body
|
44
|
+
attr_reader :post_data
|
45
|
+
|
32
46
|
#
|
33
47
|
# SSRFProxy::HTTP errors
|
34
48
|
#
|
35
49
|
module Error
|
36
|
-
# SSRFProxy::HTTP
|
50
|
+
# SSRFProxy::HTTP errors
|
37
51
|
class Error < StandardError; end
|
38
|
-
exceptions = %w
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
52
|
+
exceptions = %w[NoUrlPlaceholder
|
53
|
+
InvalidSsrfRequest
|
54
|
+
InvalidSsrfRequestMethod
|
55
|
+
InvalidUpstreamProxy
|
56
|
+
InvalidIpEncoding
|
57
|
+
InvalidClientRequest
|
58
|
+
InvalidResponse
|
59
|
+
ConnectionFailed
|
60
|
+
ConnectionTimeout]
|
46
61
|
exceptions.each { |e| const_set(e, Class.new(Error)) }
|
47
62
|
end
|
48
63
|
|
49
64
|
#
|
50
65
|
# SSRFProxy::HTTP accepts SSRF connection information,
|
51
|
-
# and configuration options for request
|
66
|
+
# and configuration options for request modification
|
52
67
|
# and response modification.
|
53
68
|
#
|
54
|
-
# @param [String]
|
55
|
-
# @param [Hash] opts SSRF and HTTP connection options:
|
56
|
-
# @option opts [String] proxy
|
57
|
-
# @option opts [String] method
|
58
|
-
# @option opts [String] post_data
|
59
|
-
# @option opts [String] rules
|
60
|
-
# @option opts [String] ip_encoding
|
61
|
-
# @option opts [Regex] match
|
62
|
-
# @option opts [String] strip
|
63
|
-
# @option opts [Boolean] decode_html
|
64
|
-
# @option opts [Boolean] guess_status
|
65
|
-
# @option opts [Boolean] guess_mime
|
66
|
-
# @option opts [Boolean] ask_password
|
67
|
-
# @option opts [Boolean] forward_cookies
|
68
|
-
# @option opts [Boolean] body_to_uri
|
69
|
-
# @option opts [Boolean] auth_to_uri
|
70
|
-
# @option opts [Boolean] cookies_to_uri
|
71
|
-
# @option opts [String] cookie
|
72
|
-
# @option opts [Integer] timeout
|
73
|
-
# @option opts [String] user_agent
|
74
|
-
# @option opts [Boolean] insecure
|
75
|
-
#
|
76
|
-
# @example SSRF with default options
|
77
|
-
# SSRFProxy::HTTP.new('http://example.local/index.php?url=xxURLxx')
|
69
|
+
# @param url [String] Target URL vulnerable to SSRF
|
78
70
|
#
|
79
|
-
# @
|
80
|
-
# Invalid SSRF request specified.
|
71
|
+
# @param file [String] Load HTTP request from a file
|
81
72
|
#
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
73
|
+
# @param proxy [String] Use a proxy to connect to the server.
|
74
|
+
# (Supported proxies: http, https, socks)
|
75
|
+
#
|
76
|
+
# @param ssl [Boolean] Connect using SSL/TLS
|
77
|
+
#
|
78
|
+
# @param method [String] HTTP method (GET/HEAD/DELETE/POST/PUT/OPTIONS)
|
79
|
+
# (Default: GET)
|
80
|
+
#
|
81
|
+
# @param post_data [String] HTTP post data
|
82
|
+
#
|
83
|
+
# @param user [String] HTTP basic authentication credentials
|
84
|
+
#
|
85
|
+
# @param rules [String] Rules for parsing client request
|
86
|
+
# (separated by ',') (Default: none)
|
87
|
+
#
|
88
|
+
# @param no_urlencode [Boolean] Do not URL encode client request
|
89
|
+
#
|
90
|
+
# @param ip_encoding [String] Encode client request host IP address.
|
91
|
+
# (Modes: int, ipv6, oct, hex, dotted_hex)
|
92
|
+
#
|
93
|
+
# @param match [String] Regex to match response body content.
|
94
|
+
# (Default: \A(.*)\z)
|
95
|
+
#
|
96
|
+
# @param strip [String] Headers to remove from the response.
|
97
|
+
# (separated by ',') (Default: none)
|
98
|
+
#
|
99
|
+
# @param decode_html [Boolean] Decode HTML entities in response body
|
100
|
+
#
|
101
|
+
# @param unescape [Boolean] Unescape special characters in response body
|
102
|
+
#
|
103
|
+
# @param guess_status [Boolean] Replaces response status code and message
|
104
|
+
# headers (determined by common strings in the
|
105
|
+
# response body, such as 404 Not Found.)
|
106
|
+
#
|
107
|
+
# @param guess_mime [Boolean] Replaces response content-type header with the
|
108
|
+
# appropriate mime type (determined by the file
|
109
|
+
# extension of the requested resource.)
|
110
|
+
#
|
111
|
+
# @param sniff_mime [Boolean] Replaces response content-type header with the
|
112
|
+
# appropriate mime type (determined by magic bytes
|
113
|
+
# in the response body.)
|
114
|
+
#
|
115
|
+
# @param timeout_ok [Boolean] Replaces timeout HTTP status code 504 with 200.
|
116
|
+
#
|
117
|
+
# @param detect_headers [Boolean] Replaces response headers if response headers
|
118
|
+
# are identified in the response body.
|
119
|
+
#
|
120
|
+
# @param fail_no_content [Boolean] Return HTTP status 502 if response body
|
121
|
+
# is empty.
|
122
|
+
#
|
123
|
+
# @param forward_method [Boolean] Forward client request method
|
124
|
+
#
|
125
|
+
# @param forward_headers [Boolean] Forward all client request headers
|
126
|
+
#
|
127
|
+
# @param forward_body [Boolean] Forward client request body
|
128
|
+
#
|
129
|
+
# @param forward_cookies [Boolean] Forward client request cookies
|
130
|
+
#
|
131
|
+
# @param body_to_uri [Boolean] Add client request body to URI query string
|
132
|
+
#
|
133
|
+
# @param auth_to_uri [Boolean] Use client request basic authentication
|
134
|
+
# credentials in request URI.
|
135
|
+
#
|
136
|
+
# @param cookies_to_uri [Boolean] Add client request cookies to URI query string
|
137
|
+
#
|
138
|
+
# @param cache_buster [Boolean] Append a random value to the client request
|
139
|
+
# query string
|
140
|
+
#
|
141
|
+
# @param timeout [Integer] Connection timeout in seconds (Default: 10)
|
106
142
|
#
|
107
|
-
#
|
143
|
+
# @param user_agent [String] HTTP user-agent (Default: none)
|
108
144
|
#
|
109
|
-
# @param [
|
145
|
+
# @param insecure [Boolean] Skip server SSL certificate validation
|
146
|
+
#
|
147
|
+
#
|
148
|
+
#
|
149
|
+
# @example Configure SSRF with URL, GET method
|
150
|
+
# SSRFProxy::HTTP.new(url: 'http://example.local/?url=xxURLxx')
|
151
|
+
#
|
152
|
+
# @example Configure SSRF with URL, POST method
|
153
|
+
# SSRFProxy::HTTP.new(url: 'http://example.local/',
|
154
|
+
# method: 'POST',
|
155
|
+
# post_data: 'url=xxURLxx')
|
156
|
+
#
|
157
|
+
# @example Configure SSRF with raw HTTP request file
|
158
|
+
# SSRFProxy::HTTP.new(file: 'ssrf.txt')
|
159
|
+
#
|
160
|
+
# @example Configure SSRF with raw HTTP request file and force SSL/TLS
|
161
|
+
# SSRFProxy::HTTP.new(file: 'ssrf.txt', ssl: true)
|
162
|
+
#
|
163
|
+
# @example Configure SSRF with raw HTTP request StringIO
|
164
|
+
# SSRFProxy::HTTP.new(file: StringIO.new("GET http://example.local/?url=xxURLxx HTTP/1.1\n\n"))
|
165
|
+
#
|
166
|
+
# @raise [SSRFProxy::HTTP::Error::InvalidSsrfRequest]
|
167
|
+
# Invalid SSRF request specified.
|
110
168
|
#
|
111
169
|
# @raise [SSRFProxy::HTTP::Error::InvalidUpstreamProxy]
|
112
170
|
# Invalid upstream proxy specified.
|
171
|
+
#
|
113
172
|
# @raise [SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod]
|
114
173
|
# Invalid SSRF request method specified.
|
115
|
-
#
|
174
|
+
# Supported methods: GET, HEAD, DELETE, POST, PUT, OPTIONS.
|
175
|
+
#
|
116
176
|
# @raise [SSRFProxy::HTTP::Error::NoUrlPlaceholder]
|
117
177
|
# 'xxURLxx' URL placeholder must be specified in the
|
118
178
|
# SSRF request URL or body.
|
179
|
+
#
|
119
180
|
# @raise [SSRFProxy::HTTP::Error::InvalidIpEncoding]
|
120
181
|
# Invalid IP encoding method specified.
|
121
182
|
#
|
122
|
-
def
|
183
|
+
def initialize(url: nil,
|
184
|
+
file: nil,
|
185
|
+
proxy: nil,
|
186
|
+
ssl: false,
|
187
|
+
method: 'GET',
|
188
|
+
placeholder: 'xxURLxx',
|
189
|
+
post_data: nil,
|
190
|
+
rules: nil,
|
191
|
+
no_urlencode: false,
|
192
|
+
ip_encoding: nil,
|
193
|
+
match: '\A(.*)\z',
|
194
|
+
strip: nil,
|
195
|
+
decode_html: false,
|
196
|
+
unescape: false,
|
197
|
+
guess_mime: false,
|
198
|
+
sniff_mime: false,
|
199
|
+
guess_status: false,
|
200
|
+
cors: false,
|
201
|
+
timeout_ok: false,
|
202
|
+
detect_headers: false,
|
203
|
+
fail_no_content: false,
|
204
|
+
forward_method: false,
|
205
|
+
forward_headers: false,
|
206
|
+
forward_body: false,
|
207
|
+
forward_cookies: false,
|
208
|
+
body_to_uri: false,
|
209
|
+
auth_to_uri: false,
|
210
|
+
cookies_to_uri: false,
|
211
|
+
cache_buster: false,
|
212
|
+
cookie: nil,
|
213
|
+
user: nil,
|
214
|
+
timeout: 10,
|
215
|
+
user_agent: nil,
|
216
|
+
insecure: false)
|
217
|
+
|
218
|
+
@SUPPORTED_METHODS = %w[GET HEAD DELETE POST PUT OPTIONS].freeze
|
219
|
+
@SUPPORTED_IP_ENCODINGS = %w[int ipv6 oct hex dotted_hex].freeze
|
220
|
+
|
221
|
+
@logger = ::Logger.new(STDOUT).tap do |log|
|
222
|
+
log.progname = 'ssrf-proxy'
|
223
|
+
log.level = ::Logger::WARN
|
224
|
+
log.datetime_format = '%Y-%m-%d %H:%M:%S '
|
225
|
+
end
|
226
|
+
|
123
227
|
# SSRF configuration options
|
124
|
-
@
|
228
|
+
@proxy = nil
|
229
|
+
@placeholder = placeholder.to_s || 'xxURLxx'
|
125
230
|
@method = 'GET'
|
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
|
-
|
231
|
+
@headers ||= {}
|
232
|
+
@post_data = post_data.to_s || ''
|
233
|
+
@rules = rules.to_s.split(/,/) || []
|
234
|
+
@no_urlencode = no_urlencode || false
|
235
|
+
|
236
|
+
# client request modification
|
237
|
+
@ip_encoding = nil
|
238
|
+
@forward_method = forward_method || false
|
239
|
+
@forward_headers = forward_headers || false
|
240
|
+
@forward_body = forward_body || false
|
241
|
+
@forward_cookies = forward_cookies || false
|
242
|
+
@body_to_uri = body_to_uri || false
|
243
|
+
@auth_to_uri = auth_to_uri || false
|
244
|
+
@cookies_to_uri = cookies_to_uri || false
|
245
|
+
@cache_buster = cache_buster || false
|
246
|
+
|
247
|
+
# SSRF connection options
|
248
|
+
@user = ''
|
249
|
+
@pass = ''
|
250
|
+
@timeout = timeout.to_i || 10
|
251
|
+
@insecure = insecure || false
|
252
|
+
|
253
|
+
# HTTP response modification options
|
254
|
+
@match_regex = match.to_s || '\A(.*)\z'
|
255
|
+
@strip = strip.to_s.downcase.split(/,/) || []
|
256
|
+
@decode_html = decode_html || false
|
257
|
+
@unescape = unescape || false
|
258
|
+
@guess_status = guess_status || false
|
259
|
+
@guess_mime = guess_mime || false
|
260
|
+
@sniff_mime = sniff_mime || false
|
261
|
+
@detect_headers = detect_headers || false
|
262
|
+
@fail_no_content = fail_no_content || false
|
263
|
+
@timeout_ok = timeout_ok || false
|
264
|
+
@cors = cors || false
|
265
|
+
|
266
|
+
# ensure either a URL or file path was provided
|
267
|
+
if url.to_s.eql?('') && file.to_s.eql?('')
|
268
|
+
raise ArgumentError,
|
269
|
+
"Option 'url' or 'file' must be provided."
|
270
|
+
end
|
271
|
+
|
272
|
+
# parse HTTP request file
|
273
|
+
unless file.to_s.eql?('')
|
274
|
+
unless url.to_s.eql?('')
|
275
|
+
raise ArgumentError,
|
276
|
+
"Options 'url' and 'file' are mutually exclusive."
|
277
|
+
end
|
278
|
+
|
279
|
+
if file.is_a?(String)
|
280
|
+
if File.exist?(file) && File.readable?(file)
|
281
|
+
http = File.read(file).to_s
|
158
282
|
else
|
159
|
-
raise SSRFProxy::HTTP::Error::
|
160
|
-
|
283
|
+
raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
|
284
|
+
"Invalid SSRF request specified : Could not read file #{file.inspect}"
|
161
285
|
end
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
286
|
+
elsif file.is_a?(StringIO)
|
287
|
+
http = file.read
|
288
|
+
end
|
289
|
+
|
290
|
+
req = parse_http_request(http)
|
291
|
+
url = req['uri']
|
292
|
+
@method = req['method']
|
293
|
+
@headers = {}
|
294
|
+
req['headers'].each do |k, v|
|
295
|
+
@headers[k.downcase] = v.flatten.first
|
166
296
|
end
|
297
|
+
@headers.delete('host')
|
298
|
+
@post_data = req['body']
|
167
299
|
end
|
168
|
-
|
169
|
-
|
170
|
-
|
300
|
+
|
301
|
+
# parse target URL
|
302
|
+
begin
|
303
|
+
@url = URI.parse(url.to_s)
|
304
|
+
rescue URI::InvalidURIError
|
305
|
+
raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
|
306
|
+
'Invalid SSRF request specified : Could not parse URL.'
|
171
307
|
end
|
172
308
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
@
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
309
|
+
if @url.scheme.nil? || @url.host.nil? || @url.port.nil?
|
310
|
+
raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
|
311
|
+
'Invalid SSRF request specified : Invalid URL.'
|
312
|
+
end
|
313
|
+
|
314
|
+
unless @url.scheme.eql?('http') || @url.scheme.eql?('https')
|
315
|
+
raise SSRFProxy::HTTP::Error::InvalidSsrfRequest.new,
|
316
|
+
'Invalid SSRF request specified : URL scheme must be http(s).'
|
317
|
+
end
|
318
|
+
|
319
|
+
if proxy
|
320
|
+
begin
|
321
|
+
@proxy = URI.parse(proxy.to_s)
|
322
|
+
rescue URI::InvalidURIError
|
323
|
+
raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
|
324
|
+
'Invalid upstream proxy specified.'
|
325
|
+
end
|
326
|
+
if @proxy.host.nil? || @proxy.port.nil?
|
327
|
+
raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
|
328
|
+
'Invalid upstream proxy specified.'
|
329
|
+
end
|
330
|
+
if @proxy.scheme !~ /\A(socks|https?)\z/
|
331
|
+
raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
|
332
|
+
'Unsupported upstream proxy specified. ' \
|
333
|
+
'Scheme must be http(s) or socks.'
|
196
334
|
end
|
197
335
|
end
|
198
336
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
when '
|
208
|
-
@
|
209
|
-
when '
|
210
|
-
@
|
211
|
-
when '
|
212
|
-
@
|
213
|
-
when '
|
214
|
-
@
|
337
|
+
if ssl
|
338
|
+
@url.scheme = 'https'
|
339
|
+
end
|
340
|
+
|
341
|
+
if method
|
342
|
+
case method.to_s.downcase
|
343
|
+
when 'get'
|
344
|
+
@method = 'GET'
|
345
|
+
when 'head'
|
346
|
+
@method = 'HEAD'
|
347
|
+
when 'delete'
|
348
|
+
@method = 'DELETE'
|
349
|
+
when 'post'
|
350
|
+
@method = 'POST'
|
351
|
+
when 'put'
|
352
|
+
@method = 'PUT'
|
353
|
+
when 'options'
|
354
|
+
@method = 'OPTIONS'
|
355
|
+
else
|
356
|
+
raise SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod.new,
|
357
|
+
'Invalid SSRF request method specified. ' \
|
358
|
+
"Supported methods: #{@SUPPORTED_METHODS.join(', ')}."
|
215
359
|
end
|
216
360
|
end
|
217
361
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
@guess_status = false
|
223
|
-
@guess_mime = false
|
224
|
-
@ask_password = false
|
225
|
-
opts.each do |option, value|
|
226
|
-
next if value.eql?('')
|
227
|
-
case option
|
228
|
-
when 'match'
|
229
|
-
@match_regex = value.to_s
|
230
|
-
when 'strip'
|
231
|
-
@strip = value.to_s.split(/,/)
|
232
|
-
when 'decode_html'
|
233
|
-
@decode_html = true if value
|
234
|
-
when 'guess_status'
|
235
|
-
@guess_status = true if value
|
236
|
-
when 'guess_mime'
|
237
|
-
@guess_mime = true if value
|
238
|
-
when 'ask_password'
|
239
|
-
@ask_password = true if value
|
362
|
+
if ip_encoding
|
363
|
+
unless @SUPPORTED_IP_ENCODINGS.include?(ip_encoding)
|
364
|
+
raise SSRFProxy::HTTP::Error::InvalidIpEncoding.new,
|
365
|
+
'Invalid IP encoding method specified.'
|
240
366
|
end
|
367
|
+
@ip_encoding = ip_encoding.to_s
|
241
368
|
end
|
242
|
-
end
|
243
369
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
# @return [Logger] class logger object
|
248
|
-
#
|
249
|
-
def logger
|
250
|
-
@logger
|
251
|
-
end
|
370
|
+
if cookie
|
371
|
+
@headers['cookie'] = cookie.to_s
|
372
|
+
end
|
252
373
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
374
|
+
if user
|
375
|
+
if user.to_s =~ /^(.*?):(.*)/
|
376
|
+
@user = $1
|
377
|
+
@pass = $2
|
378
|
+
else
|
379
|
+
@user = user.to_s
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
if user_agent
|
384
|
+
@headers['user-agent'] = user_agent
|
385
|
+
end
|
386
|
+
|
387
|
+
# Ensure a URL placeholder was provided
|
388
|
+
unless @url.request_uri.to_s.include?(@placeholder) ||
|
389
|
+
@post_data.to_s.include?(@placeholder) ||
|
390
|
+
@headers.to_s.include?(@placeholder)
|
391
|
+
raise SSRFProxy::HTTP::Error::NoUrlPlaceholder.new,
|
392
|
+
'You must specify a URL placeholder with ' \
|
393
|
+
"'#{@placeholder}' in the SSRF request"
|
394
|
+
end
|
260
395
|
end
|
261
396
|
|
262
397
|
#
|
263
|
-
#
|
264
|
-
#
|
265
|
-
# @return [String] SSRF host
|
398
|
+
# Parse a raw HTTP request as a string
|
266
399
|
#
|
267
|
-
|
268
|
-
@ssrf_url.host
|
269
|
-
end
|
270
|
-
|
400
|
+
# @param [String] request raw HTTP request
|
271
401
|
#
|
272
|
-
#
|
402
|
+
# @raise [SSRFProxy::HTTP::Error::InvalidClientRequest]
|
403
|
+
# An invalid client HTTP request was supplied.
|
273
404
|
#
|
274
|
-
# @return [
|
405
|
+
# @return [Hash] HTTP request hash (url, method, headers, body)
|
275
406
|
#
|
276
|
-
def
|
277
|
-
|
407
|
+
def parse_http_request(request)
|
408
|
+
# parse method
|
409
|
+
if request.to_s !~ /\A(GET|HEAD|DELETE|POST|PUT|OPTIONS) /
|
410
|
+
logger.warn('HTTP request method is not supported')
|
411
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
412
|
+
'HTTP request method is not supported.'
|
413
|
+
end
|
414
|
+
|
415
|
+
# parse client request
|
416
|
+
begin
|
417
|
+
req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
|
418
|
+
req.parse(StringIO.new(request))
|
419
|
+
rescue
|
420
|
+
logger.warn('HTTP request is malformed.')
|
421
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
422
|
+
'HTTP request is malformed.'
|
423
|
+
end
|
424
|
+
|
425
|
+
# validate host
|
426
|
+
if request.to_s !~ %r{\A(GET|HEAD|DELETE|POST|PUT|OPTIONS) https?://}
|
427
|
+
if request.to_s =~ /^Host: ([^\s]+)\r?\n/
|
428
|
+
logger.info("Using host header: #{$1}")
|
429
|
+
else
|
430
|
+
logger.warn('HTTP request is malformed : No host specified.')
|
431
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
432
|
+
'HTTP request is malformed : No host specified.'
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# return request hash
|
437
|
+
uri = req.request_uri
|
438
|
+
method = req.request_method
|
439
|
+
headers = req.header
|
440
|
+
begin
|
441
|
+
body = req.body.to_s
|
442
|
+
rescue WEBrick::HTTPStatus::BadRequest => e
|
443
|
+
logger.warn("HTTP request is malformed : #{e.message}")
|
444
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
445
|
+
"HTTP request is malformed : #{e.message}"
|
446
|
+
rescue WEBrick::HTTPStatus::LengthRequired
|
447
|
+
logger.warn("HTTP request is malformed : Request body without 'Content-Length' header.")
|
448
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
449
|
+
"HTTP request is malformed : Request body without 'Content-Length' header."
|
450
|
+
end
|
451
|
+
|
452
|
+
{ 'uri' => uri,
|
453
|
+
'method' => method,
|
454
|
+
'headers' => headers,
|
455
|
+
'body' => body }
|
278
456
|
end
|
279
457
|
|
280
458
|
#
|
281
|
-
#
|
459
|
+
# Parse a raw HTTP request as a string,
|
460
|
+
# then send the requested URL and HTTP headers to #send_uri
|
282
461
|
#
|
283
|
-
# @
|
462
|
+
# @param request [String] Raw HTTP request
|
463
|
+
# @param use_ssl [Boolean] Connect using SSL/TLS
|
284
464
|
#
|
285
|
-
|
286
|
-
|
465
|
+
# @return [Hash] HTTP response hash (version, code, message, headers, body)
|
466
|
+
#
|
467
|
+
def send_request(request, use_ssl: false)
|
468
|
+
req = parse_http_request(request)
|
469
|
+
req['uri'].scheme = 'https' if use_ssl
|
470
|
+
send_uri(req['uri'],
|
471
|
+
method: req['method'],
|
472
|
+
headers: req['headers'],
|
473
|
+
body: req['body'])
|
287
474
|
end
|
288
475
|
|
289
476
|
#
|
290
|
-
#
|
291
|
-
# and HTTP headers to send_uri
|
477
|
+
# Fetch a URI via SSRF
|
292
478
|
#
|
293
|
-
# @param [String]
|
479
|
+
# @param [String] uri URI to fetch
|
480
|
+
# @param [String] method HTTP request method
|
481
|
+
# @param [Hash] headers HTTP request headers
|
482
|
+
# @param [String] body HTTP request body
|
294
483
|
#
|
295
484
|
# @raise [SSRFProxy::HTTP::Error::InvalidClientRequest]
|
296
485
|
# An invalid client HTTP request was supplied.
|
297
486
|
#
|
298
487
|
# @return [Hash] HTTP response hash (version, code, message, headers, body, etc)
|
299
488
|
#
|
300
|
-
def
|
301
|
-
|
302
|
-
|
489
|
+
def send_uri(uri, method: 'GET', headers: {}, body: '')
|
490
|
+
uri = uri.to_s
|
491
|
+
body = body.to_s
|
492
|
+
headers = {} unless headers.is_a?(Hash)
|
493
|
+
|
494
|
+
# validate url
|
495
|
+
unless uri.start_with?('http://', 'https://')
|
303
496
|
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
304
|
-
'
|
497
|
+
'Invalid request URI'
|
305
498
|
end
|
306
|
-
|
307
|
-
|
308
|
-
|
499
|
+
|
500
|
+
# set request method
|
501
|
+
if @forward_method
|
502
|
+
if @SUPPORTED_METHODS.include?(method)
|
503
|
+
request_method = method
|
309
504
|
else
|
310
|
-
logger.warn('No host specified')
|
311
505
|
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
312
|
-
'
|
506
|
+
"Request method '#{method}' is not supported"
|
313
507
|
end
|
508
|
+
else
|
509
|
+
request_method = @method
|
314
510
|
end
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
511
|
+
|
512
|
+
# parse request headers
|
513
|
+
client_headers = {}
|
514
|
+
headers.each do |k, v|
|
515
|
+
if v.is_a?(Array)
|
516
|
+
client_headers[k.downcase] = v.flatten.first
|
517
|
+
elsif v.is_a?(String)
|
518
|
+
client_headers[k.downcase] = v.to_s
|
519
|
+
else
|
520
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
521
|
+
"Request header #{k.inspect} value is malformed: #{v}"
|
522
|
+
end
|
323
523
|
end
|
324
|
-
|
325
|
-
|
524
|
+
|
525
|
+
# reject websocket requests
|
526
|
+
if client_headers['upgrade'].to_s.start_with?('WebSocket')
|
527
|
+
logger.warn('WebSocket tunneling is not supported')
|
326
528
|
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
327
|
-
|
529
|
+
'WebSocket tunneling is not supported'
|
328
530
|
end
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
531
|
+
|
532
|
+
# copy request body to URL
|
533
|
+
if @body_to_uri && !body.eql?('')
|
534
|
+
logger.debug("Parsing request body: #{body}")
|
535
|
+
separator = uri.include?('?') ? '&' : '?'
|
536
|
+
uri = "#{uri}#{separator}#{body}"
|
537
|
+
logger.info("Added request body to URI: #{body.inspect}")
|
333
538
|
end
|
334
539
|
|
335
|
-
#
|
336
|
-
if @
|
337
|
-
logger.debug("Parsing
|
540
|
+
# copy basic authentication credentials to uri
|
541
|
+
if @auth_to_uri && client_headers['authorization'].to_s.downcase.start_with?('basic ')
|
542
|
+
logger.debug("Parsing basic authentication header: #{client_headers['authorization']}")
|
338
543
|
begin
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
end
|
344
|
-
logger.info("Added request body to URI: #{req.body}")
|
544
|
+
creds = client_headers['authorization'].split(' ')[1]
|
545
|
+
user = Base64.decode64(creds).chomp
|
546
|
+
uri = uri.gsub!(%r{://}, "://#{CGI.escape(user).gsub(/\+/, '%20').gsub('%3A', ':')}@")
|
547
|
+
logger.info("Using basic authentication credentials: #{user}")
|
345
548
|
rescue
|
346
|
-
logger.warn('Could not parse
|
347
|
-
|
348
|
-
end
|
349
|
-
|
350
|
-
# move basic authentication credentials to uri
|
351
|
-
if @auth_to_uri && !req.header.nil?
|
352
|
-
req.header['authorization'].each do |header|
|
353
|
-
logger.debug("Parsing basic authentication header: #{header}")
|
354
|
-
next unless header.split(' ').first =~ /^basic$/i
|
355
|
-
begin
|
356
|
-
creds = header.split(' ')[1]
|
357
|
-
user = Base64.decode64(creds).chomp
|
358
|
-
uri = uri.to_s.gsub!(%r{:(//)}, "://#{user}@")
|
359
|
-
logger.info("Using basic authentication credentials: #{user}")
|
360
|
-
rescue
|
361
|
-
logger.warn "Could not parse request authorization header: #{header}"
|
362
|
-
end
|
363
|
-
break
|
549
|
+
logger.warn('Could not parse request authorization header: ' \
|
550
|
+
"#{client_headers['authorization']}")
|
364
551
|
end
|
365
552
|
end
|
366
553
|
|
367
554
|
# copy cookies to uri
|
368
555
|
cookies = []
|
369
|
-
if @cookies_to_uri && !
|
370
|
-
logger.debug("Parsing request cookies: #{
|
371
|
-
|
372
|
-
|
373
|
-
req.cookies.each do |c|
|
374
|
-
cookies << c.to_s.gsub(/;\z/, '').to_s unless c.nil?
|
375
|
-
end
|
376
|
-
query_string = uri.to_s.split('?')[1..-1]
|
377
|
-
if query_string.empty?
|
378
|
-
s = '?'
|
379
|
-
else
|
380
|
-
s = '&'
|
381
|
-
end
|
382
|
-
uri = "#{uri}#{s}#{cookies.join('&')}"
|
383
|
-
logger.info("Added cookies to URI: #{cookies.join('&')}")
|
384
|
-
rescue => e
|
385
|
-
logger.warn "Could not parse request coookies: #{e}"
|
556
|
+
if @cookies_to_uri && !client_headers['cookie'].nil?
|
557
|
+
logger.debug("Parsing request cookies: #{client_headers['cookie']}")
|
558
|
+
client_headers['cookie'].split(/;\s*/).each do |c|
|
559
|
+
cookies << c.to_s unless c.nil?
|
386
560
|
end
|
561
|
+
separator = uri.include?('?') ? '&' : '?'
|
562
|
+
uri = "#{uri}#{separator}#{cookies.join('&')}"
|
563
|
+
logger.info("Added cookies to URI: #{cookies.join('&')}")
|
387
564
|
end
|
388
565
|
|
389
|
-
#
|
390
|
-
|
566
|
+
# add cache buster
|
567
|
+
if @cache_buster
|
568
|
+
separator = uri.include?('?') ? '&' : '?'
|
569
|
+
junk = "#{rand(36**6).to_s(36)}=#{rand(36**6).to_s(36)}"
|
570
|
+
uri = "#{uri}#{separator}#{junk}"
|
571
|
+
end
|
572
|
+
|
573
|
+
# set request headers
|
574
|
+
request_headers = @headers.dup
|
391
575
|
|
392
|
-
# forward
|
576
|
+
# forward request cookies
|
393
577
|
new_cookie = []
|
394
|
-
new_cookie << @cookie unless @cookie.
|
395
|
-
if @forward_cookies
|
396
|
-
|
397
|
-
new_cookie << c.to_s
|
578
|
+
new_cookie << @headers['cookie'] unless @headers['cookie'].to_s.eql?('')
|
579
|
+
if @forward_cookies && !client_headers['cookie'].nil?
|
580
|
+
client_headers['cookie'].split(/;\s*/).each do |c|
|
581
|
+
new_cookie << c.to_s unless c.nil?
|
398
582
|
end
|
399
583
|
end
|
400
584
|
unless new_cookie.empty?
|
401
|
-
|
402
|
-
logger.info("Using cookie: #{
|
585
|
+
request_headers['cookie'] = new_cookie.uniq.join('; ')
|
586
|
+
logger.info("Using cookie: #{new_cookie.join('; ')}")
|
403
587
|
end
|
404
|
-
send_uri(uri, headers)
|
405
|
-
end
|
406
588
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
def send_uri(uri, headers = {})
|
419
|
-
if uri.nil?
|
420
|
-
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
421
|
-
'Request URI is nil'
|
589
|
+
# forward request headers and strip proxy headers
|
590
|
+
if @forward_headers && !client_headers.empty?
|
591
|
+
client_headers.each do |k, v|
|
592
|
+
next if k.eql?('proxy-connection')
|
593
|
+
next if k.eql?('proxy-authorization')
|
594
|
+
if v.is_a?(Array)
|
595
|
+
request_headers[k.downcase] = v.flatten.first
|
596
|
+
elsif v.is_a?(String)
|
597
|
+
request_headers[k.downcase] = v.to_s
|
598
|
+
end
|
599
|
+
end
|
422
600
|
end
|
423
601
|
|
424
602
|
# encode target host ip
|
425
|
-
|
426
|
-
|
603
|
+
ip_encoded_uri = @ip_encoding ? encode_ip(uri, @ip_encoding) : uri
|
604
|
+
|
605
|
+
# run request URI through rules
|
606
|
+
target_uri = run_rules(ip_encoded_uri, @rules).to_s
|
607
|
+
|
608
|
+
# URL encode target URI
|
609
|
+
unless @no_urlencode
|
610
|
+
target_uri = CGI.escape(target_uri).gsub(/\+/, '%20').to_s
|
611
|
+
end
|
612
|
+
|
613
|
+
# set path and query string
|
614
|
+
if @url.query.to_s.eql?('')
|
615
|
+
ssrf_url = @url.path.to_s
|
427
616
|
else
|
428
|
-
|
617
|
+
ssrf_url = "#{@url.path}?#{@url.query}"
|
429
618
|
end
|
430
619
|
|
431
|
-
#
|
432
|
-
|
620
|
+
# replace xxURLxx placeholder in request URL
|
621
|
+
ssrf_url.gsub!(/#{@placeholder}/, target_uri)
|
433
622
|
|
434
|
-
# replace xxURLxx placeholder in
|
435
|
-
|
623
|
+
# replace xxURLxx placeholder in request body
|
624
|
+
post_data = @post_data.gsub(/#{@placeholder}/, target_uri)
|
436
625
|
|
437
|
-
#
|
438
|
-
if @
|
439
|
-
|
626
|
+
# set request body
|
627
|
+
if @forward_body && !body.eql?('')
|
628
|
+
request_body = post_data.eql?('') ? body : "#{post_data}&#{body}"
|
440
629
|
else
|
441
|
-
|
630
|
+
request_body = post_data
|
442
631
|
end
|
443
632
|
|
444
|
-
#
|
445
|
-
|
633
|
+
# replace xxURLxx in request header values
|
634
|
+
request_headers.each do |k, v|
|
635
|
+
request_headers[k] = v.gsub(/#{@placeholder}/, target_uri)
|
636
|
+
end
|
446
637
|
|
447
638
|
# set content type
|
448
|
-
if
|
449
|
-
|
639
|
+
if request_headers['content-type'].nil? && !request_body.eql?('')
|
640
|
+
request_headers['content-type'] = 'application/x-www-form-urlencoded'
|
450
641
|
end
|
451
642
|
|
643
|
+
# set content length
|
644
|
+
request_headers['content-length'] = request_body.length.to_s
|
645
|
+
|
452
646
|
# send request
|
647
|
+
response = nil
|
453
648
|
start_time = Time.now
|
454
|
-
|
649
|
+
begin
|
650
|
+
response = send_http_request(ssrf_url,
|
651
|
+
request_method,
|
652
|
+
request_headers,
|
653
|
+
request_body)
|
654
|
+
if response['content-encoding'].to_s.downcase.eql?('gzip') && response.body
|
655
|
+
begin
|
656
|
+
sio = StringIO.new(response.body)
|
657
|
+
gz = Zlib::GzipReader.new(sio)
|
658
|
+
response.body = gz.read
|
659
|
+
rescue
|
660
|
+
logger.warn('Could not decompress response body')
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
result = { 'url' => uri,
|
665
|
+
'http_version' => response.http_version,
|
666
|
+
'code' => response.code,
|
667
|
+
'message' => response.message,
|
668
|
+
'headers' => '',
|
669
|
+
'body' => response.body.to_s || '' }
|
670
|
+
rescue SSRFProxy::HTTP::Error::ConnectionTimeout => e
|
671
|
+
unless @timeout_ok
|
672
|
+
raise SSRFProxy::HTTP::Error::ConnectionTimeout, e.message
|
673
|
+
end
|
674
|
+
result = { 'url' => uri,
|
675
|
+
'http_version' => '1.0',
|
676
|
+
'code' => 200,
|
677
|
+
'message' => 'Timeout',
|
678
|
+
'headers' => '',
|
679
|
+
'body' => '' }
|
680
|
+
logger.info('Changed HTTP status code 504 to 200')
|
681
|
+
end
|
682
|
+
|
683
|
+
# set duration
|
455
684
|
end_time = Time.now
|
456
685
|
duration = ((end_time - start_time) * 1000).round(3)
|
457
|
-
result =
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
686
|
+
result['duration'] = duration
|
687
|
+
|
688
|
+
# body content encoding
|
689
|
+
result['body'].force_encoding('BINARY')
|
690
|
+
unless result['body'].valid_encoding?
|
691
|
+
begin
|
692
|
+
result['body'] = result['body'].encode(
|
693
|
+
'UTF-8',
|
694
|
+
'binary',
|
695
|
+
:invalid => :replace,
|
696
|
+
:undef => :replace,
|
697
|
+
:replace => ''
|
698
|
+
)
|
699
|
+
rescue
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
logger.info("Received #{result['body'].bytes.length} bytes in #{duration} ms")
|
704
|
+
|
705
|
+
# match response content
|
706
|
+
unless @match_regex.nil?
|
707
|
+
matches = result['body'].scan(/#{@match_regex}/m)
|
708
|
+
if !matches.empty?
|
709
|
+
result['body'] = matches.flatten.first.to_s
|
710
|
+
logger.info("Response body matches pattern '#{@match_regex}'")
|
711
|
+
else
|
712
|
+
result['body'] = ''
|
713
|
+
logger.warn("Response body does not match pattern '#{@match_regex}'")
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
# return 502 if matched response body is empty
|
718
|
+
if @fail_no_content
|
719
|
+
if result['body'].to_s.eql?('')
|
720
|
+
result['code'] = 502
|
721
|
+
result['message'] = 'Bad Gateway'
|
722
|
+
result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"
|
723
|
+
return result
|
724
|
+
end
|
725
|
+
end
|
726
|
+
|
727
|
+
# unescape response body
|
728
|
+
if @unescape
|
729
|
+
# unescape slashes
|
730
|
+
result['body'] = result['body'].tr('\\', '\\')
|
731
|
+
result['body'] = result['body'].gsub('\\/', '/')
|
732
|
+
# unescape whitespace
|
733
|
+
result['body'] = result['body'].gsub('\r', "\r")
|
734
|
+
result['body'] = result['body'].gsub('\n', "\n")
|
735
|
+
result['body'] = result['body'].gsub('\t', "\t")
|
736
|
+
# unescape quotes
|
737
|
+
result['body'] = result['body'].gsub('\"', '"')
|
738
|
+
result['body'] = result['body'].gsub("\\'", "'")
|
739
|
+
end
|
740
|
+
|
741
|
+
# decode HTML entities
|
742
|
+
if @decode_html
|
743
|
+
result['body'] = HTMLEntities.new.decode(result['body'])
|
744
|
+
end
|
745
|
+
|
746
|
+
# set title
|
747
|
+
result['title'] = result['body'][0..8192] =~ %r{<title>([^<]*)</title>}im ? $1.to_s : ''
|
466
748
|
|
467
749
|
# guess HTTP response code and message
|
468
750
|
if @guess_status
|
@@ -474,24 +756,72 @@ module SSRFProxy
|
|
474
756
|
logger.info("Using HTTP response status: #{result['code']} #{result['message']}")
|
475
757
|
end
|
476
758
|
end
|
759
|
+
|
760
|
+
# replace timeout response with 200 OK
|
761
|
+
if @timeout_ok
|
762
|
+
if result['code'].eql?('504')
|
763
|
+
logger.info('Changed HTTP status code 504 to 200')
|
764
|
+
result['code'] = 200
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
# detect headers in response body
|
769
|
+
if @detect_headers
|
770
|
+
headers = ''
|
771
|
+
head = result['body'][0..8192] # use first 8192 byes
|
772
|
+
detected_headers = head.scan(%r{HTTP/(1\.\d) (\d+) (.*?)\r?\n(.*?)\r?\n\r?\n}m)
|
773
|
+
|
774
|
+
if detected_headers.empty?
|
775
|
+
logger.info('Found no HTTP response headers in response body.')
|
776
|
+
else
|
777
|
+
# HTTP redirects may contain more than one set of HTTP response headers
|
778
|
+
# Use the last
|
779
|
+
logger.info("Found #{detected_headers.count} sets of HTTP response headers in reponse. Using last.")
|
780
|
+
version = detected_headers.last[0]
|
781
|
+
code = detected_headers.last[1]
|
782
|
+
message = detected_headers.last[2]
|
783
|
+
detected_headers.last[3].split(/\r?\n/).each do |line|
|
784
|
+
if line =~ /^[A-Za-z0-9\-_\.]+: /
|
785
|
+
k = line.split(': ').first
|
786
|
+
v = line.split(': ')[1..-1].flatten.first
|
787
|
+
headers << "#{k}: #{v}\n"
|
788
|
+
else
|
789
|
+
logger.warn('Could not use response headers in response body : Headers are malformed.')
|
790
|
+
headers = ''
|
791
|
+
break
|
792
|
+
end
|
793
|
+
end
|
794
|
+
end
|
795
|
+
unless headers.eql?('')
|
796
|
+
result['http_version'] = version
|
797
|
+
result['code'] = code.to_i
|
798
|
+
result['message'] = message
|
799
|
+
result['headers'] = headers
|
800
|
+
result['body'] = result['body'].split(/\r?\n\r?\n/)[detected_headers.count..-1].flatten.join("\n\n")
|
801
|
+
end
|
802
|
+
end
|
803
|
+
|
804
|
+
# set status line
|
477
805
|
result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"
|
478
806
|
|
479
807
|
# strip unwanted HTTP response headers
|
480
|
-
response.
|
481
|
-
|
482
|
-
|
483
|
-
|
808
|
+
unless response.nil?
|
809
|
+
response.each_header do |header_name, header_value|
|
810
|
+
if header_name.downcase.eql?('content-encoding')
|
811
|
+
next if header_value.downcase.eql?('gzip')
|
812
|
+
end
|
813
|
+
|
814
|
+
if @strip.include?(header_name.downcase)
|
815
|
+
logger.info("Removed response header: #{header_name}")
|
816
|
+
next
|
817
|
+
end
|
818
|
+
result['headers'] << "#{header_name}: #{header_value}\n"
|
484
819
|
end
|
485
|
-
result['headers'] << "#{header_name}: #{header_value}\n"
|
486
820
|
end
|
487
821
|
|
488
|
-
#
|
489
|
-
if @
|
490
|
-
|
491
|
-
# SafeCurl (safe_curl) InvalidURLException
|
492
|
-
if head =~ /fin1te\\SafeCurl\\Exception\\InvalidURLException/
|
493
|
-
logger.info('SafeCurl protection mechanism appears to be in use')
|
494
|
-
end
|
822
|
+
# add wildcard CORS header
|
823
|
+
if @cors
|
824
|
+
result['headers'] << "Access-Control-Allow-Origin: *\n"
|
495
825
|
end
|
496
826
|
|
497
827
|
# advise client to close HTTP connection
|
@@ -502,43 +832,30 @@ module SSRFProxy
|
|
502
832
|
end
|
503
833
|
|
504
834
|
# guess mime type and add content-type header
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
else
|
512
|
-
result['headers'] << "Content-Type: #{content_type}\n"
|
513
|
-
end
|
835
|
+
content_type = nil
|
836
|
+
if @sniff_mime
|
837
|
+
head = result['body'][0..8192] # use first 8192 byes
|
838
|
+
content_type = sniff_mime(head)
|
839
|
+
if content_type.nil?
|
840
|
+
content_type = guess_mime(File.extname(uri.to_s.split('?').first))
|
514
841
|
end
|
842
|
+
elsif @guess_mime
|
843
|
+
content_type = guess_mime(File.extname(uri.to_s.split('?').first))
|
515
844
|
end
|
516
845
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
logger.info("Response body matches pattern '#{@match_regex}'")
|
846
|
+
unless content_type.nil?
|
847
|
+
logger.info("Using content-type: #{content_type}")
|
848
|
+
if result['headers'] =~ /^content\-type:.*$/i
|
849
|
+
result['headers'].gsub!(/^content\-type:.*$/i,
|
850
|
+
"Content-Type: #{content_type}")
|
523
851
|
else
|
524
|
-
result['
|
525
|
-
logger.warn("Response body does not match pattern '#{@match_regex}'")
|
852
|
+
result['headers'] << "Content-Type: #{content_type}\n"
|
526
853
|
end
|
527
854
|
end
|
528
855
|
|
529
|
-
#
|
530
|
-
if
|
531
|
-
result['
|
532
|
-
result['body'].encode(
|
533
|
-
'UTF-8',
|
534
|
-
:invalid => :replace,
|
535
|
-
:undef => :replace,
|
536
|
-
:replace => '?'))
|
537
|
-
end
|
538
|
-
|
539
|
-
# prompt for password
|
540
|
-
if @ask_password
|
541
|
-
if result['code'].to_i == 401
|
856
|
+
# prompt for password if unauthorised
|
857
|
+
if result['code'] == 401
|
858
|
+
if result['headers'] !~ /^WWW-Authenticate:.*$/i
|
542
859
|
auth_uri = URI.parse(uri.to_s.split('?').first)
|
543
860
|
realm = "#{auth_uri.host}:#{auth_uri.port}"
|
544
861
|
result['headers'] << "WWW-Authenticate: Basic realm=\"#{realm}\"\n"
|
@@ -546,25 +863,39 @@ module SSRFProxy
|
|
546
863
|
end
|
547
864
|
end
|
548
865
|
|
866
|
+
# set location header if redirected
|
867
|
+
if result['code'] == 301 || result['code'] == 302
|
868
|
+
if result['headers'] !~ /^location:.*$/i
|
869
|
+
location = nil
|
870
|
+
if result['body'] =~ /This document may be found <a href="(.+)">/i
|
871
|
+
location = $1
|
872
|
+
elsif result['body'] =~ /The document has moved <a href="(.+)">/i
|
873
|
+
location = $1
|
874
|
+
end
|
875
|
+
unless location.nil?
|
876
|
+
result['headers'] << "Location: #{location}\n"
|
877
|
+
logger.info("Added Location header: #{location}")
|
878
|
+
end
|
879
|
+
end
|
880
|
+
end
|
881
|
+
|
549
882
|
# set content length
|
550
883
|
content_length = result['body'].length
|
551
884
|
if result['headers'] =~ /^transfer\-encoding:.*$/i
|
552
|
-
result['headers'].gsub!(/^transfer\-encoding:.*$/i,
|
885
|
+
result['headers'].gsub!(/^transfer\-encoding:.*$/i,
|
886
|
+
"Content-Length: #{content_length}")
|
553
887
|
elsif result['headers'] =~ /^content\-length:.*$/i
|
554
|
-
result['headers'].gsub!(/^content\-length:.*$/i,
|
888
|
+
result['headers'].gsub!(/^content\-length:.*$/i,
|
889
|
+
"Content-Length: #{content_length}")
|
555
890
|
else
|
556
891
|
result['headers'] << "Content-Length: #{content_length}\n"
|
557
892
|
end
|
558
893
|
|
559
|
-
# set title
|
560
|
-
if result['body'][0..1024] =~ %r{<title>([^<]*)<\/title>}im
|
561
|
-
result['title'] = $1.to_s
|
562
|
-
else
|
563
|
-
result['title'] = ''
|
564
|
-
end
|
565
|
-
|
566
894
|
# return HTTP response
|
567
|
-
logger.debug("Response:\n
|
895
|
+
logger.debug("Response:\n" \
|
896
|
+
"#{result['status_line']}\n" \
|
897
|
+
"#{result['headers']}\n" \
|
898
|
+
"#{result['body']}")
|
568
899
|
result
|
569
900
|
end
|
570
901
|
|
@@ -584,7 +915,7 @@ module SSRFProxy
|
|
584
915
|
ip = IPAddress::IPv4.new(host)
|
585
916
|
rescue
|
586
917
|
logger.warn("Could not parse requested host as IPv4 address: #{host}")
|
587
|
-
return
|
918
|
+
return url
|
588
919
|
end
|
589
920
|
case mode
|
590
921
|
when 'int'
|
@@ -644,9 +975,14 @@ module SSRFProxy
|
|
644
975
|
when 'rot13'
|
645
976
|
str = str.tr('A-Za-z', 'N-ZA-Mn-za-m')
|
646
977
|
when 'urlencode'
|
647
|
-
str = CGI.escape(str)
|
978
|
+
str = CGI.escape(str).gsub(/\+/, '%20')
|
648
979
|
when 'urldecode'
|
649
980
|
str = CGI.unescape(str)
|
981
|
+
when 'append-hash'
|
982
|
+
str = "#{str}##{rand(36**6).to_s(36)}"
|
983
|
+
when 'append-method-get'
|
984
|
+
separator = str.include?('?') ? '&' : '?'
|
985
|
+
str = "#{str}#{separator}method=get&_method=get"
|
650
986
|
else
|
651
987
|
logger.warn("Unknown rule: #{rule}")
|
652
988
|
end
|
@@ -664,7 +1000,7 @@ module SSRFProxy
|
|
664
1000
|
#
|
665
1001
|
# @raise [SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod]
|
666
1002
|
# Invalid SSRF request method specified.
|
667
|
-
# Method must be GET/HEAD/DELETE/POST/PUT.
|
1003
|
+
# Method must be GET/HEAD/DELETE/POST/PUT/OPTIONS.
|
668
1004
|
# @raise [SSRFProxy::HTTP::Error::ConnectionTimeout]
|
669
1005
|
# The request to the remote host timed out.
|
670
1006
|
# @raise [SSRFProxy::HTTP::Error::InvalidUpstreamProxy]
|
@@ -674,50 +1010,83 @@ module SSRFProxy
|
|
674
1010
|
#
|
675
1011
|
def send_http_request(url, method, headers, body)
|
676
1012
|
# use upstream proxy
|
677
|
-
if @
|
678
|
-
http = Net::HTTP.new(
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
1013
|
+
if @proxy.nil?
|
1014
|
+
http = Net::HTTP::Proxy(nil).new(
|
1015
|
+
@url.host,
|
1016
|
+
@url.port
|
1017
|
+
)
|
1018
|
+
elsif @proxy.scheme.eql?('http') || @proxy.scheme.eql?('https')
|
1019
|
+
http = Net::HTTP::Proxy(
|
1020
|
+
@proxy.host,
|
1021
|
+
@proxy.port
|
1022
|
+
).new(
|
1023
|
+
@url.host,
|
1024
|
+
@url.port
|
1025
|
+
)
|
1026
|
+
elsif @proxy.scheme.eql?('socks')
|
1027
|
+
http = Net::HTTP.SOCKSProxy(
|
1028
|
+
@proxy.host,
|
1029
|
+
@proxy.port
|
1030
|
+
).new(
|
1031
|
+
@url.host,
|
1032
|
+
@url.port
|
1033
|
+
)
|
683
1034
|
else
|
684
1035
|
raise SSRFProxy::HTTP::Error::InvalidUpstreamProxy.new,
|
685
1036
|
'Unsupported upstream proxy specified. Scheme must be http(s) or socks.'
|
686
1037
|
end
|
687
|
-
|
1038
|
+
|
1039
|
+
# set SSL
|
1040
|
+
if @url.scheme.eql?('https')
|
688
1041
|
http.use_ssl = true
|
689
|
-
|
690
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
691
|
-
else
|
692
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
693
|
-
end
|
1042
|
+
http.verify_mode = @insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
694
1043
|
end
|
1044
|
+
|
695
1045
|
# set socket options
|
696
1046
|
http.open_timeout = @timeout
|
697
1047
|
http.read_timeout = @timeout
|
1048
|
+
|
1049
|
+
# set http request method
|
1050
|
+
case method
|
1051
|
+
when 'GET'
|
1052
|
+
request = Net::HTTP::Get.new(url, headers.to_hash)
|
1053
|
+
when 'HEAD'
|
1054
|
+
request = Net::HTTP::Head.new(url, headers.to_hash)
|
1055
|
+
when 'DELETE'
|
1056
|
+
request = Net::HTTP::Delete.new(url, headers.to_hash)
|
1057
|
+
when 'POST'
|
1058
|
+
request = Net::HTTP::Post.new(url, headers.to_hash)
|
1059
|
+
when 'PUT'
|
1060
|
+
request = Net::HTTP::Put.new(url, headers.to_hash)
|
1061
|
+
when 'OPTIONS'
|
1062
|
+
request = Net::HTTP::Options.new(url, headers.to_hash)
|
1063
|
+
else
|
1064
|
+
logger.info("Request method #{method.inspect} not implemented")
|
1065
|
+
raise SSRFProxy::HTTP::Error::InvalidClientRequest,
|
1066
|
+
"Request method #{method.inspect} not implemented"
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
# set http request credentials
|
1070
|
+
request.basic_auth(@user, @pass) unless @user.eql?('') && @pass.eql?('')
|
1071
|
+
|
698
1072
|
# send http request
|
699
1073
|
response = {}
|
700
|
-
logger.info(
|
1074
|
+
logger.info('Sending request: ' \
|
1075
|
+
"#{@url.scheme}://#{@url.host}:#{@url.port}#{url}")
|
701
1076
|
begin
|
702
|
-
|
703
|
-
response = http.request Net::HTTP::Get.new(url, headers.to_hash)
|
704
|
-
elsif method == 'HEAD'
|
705
|
-
response = http.request Net::HTTP::Head.new(url, headers.to_hash)
|
706
|
-
elsif method == 'DELETE'
|
707
|
-
response = http.request Net::HTTP::Delete.new(url, headers.to_hash)
|
708
|
-
elsif method == 'POST'
|
709
|
-
request = Net::HTTP::Post.new(url, headers.to_hash)
|
1077
|
+
unless body.eql?('')
|
710
1078
|
request.body = body
|
711
|
-
|
712
|
-
elsif method == 'PUT'
|
713
|
-
request = Net::HTTP::Put.new(url, headers.to_hash)
|
714
|
-
request.body = body
|
715
|
-
response = http.request(request)
|
716
|
-
else
|
717
|
-
logger.info("SSRF request method not implemented -- Method[#{method}]")
|
718
|
-
raise SSRFProxy::HTTP::Error::InvalidSsrfRequestMethod,
|
719
|
-
"Request method not implemented -- Method[#{method}]"
|
1079
|
+
logger.info("Using request body: #{request.body.inspect}")
|
720
1080
|
end
|
1081
|
+
response = http.request(request)
|
1082
|
+
rescue Net::HTTPBadResponse, EOFError
|
1083
|
+
logger.info('Server returned an invalid HTTP response')
|
1084
|
+
raise SSRFProxy::HTTP::Error::InvalidResponse,
|
1085
|
+
'Server returned an invalid HTTP response'
|
1086
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET
|
1087
|
+
logger.info('Connection failed')
|
1088
|
+
raise SSRFProxy::HTTP::Error::ConnectionFailed,
|
1089
|
+
'Connection failed'
|
721
1090
|
rescue Timeout::Error, Errno::ETIMEDOUT
|
722
1091
|
logger.info("Connection timed out [#{@timeout}]")
|
723
1092
|
raise SSRFProxy::HTTP::Error::ConnectionTimeout,
|
@@ -726,6 +1095,15 @@ module SSRFProxy
|
|
726
1095
|
logger.error("Unhandled exception: #{e}")
|
727
1096
|
raise e
|
728
1097
|
end
|
1098
|
+
|
1099
|
+
if response.code.eql?('401')
|
1100
|
+
if @user.eql?('') && @pass.eql?('')
|
1101
|
+
logger.warn('Authentication required')
|
1102
|
+
else
|
1103
|
+
logger.warn('Authentication failed')
|
1104
|
+
end
|
1105
|
+
end
|
1106
|
+
|
729
1107
|
response
|
730
1108
|
end
|
731
1109
|
|
@@ -740,8 +1118,18 @@ module SSRFProxy
|
|
740
1118
|
#
|
741
1119
|
def guess_status(response)
|
742
1120
|
result = {}
|
1121
|
+
# response status code returned by php-simple-proxy and php-json-proxy
|
1122
|
+
if response =~ /"status":{"http_code":([\d]+)}/
|
1123
|
+
result['code'] = $1
|
1124
|
+
result['message'] = ''
|
743
1125
|
# generic page titles containing HTTP status
|
744
|
-
|
1126
|
+
elsif response =~ />301 Moved</ || response =~ />Document Moved</ || response =~ />Object Moved</ || response =~ />301 Moved Permanently</
|
1127
|
+
result['code'] = 301
|
1128
|
+
result['message'] = 'Document Moved'
|
1129
|
+
elsif response =~ />302 Found</ || response =~ />302 Moved Temporarily</
|
1130
|
+
result['code'] = 302
|
1131
|
+
result['message'] = 'Found'
|
1132
|
+
elsif response =~ />400 Bad Request</
|
745
1133
|
result['code'] = 400
|
746
1134
|
result['message'] = 'Bad Request'
|
747
1135
|
elsif response =~ />401 Unauthorized</
|
@@ -753,6 +1141,12 @@ module SSRFProxy
|
|
753
1141
|
elsif response =~ />404 Not Found</
|
754
1142
|
result['code'] = 404
|
755
1143
|
result['message'] = 'Not Found'
|
1144
|
+
elsif response =~ />The page is not found</
|
1145
|
+
result['code'] = 404
|
1146
|
+
result['message'] = 'Not Found'
|
1147
|
+
elsif response =~ />413 Request Entity Too Large</
|
1148
|
+
result['code'] = 413
|
1149
|
+
result['message'] = 'Request Entity Too Large'
|
756
1150
|
elsif response =~ />500 Internal Server Error</
|
757
1151
|
result['code'] = 500
|
758
1152
|
result['message'] = 'Internal Server Error'
|
@@ -823,7 +1217,7 @@ module SSRFProxy
|
|
823
1217
|
result['message'] = 'Timeout'
|
824
1218
|
end
|
825
1219
|
# C errno
|
826
|
-
elsif response =~ /\[Errno -?[\d]{1,
|
1220
|
+
elsif response =~ /\[Errno -?[\d]{1,5}\]/
|
827
1221
|
if response =~ /\[Errno -2\] Name or service not known/
|
828
1222
|
result['code'] = 502
|
829
1223
|
result['message'] = 'Bad Gateway'
|
@@ -842,6 +1236,24 @@ module SSRFProxy
|
|
842
1236
|
elsif response =~ /\[Errno 113\] No route to host/
|
843
1237
|
result['code'] = 502
|
844
1238
|
result['message'] = 'Bad Gateway'
|
1239
|
+
elsif response =~ /\[Errno 11004\] getaddrinfo failed/
|
1240
|
+
result['code'] = 502
|
1241
|
+
result['message'] = 'Bad Gateway'
|
1242
|
+
elsif response =~ /\[Errno 10053\] An established connection was aborted/
|
1243
|
+
result['code'] = 502
|
1244
|
+
result['message'] = 'Bad Gateway'
|
1245
|
+
elsif response =~ /\[Errno 10054\] An existing connection was forcibly closed/
|
1246
|
+
result['code'] = 502
|
1247
|
+
result['message'] = 'Bad Gateway'
|
1248
|
+
elsif response =~ /\[Errno 10055\] An operation on a socket could not be performed/
|
1249
|
+
result['code'] = 502
|
1250
|
+
result['message'] = 'Bad Gateway'
|
1251
|
+
elsif response =~ /\[Errno 10060\] A connection attempt failed/
|
1252
|
+
result['code'] = 502
|
1253
|
+
result['message'] = 'Bad Gateway'
|
1254
|
+
elsif response =~ /\[Errno 10061\] No connection could be made/
|
1255
|
+
result['code'] = 502
|
1256
|
+
result['message'] = 'Bad Gateway'
|
845
1257
|
end
|
846
1258
|
# Python urllib errors
|
847
1259
|
elsif response =~ /HTTPError: HTTP Error \d+/
|
@@ -944,47 +1356,56 @@ module SSRFProxy
|
|
944
1356
|
end
|
945
1357
|
|
946
1358
|
#
|
947
|
-
#
|
1359
|
+
# Guess content type based on file extension
|
948
1360
|
#
|
949
|
-
# @param [String]
|
1361
|
+
# @param [String] ext File extension including dots
|
1362
|
+
#
|
1363
|
+
# @example Return mime type for extension '.png'
|
1364
|
+
# guess_mime('favicon.png')
|
950
1365
|
#
|
951
|
-
# @return [
|
1366
|
+
# @return [String] content-type value
|
952
1367
|
#
|
953
|
-
def
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
1368
|
+
def guess_mime(ext)
|
1369
|
+
content_types = WEBrick::HTTPUtils::DefaultMimeTypes
|
1370
|
+
common_content_types = { 'ico' => 'image/x-icon' }
|
1371
|
+
content_types.merge!(common_content_types)
|
1372
|
+
content_types.each do |k, v|
|
1373
|
+
return v.to_s if ext.eql?(".#{k}")
|
959
1374
|
end
|
960
|
-
|
1375
|
+
nil
|
961
1376
|
end
|
962
1377
|
|
963
1378
|
#
|
964
|
-
# Guess content type based on
|
1379
|
+
# Guess content type based on magic bytes
|
965
1380
|
#
|
966
|
-
# @param [String]
|
1381
|
+
# @param [String] content File contents
|
967
1382
|
#
|
968
1383
|
# @return [String] content-type value
|
969
1384
|
#
|
970
|
-
def
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
1385
|
+
def sniff_mime(content)
|
1386
|
+
m = MimeMagic.by_magic(content)
|
1387
|
+
return if m.nil?
|
1388
|
+
|
1389
|
+
# Overwrite incorrect mime types
|
1390
|
+
case m.type.to_s
|
1391
|
+
when 'application/xhtml+xml'
|
1392
|
+
return 'text/html'
|
1393
|
+
when 'text/x-csrc'
|
1394
|
+
return 'text/css'
|
977
1395
|
end
|
1396
|
+
|
1397
|
+
m.type
|
1398
|
+
rescue
|
978
1399
|
nil
|
979
1400
|
end
|
980
1401
|
|
981
1402
|
# private methods
|
982
|
-
private :
|
1403
|
+
private :parse_http_request,
|
983
1404
|
:send_http_request,
|
984
1405
|
:run_rules,
|
985
1406
|
:encode_ip,
|
986
1407
|
:guess_mime,
|
987
|
-
:
|
988
|
-
:
|
1408
|
+
:sniff_mime,
|
1409
|
+
:guess_status
|
989
1410
|
end
|
990
1411
|
end
|