#!/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<
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