Class: Rex::Proto::Http::Packet

Inherits:
Object
  • Object
show all
Defined in:
lib/rex/proto/http/packet.rb

Overview

This class represents an HTTP packet.

Direct Known Subclasses

Request, Response

Defined Under Namespace

Modules: ParseCode, ParseState Classes: Header

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializePacket

Initializes an instance of an HTTP packet.



39
40
41
42
43
44
# File 'lib/rex/proto/http/packet.rb', line 39

def initialize()
  self.headers = Header.new
  self.auto_cl = true

  reset
end

Instance Attribute Details

#auto_clObject

Returns the value of attribute auto_cl.



274
275
276
# File 'lib/rex/proto/http/packet.rb', line 274

def auto_cl
  @auto_cl
end

#body_bytes_leftObject (protected)

Returns the value of attribute body_bytes_left.



286
287
288
# File 'lib/rex/proto/http/packet.rb', line 286

def body_bytes_left
  @body_bytes_left
end

#bufqObject

Returns the value of attribute bufq.



273
274
275
# File 'lib/rex/proto/http/packet.rb', line 273

def bufq
  @bufq
end

#chunk_max_sizeObject

Returns the value of attribute chunk_max_size.



281
282
283
# File 'lib/rex/proto/http/packet.rb', line 281

def chunk_max_size
  @chunk_max_size
end

#chunk_min_sizeObject

Returns the value of attribute chunk_min_size.



280
281
282
# File 'lib/rex/proto/http/packet.rb', line 280

def chunk_min_size
  @chunk_min_size
end

#compressObject

Returns the value of attribute compress.



277
278
279
# File 'lib/rex/proto/http/packet.rb', line 277

def compress
  @compress
end

#errorObject

Returns the value of attribute error.



271
272
273
# File 'lib/rex/proto/http/packet.rb', line 271

def error
  @error
end

#headersObject

Returns the value of attribute headers.



270
271
272
# File 'lib/rex/proto/http/packet.rb', line 270

def headers
  @headers
end

#incompleteObject

Returns the value of attribute incomplete.



278
279
280
# File 'lib/rex/proto/http/packet.rb', line 278

def incomplete
  @incomplete
end

#inside_chunkObject (protected)

Returns the value of attribute inside_chunk.



287
288
289
# File 'lib/rex/proto/http/packet.rb', line 287

def inside_chunk
  @inside_chunk
end

#keepaliveObject (protected)

Returns the value of attribute keepalive.



288
289
290
# File 'lib/rex/proto/http/packet.rb', line 288

def keepalive
  @keepalive
end

#max_dataObject

Returns the value of attribute max_data.



275
276
277
# File 'lib/rex/proto/http/packet.rb', line 275

def max_data
  @max_data
end

#stateObject

Returns the value of attribute state.



272
273
274
# File 'lib/rex/proto/http/packet.rb', line 272

def state
  @state
end

#transfer_chunkedObject

Returns the value of attribute transfer_chunked.



276
277
278
# File 'lib/rex/proto/http/packet.rb', line 276

def transfer_chunked
  @transfer_chunked
end

Instance Method Details

#[](key) ⇒ Object

Return the associated header value, if any.



49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rex/proto/http/packet.rb', line 49

def [](key)
  if (self.headers.include?(key))
    return self.headers[key]
  end

  self.headers.each_pair do |k,v|
    if (k.downcase == key.downcase)
      return v
    end
  end

  return nil
end

#[]=(key, value) ⇒ Object

Set the associated header value.



66
67
68
# File 'lib/rex/proto/http/packet.rb', line 66

def []=(key, value)
  self.headers[key] = value
end

#bodyObject



83
84
85
# File 'lib/rex/proto/http/packet.rb', line 83

def body
  @body_bytes
end

#body=(val) ⇒ Object

The ‘body` attribute was overridden by subclasses, causing quirky behaviour issues, such as when a POST request contained query string parameters resulting in the query string being prepended to the data that was contained in the body. To avoid this utterly ridiculous behaviour while maintaning the status-quo of having the request class shoe-horn query strings into POST bodies, we’re using ‘body_bytes` as an internal buffer to collect the request’s body, rather than using the attribute, which prevents this insanity from happening.



79
80
81
# File 'lib/rex/proto/http/packet.rb', line 79

def body=(val)
  @body_bytes = val
end

#check_100Object (protected)

Override this as needed



453
454
# File 'lib/rex/proto/http/packet.rb', line 453

def check_100
end

#chunk(str, min_size = 1, max_size = 1000) ⇒ Object

Build a ‘Transfer-Encoding: chunked’ payload with random chunk sizes



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/rex/proto/http/packet.rb', line 171

def chunk(str, min_size = 1, max_size = 1000)
  chunked = ''

  # min chunk size is 1 byte
  if (min_size < 1); min_size = 1; end

  # don't be dumb
  if (max_size < min_size); max_size = min_size; end

  while (str.size > 0)
    chunk = str.slice!(0, rand(max_size - min_size) + min_size)
    chunked += sprintf("%x", chunk.size) + "\r\n" + chunk + "\r\n"
  end
  chunked += "0\r\n\r\n"
