require 'bindata'

# This is a simple protocol analyser for examining IPv4 packets
# captured with libpcap on an ethernet network.
#
# The dump file can be obtained like this:
#
#     sudo tcpdump -i eth0 -s 0 -n -w dump.pcap
#


# Present MAC addresses in a human readable way
class MacAddr < BinData::Primitive
  array :octets, type: :uint8, initial_length: 6

  def set(val)
    self.octets = val.split(/:/).collect(&:to_i)
  end

  def get
    self.octets.collect { |octet| "%02x" % octet }.join(":")
  end
end

# Present IP addresses in a human readable way
class IP_Addr < BinData::Primitive
  array :octets, type: :uint8, initial_length: 4

  def set(val)
    self.octets = val.split(/\./).collect(&:to_i)
  end

  def get
    self.octets.collect { |octet| "%d" % octet }.join(".")
  end
end

# TCP Protocol Data Unit
class TCP_PDU < BinData::Record
  endian :big

  uint16 :src_port
  uint16 :dst_port
  uint32 :seq
  uint32 :ack_seq
  bit4   :doff
  bit4   :res1
  bit2   :res2
  bit1   :urg
  bit1   :ack
  bit1   :psh
  bit1   :rst
  bit1   :syn
  bit1   :fin
  uint16 :window
  uint16 :checksum
  uint16 :urg_ptr
  string :options, read_length: :options_length_in_bytes
  rest   :payload

  def options_length_in_bytes
    (doff - 5 ) * 4
  end
end

# UDP Protocol Data Unit
class UDP_PDU < BinData::Record
  endian :big

  uint16 :src_port
  uint16 :dst_port
  uint16 :len
  uint16 :checksum
  rest   :payload
end

# IP Protocol Data Unit
class IP_PDU < BinData::Record
  endian :big

  bit4   :version, asserted_value: 4
  bit4   :header_length
  uint8  :tos
  uint16 :total_length
  uint16 :ident
  bit3   :flags
  bit13  :frag_offset
  uint8  :ttl
  uint8  :protocol
  uint16 :checksum
  ip_addr :src_addr
  ip_addr :dest_addr
  string :options, read_length: :options_length_in_bytes
  buffer :payload, length: :payload_length_in_bytes do
    choice :payload, selection: :protocol do
      tcp_pdu  6
      udp_pdu 17
      rest    :default
    end
  end

  def header_length_in_bytes
    header_length * 4
  end

  def options_length_in_bytes
    header_length_in_bytes - options.rel_offset
  end

  def payload_length_in_bytes
    total_length - header_length_in_bytes
  end
end

# Ethernet Frame - NOTE only ipv4 is supported
class Ether < BinData::Record
  IPV4 = 0x0800

  endian :big
  mac_addr :dst
  mac_addr :src
  uint16   :ether_type
  choice   :payload, selection: :ether_type do
    ip_pdu IPV4
    rest   :default
  end
end

class Pcap
  def initialize(filename)
    @filename = filename
  end

  def each_record
    File.open(@filename) do |io|
      file = PcapFile.read(io)
      file.records.each do |rec|
        yield rec.data
      end
    end
  end

  class PcapFile < BinData::Record
    endian :little

    struct :head do
      uint32 :magic
      uint16 :major
      uint16 :minor
      int32  :this_zone
      uint32 :sig_figs
      uint32 :snaplen
      uint32 :linktype
    end

    array :records, read_until: :eof do
      uint32 :ts_sec
      uint32 :ts_usec
      uint32 :incl_len
      uint32 :orig_len
      string :data, length: :incl_len
    end
  end
end

require 'pp'
unless File.exist?('dump.pcap')
  puts "No dump file found. Create one with: sudo tcpdump -i eth0 -s 0 -n -w dump.pcap"
  exit 1
end

cap = Pcap.new('dump.pcap')
cap.each_record do |str|
  pp Ether.read(str)
end
