require 'packetfu/tcpopts'
module PacketFu

	# Implements the Explict Congestion Notification for TCPHeader.
	#
	# ==== Header Definition
	#
	#
	#  bit1  :n
	#  bit1  :c
	#  bit1  :e
	class TcpEcn < BinData::MultiValue
		bit1	:n
		bit1	:c
		bit1	:e

		# Returns the TcpEcn field as an integer.
		def to_i
			(n << 2) + (c << 1) + e
		end
	end

	# Implements flags for TCPHeader.
	#
	# ==== Header Definition
	#
	#  bit1  :urg
	#  bit1  :ack
	#  bit1  :psh
	#  bit1  :rst
	#  bit1  :syn
	#  bit1  :fin
	class TcpFlags < BinData::MultiValue
		bit1	:urg
		bit1	:ack
		bit1	:psh
		bit1	:rst
		bit1	:syn
		bit1	:fin

		# Returns the TcpFlags as an integer.
		def to_i
			(urg << 5) + (ack << 4) + (psh << 3) + (rst << 2) + (syn << 1) + fin
		end
	end

	# TCPHeader is a complete TCP struct, used in TCPPacket. Most IP traffic is TCP-based, by
	# volume.
	#
	# For more on TCP packets, see http://www.networksorcery.com/enp/protocol/tcp.htm
	#
	# ==== Header Definition
	# 
	#   uint16be  :tcp_src,  :initial_value => lambda {tcp_calc_src}
	#   uint16be  :tcp_dst
	#   uint32be  :tcp_seq,  :initial_value => lambda {tcp_calc_seq}
	#   uint32be  :tcp_ack
	#   bit4      :tcp_hlen, :initial_value => 5       # Must recalc as options are set. 
	#   bit3      :tcp_reserved
	#   tcp_ecn   :tcp_ecn
	#   tcp_flags :tcp_flags
	#   uint16be  :tcp_win,  :initial_value => 0x4000 # WinXP's default syn packet
	#   uint16be  :tcp_sum,  :initial_value => 0      # Must set this upon generation.
	#   uint16be  :tcp_urg
	#   string    :tcp_opts
	#   rest      :body
	#
	# See also TcpEcn, TcpFlags, TcpOpts
	class TCPHeader < BinData::MultiValue

		uint16be	:tcp_src,		:initial_value => lambda {tcp_calc_src} 
		uint16be	:tcp_dst
		uint32be	:tcp_seq,		:initial_value => lambda {tcp_calc_seq}
		uint32be	:tcp_ack
		bit4			:tcp_hlen,	:initial_value => 5 # Must recalc as options are set. 
		bit3			:tcp_reserved
		tcp_ecn		:tcp_ecn
		tcp_flags	:tcp_flags
		uint16be	:tcp_win,		:initial_value => 0x4000 # WinXP's default syn packet
		uint16be	:tcp_sum, 	:initial_value =>	0 # Must set this upon generation.
		uint16be	:tcp_urg
		string		:tcp_opts
		rest			:body

		# Create a new TCPHeader object, and intialize with a random sequence number. 
		def initialize(args={})
			@random_seq = rand(0xffffffff)
			@random_src = rand_port
			super
		end

		attr_accessor :flavor

		# tcp_calc_hlen adjusts the header length to account for tcp_opts. Note
		# that if tcp_opts does not fall on a 32-bit boundry, tcp_calc_hlen will
		# additionally pad the option string with nulls. Most stacks avoid this 
		# eventuality by padding with NOP options at OS-specific points in the 
		# option field. The practical effect of this is, you should tcp_calc_hlen
		# only when all the options are already set; otherwise, additional options
		# will be lost to the receiver as \x00 is an EOL option. Additionally,
		# (and this is almost certainly a bug), there is no sanity checking to
		# ensure the final tcp_opts value is 44 bytes or less (any more will bleed
		# over into the tcp payload). You are forewarned!
		#
		# If you would like to craft specifically malformed packets with 
		# nonsense lengths of opts fields, you should avoid tcp_calc_hlen 
		# altogether, and simply set the values for tcp_hlen and tcp_opts manually.
		def tcp_calc_hlen
			pad = (self.tcp_opts.to_s.size % 4)
			if (pad > 0)
				self.tcp_opts += ("\x00" * pad)
			end
			self.tcp_hlen = ((20 + self.tcp_opts.to_s.size) / 4)
		end

		def tcp_calc_seq
			@random_seq
		end

		def tcp_calc_src
			@random_src
		end

		# Generates a random high port. This is affected by packet flavor.
		def rand_port
			rand(0xffff - 1025) + 1025
		end

		# Returns the actual length of the TCP options.
		def tcp_opts_len
			tcp_opts.to_s.size * 4
		end

		# Returns a more readable option list. Note, it can lack fidelity on bad option strings.
		# For more on TCP options, see the TcpOpts class.
		def tcp_options
			TcpOpts.decode(self.tcp_opts)
		end

		# Allows a more writable version of TCP options. 
		# For more on TCP options, see the TcpOpts class.
		def tcp_options=(arg)
			self.tcp_opts=TcpOpts.encode(arg) 
		end

		# Equivalent to tcp_src
		def tcp_sport
			self.tcp_src
		end

		# Equivalent to tcp_src=
		def tcp_sport=(arg)
			self.tcp_src=(arg)
		end

		# Equivalent to tcp_dst
		def tcp_dport
			self.tcp_dst
		end
		
		# Equivalent to tcp_dst=
		def tcp_dport=(arg)
			self.tcp_dst=(arg)
		end

		# Recalculates calculated fields for TCP (except checksum which is at the Packet level).
		def tcp_recalc(arg=:all)
			case arg
			when :tcp_hlen
				tcp_calc_hlen
			when :tcp_src
				@random_tcp_src = rand_port
			when :tcp_sport
				@random_tcp_src = rand_port
			when :tcp_seq
				@random_tcp_seq = rand(0xffffffff) 
			when :all
				tcp_calc_hlen
				@random_tcp_src = rand_port
				@random_tcp_seq = rand(0xffffffff) 
			else
				raise ArgumentError, "No such field `#{arg}'"
			end
		end

	end

	# TCPPacket is used to construct TCP packets. They contain an EthHeader, an IPHeader, and a TCPHeader.
	#
	# == Example
	#
  #    tcp_pkt = PacketFu::TCPPacket.new
  #    tcp_pkt.tcp_flags.syn=1
  #    tcp_pkt.tcp_dst=80
  #    tcp_pkt.tcp_win=5840
  #    tcp_pkt.tcp_options="mss:1460,sack.ok,ts:#{rand(0xffffffff)};0,nop,ws:7"
	#
  #    tcp_pkt.ip_saddr=[rand(0xff),rand(0xff),rand(0xff),rand(0xff)].join('.')
  #    tcp_pkt.ip_daddr=[rand(0xff),rand(0xff),rand(0xff),rand(0xff)].join('.')
	#
  #    tcp_pkt.recalc
  #    tcp_pkt.to_f('/tmp/tcp.pcap')
	#
	# == Parameters
	#  :eth
	#    A pre-generated EthHeader object.
	#  :ip
	#    A pre-generated IPHeader object.
	#  :flavor
	#    TODO: Sets the "flavor" of the TCP packet. This will include TCP options and the initial window
	#    size, per stack. There is a lot of variety here, and it's one of the most useful methods to
	#    remotely fingerprint devices. :flavor will span both ip and tcp for consistency.
	#   :type
	#    TODO: Set up particular types of packets (syn, psh_ack, rst, etc). This can change the initial flavor.
	#  :config
	#   A hash of return address details, often the output of Utils.whoami?
	class TCPPacket < Packet

		attr_accessor :eth_header, :ip_header, :tcp_header, :headers
		
		def initialize(args={})
			@eth_header = 	(args[:eth] || EthHeader.new)
			@ip_header 	= 	(args[:ip]	|| IPHeader.new)
			@tcp_header = 	(args[:tcp] || TCPHeader.new)
			@tcp_header.flavor = args[:flavor].to_s.downcase

			@ip_header.body = @tcp_header
			@eth_header.body = @ip_header
			@headers = [@eth_header, @ip_header, @tcp_header]

			@ip_header.ip_proto=0x06
			super
			if args[:flavor]
				tcp_calc_flavor(@tcp_header.flavor)
			else
				tcp_calc_sum
			end
		end

		# Sets the correct flavor for TCP Packets. Recognized flavors are:
		#   windows, linux, freebsd
		def tcp_calc_flavor(str)
			ts_val = Time.now.to_i + rand(0x4fffffff)
			ts_sec = rand(0xffffff)
			case @tcp_header.flavor = str.to_s.downcase
			when "windows" # WinXP's default syn
				@tcp_header.tcp_win = 0x4000
				@tcp_header.tcp_options="MSS:1460,NOP,NOP,SACK.OK"
				@tcp_header.tcp_src = rand(5000 - 1026) + 1026
				@ip_header.ip_ttl = 64
			when "linux" # Ubuntu Linux 2.6.24-19-generic default syn
				@tcp_header.tcp_win = 5840
				@tcp_header.tcp_options="MSS:1460,SACK.OK,TS:#{ts_val};0,NOP,WS:7"
				@tcp_header.tcp_src = rand(61_000 - 32_000) + 32_000
				@ip_header.ip_ttl = 64
			when "freebsd" # Freebsd
				@tcp_header.tcp_win = 0xffff
				@tcp_header.tcp_options="MSS:1460,NOP,WS:3,NOP,NOP,TS:#{ts_val};#{ts_sec},SACK.OK,EOL,EOL"
				@ip_header.ip_ttl = 64
			else
				@tcp_header.tcp_options="MSS:1460,NOP,NOP,SACK.OK"
			end
			tcp_calc_sum
		end

		# tcp_calc_sum() computes the TCP checksum, and is called upon intialization. It usually
		# should be called just prior to dropping packets to a file or on the wire.
		#--
		# This is /not/ delegated down to @tcp_header since we need info
		# from the IP header, too.
		#++
		def tcp_calc_sum
			checksum =  (ip_src.to_i >> 16)
			checksum += (ip_src.to_i & 0xffff)
			checksum += (ip_dst.to_i >> 16)
			checksum += (ip_dst.to_i & 0xffff)
			checksum += 0x06 # TCP Protocol.
			checksum +=	(ip_len.to_i - ((ip_hl.to_i) * 4))
			checksum += tcp_src
			checksum += tcp_dst
			checksum += (tcp_seq.to_i >> 16)
			checksum += (tcp_seq.to_i & 0xffff)
			checksum += (tcp_ack.to_i >> 16)
			checksum += (tcp_ack.to_i & 0xffff)
			checksum += ((tcp_hlen << 12) + 
									 (tcp_reserved << 9) + 
									 (tcp_ecn.to_i << 6) + 
									 tcp_flags.to_i
									)
			checksum += tcp_win
			checksum += tcp_urg

			chk_tcp_opts = (tcp_opts.to_s.size % 2 == 0 ? tcp_opts.to_s : tcp_opts.to_s + "\x00") 
			chk_tcp_opts.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
			if (ip_len - ((ip_hl + tcp_hlen) * 4)) >= 0
				real_tcp_payload = payload[0,( ip_len - ((ip_hl + tcp_hlen) * 4) )] # Can't forget those pesky FCSes!
			else
				real_tcp_payload = payload # Something's amiss here so don't bother figuring out where the real payload is.
			end
			chk_payload = (real_tcp_payload.size % 2 == 0 ? real_tcp_payload : real_tcp_payload + "\x00") # Null pad if it's odd.
			chk_payload.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
			checksum = checksum % 0xffff
			checksum = 0xffff - checksum
			checksum == 0 ? 0xffff : checksum
			@tcp_header.tcp_sum = checksum
		end

		# Recalculates various fields of the TCP packet.
		#
		# ==== Parameters
		#
		#   :all
		#     Recomputes all calculated fields.
		#   :tcp_sum
		#     Recomputes the TCP checksum.
		#   :tcp_hlen
		#     Recomputes the TCP header length. Useful after options are added.
		def tcp_recalc(arg=:all)
			case arg
			when :tcp_sum
				tcp_calc_sum
			when :tcp_hlen
				@tcp_header.tcp_recalc :tcp_hlen
			when :all
				@tcp_header.tcp_recalc :all
				tcp_calc_sum
			else
				raise ArgumentError, "No such field `#{arg}'"
			end
		end

		# Peek provides summary data on packet contents.
		def peek(args={})
			peek_data = ["T "]
			peek_data << "%-5d" % self.to_s.size
			peek_data << "%-21s" % "#{self.ip_saddr}:#{self.tcp_src}"
			peek_data << "->"
			peek_data << "%21s" % "#{self.ip_daddr}:#{self.tcp_dst}"
			flags = ' ['
			flags << (self.tcp_flags.urg.zero? ? "." : "U")
			flags << (self.tcp_flags.ack.zero? ? "." : "A")
			flags << (self.tcp_flags.psh.zero? ? "." : "P")
			flags << (self.tcp_flags.rst.zero? ? "." : "R")
			flags << (self.tcp_flags.syn.zero? ? "." : "S")
			flags << (self.tcp_flags.fin.zero? ? "." : "F")
			flags << '] '
			peek_data << flags
			peek_data << "S:"
			peek_data << "%08x" % self.tcp_seq
			peek_data << "|I:"
			peek_data << "%04x" % self.ip_id
			peek_data.join
		end

	end

end # module PacketFu