end

#cmd_stringObject

Returns the command string, such as:

HTTP/1.0 200 OK for a response

or

GET /foo HTTP/1.0 for a request



266
267
268
# File 'lib/rex/proto/http/packet.rb', line 266

def cmd_string
  self.headers.cmd_string
end

#completed?Boolean

Returns whether or not parsing has completed.

Returns:

  • (Boolean)


153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/rex/proto/http/packet.rb', line 153

def completed?

  return true if self.state == ParseState::Completed

  # If the parser state is processing the body and there are an
  # undetermined number of bytes left to read, we just need to say that
  # things are completed as it's hard to tell whether or not they really
  # are.
  if (self.state == ParseState::ProcessingBody and self.body_bytes_left < 0)
    return true
  end

  false
end

#from_s(str) ⇒ Object

Converts the packet from a string.



252
253
254
255
# File 'lib/rex/proto/http/packet.rb', line 252

def from_s(str)
  reset
  parse(str)
end

#output_packet(ignore_chunk = false, headers_only: false) ⇒ Object

Converts the packet to a string. If ignore_chunk is set the chunked encoding is omitted (for pretty print)



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/rex/proto/http/packet.rb', line 205

def output_packet(ignore_chunk = false, headers_only: false)
  content = self.body.to_s.dup

  # Update the content length field in the header with the body length.
  if (content)
    if !self.compress.nil?
      case self.compress
        when 'gzip'
          self.headers['Content-Encoding'] = 'gzip'
          content = Rex::Text.gzip(content)
        when 'deflate'
          self.headers['Content-Encoding'] = 'deflate'
          content = Rex::Text.zlib_deflate(content)
        when 'none'
        # this one is fine...
        # when 'compress'
        else
          raise RuntimeError, 'Invalid Content-Encoding'
      end
    end

    unless ignore_chunk
      if self.auto_cl && self.transfer_chunked
        raise RuntimeError, "'Content-Length' and 'Transfer-Encoding: chunked' are incompatible"
      end

      if self.auto_cl
        self.headers['Content-Length'] = content.length
      elsif self.transfer_chunked
        if self.proto != '1.1'
          raise RuntimeError, 'Chunked encoding is only available via 1.1'
        end
        self.headers['Transfer-Encoding'] = 'chunked'
        content = self.chunk(content, self.chunk_min_size, self.chunk_max_size)
      end
    end
  end

  str  = self.headers.to_s(cmd_string)
  str += content || '' unless headers_only

  str
end

#parse(buf, opts = {}) ⇒ Object

Parses the supplied buffer. Returns one of the two parser processing codes (Completed, Partial, or Error).

Parameters:

  • buf (String)

    The buffer to parse; possibly not a complete request/response

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

    Parsing options

  • [Boolean] (Hash)

    a customizable set of options



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
# File 'lib/rex/proto/http/packet.rb', line 94

def parse(buf, opts={})

  # Append the incoming buffer to the buffer queue.
  self.bufq += buf.to_s

  begin

    # Process the header
    if(self.state == ParseState::ProcessingHeader)
      parse_header(opts)
    end

    # Continue on to the body if the header was processed
    if(self.state == ParseState::ProcessingBody)
      # Chunked encoding sets the parsing state on its own.
      # HEAD requests can return immediately.
      orig_method = opts.fetch(:orig_method) { '' }
      if (self.body_bytes_left == 0 && (!self.transfer_chunked || orig_method == 'HEAD'))
        self.state = ParseState::Completed
      else
        parse_body
      end
    end
  rescue
    # XXX: BUG: This rescue might be a problem because it will swallow TimeoutError
    self.error = $!
    return ParseCode::Error
  end

  # Return completed or partial to the parsing status to the caller
  (self.state == ParseState::Completed) ? ParseCode::Completed : ParseCode::Partial
end

#parse_bodyObject (protected)

Parses the body portion of the request.



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/rex/proto/http/packet.rb', line 375

