CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/http/packet.rb
Views: 11704
1
# -*- coding: binary -*-
2
3
4
module Rex
5
module Proto
6
module Http
7
8
DefaultProtocol = '1.1'
9
10
###
11
#
12
# This class represents an HTTP packet.
13
#
14
###
15
class Packet
16
17
#
18
# Parser processing codes
19
#
20
module ParseCode
21
Completed = 1
22
Partial = 2
23
Error = 3
24
end
25
26
#
27
# Parser states
28
#
29
module ParseState
30
ProcessingHeader = 1
31
ProcessingBody = 2
32
Completed = 3
33
end
34
35
36
#
37
# Initializes an instance of an HTTP packet.
38
#
39
def initialize()
40
self.headers = Header.new
41
self.auto_cl = true
42
43
reset
44
end
45
46
#
47
# Return the associated header value, if any.
48
#
49
def [](key)
50
if (self.headers.include?(key))
51
return self.headers[key]
52
end
53
54
self.headers.each_pair do |k,v|
55
if (k.downcase == key.downcase)
56
return v
57
end
58
end
59
60
return nil
61
end
62
63
#
64
# Set the associated header value.
65
#
66
def []=(key, value)
67
self.headers[key] = value
68
end
69
70
#
71
# Parses the supplied buffer. Returns one of the two parser processing
72
# codes (Completed, Partial, or Error).
73
#
74
# @param [String] buf The buffer to parse; possibly not a complete request/response
75
# @param [Hash] opts Parsing options
76
# @option [Boolean] orig_method The HTTP method used in an associated request, if applicable
77
def parse(buf, opts={})
78
79
# Append the incoming buffer to the buffer queue.
80
self.bufq += buf.to_s
81
82
begin
83
84
# Process the header
85
if(self.state == ParseState::ProcessingHeader)
86
parse_header(opts)
87
end
88
89
# Continue on to the body if the header was processed
90
if(self.state == ParseState::ProcessingBody)
91
# Chunked encoding sets the parsing state on its own.
92
# HEAD requests can return immediately.
93
orig_method = opts.fetch(:orig_method) { '' }
94
if (self.body_bytes_left == 0 && (!self.transfer_chunked || orig_method == 'HEAD'))
95
self.state = ParseState::Completed
96
else
97
parse_body
98
end
99
end
100
rescue
101
# XXX: BUG: This rescue might be a problem because it will swallow TimeoutError
102
self.error = $!
103
return ParseCode::Error
104
end
105
106
# Return completed or partial to the parsing status to the caller
107
(self.state == ParseState::Completed) ? ParseCode::Completed : ParseCode::Partial
108
end
109
110
#
111
# Reset the parsing state and buffers.
112
#
113
def reset
114
self.state = ParseState::ProcessingHeader
115
self.transfer_chunked = false
116
self.inside_chunk = false
117
self.headers.reset
118
self.bufq = ''
119
self.body = ''
120
end
121
122
#
123
# Reset the parsing state but leave the buffers.
124
#
125
def reset_except_queue
126
self.state = ParseState::ProcessingHeader
127
self.transfer_chunked = false
128
self.inside_chunk = false
129
self.headers.reset
130
self.body = ''
131
end
132
133
#
134
# Returns whether or not parsing has completed.
135
#
136
def completed?
137
138
return true if self.state == ParseState::Completed
139
140
# If the parser state is processing the body and there are an
141
# undetermined number of bytes left to read, we just need to say that
142
# things are completed as it's hard to tell whether or not they really
143
# are.
144
if (self.state == ParseState::ProcessingBody and self.body_bytes_left < 0)
145
return true
146
end
147
148
false
149
end
150
151
#
152
# Build a 'Transfer-Encoding: chunked' payload with random chunk sizes
153
#
154
def chunk(str, min_size = 1, max_size = 1000)
155
chunked = ''
156
157
# min chunk size is 1 byte
158
if (min_size < 1); min_size = 1; end
159
160
# don't be dumb
161
if (max_size < min_size); max_size = min_size; end
162
163
while (str.size > 0)
164
chunk = str.slice!(0, rand(max_size - min_size) + min_size)
165
chunked += sprintf("%x", chunk.size) + "\r\n" + chunk + "\r\n"
166
end
167
chunked += "0\r\n\r\n"
168
end
169
170
#
171
# Outputs a readable string of the packet for terminal output
172
#
173
def to_terminal_output(headers_only: false)
174
output_packet(true, headers_only: headers_only)
175
end
176
177
#
178
# Converts the packet to a string.
179
#
180
def to_s(headers_only: false)
181
output_packet(false, headers_only: headers_only)
182
end
183
184
#
185
# Converts the packet to a string.
186
# If ignore_chunk is set the chunked encoding is omitted (for pretty print)
187
#
188
def output_packet(ignore_chunk = false, headers_only: false)
189
content = self.body.to_s.dup
190
191
# Update the content length field in the header with the body length.
192
if (content)
193
if !self.compress.nil?
194
case self.compress
195
when 'gzip'
196
self.headers['Content-Encoding'] = 'gzip'
197
content = Rex::Text.gzip(content)
198
when 'deflate'
199
self.headers['Content-Encoding'] = 'deflate'
200
content = Rex::Text.zlib_deflate(content)
201
when 'none'
202
# this one is fine...
203
# when 'compress'
204
else
205
raise RuntimeError, 'Invalid Content-Encoding'
206
end
207
end
208
209
unless ignore_chunk
210
if self.auto_cl && self.transfer_chunked
211
raise RuntimeError, "'Content-Length' and 'Transfer-Encoding: chunked' are incompatible"
212
end
213
214
if self.auto_cl
215
self.headers['Content-Length'] = content.length
216
elsif self.transfer_chunked
217
if self.proto != '1.1'
218
raise RuntimeError, 'Chunked encoding is only available via 1.1'
219
end
220
self.headers['Transfer-Encoding'] = 'chunked'
221
content = self.chunk(content, self.chunk_min_size, self.chunk_max_size)
222
end
223
end
224
end
225
226
str = self.headers.to_s(cmd_string)
227
str += content || '' unless headers_only
228
229
str
230
end
231
232
#
233
# Converts the packet from a string.
234
#
235
def from_s(str)
236
reset
237
parse(str)
238
end
239
240
#
241
# Returns the command string, such as:
242
#
243
# HTTP/1.0 200 OK for a response
244
#
245
# or
246
#
247
# GET /foo HTTP/1.0 for a request
248
#
249
def cmd_string
250
self.headers.cmd_string
251
end
252
253
attr_accessor :headers
254
attr_accessor :error
255
attr_accessor :state
256
attr_accessor :bufq
257
attr_accessor :body
258
attr_accessor :auto_cl
259
attr_accessor :max_data
260
attr_accessor :transfer_chunked
261
attr_accessor :compress
262
attr_reader :incomplete
263
264
attr_accessor :chunk_min_size
265
attr_accessor :chunk_max_size
266
267
protected
268
269
attr_writer :incomplete
270
attr_accessor :body_bytes_left
271
attr_accessor :inside_chunk
272
attr_accessor :keepalive
273
274
##
275
#
276
# Overridable methods
277
#
278
##
279
280
#
281
# Allows derived classes to split apart the command string.
282
#
283
def update_cmd_parts(str)
284
end
285
286
##
287
#
288
# Parse the HTTP header returned by the target server.
289
#
290
# @param [Hash] opts Parsing options
291
# @option [Boolean] orig_method The HTTP method used in an associated request, if applicable
292
##
293
def parse_header(opts)
294
295
head,data = self.bufq.split(/\r?\n\r?\n/, 2)
296
297
return if data.nil?
298
299
self.headers.from_s(head)
300
self.bufq = data || ""
301
302
# Set the content-length to -1 as a placeholder (read until EOF)
303
self.body_bytes_left = -1
304
orig_method = opts.fetch(:orig_method) { '' }
305
self.body_bytes_left = 0 if orig_method == 'HEAD'
306
307
# Extract the content length if it was specified (ignoring it for HEAD requests, per RFC9110)
308
if (self.headers['Content-Length'] && orig_method != 'HEAD')
309
self.body_bytes_left = self.headers['Content-Length'].to_i
310
end
311
312
# Look for a chunked transfer header
313
if (self.headers['Transfer-Encoding'].to_s.downcase == 'chunked')
314
self.transfer_chunked = true
315
self.auto_cl = false
316
end
317
318
# Determine how to handle data when there is no length header
319
if (self.body_bytes_left == -1)
320
if (not self.transfer_chunked)
321
if (self.headers['Connection'].to_s.downcase.include?('keep-alive'))
322
# If we are using keep-alive, but have no content-length and
323
# no chunked transfer header, pretend this is the entire
324
# buffer and call it done
325
self.body_bytes_left = self.bufq.length
326
elsif (not self.headers['Content-Length'] and self.class == Rex::Proto::Http::Request)
327
# RFC 2616 says: "The presence of a message-body in a request
328
# is signaled by the inclusion of a Content-Length or
329
# Transfer-Encoding header field in the request's
330
# message-headers."
331
#
332
# So if we haven't seen either a Content-Length or a
333
# Transfer-Encoding header, there shouldn't be a message body.
334
self.body_bytes_left = 0
335
elsif (self.headers['Connection']&.downcase == 'upgrade' && self.headers['Upgrade']&.downcase == 'websocket')
336
# The server appears to be responding to a websocket request
337
self.body_bytes_left = 0
338
#else
339
# Otherwise we need to keep reading until EOF
340
end
341
end
342
end
343
344
# Throw an error if we didnt parse the header properly
345
if !self.headers.cmd_string
346
raise RuntimeError, "Invalid command string", caller
347
end
348
349
# Move the state into body processing
350
self.state = ParseState::ProcessingBody
351
352
# Allow derived classes to update the parts of the command string
353
self.update_cmd_parts(self.headers.cmd_string)
354
end
355
356
#
357
# Parses the body portion of the request.
358
#
359
def parse_body
360
# Just return if the buffer is empty
361
if (self.bufq.length == 0)
362
return
363
end
364
365
# Handle chunked transfer-encoding responses
366
if (self.transfer_chunked and self.inside_chunk != 1 and self.bufq.length)
367
368
# Remove any leading newlines or spaces
369
self.bufq.lstrip!
370
371
# If we didn't get a newline, then this might not be the full
372
# length, go back and get more.
373
# e.g.
374
# first packet: "200"
375
# second packet: "0\r\n\r\n<html>..."
376
if not bufq.index("\n")
377
return
378
end
379
380
# Extract the actual hexadecimal length value
381
clen = self.bufq.slice!(/^[a-fA-F0-9]+\r?\n/)
382
383
clen.rstrip! if (clen)
384
385
# if we happen to fall upon the end of the buffer for the next chunk len and have no data left, go get some more...
386
if clen.nil? and self.bufq.length == 0
387
return
388
end
389
390
# Invalid chunk length, exit out early
391
if clen.nil?
392
self.state = ParseState::Completed
393
return
394
end
395
396
self.body_bytes_left = clen.to_i(16)
397
398
if (self.body_bytes_left == 0)
399
self.bufq.sub!(/^\r?\n/s,'')
400
self.state = ParseState::Completed
401
self.check_100
402
return
403
end
404
405
self.inside_chunk = 1
406
end
407
408
# If there are bytes remaining, slice as many as we can and append them
409
# to our body state.
410
if (self.body_bytes_left > 0)
411
part = self.bufq.slice!(0, self.body_bytes_left)
412
self.body += part
413
self.body_bytes_left -= part.length
414
# Otherwise, just read it all.
415
else
416
self.body += self.bufq
417
self.bufq = ''
418
end
419
420
# Finish this chunk and move on to the next one
421
if (self.transfer_chunked and self.body_bytes_left == 0)
422
self.inside_chunk = 0
423
self.parse_body
424
return
425
end
426
427
# If there are no more bytes left, then parsing has completed and we're
428
# ready to go.
429
if (not self.transfer_chunked and self.body_bytes_left == 0)
430
self.state = ParseState::Completed
431
self.check_100
432
return
433
end
434
end
435
436
# Override this as needed
437
def check_100
438
end
439
440
end
441
442
end
443
end
444
end
445
446