dap 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/dap/filter/http.rb +75 -44
- data/lib/dap/version.rb +1 -1
- data/spec/dap/filter/http_filter_spec.rb +13 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1cc592fe131d06b96989baa2072f92ac1c513465
|
4
|
+
data.tar.gz: 2550d569f533d19d43c05f7a1e778e111f4c96e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a162fc9fb0ae6f9caa0ac9fe1b4c6a3437eccb083d4c6127b334da28906b88584f06ef49ddd3f3a760c844cefe030c90141c6933359e5f44b18b21ba37c1f15
|
7
|
+
data.tar.gz: bbecce1d8ae4a2b1edcbc0e89c282fc04066431954a99520a80a910980dab9b552d4346aaf0016039885a5582d0c7c78c93a092be892a09cbd7f12e93d49532d
|
data/lib/dap/filter/http.rb
CHANGED
@@ -134,18 +134,77 @@ end
|
|
134
134
|
class FilterDecodeHTTPReply
|
135
135
|
include BaseDecoder
|
136
136
|
|
137
|
-
# TODO: Decode transfer-chunked responses
|
138
137
|
def decode(data)
|
139
138
|
lines = data.split(/\r?\n/)
|
140
|
-
resp
|
139
|
+
resp = lines.shift
|
141
140
|
save = {}
|
142
141
|
return save if resp !~ /^HTTP\/\d+\.\d+\s+(\d+)(?:\s+(.*))?/
|
143
142
|
|
144
143
|
save["http_code"] = $1.to_i
|
145
144
|
save["http_message"] = ($2 ? $2.strip : '')
|
146
145
|
save["http_raw_headers"] = {}
|
146
|
+
save.merge!(parse_headers(lines))
|
147
147
|
|
148
|
-
|
148
|
+
head, raw_body = data.split(/\r?\n\r?\n/, 2)
|
149
|
+
|
150
|
+
# Some buggy systems exclude the header entirely
|
151
|
+
raw_body ||= head
|
152
|
+
|
153
|
+
save["http_raw_body"] = [raw_body].pack("m*").gsub(/\s+/n, "")
|
154
|
+
body = raw_body
|
155
|
+
|
156
|
+
transfer_encoding = save["http_raw_headers"]["transfer-encoding"]
|
157
|
+
if transfer_encoding && transfer_encoding.include?("chunked")
|
158
|
+
offset = 0
|
159
|
+
body = ''
|
160
|
+
while (true)
|
161
|
+
# read the chunk size from where we currently are. The chunk size will
|
162
|
+
# be specified in hex, at the beginning, and is followed by \r\n.
|
163
|
+
if /^(?<chunk_size_str>[a-z0-9]+)\r\n/i =~ raw_body.slice(offset, raw_body.size)
|
164
|
+
# convert chunk size
|
165
|
+
chunk_size = chunk_size_str.to_i(16)
|
166
|
+
# advance past this chunk marker and its trailing \r\n
|
167
|
+
offset += chunk_size_str.size + 2
|
168
|
+
# read this chunk, starting from just past the chunk marker and
|
169
|
+
# stopping at the supposed end of the chunk
|
170
|
+
body << raw_body.slice(offset, chunk_size)
|
171
|
+
# advance the offset to past the end of the chunk and its trailing \r\n
|
172
|
+
offset += chunk_size + 2
|
173
|
+
else
|
174
|
+
break
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# chunked-encoding allows headers to occur after the chunks, so parse those
|
179
|
+
if offset < raw_body.size
|
180
|
+
save.merge!(parse_headers(raw_body.slice(offset, raw_body.size).split(/\r?\n/)))
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
content_encoding = save["http_raw_headers"]["content-encoding"]
|
185
|
+
if content_encoding && content_encoding.include?("gzip")
|
186
|
+
begin
|
187
|
+
gunzip = Zlib::GzipReader.new(StringIO.new(body))
|
188
|
+
body = gunzip.read.encode('UTF-8', :invalid=>:replace, :replace=>'?')
|
189
|
+
gunzip.close()
|
190
|
+
rescue
|
191
|
+
end
|
192
|
+
end
|
193
|
+
save["http_body"] = body
|
194
|
+
|
195
|
+
if body =~ /<title>([^>]+)</mi
|
196
|
+
save["http_title"] = $1.strip
|
197
|
+
end
|
198
|
+
|
199
|
+
save
|
200
|
+
end
|
201
|
+
|
202
|
+
def valid_header_name?(name)
|
203
|
+
return name !~ /[\x00-\x1f()<>@,;:\\\"\/\[\]?={}\s]/
|
204
|
+
end
|
205
|
+
|
206
|
+
def parse_headers(lines)
|
207
|
+
headers = {}
|
149
208
|
|
150
209
|
while lines.length > 0
|
151
210
|
hline = lines.shift
|
@@ -154,41 +213,41 @@ class FilterDecodeHTTPReply
|
|
154
213
|
header_name.downcase!
|
155
214
|
|
156
215
|
if valid_header_name?(header_name)
|
157
|
-
|
158
|
-
|
159
|
-
|
216
|
+
headers["http_raw_headers"] ||= {}
|
217
|
+
headers["http_raw_headers"][header_name] ||= []
|
218
|
+
headers["http_raw_headers"][header_name] << header_value
|
160
219
|
|
161
220
|
# XXX: warning, all of these mishandle duplicate headers
|
162
221
|
case header_name
|
163
222
|
when 'etag'
|
164
|
-
|
223
|
+
headers["http_etag"] = header_value
|
165
224
|
|
166
225
|
when 'set-cookie'
|
167
226
|
bits = header_value.gsub(/\;?\s*path=.*/i, '').gsub(/\;?\s*expires=.*/i, '').gsub(/\;\s*HttpOnly.*/, '')
|
168
|
-
|
227
|
+
headers["http_cookie"] = bits
|
169
228
|
|
170
229
|
when 'server'
|
171
|
-
|
230
|
+
headers["http_server"] = header_value
|
172
231
|
|
173
232
|
when 'x-powered-by'
|
174
|
-
|
233
|
+
headers["http_powered"] = header_value
|
175
234
|
|
176
235
|
when 'date'
|
177
236
|
d = DateTime.parse(header_value) rescue nil
|
178
|
-
|
237
|
+
headers["http_date"] = d.to_time.utc.strftime("%Y%m%dT%H:%M:%S%z") if d
|
179
238
|
|
180
239
|
when 'last-modified'
|
181
240
|
d = DateTime.parse(header_value) rescue nil
|
182
|
-
|
241
|
+
headers["http_modified"] = d.to_time.utc.strftime("%Y%m%dT%H:%M:%S%z") if d
|
183
242
|
|
184
243
|
when 'location'
|
185
|
-
|
244
|
+
headers["http_location"] = header_value
|
186
245
|
|
187
246
|
when 'www-authenticate'
|
188
|
-
|
247
|
+
headers["http_auth"] = header_value
|
189
248
|
|
190
249
|
when 'content-length'
|
191
|
-
|
250
|
+
headers["content-length"] = header_value.to_i
|
192
251
|
end
|
193
252
|
else
|
194
253
|
# not a valid header. XXX, eventually we should log or do something more useful here
|
@@ -198,36 +257,8 @@ class FilterDecodeHTTPReply
|
|
198
257
|
end
|
199
258
|
end
|
200
259
|
|
201
|
-
|
202
|
-
|
203
|
-
# Some buggy systems exclude the header entirely
|
204
|
-
body ||= head
|
205
|
-
|
206
|
-
save["http_raw_body"] = [body].pack("m*").gsub(/\s+/n, "")
|
207
|
-
|
208
|
-
content_encoding = save["http_raw_headers"]["content-encoding"]
|
209
|
-
|
210
|
-
if content_encoding && content_encoding.include?("gzip")
|
211
|
-
begin
|
212
|
-
gunzip = Zlib::GzipReader.new(StringIO.new(body))
|
213
|
-
body = gunzip.read.encode('UTF-8', :invalid=>:replace, :replace=>'?')
|
214
|
-
gunzip.close()
|
215
|
-
rescue
|
216
|
-
end
|
217
|
-
end
|
218
|
-
save["http_body"] = body
|
219
|
-
|
220
|
-
if body =~ /<title>([^>]+)</mi
|
221
|
-
save["http_title"] = $1.strip
|
222
|
-
end
|
223
|
-
|
224
|
-
save
|
225
|
-
end
|
226
|
-
|
227
|
-
def valid_header_name?(name)
|
228
|
-
return name !~ /[\x00-\x1f()<>@,;:\\\"\/\[\]?={}\s]/
|
260
|
+
return headers
|
229
261
|
end
|
230
262
|
end
|
231
|
-
|
232
263
|
end
|
233
264
|
end
|
data/lib/dap/version.rb
CHANGED
@@ -72,6 +72,19 @@ describe Dap::Filter::FilterDecodeHTTPReply do
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
+
context 'decoding chunked response' do
|
76
|
+
let(:body) { "5\r\nabcde\r\n0F\r\nfghijklmnopqrst\r\n06\r\nuvwxyz\r\n0\r\n" }
|
77
|
+
let(:decode) { filter.decode("HTTP/1.0 200 OK\r\nTransfer-encoding: chunked\r\n\r\n#{body}\r\nSecret: magic\r\n") }
|
78
|
+
|
79
|
+
it 'correctly dechunks body' do
|
80
|
+
expect(decode['http_body']).to eq(('a'..'z').to_a.join)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'finds trailing headers' do
|
84
|
+
expect(decode['http_raw_headers']['secret']).to eq(%w(magic))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
75
88
|
context 'decoding responses that are missing the "reason phrase", an RFC anomaly' do
|
76
89
|
let(:decode) { filter.decode("HTTP/1.1 301\r\nDate: Tue, 28 Mar 2017 20:46:52 GMT\r\nContent-Type: text/html\r\nContent-Length: 177\r\nConnection: close\r\nLocation: http://www.example.com/\r\n\r\nstuff") }
|
77
90
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rapid7 Research
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|