def parse_body
  # Just return if the buffer is empty
  if (self.bufq.length == 0)
    return
  end

  # Handle chunked transfer-encoding responses
  if (self.transfer_chunked and self.inside_chunk != 1 and self.bufq.length)

    # Remove any leading newlines or spaces
    self.bufq.lstrip!

    # If we didn't get a newline, then this might not be the full
    # length, go back and get more.
    # e.g.
    #  first packet: "200"
    #  second packet: "0\r\n\r\n<html>..."
    if not bufq.index("\n")
      return
    end

    # Extract the actual hexadecimal length value
    clen = self.bufq.slice!(/^[a-fA-F0-9]+\r?\n/)

    clen.rstrip! if (clen)

    # 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...
    if clen.nil? and self.bufq.length == 0
      return
    end

    # Invalid chunk length, exit out early
    if clen.nil?
      self.state = ParseState::Completed
      return
    end

    self.body_bytes_left = clen.to_i(16)

    if (self.body_bytes_left == 0)
      self.bufq.sub!(/^\r?\n/s,'')
      self.state = ParseState::Completed
      self.check_100
      return
    end

    self.inside_chunk = 1
  end

  # If there are bytes remaining, slice as many as we can and append them
  # to our body state.
  if (self.body_bytes_left > 0)
    part = self.bufq.slice!(0, self.body_bytes_left)
    @body_bytes += part
    self.body_bytes_left -= part.length
  # Otherwise, just read it all.
  else
    @body_bytes += self.bufq
    self.bufq  = ''
  end

  # Finish this chunk and move on to the next one
  if (self.transfer_chunked and self.body_bytes_left == 0)
    self.inside_chunk = 0
    self.parse_body
    return
  end

  # If there are no more bytes left, then parsing has completed and we're
  # ready to go.
  if (not self.transfer_chunked and self.body_bytes_left == 0)
    self.state = ParseState::Completed
    self.check_100
    return
  end
end

#parse_header(opts) ⇒ Object (protected)

Parse the HTTP header returned by the target server.

Parameters:

  • opts (Hash)

    Parsing options

  • [Boolean] (Hash)

    a customizable set of options



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/rex/proto/http/packet.rb', line 309

def parse_header(opts)

  head,data = self.bufq.split(/\r?\n\r?\n/, 2)

  return if data.nil?

  self.headers.from_s(head)
  self.bufq = data || ""

  # Set the content-length to -1 as a placeholder (read until EOF)
  self.body_bytes_left = -1
  orig_method = opts.fetch(:orig_method) { '' }
  self.body_bytes_left = 0 if orig_method == 'HEAD'

  # Extract the content length if it was specified (ignoring it for HEAD requests, per RFC9110)
  if (self.headers['Content-Length'] && orig_method != 'HEAD')
    self.body_bytes_left = self.headers['Content-Length'].to_i
  end

  # Look for a chunked transfer header
  if (self.headers['Transfer-Encoding'].to_s.downcase == 'chunked')
    self.transfer_chunked = true
    self.auto_cl = false
  end

  # Determine how to handle data when there is no length header
  if (self.body_bytes_left == -1)
    if (not self.transfer_chunked)
      if (self.headers['Connection'].to_s.downcase.include?('keep-alive'))
        # If we are using keep-alive, but have no content-length and
        # no chunked transfer header, pretend this is the entire
        # buffer and call it done
        self.body_bytes_left = self.bufq.length
      elsif (not self.headers['Content-Length'] and self.class == Rex::Proto::Http::Request)
        # RFC 2616 says: "The presence of a message-body in a request
        # is signaled by the inclusion of a Content-Length or
        # Transfer-Encoding header field in the request's
        # message-headers."
        #
        # So if we haven't seen either a Content-Length or a
        # Transfer-Encoding header, there shouldn't be a message body.
        self.body_bytes_left = 0
      elsif (self.headers['Connection']&.downcase == 'upgrade' && self.headers['Upgrade']&.downcase == 'websocket')
        # The server appears to be responding to a websocket request
        self.body_bytes_left = 0
      #else
      # Otherwise we need to keep reading until EOF
      end
    end
  end

  # Throw an error if we didnt parse the header properly
  if !self.headers.cmd_string
    raise RuntimeError, "Invalid command string", caller
  end

  # Move the state into body processing
  self.state = ParseState::ProcessingBody

  # Allow derived classes to update the parts of the command string
  self.update_cmd_parts(self.headers.cmd_string)
end

#resetObject

Reset the parsing state and buffers.



130
131
132
133
134
135
136
137
# File 'lib/rex/proto/http/packet.rb', line 130

def reset
  self.state = ParseState::ProcessingHeader
  self.transfer_chunked = false
  self.inside_chunk     = false
  self.headers.reset
  self.bufq  = ''
  @body_bytes  = ''
end

#reset_except_queueObject

Reset the parsing state but leave the buffers.



142
143
144
145
146
147
148
# File 'lib/rex/proto/http/packet.rb', line 142

def reset_except_queue
  self.state = ParseState::ProcessingHeader
  self.transfer_chunked = false
  self.inside_chunk     = false
  self.headers.reset
  @body_bytes  = ''
end

#to_s(headers_only: false) ⇒ Object

Converts the packet to a string.



197
198
199
# File 'lib/rex/proto/http/packet.rb', line 197

def to_s(headers_only: false)
  output_packet(false, headers_only: headers_only)
end

#to_terminal_output(headers_only: false) ⇒ Object

Outputs a readable string of the packet for terminal output



190
191
192
# File 'lib/rex/proto/http/packet.rb', line 190

def to_terminal_output(headers_only: false)
  output_packet(true, headers_only: headers_only)
end

#update_cmd_parts(str) ⇒ Object (protected)

Allows derived classes to split apart the command string.



299
300
# File 'lib/rex/proto/http/packet.rb', line 299

def update_cmd_parts(str)
end