503 lines
15 KiB
Ruby
503 lines
15 KiB
Ruby
|
#!/usr/bin/env ruby
|
||
|
|
||
|
module StructFu
|
||
|
|
||
|
# Set the endianness for the various Int classes. Takes either :little or :big.
|
||
|
def set_endianness(e=nil)
|
||
|
unless [:little, :big].include? e
|
||
|
raise ArgumentError, "Unknown endianness for #{self.class}"
|
||
|
end
|
||
|
@int32 = e == :little ? Int32le : Int32be
|
||
|
@int16 = e == :little ? Int16le : Int16be
|
||
|
return e
|
||
|
end
|
||
|
|
||
|
# Instead of returning the "size" of the object, which is usually the
|
||
|
# number of elements of the Struct, returns the size of the object after
|
||
|
# a to_s. Essentially, a short version of self.to_size.size
|
||
|
def sz
|
||
|
self.to_s.size
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
module PacketFu
|
||
|
|
||
|
# PcapHeader represents the header portion of a libpcap file (the packets
|
||
|
# themselves are in the PcapPackets array). See
|
||
|
# http://wiki.wireshark.org/Development/LibpcapFileFormat for details.
|
||
|
#
|
||
|
# Depending on the endianness (set with :endian), elements are either
|
||
|
# :little endian or :big endian.
|
||
|
#
|
||
|
# ==== PcapHeader Definition
|
||
|
#
|
||
|
# Symbol :endian Default: :little
|
||
|
# Int32 :magic Default: 0xa1b2c3d4 # :big is 0xd4c3b2a1
|
||
|
# Int16 :ver_major Default: 2
|
||
|
# Int16 :ver_minor Default: 4
|
||
|
# Int32 :thiszone
|
||
|
# Int32 :sigfigs
|
||
|
# Int32 :snaplen Default: 0xffff
|
||
|
# Int32 :network Default: 1
|
||
|
class PcapHeader < Struct.new(:endian, :magic, :ver_major, :ver_minor,
|
||
|
:thiszone, :sigfigs, :snaplen, :network)
|
||
|
include StructFu
|
||
|
|
||
|
def initialize(args={})
|
||
|
set_endianness(args[:endian] ||= :little)
|
||
|
init_fields(args)
|
||
|
super(args[:endian], args[:magic], args[:ver_major],
|
||
|
args[:ver_minor], args[:thiszone], args[:sigfigs],
|
||
|
args[:snaplen], args[:network])
|
||
|
end
|
||
|
|
||
|
# Called by initialize to set the initial fields.
|
||
|
def init_fields(args={})
|
||
|
args[:magic] = @int32.new(args[:magic] || 0xa1b2c3d4)
|
||
|
args[:ver_major] = @int16.new(args[:ver_major] || 2)
|
||
|
args[:ver_minor] ||= @int16.new(args[:ver_minor] || 4)
|
||
|
args[:thiszone] ||= @int32.new(args[:thiszone])
|
||
|
args[:sigfigs] ||= @int32.new(args[:sigfigs])
|
||
|
args[:snaplen] ||= @int32.new(args[:snaplen] || 0xffff)
|
||
|
args[:network] ||= @int32.new(args[:network] || 1)
|
||
|
return args
|
||
|
end
|
||
|
|
||
|
# Returns the object in string form.
|
||
|
def to_s
|
||
|
self.to_a[1,7].map {|x| x.to_s}.join
|
||
|
end
|
||
|
|
||
|
# Reads a string to populate the object.
|
||
|
# TODO: Need to test this by getting a hold of a big endian pcap file.
|
||
|
# Conversion from big to little shouldn't be that big of a deal.
|
||
|
def read(str)
|
||
|
force_binary(str)
|
||
|
return self if str.nil?
|
||
|
str.force_encoding("binary") if str.respond_to? :force_encoding
|
||
|
if str[0,4] == self[:magic].to_s || true # TODO: raise if it's not magic.
|
||
|
self[:magic].read str[0,4]
|
||
|
self[:ver_major].read str[4,2]
|
||
|
self[:ver_minor].read str[6,2]
|
||
|
self[:thiszone].read str[8,4]
|
||
|
self[:sigfigs].read str[12,4]
|
||
|
self[:snaplen].read str[16,4]
|
||
|
self[:network].read str[20,4]
|
||
|
end
|
||
|
self
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
# The Timestamp class defines how Timestamps appear in libpcap files.
|
||
|
#
|
||
|
# ==== Header Definition
|
||
|
#
|
||
|
# Symbol :endian Default: :little
|
||
|
# Int32 :sec
|
||
|
# Int32 :usec
|
||
|
class Timestamp < Struct.new(:endian, :sec, :usec)
|
||
|
include StructFu
|
||
|
|
||
|
def initialize(args={})
|
||
|
set_endianness(args[:endian] ||= :little)
|
||
|
init_fields(args)
|
||
|
super(args[:endian], args[:sec], args[:usec])
|
||
|
end
|
||
|
|
||
|
# Called by initialize to set the initial fields.
|
||
|
def init_fields(args={})
|
||
|
args[:sec] = @int32.new(args[:sec])
|
||
|
args[:usec] = @int32.new(args[:usec])
|
||
|
return args
|
||
|
end
|
||
|
|
||
|
# Returns the object in string form.
|
||
|
def to_s
|
||
|
self.to_a[1,2].map {|x| x.to_s}.join
|
||
|
end
|
||
|
|
||
|
# Reads a string to populate the object.
|
||
|
def read(str)
|
||
|
force_binary(str)
|
||
|
return self if str.nil?
|
||
|
self[:sec].read str[0,4]
|
||
|
self[:usec].read str[4,4]
|
||
|
self
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
# PcapPacket defines how individual packets are stored in a libpcap-formatted
|
||
|
# file.
|
||
|
#
|
||
|
# ==== Header Definition
|
||
|
#
|
||
|
# Timestamp :timestamp
|
||
|
# Int32 :incl_len
|
||
|
# Int32 :orig_len
|
||
|
# String :data
|
||
|
class PcapPacket < Struct.new(:endian, :timestamp, :incl_len,
|
||
|
:orig_len, :data)
|
||
|
include StructFu
|
||
|
def initialize(args={})
|
||
|
set_endianness(args[:endian] ||= :little)
|
||
|
init_fields(args)
|
||
|
super(args[:endian], args[:timestamp], args[:incl_len],
|
||
|
args[:orig_len], args[:data])
|
||
|
end
|
||
|
|
||
|
# Called by initialize to set the initial fields.
|
||
|
def init_fields(args={})
|
||
|
args[:timestamp] = Timestamp.new(:endian => args[:endian]).read(args[:timestamp])
|
||
|
args[:incl_len] = args[:incl_len].nil? ? @int32.new(args[:data].to_s.size) : @int32.new(args[:incl_len])
|
||
|
args[:orig_len] = @int32.new(args[:orig_len])
|
||
|
args[:data] = StructFu::String.new.read(args[:data])
|
||
|
end
|
||
|
|
||
|
# Returns the object in string form.
|
||
|
def to_s
|
||
|
self.to_a[1,4].map {|x| x.to_s}.join
|
||
|
end
|
||
|
|
||
|
# Reads a string to populate the object.
|
||
|
def read(str)
|
||
|
force_binary(str)
|
||
|
self[:timestamp].read str[0,8]
|
||
|
self[:incl_len].read str[8,4]
|
||
|
self[:orig_len].read str[12,4]
|
||
|
self[:data].read str[16,self[:incl_len].to_i]
|
||
|
self
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
# PcapPackets is a collection of PcapPacket objects.
|
||
|
class PcapPackets < Array
|
||
|
|
||
|
include StructFu
|
||
|
|
||
|
attr_accessor :endian # probably ought to be read-only but who am i.
|
||
|
|
||
|
def initialize(args={})
|
||
|
@endian = args[:endian] || :little
|
||
|
end
|
||
|
|
||
|
def force_binary(str)
|
||
|
str.force_encoding "binary" if str.respond_to? :force_encoding
|
||
|
end
|
||
|
|
||
|
# Reads a string to populate the object. Note, this read takes in the
|
||
|
# whole pcap file, since we need to see the magic to know what
|
||
|
# endianness we're dealing with.
|
||
|
def read(str)
|
||
|
force_binary(str)
|
||
|
return self if str.nil?
|
||
|
magic = "\xa1\xb2\xc3\xd4"
|
||
|
if str[0,4] == magic
|
||
|
@endian = :big
|
||
|
elsif str[0,4] == magic.reverse
|
||
|
@endian = :little
|
||
|
else
|
||
|
raise ArgumentError, "Unknown file format for #{self.class}"
|
||
|
end
|
||
|
body = str[24,str.size]
|
||
|
while body.size > 16 # TODO: catch exceptions on malformed packets at end
|
||
|
p = PcapPacket.new(:endian => @endian)
|
||
|
p.read(body)
|
||
|
self<<p
|
||
|
body = body[p.sz,body.size]
|
||
|
end
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def to_s
|
||
|
self.join
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
# PcapFile is a complete libpcap file struct, made up of two elements, a
|
||
|
# PcapHeader and PcapPackets.
|
||
|
#
|
||
|
# See http://wiki.wireshark.org/Development/LibpcapFileFormat
|
||
|
class PcapFile < Struct.new(:endian, :head, :body)
|
||
|
include StructFu
|
||
|
|
||
|
def initialize(args={})
|
||
|
init_fields(args)
|
||
|
super(args[:endian], args[:head], args[:body])
|
||
|
end
|
||
|
|
||
|
# Called by initialize to set the initial fields.
|
||
|
def init_fields(args={})
|
||
|
args[:head] = PcapHeader.new(:endian => args[:endian]).read(args[:head])
|
||
|
args[:body] = PcapPackets.new(:endian => args[:endian]).read(args[:body])
|
||
|
return args
|
||
|
end
|
||
|
|
||
|
# Returns the object in string form.
|
||
|
def to_s
|
||
|
self[:head].to_s + self[:body].map {|p| p.to_s}.join
|
||
|
end
|
||
|
|
||
|
# Clears the contents of the PcapFile.
|
||
|
def clear
|
||
|
self[:body].clear
|
||
|
end
|
||
|
|
||
|
# Reads a string to populate the object. Note that this appends new packets to
|
||
|
# any existing packets in the PcapFile.
|
||
|
def read(str)
|
||
|
force_binary(str)
|
||
|
self[:head].read str[0,24]
|
||
|
self[:body].read str
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# Clears the contents of the PcapFile prior to reading in a new string.
|
||
|
def read!(str)
|
||
|
clear
|
||
|
self.read str
|
||
|
end
|
||
|
|
||
|
# A shorthand method for opening a file and reading in the packets. Note
|
||
|
# that readfile clears any existing packets, since that seems to be the
|
||
|
# typical use.
|
||
|
def readfile(file)
|
||
|
fdata = File.open(file, "rb") {|f| f.read}
|
||
|
self.read! fdata
|
||
|
end
|
||
|
|
||
|
# file_to_array() translates a libpcap file into an array of packets.
|
||
|
# Note that this strips out pcap timestamps -- if you'd like to retain
|
||
|
# timestamps and other libpcap file information, you will want to
|
||
|
# use read() instead.
|
||
|
#
|
||
|
# Note, invoking this requires the somewhat clumsy sytax of,
|
||
|
# PcapFile.new.file_to_array(:f => 'filename.pcap')
|
||
|
def file_to_array(args={})
|
||
|
filename = args[:filename] || args[:file] || args[:f]
|
||
|
if filename
|
||
|
self.read! File.open(filename, "rb") {|f| f.read}
|
||
|
end
|
||
|
if args[:keep_timestamps] || args[:keep_ts] || args[:ts]
|
||
|
self[:body].map {|x| {x.timestamp.to_s => x.data.to_s} }
|
||
|
else
|
||
|
self[:body].map {|x| x.data.to_s}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
alias_method :f2a, :file_to_array
|
||
|
|
||
|
# Takes an array of packets (as generated by file_to_array), and writes them
|
||
|
# to a file. Valid arguments are:
|
||
|
#
|
||
|
# :filename
|
||
|
# :array # Can either be an array of packet data, or a hash-value pair of timestamp => data.
|
||
|
# :timestamp # Sets an initial timestamp
|
||
|
# :ts_inc # Sets the increment between timestamps. Defaults to 1 second.
|
||
|
# :append # If true, then the packets are appended to the end of a file.
|
||
|
def array_to_file(args={})
|
||
|
if args.kind_of? Hash
|
||
|
filename = args[:filename] || args[:file] || args[:f]
|
||
|
arr = args[:array] || args[:arr] || args[:a]
|
||
|
ts = args[:timestamp] || args[:ts] || Time.now.to_i
|
||
|
ts_inc = args[:timestamp_increment] || args[:ts_inc] || 1
|
||
|
append = !!args[:append]
|
||
|
elsif args.kind_of? Array
|
||
|
arr = args
|
||
|
filename = append = nil
|
||
|
else
|
||
|
raise ArgumentError, "Unknown argument. Need either a Hash or Array."
|
||
|
end
|
||
|
unless arr.kind_of? Array
|
||
|
raise ArgumentError, "Need an array to read packets from"
|
||
|
end
|
||
|
arr.each_with_index do |p,i|
|
||
|
if p.kind_of? Hash # Binary timestamps are included
|
||
|
this_ts = p.keys.first
|
||
|
this_incl_len = p.values.first.size
|
||
|
this_orig_len = this_incl_len
|
||
|
this_data = p.values.first
|
||
|
else # it's an array
|
||
|
this_ts = Timestamp.new(:endian => self[:endian], :sec => ts + (ts_inc * i)).to_s
|
||
|
this_incl_len = p.to_s.size
|
||
|
this_orig_len = this_incl_len
|
||
|
this_data = p.to_s
|
||
|
end
|
||
|
this_pkt = PcapPacket.new({:endian => self[:endian],
|
||
|
:timestamp => this_ts,
|
||
|
:incl_len => this_incl_len,
|
||
|
:orig_len => this_orig_len,
|
||
|
:data => this_data }
|
||
|
)
|
||
|
self[:body] << this_pkt
|
||
|
end
|
||
|
if filename
|
||
|
self.to_f(:filename => filename, :append => append)
|
||
|
else
|
||
|
self
|
||
|
end
|
||
|
end
|
||
|
|
||
|
alias_method :a2f, :array_to_file
|
||
|
|
||
|
# Just like array_to_file, but clears any existing packets from the array first.
|
||
|
def array_to_file!(arr)
|
||
|
clear
|
||
|
array_to_file(arr)
|
||
|
end
|
||
|
|
||
|
alias_method :a2f!, :array_to_file!
|
||
|
|
||
|
# Writes the PcapFile to a file. Takes the following arguments:
|
||
|
#
|
||
|
# :filename # The file to write to.
|
||
|
# :append # If set to true, the packets are appended to the file, rather than overwriting.
|
||
|
def to_file(args={})
|
||
|
filename = args[:filename] || args[:file] || args[:f]
|
||
|
unless (!filename.nil? || filename.kind_of?(String))
|
||
|
raise ArgumentError, "Need a :filename for #{self.class}"
|
||
|
end
|
||
|
append = args[:append]
|
||
|
if append
|
||
|
File.open(filename,'ab') {|file| file.write(self.body.to_s)}
|
||
|
else
|
||
|
File.open(filename,'wb') {|file| file.write(self.to_s)}
|
||
|
end
|
||
|
[filename, self.body.sz, self.body.size]
|
||
|
end
|
||
|
|
||
|
alias_method :to_f, :to_file
|
||
|
|
||
|
# Shorthand method for writing to a file. Can take either :file => 'name.pcap' or
|
||
|
# simply 'name.pcap'
|
||
|
def write(filename='out.pcap')
|
||
|
if filename.kind_of?(Hash)
|
||
|
f = filename[:filename] || filename[:file] || filename[:f] || 'out.pcap'
|
||
|
else
|
||
|
f = filename.to_s
|
||
|
end
|
||
|
self.to_file(:filename => f.to_s, :append => false)
|
||
|
end
|
||
|
|
||
|
# Shorthand method for appending to a file. Can take either :file => 'name.pcap' or
|
||
|
# simply 'name.pcap'
|
||
|
def append(filename='out.pcap')
|
||
|
if filename.kind_of?(Hash)
|
||
|
f = filename[:filename] || filename[:file] || filename[:f] || 'out.pcap'
|
||
|
else
|
||
|
f = filename.to_s
|
||
|
end
|
||
|
self.to_file(:filename => f, :append => true)
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
module PacketFu
|
||
|
|
||
|
# Read is largely deprecated. It was current in PacketFu 0.2.0, but isn't all that useful
|
||
|
# in 0.3.0 and beyond. Expect it to go away completely by version 1.0. So, the main use
|
||
|
# of this class is to learn how to do exactly the same things using the PcapFile object.
|
||
|
class Read
|
||
|
|
||
|
class << self
|
||
|
|
||
|
# Reads the magic string of a pcap file, and determines
|
||
|
# if it's :little or :big endian.
|
||
|
def get_byte_order(pcap_file)
|
||
|
byte_order = ((pcap_file[0,4] == "\xd4\xc3\xb2\xa1") ? :little : :big)
|
||
|
return byte_order
|
||
|
end
|
||
|
|
||
|
# set_byte_order is pretty much totally deprecated.
|
||
|
def set_byte_order(byte_order)
|
||
|
PacketFu.instance_variable_set("@byte_order",byte_order)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
# A wrapper for PcapFile#file_to_array, but only returns the array. Actually
|
||
|
# using the PcapFile object is going to be more useful.
|
||
|
def file_to_array(args={})
|
||
|
filename = args[:filename] || args[:file] || args[:out]
|
||
|
raise ArgumentError, "Need a :filename in string form to read from." if (filename.nil? || filename.class != String)
|
||
|
PcapFile.new.file_to_array(args)
|
||
|
end
|
||
|
|
||
|
alias_method :f2a, :file_to_array
|
||
|
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
module PacketFu
|
||
|
|
||
|
# Write is largely deprecated. It was current in PacketFu 0.2.0, but isn't all that useful
|
||
|
# in 0.3.0 and beyond. Expect it to go away completely by version 1.0, as working with
|
||
|
# PacketFu::PcapFile directly is generally going to be more rewarding.
|
||
|
class Write
|
||
|
|
||
|
class << self
|
||
|
|
||
|
# format_packets: Pretty much totally deprecated.
|
||
|
def format_packets(args={})
|
||
|
arr = args[:arr] || args[:array] || []
|
||
|
ts = args[:ts] || args[:timestamp] || Time.now.to_i
|
||
|
ts_inc = args[:ts_inc] || args[:timestamp_increment]
|
||
|
pkts = PcapFile.new.array_to_file(:endian => PacketFu.instance_variable_get("@byte_order"),
|
||
|
:arr => arr,
|
||
|
:ts => ts,
|
||
|
:ts_inc => ts_inc)
|
||
|
pkts.body
|
||
|
end
|
||
|
|
||
|
# array_to_file is a largely deprecated function for writing arrays of pcaps to a file.
|
||
|
# Use PcapFile#array_to_file instead.
|
||
|
def array_to_file(args={})
|
||
|
filename = args[:filename] || args[:file] || args[:out] || :nowrite
|
||
|
arr = args[:arr] || args[:array] || []
|
||
|
ts = args[:ts] || args[:timestamp] || args[:time_stamp] || Time.now.to_f
|
||
|
ts_inc = args[:ts_inc] || args[:timestamp_increment] || args[:time_stamp_increment]
|
||
|
byte_order = args[:byte_order] || args[:byteorder] || args[:endian] || args[:endianness] || :little
|
||
|
append = args[:append]
|
||
|
Read.set_byte_order(byte_order) if [:big, :little].include? byte_order
|
||
|
pf = PcapFile.new
|
||
|
pf.array_to_file(:endian => PacketFu.instance_variable_get("@byte_order"),
|
||
|
:arr => arr,
|
||
|
:ts => ts,
|
||
|
:ts_inc => ts_inc)
|
||
|
if filename && filename != :nowrite
|
||
|
if append
|
||
|
pf.append(filename)
|
||
|
else
|
||
|
pf.write(filename)
|
||
|
end
|
||
|
return [filename,pf.to_s.size,arr.size,ts,ts_inc]
|
||
|
else
|
||
|
return [nil,pf.to_s.size,arr.size,ts,ts_inc]
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
alias_method :a2f, :array_to_file
|
||
|
|
||
|
# Shorthand method for appending to a file. Also shouldn't use.
|
||
|
def append(args={})
|
||
|
array_to_file(args.merge(:append => true))
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
|
||
|
# vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
|