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.



258
259
260
# File 'lib/rex/proto/http/packet.rb', line 258

def auto_cl
  @auto_cl
end

#bodyObject

Returns the value of attribute body.



257
258
259
# File 'lib/rex/proto/http/packet.rb', line 257

def body
  @body
end

#body_bytes_leftObject (protected)

Returns the value of attribute body_bytes_left.



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

def body_bytes_left
  @body_bytes_left
end

#bufqObject

Returns the value of attribute bufq.



256
257
258
# File 'lib/rex/proto/http/packet.rb', line 256

def bufq
  @bufq
end

#chunk_max_sizeObject

Returns the value of attribute chunk_max_size.



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

def chunk_max_size
  @chunk_max_size
end

#chunk_min_sizeObject

Returns the value of attribute chunk_min_size.



264
265
266
# File 'lib/rex/proto/http/packet.rb', line 264

def chunk_min_size
  @chunk_min_size
end

#compressObject

Returns the value of attribute compress.



261
262
263
# File 'lib/rex/proto/http/packet.rb', line 261

def compress
  @compress
end

#errorObject

Returns the value of attribute error.



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

def error
  @error
end

#headersObject

Returns the value of attribute headers.



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

def headers
  @headers
end

#incompleteObject

Returns the value of attribute incomplete.



262
263
264
# File 'lib/rex/proto/http/packet.rb', line 262

def incomplete
  @incomplete
end

#inside_chunkObject (protected)

Returns the value of attribute inside_chunk.



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

def inside_chunk
  @inside_chunk
end

#keepaliveObject (protected)

Returns the value of attribute keepalive.



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

def keepalive
  @keepalive
end

#max_dataObject

Returns the value of attribute max_data.



259
260
261
# File 'lib/rex/proto/http/packet.rb', line 259

def max_data
  @max_data
end

#stateObject

Returns the value of attribute state.



255
256
257
# File 'lib/rex/proto/http/packet.rb', line 255

def state
  @state
end

#transfer_chunkedObject

Returns the value of attribute transfer_chunked.



260
261
262
# File 'lib/rex/proto/http/packet.rb', line 260

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

#check_100Object (protected)

Override this as needed



437
438
# File 'lib/rex/proto/http/packet.rb', line 437

def check_100
end

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

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



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

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



249
250
251
# File 'lib/rex/proto/http/packet.rb', line 249

def cmd_string
  self.headers.cmd_string
end

#completed?Boolean

Returns whether or not parsing has completed.

Returns:

  • (Boolean)


136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/rex/proto/http/packet.rb', line 136

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.



235
236
237
238
# File 'lib/rex/proto/http/packet.rb', line 235

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)



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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/rex/proto/http/packet.rb', line 188

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



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
102
103
104
105
106
107
108
# File 'lib/rex/proto/http/packet.rb', line 77

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.



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
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
# File 'lib/rex/proto/http/packet.rb', line 359

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)
    self.body += part
    self.body_bytes_left -= part.length
  # Otherwise, just read it all.
  else
    self.body += 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



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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
# File 'lib/rex/proto/http/packet.rb', line 293

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.



113
114
115
116
117
118
119
120
# File 'lib/rex/proto/http/packet.rb', line 113

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

#reset_except_queueObject

Reset the parsing state but leave the buffers.



125
126
127
128
129
130
131
# File 'lib/rex/proto/http/packet.rb', line 125

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

#to_s(headers_only: false) ⇒ Object

Converts the packet to a string.



180
181
182
# File 'lib/rex/proto/http/packet.rb', line 180

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



173
174
175
# File 'lib/rex/proto/http/packet.rb', line 173

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.



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

def update_cmd_parts(str)
end