Import the bindata and packetfu libraries (thanks Tod)

git-svn-id: file:///home/svn/framework3/trunk@5727 4d416f70-5f16-0410-b530-b9f4589650da
unstable
HD Moore 2008-10-10 02:23:05 +00:00
parent 137b9c6cfb
commit 10619f3af0
43 changed files with 7120 additions and 0 deletions

22
lib/bindata.rb Normal file
View File

@ -0,0 +1,22 @@
# BinData -- Binary data manipulator.
# Copyright (c) 2007,2008 Dion Mendel.
require 'bindata/array'
require 'bindata/bits'
require 'bindata/choice'
require 'bindata/float'
require 'bindata/int'
require 'bindata/multi_value'
require 'bindata/rest'
require 'bindata/single_value'
require 'bindata/string'
require 'bindata/stringz'
require 'bindata/struct'
# = BinData
#
# A declarative way to read and write structured binary data.
#
module BinData
VERSION = "0.9.2-eofpatch" # Temporary fork for PacketFu.
end

339
lib/bindata/GPL Normal file
View File

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

52
lib/bindata/LICENSE Normal file
View File

@ -0,0 +1,52 @@
BinData is copyrighted free software by Dion Mendel <dion@lostrealm.com>.
You can redistribute it and/or modify it under either the terms of the GPL
version 2 (see the file GPL), or the conditions below:
1. You may make and give away verbatim copies of the source form of the
software without restriction, provided that you duplicate all of the
original copyright notices and associated disclaimers.
2. You may modify your copy of the software in any way, provided that
you do at least ONE of the following:
a) place your modifications in the Public Domain or otherwise
make them Freely Available, such as by posting said
modifications to Usenet or an equivalent medium, or by allowing
the author to include your modifications in the software.
b) use the modified software only within your corporation or
organization.
c) give non-standard binaries non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
3. You may distribute the software in object code or binary form,
provided that you do at least ONE of the following:
a) distribute the binaries and library files of the software,
together with instructions (in the manual page or equivalent)
on where to get the original distribution.
b) accompany the distribution with the machine-readable source of
the software.
c) give non-standard binaries non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
4. You may modify and include the part of the software into any other
software (possibly commercial).
5. The scripts and library files supplied as input to or produced as
output from the software do not automatically fall under the
copyright of the software, but belong to whomever generated them,
and may be sold commercially, and may be aggregated with this
software.
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.

330
lib/bindata/array.rb Normal file
View File

@ -0,0 +1,330 @@
require 'bindata/base'
require 'bindata/sanitize'
module BinData
# An Array is a list of data objects of the same type.
#
# require 'bindata'
#
# data = "\x03\x04\x05\x06\x07\x08\x09"
#
# obj = BinData::Array.new(:type => :int8, :initial_length => 6)
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7, 8]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { index == 1 })
# obj.read(data)
# obj.snapshot #=> [3, 4]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { element >= 6 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { array[index] + array[index - 1] == 13 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7]
#
# obj = BinData::Array.new(:type => :int8, :read_until => :eof)
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# <tt>:type</tt>:: The symbol representing the data type of the
# array elements. If the type is to have params
# passed to it, then it should be provided as
# <tt>[type_symbol, hash_params]</tt>.
# <tt>:initial_length</tt>:: The initial length of the array.
# <tt>:read_until</tt>:: While reading, elements are read until this
# condition is true. This is typically used to
# read an array until a sentinel value is found.
# The variables +index+, +element+ and +array+
# are made available to any lambda assigned to
# this parameter. If the value of this parameter
# is the symbol :eof, then the array will read
# as much data from the stream as possible.
#
# Each data object in an array has the variable +index+ made available
# to any lambda evaluated as a parameter of that data object.
class Array < BinData::Base
include Enumerable
# Register this class
register(self.name, self)
# These are the parameters used by this class.
mandatory_parameter :type
optional_parameters :initial_length, :read_until
mutually_exclusive_parameters :initial_length, :read_until
class << self
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
unless params.has_key?(:initial_length) or params.has_key?(:read_until)
# ensure one of :initial_length and :read_until exists
params[:initial_length] = 0
end
if params.has_key?(:read_length)
warn ":read_length is not used with arrays. You probably want to change this to :initial_length"
end
if params.has_key?(:type)
type, el_params = params[:type]
params[:type] = sanitizer.sanitize(type, el_params)
end
super(sanitizer, params)
end
end
# Creates a new Array
def initialize(params = {}, env = nil)
super(params, env)
klass, el_params = param(:type)
@element_list = nil
@element_klass = klass
@element_params = el_params
end
# Returns if the element at position +index+ is clear?. If +index+
# is not given, then returns whether all fields are clear.
def clear?(index = nil)
if @element_list.nil?
true
elsif index.nil?
elements.each { |f| return false if not f.clear? }
true
else
(index < elements.length) ? elements[index].clear? : true
end
end
# Clears the element at position +index+. If +index+ is not given, then
# the internal state of the array is reset to that of a newly created
# object.
def clear(index = nil)
if @element_list.nil?
# do nothing as the array is already clear
elsif index.nil?
@element_list = nil
elsif index < elements.length
elements[index].clear
end
end
# Returns whether this data object contains a single value. Single
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
def single_value?
return false
end
# To be called after calling #do_read.
def done_read
elements.each { |f| f.done_read }
end
# Appends a new element to the end of the array. If the array contains
# single_values then the +value+ may be provided to the call.
# Returns the appended object, or value in the case of single_values.
def append(value = nil)
# TODO: deprecate #append as it can be replaced with #push
append_new_element
self[-1] = value unless value.nil?
self.last
end
# Pushes the given object(s) on to the end of this array.
# This expression returns the array itself, so several appends may
# be chained together.
def push(*args)
args.each do |arg|
if @element_klass == arg.class
# TODO: need to modify arg.env to add_variable(:index) and
# to link arg.env to self.env
elements.push(arg)
else
append(arg)
end
end
self
end
# Returns the element at +index+. If the element is a single_value
# then the value of the element is returned instead.
def [](*args)
if args.length == 1 and ::Integer === args[0]
# extend array automatically
while args[0] >= elements.length
append_new_element
end
end
data = elements[*args]
if data.respond_to?(:each)
data.collect { |el| (el && el.single_value?) ? el.value : el }
else
(data && data.single_value?) ? data.value : data
end
end
alias_method :slice, :[]
# Sets the element at +index+. If the element is a single_value
# then the value of the element is set instead.
def []=(index, value)
# extend array automatically
while index >= elements.length
append_new_element
end
obj = elements[index]
unless obj.single_value?
# TODO: allow setting objects, not just values
raise NoMethodError, "undefined method `[]=' for #{self}", caller
end
obj.value = value
end
# Iterate over each element in the array. If the elements are
# single_values then the values of the elements are iterated instead.
def each
elements.each do |el|
yield(el.single_value? ? el.value : el)
end
end
# Returns the first element, or the first +n+ elements, of the array.
# If the array is empty, the first form returns nil, and the second
# form returns an empty array.
def first(n = nil)
if n.nil?
if elements.empty?
# explicitly return nil as arrays grow automatically
nil
else
self[0]
end
else
self[0, n]
end
end
# Returns the last element, or the last +n+ elements, of the array.
# If the array is empty, the first form returns nil, and the second
# form returns an empty array.
def last(n = nil)
if n.nil?
self[-1]
else
n = length if n > length
self[-n, n]
end
end
# The number of elements in this array.
def length
elements.length
end
alias_method :size, :length
# Returns true if self array contains no elements.
def empty?
length.zero?
end
# Allow this object to be used in array context.
def to_ary
snapshot
end
#---------------
private
# Reads the values for all fields in this object from +io+.
def _do_read(io)
if has_param?(:initial_length)
elements.each { |f| f.do_read(io) }
elsif has_param?(:read_until)
if param(:read_until) == :eof
@element_list = nil
loop do
element = append_new_element
begin
element.do_read(io)
rescue
@element_list.pop
break
end
end
else
@element_list = nil
loop do
element = append_new_element
element.do_read(io)
variables = { :index => self.length - 1, :element => self.last,
:array => self }
finished = eval_param(:read_until, variables)
break if finished
end
end
end
end
# Writes the values for all fields in this object to +io+.
def _do_write(io)
elements.each { |f| f.do_write(io) }
end
# Returns the number of bytes it will take to write the element at
# +index+. If +index+, then returns the number of bytes required
# to write all fields.
def _do_num_bytes(index)
if index.nil?
(elements.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
else
elements[index].do_num_bytes
end
end
# Returns a snapshot of the data in this array.
def _snapshot
elements.collect { |e| e.snapshot }
end
# Returns the list of all elements in the array. The elements
# will be instantiated on the first call to this method.
def elements
if @element_list.nil?
@element_list = []
if has_param?(:initial_length)
# create the desired number of instances
eval_param(:initial_length).times do
append_new_element
end
end
end
@element_list
end
# Creates a new element and appends it to the end of @element_list.
# Returns the newly created element
def append_new_element
# ensure @element_list is initialised
elements()
env = create_env
env.add_variable(:index, @element_list.length)
element = @element_klass.new(@element_params, env)
@element_list << element
element
end
end
end

380
lib/bindata/base.rb Normal file
View File

@ -0,0 +1,380 @@
require 'bindata/io'
require 'bindata/lazy'
require 'bindata/registry'
require 'bindata/sanitize'
require 'stringio'
module BinData
# Error raised when unexpected results occur when reading data from IO.
class ValidityError < StandardError ; end
# This is the abstract base class for all data objects.
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# [<tt>:readwrite</tt>] Deprecated. An alias for :onlyif.
# [<tt>:onlyif</tt>] Used to indicate a data object is optional.
# if false, calls to #read or #write will not
# perform any I/O, #num_bytes will return 0 and
# #snapshot will return nil. Default is true.
# [<tt>:check_offset</tt>] Raise an error if the current IO offset doesn't
# meet this criteria. A boolean return indicates
# success or failure. Any other return is compared
# to the current offset. The variable +offset+
# is made available to any lambda assigned to
# this parameter. This parameter is only checked
# before reading.
# [<tt>:adjust_offset</tt>] Ensures that the current IO offset is at this
# position before reading. This is like
# <tt>:check_offset</tt>, except that it will
# adjust the IO offset instead of raising an error.
class Base
class << self
# Returns the mandatory parameters used by this class. Any given args
# are appended to the parameters list. The parameters for a class will
# include the parameters of its ancestors.
def mandatory_parameters(*args)
unless defined? @mandatory_parameters
@mandatory_parameters = []
ancestors[1..-1].each do |parent|
if parent.respond_to?(:mandatory_parameters)
pmp = parent.mandatory_parameters
@mandatory_parameters.concat(pmp)
end
end
end
if not args.empty?
args.each { |arg| @mandatory_parameters << arg.to_sym }
@mandatory_parameters.uniq!
end
@mandatory_parameters
end
alias_method :mandatory_parameter, :mandatory_parameters
# Returns the optional parameters used by this class. Any given args
# are appended to the parameters list. The parameters for a class will
# include the parameters of its ancestors.
def optional_parameters(*args)
unless defined? @optional_parameters
@optional_parameters = []
ancestors[1..-1].each do |parent|
if parent.respond_to?(:optional_parameters)
pop = parent.optional_parameters
@optional_parameters.concat(pop)
end
end
end
if not args.empty?
args.each { |arg| @optional_parameters << arg.to_sym }
@optional_parameters.uniq!
end
@optional_parameters
end
alias_method :optional_parameter, :optional_parameters
# Returns the default parameters used by this class. Any given args
# are appended to the parameters list. The parameters for a class will
# include the parameters of its ancestors.
def default_parameters(params = {})
unless defined? @default_parameters
@default_parameters = {}
ancestors[1..-1].each do |parent|
if parent.respond_to?(:default_parameters)
pdp = parent.default_parameters
@default_parameters = @default_parameters.merge(pdp)
end
end
end
if not params.empty?
@default_parameters = @default_parameters.merge(params)
end
@default_parameters
end
alias_method :default_parameter, :default_parameters
# Returns the pairs of mutually exclusive parameters used by this class.
# Any given args are appended to the parameters list. The parameters for
# a class will include the parameters of its ancestors.
def mutually_exclusive_parameters(*args)
unless defined? @mutually_exclusive_parameters
@mutually_exclusive_parameters = []
ancestors[1..-1].each do |parent|
if parent.respond_to?(:mutually_exclusive_parameters)
pmep = parent.mutually_exclusive_parameters
@mutually_exclusive_parameters.concat(pmep)
end
end
end
if not args.empty?
@mutually_exclusive_parameters << [args[0].to_sym, args[1].to_sym]
end
@mutually_exclusive_parameters
end
# Returns a list of parameters that are accepted by this object
def accepted_parameters
(mandatory_parameters + optional_parameters + default_parameters.keys).uniq
end
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
# replace :readwrite with :onlyif
if params.has_key?(:readwrite)
warn ":readwrite is deprecated. Replacing with :onlyif"
params[:onlyif] = params.delete(:readwrite)
end
# add default parameters
default_parameters.each do |k,v|
params[k] = v unless params.has_key?(k)
end
# ensure mandatory parameters exist
mandatory_parameters.each do |prm|
if not params.has_key?(prm)
raise ArgumentError, "parameter ':#{prm}' must be specified " +
"in #{self}"
end
end
# ensure mutual exclusion
mutually_exclusive_parameters.each do |param1, param2|
if params.has_key?(param1) and params.has_key?(param2)
raise ArgumentError, "params #{param1} and #{param2} " +
"are mutually exclusive"
end
end
end
# Instantiates this class and reads from +io+. For single value objects
# just the value is returned, otherwise the newly created data object is
# returned.
def read(io)
data = self.new
data.read(io)
data.single_value? ? data.value : data
end
# Registers the mapping of +name+ to +klass+.
def register(name, klass)
Registry.instance.register(name, klass)
end
private :register
end
# Define the parameters we use in this class.
optional_parameters :check_offset, :adjust_offset
default_parameters :onlyif => true
mutually_exclusive_parameters :check_offset, :adjust_offset
# Creates a new data object.
#
# +params+ is a hash containing symbol keys. Some params may
# reference callable objects (methods or procs). +env+ is the
# environment that these callable objects are evaluated in.
def initialize(params = {}, env = nil)
unless SanitizedParameters === params
params = Sanitizer.sanitize(self, params)
end
@params = params.accepted_parameters
# set up the environment
@env = env || LazyEvalEnv.new
@env.params = params.extra_parameters
@env.data_object = self
end
# Reads data into this data object by calling #do_read then #done_read.
def read(io)
io = BinData::IO.new(io) unless BinData::IO === io
do_read(io)
done_read
self
end
# Reads the value for this data from +io+.
def do_read(io)
raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io
clear
check_offset(io)
if eval_param(:onlyif)
_do_read(io)
end
end
# Writes the value for this data to +io+ by calling #do_write.
def write(io)
io = BinData::IO.new(io) unless BinData::IO === io
do_write(io)
io.flush
self
end
# Writes the value for this data to +io+.
def do_write(io)
raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io
if eval_param(:onlyif)
_do_write(io)
end
end
# Returns the number of bytes it will take to write this data by calling
# #do_num_bytes.
def num_bytes(what = nil)
num = do_num_bytes(what)
num.ceil
end
# Returns the number of bytes it will take to write this data.
def do_num_bytes(what = nil)
if eval_param(:onlyif)
_do_num_bytes(what)
else
0
end
end
# Returns a snapshot of this data object.
# Returns nil if :onlyif is false
def snapshot
if eval_param(:onlyif)
_snapshot
else
nil
end
end
# Returns the string representation of this data object.
def to_s
io = StringIO.new
write(io)
io.rewind
io.read
end
# Return a human readable representation of this object.
def inspect
snapshot.inspect
end
#---------------
private
# Creates a new LazyEvalEnv for use by a child data object.
def create_env
LazyEvalEnv.new(@env)
end
# Returns the value of the evaluated parameter. +key+ references a
# parameter from the +params+ hash used when creating the data object.
# +values+ contains data that may be accessed when evaluating +key+.
# Returns nil if +key+ does not refer to any parameter.
def eval_param(key, values = nil)
@env.lazy_eval(@params[key], values)
end
# Returns the parameter from the +params+ hash referenced by +key+.
# Use this method if you are sure the parameter is not to be evaluated.
# You most likely want #eval_param.
def param(key)
@params[key]
end
# Returns whether +key+ exists in the +params+ hash used when creating
# this data object.
def has_param?(key)
@params.has_key?(key.to_sym)
end
# Checks that the current offset of +io+ is as expected. This should
# be called from #do_read before performing the reading.
def check_offset(io)
if has_param?(:check_offset)
actual_offset = io.offset
expected = eval_param(:check_offset, :offset => actual_offset)
if not expected
raise ValidityError, "offset not as expected"
elsif actual_offset != expected and expected != true
raise ValidityError, "offset is '#{actual_offset}' but " +
"expected '#{expected}'"
end
elsif has_param?(:adjust_offset)
actual_offset = io.offset
expected = eval_param(:adjust_offset)
if actual_offset != expected
begin
seek = expected - actual_offset
io.seekbytes(seek)
warn "adjusting stream position by #{seek} bytes" if $VERBOSE
rescue
# could not seek so raise an error
raise ValidityError, "offset is '#{actual_offset}' but " +
"couldn't seek to expected '#{expected}'"
end
end
end
end
###########################################################################
# To be implemented by subclasses
# Resets the internal state to that of a newly created object.
def clear
raise NotImplementedError
end
# Returns true if the object has not been changed since creation.
def clear?(*args)
raise NotImplementedError
end
# Returns whether this data object contains a single value. Single
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
def single_value?
raise NotImplementedError
end
# To be called after calling #do_read.
def done_read
raise NotImplementedError
end
# Reads the data for this data object from +io+.
def _do_read(io)
raise NotImplementedError
end
# Writes the value for this data to +io+.
def _do_write(io)
raise NotImplementedError
end
# Returns the number of bytes it will take to write this data.
def _do_num_bytes
raise NotImplementedError
end
# Returns a snapshot of this data object.
def _snapshot
raise NotImplementedError
end
# Set visibility requirements of methods to implement
public :clear, :clear?, :single_value?, :done_read
private :_do_read, :_do_write, :_do_num_bytes, :_snapshot
# End To be implemented by subclasses
###########################################################################
end
end

807
lib/bindata/bits.rb Normal file
View File

@ -0,0 +1,807 @@
require 'bindata/single'
module BinData
# Provides a number of classes that contain an integer. The integer
# is defined by endian, signedness and number of bytes.
module BitField #:nodoc: all
def self.create_methods(klass, nbits, endian)
min = 0
max = (1 << nbits) - 1
clamp = "val = (val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"
# allow single bits to be used as booleans
if nbits == 1
clamp = "val = (val == true) ? 1 : (not val) ? 0 : #{clamp}"
end
define_methods(klass, nbits, endian.inspect, clamp)
end
def self.define_methods(klass, nbits, endian, clamp)
# define methods in the given class
klass.module_eval <<-END
def value=(val)
#{clamp}
super(val)
end
#---------------
private
def _do_write(io)
raise "can't write whilst reading" if @in_read
io.writebits(_value, #{nbits}, #{endian})
end
def _do_num_bytes(ignored)
#{nbits} / 8.0
end
def read_val(io)
io.readbits(#{nbits}, #{endian})
end
def sensible_default
0
end
END
end
end
# 1 bit big endian bitfield.
class Bit1 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 1, :big)
end
# 1 bit little endian bitfield.
class Bit1le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 1, :little)
end
# 2 bit big endian bitfield.
class Bit2 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 2, :big)
end
# 2 bit little endian bitfield.
class Bit2le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 2, :little)
end
# 3 bit big endian bitfield.
class Bit3 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 3, :big)
end
# 3 bit little endian bitfield.
class Bit3le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 3, :little)
end
# 4 bit big endian bitfield.
class Bit4 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 4, :big)
end
# 4 bit little endian bitfield.
class Bit4le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 4, :little)
end
# 5 bit big endian bitfield.
class Bit5 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 5, :big)
end
# 5 bit little endian bitfield.
class Bit5le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 5, :little)
end
# 6 bit big endian bitfield.
class Bit6 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 6, :big)
end
# 6 bit little endian bitfield.
class Bit6le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 6, :little)
end
# 7 bit big endian bitfield.
class Bit7 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 7, :big)
end
# 7 bit little endian bitfield.
class Bit7le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 7, :little)
end
# 8 bit big endian bitfield.
class Bit8 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 8, :big)
end
# 8 bit little endian bitfield.
class Bit8le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 8, :little)
end
# 9 bit big endian bitfield.
class Bit9 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 9, :big)
end
# 9 bit little endian bitfield.
class Bit9le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 9, :little)
end
# 10 bit big endian bitfield.
class Bit10 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 10, :big)
end
# 10 bit little endian bitfield.
class Bit10le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 10, :little)
end
# 11 bit big endian bitfield.
class Bit11 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 11, :big)
end
# 11 bit little endian bitfield.
class Bit11le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 11, :little)
end
# 12 bit big endian bitfield.
class Bit12 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 12, :big)
end
# 12 bit little endian bitfield.
class Bit12le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 12, :little)
end
# 13 bit big endian bitfield.
class Bit13 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 13, :big)
end
# 13 bit little endian bitfield.
class Bit13le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 13, :little)
end
# 14 bit big endian bitfield.
class Bit14 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 14, :big)
end
# 14 bit little endian bitfield.
class Bit14le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 14, :little)
end
# 15 bit big endian bitfield.
class Bit15 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 15, :big)
end
# 15 bit little endian bitfield.
class Bit15le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 15, :little)
end
# 16 bit big endian bitfield.
class Bit16 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 16, :big)
end
# 16 bit little endian bitfield.
class Bit16le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 16, :little)
end
# 17 bit big endian bitfield.
class Bit17 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 17, :big)
end
# 17 bit little endian bitfield.
class Bit17le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 17, :little)
end
# 18 bit big endian bitfield.
class Bit18 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 18, :big)
end
# 18 bit little endian bitfield.
class Bit18le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 18, :little)
end
# 19 bit big endian bitfield.
class Bit19 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 19, :big)
end
# 19 bit little endian bitfield.
class Bit19le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 19, :little)
end
# 20 bit big endian bitfield.
class Bit20 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 20, :big)
end
# 20 bit little endian bitfield.
class Bit20le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 20, :little)
end
# 21 bit big endian bitfield.
class Bit21 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 21, :big)
end
# 21 bit little endian bitfield.
class Bit21le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 21, :little)
end
# 22 bit big endian bitfield.
class Bit22 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 22, :big)
end
# 22 bit little endian bitfield.
class Bit22le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 22, :little)
end
# 23 bit big endian bitfield.
class Bit23 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 23, :big)
end
# 23 bit little endian bitfield.
class Bit23le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 23, :little)
end
# 24 bit big endian bitfield.
class Bit24 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 24, :big)
end
# 24 bit little endian bitfield.
class Bit24le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 24, :little)
end
# 25 bit big endian bitfield.
class Bit25 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 25, :big)
end
# 25 bit little endian bitfield.
class Bit25le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 25, :little)
end
# 26 bit big endian bitfield.
class Bit26 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 26, :big)
end
# 26 bit little endian bitfield.
class Bit26le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 26, :little)
end
# 27 bit big endian bitfield.
class Bit27 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 27, :big)
end
# 27 bit little endian bitfield.
class Bit27le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 27, :little)
end
# 28 bit big endian bitfield.
class Bit28 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 28, :big)
end
# 28 bit little endian bitfield.
class Bit28le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 28, :little)
end
# 29 bit big endian bitfield.
class Bit29 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 29, :big)
end
# 29 bit little endian bitfield.
class Bit29le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 29, :little)
end
# 30 bit big endian bitfield.
class Bit30 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 30, :big)
end
# 30 bit little endian bitfield.
class Bit30le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 30, :little)
end
# 31 bit big endian bitfield.
class Bit31 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 31, :big)
end
# 31 bit little endian bitfield.
class Bit31le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 31, :little)
end
# 32 bit big endian bitfield.
class Bit32 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 32, :big)
end
# 32 bit little endian bitfield.
class Bit32le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 32, :little)
end
# 33 bit big endian bitfield.
class Bit33 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 33, :big)
end
# 33 bit little endian bitfield.
class Bit33le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 33, :little)
end
# 34 bit big endian bitfield.
class Bit34 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 34, :big)
end
# 34 bit little endian bitfield.
class Bit34le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 34, :little)
end
# 35 bit big endian bitfield.
class Bit35 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 35, :big)
end
# 35 bit little endian bitfield.
class Bit35le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 35, :little)
end
# 36 bit big endian bitfield.
class Bit36 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 36, :big)
end
# 36 bit little endian bitfield.
class Bit36le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 36, :little)
end
# 37 bit big endian bitfield.
class Bit37 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 37, :big)
end
# 37 bit little endian bitfield.
class Bit37le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 37, :little)
end
# 38 bit big endian bitfield.
class Bit38 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 38, :big)
end
# 38 bit little endian bitfield.
class Bit38le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 38, :little)
end
# 39 bit big endian bitfield.
class Bit39 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 39, :big)
end
# 39 bit little endian bitfield.
class Bit39le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 39, :little)
end
# 40 bit big endian bitfield.
class Bit40 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 40, :big)
end
# 40 bit little endian bitfield.
class Bit40le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 40, :little)
end
# 41 bit big endian bitfield.
class Bit41 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 41, :big)
end
# 41 bit little endian bitfield.
class Bit41le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 41, :little)
end
# 42 bit big endian bitfield.
class Bit42 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 42, :big)
end
# 42 bit little endian bitfield.
class Bit42le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 42, :little)
end
# 43 bit big endian bitfield.
class Bit43 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 43, :big)
end
# 43 bit little endian bitfield.
class Bit43le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 43, :little)
end
# 44 bit big endian bitfield.
class Bit44 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 44, :big)
end
# 44 bit little endian bitfield.
class Bit44le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 44, :little)
end
# 45 bit big endian bitfield.
class Bit45 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 45, :big)
end
# 45 bit little endian bitfield.
class Bit45le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 45, :little)
end
# 46 bit big endian bitfield.
class Bit46 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 46, :big)
end
# 46 bit little endian bitfield.
class Bit46le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 46, :little)
end
# 47 bit big endian bitfield.
class Bit47 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 47, :big)
end
# 47 bit little endian bitfield.
class Bit47le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 47, :little)
end
# 48 bit big endian bitfield.
class Bit48 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 48, :big)
end
# 48 bit little endian bitfield.
class Bit48le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 48, :little)
end
# 49 bit big endian bitfield.
class Bit49 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 49, :big)
end
# 49 bit little endian bitfield.
class Bit49le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 49, :little)
end
# 50 bit big endian bitfield.
class Bit50 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 50, :big)
end
# 50 bit little endian bitfield.
class Bit50le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 50, :little)
end
# 51 bit big endian bitfield.
class Bit51 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 51, :big)
end
# 51 bit little endian bitfield.
class Bit51le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 51, :little)
end
# 52 bit big endian bitfield.
class Bit52 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 52, :big)
end
# 52 bit little endian bitfield.
class Bit52le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 52, :little)
end
# 53 bit big endian bitfield.
class Bit53 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 53, :big)
end
# 53 bit little endian bitfield.
class Bit53le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 53, :little)
end
# 54 bit big endian bitfield.
class Bit54 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 54, :big)
end
# 54 bit little endian bitfield.
class Bit54le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 54, :little)
end
# 55 bit big endian bitfield.
class Bit55 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 55, :big)
end
# 55 bit little endian bitfield.
class Bit55le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 55, :little)
end
# 56 bit big endian bitfield.
class Bit56 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 56, :big)
end
# 56 bit little endian bitfield.
class Bit56le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 56, :little)
end
# 57 bit big endian bitfield.
class Bit57 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 57, :big)
end
# 57 bit little endian bitfield.
class Bit57le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 57, :little)
end
# 58 bit big endian bitfield.
class Bit58 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 58, :big)
end
# 58 bit little endian bitfield.
class Bit58le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 58, :little)
end
# 59 bit big endian bitfield.
class Bit59 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 59, :big)
end
# 59 bit little endian bitfield.
class Bit59le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 59, :little)
end
# 60 bit big endian bitfield.
class Bit60 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 60, :big)
end
# 60 bit little endian bitfield.
class Bit60le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 60, :little)
end
# 61 bit big endian bitfield.
class Bit61 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 61, :big)
end
# 61 bit little endian bitfield.
class Bit61le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 61, :little)
end
# 62 bit big endian bitfield.
class Bit62 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 62, :big)
end
# 62 bit little endian bitfield.
class Bit62le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 62, :little)
end
# 63 bit big endian bitfield.
class Bit63 < BinData::Single
register(self.name, self)
BitField.create_methods(self, 63, :big)
end
# 63 bit little endian bitfield.
class Bit63le < BinData::Single
register(self.name, self)
BitField.create_methods(self, 63, :little)
end
end

163
lib/bindata/choice.rb Normal file
View File

@ -0,0 +1,163 @@
require 'forwardable'
require 'bindata/base'
require 'bindata/sanitize'
module BinData
# A Choice is a collection of data objects of which only one is active
# at any particular time.
#
# require 'bindata'
#
# type1 = [:string, {:value => "Type1"}]
# type2 = [:string, {:value => "Type2"}]
#
# choices = [ type1, type2 ]
# a = BinData::Choice.new(:choices => choices, :selection => 1)
# a.value # => "Type2"
#
# choices = [ nil, nil, nil, type1, nil, type2 ]
# a = BinData::Choice.new(:choices => choices, :selection => 3)
# a.value # => "Type1"
#
# choices = {5 => type1, 17 => type2}
# a = BinData::Choice.new(:choices => choices, :selection => 5)
# a.value # => "Type1"
#
# mychoice = 'big'
# choices = {'big' => :uint16be, 'little' => :uint16le}
# a = BinData::Choice.new(:choices => choices,
# :selection => lambda { mychoice })
# a.value = 256
# a.to_s #=> "\001\000"
# mychoice[0..-1] = 'little'
# a.to_s #=> "\000\001"
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# <tt>:choices</tt>:: Either an array or a hash specifying the possible
# data objects. The format of the array/hash.values is
# a list of symbols representing the data object type.
# If a choice is to have params passed to it, then it
# should be provided as [type_symbol, hash_params].
# An implementation gotcha is that the hash may not
# contain symbols as keys.
# <tt>:selection</tt>:: An index/key into the :choices array/hash which
# specifies the currently active choice.
class Choice < BinData::Base
extend Forwardable
# Register this class
register(self.name, self)
# These are the parameters used by this class.
mandatory_parameters :choices, :selection
class << self
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
if params.has_key?(:choices)
choices = params[:choices]
case choices
when ::Hash
new_choices = {}
choices.keys.each do |key|
# ensure valid hash keys
if Symbol === key
msg = ":choices hash may not have symbols for keys"
raise ArgumentError, msg
elsif key.nil?
raise ArgumentError, ":choices hash may not have nil key"
end
# collect sanitized choice values
type, param = choices[key]
new_choices[key] = sanitizer.sanitize(type, param)
end
params[:choices] = new_choices
when ::Array
choices.collect! do |type, param|
if type.nil?
# allow sparse arrays
nil
else
sanitizer.sanitize(type, param)
end
end
params[:choices] = choices
else
raise ArgumentError, "unknown type for :choices (#{choices.class})"
end
end
super(sanitizer, params)
end
end
def initialize(params = {}, env = nil)
super(params, env)
# prepare collection of instantiated choice objects
@choices = (param(:choices) === ::Array) ? [] : {}
@last_key = nil
end
def_delegators :the_choice, :clear, :clear?, :single_value?
def_delegators :the_choice, :done_read, :_snapshot
def_delegators :the_choice, :_do_read, :_do_write, :_do_num_bytes
# Override to include selected data object.
def respond_to?(symbol, include_private = false)
super || the_choice.respond_to?(symbol, include_private)
end
def method_missing(symbol, *args, &block)
if the_choice.respond_to?(symbol)
the_choice.__send__(symbol, *args, &block)
else
super
end
end
#---------------
private
# Returns the selected data object.
def the_choice
key = eval_param(:selection)
if key.nil?
raise IndexError, ":selection returned nil value"
end
obj = @choices[key]
if obj.nil?
# instantiate choice object
choice_klass, choice_params = param(:choices)[key]
if choice_klass.nil?
raise IndexError, "selection #{key} does not exist in :choices"
end
obj = choice_klass.new(choice_params, create_env)
@choices[key] = obj
end
# for single_values copy the value when the selected object changes
if key != @last_key
if @last_key != nil
prev = @choices[@last_key]
if prev != nil and prev.single_value? and obj.single_value?
obj.value = prev.value
end
end
@last_key = key
end
obj
end
end
end

88
lib/bindata/float.rb Normal file
View File

@ -0,0 +1,88 @@
require 'bindata/single'
module BinData
# Provides a number of classes that contain a floating point number.
# The float is defined by endian, and precision.
module Float #:nodoc: all
def self.create_float_methods(klass, single_precision, endian)
read = create_read_code(single_precision, endian)
to_s = create_to_s_code(single_precision, endian)
define_methods(klass, single_precision, read, to_s)
end
def self.create_read_code(single_precision, endian)
if single_precision
unpack = (endian == :little) ? 'e' : 'g'
nbytes = 4
else # double_precision
unpack = (endian == :little) ? 'E' : 'G'
nbytes = 8
end
"io.readbytes(#{nbytes}).unpack('#{unpack}').at(0)"
end
def self.create_to_s_code(single_precision, endian)
if single_precision
pack = (endian == :little) ? 'e' : 'g'
else # double_precision
pack = (endian == :little) ? 'E' : 'G'
end
"[val].pack('#{pack}')"
end
def self.define_methods(klass, single_precision, read, to_s)
nbytes = single_precision ? 4 : 8
# define methods in the given class
klass.module_eval <<-END
def _do_num_bytes(ignored)
#{nbytes}
end
#---------------
private
def sensible_default
0.0
end
def val_to_str(val)
#{to_s}
end
def read_val(io)
#{read}
end
END
end
end
# Single precision floating point number in little endian format
class FloatLe < BinData::Single
register(self.name, self)
Float.create_float_methods(self, true, :little)
end
# Single precision floating point number in big endian format
class FloatBe < BinData::Single
register(self.name, self)
Float.create_float_methods(self, true, :big)
end
# Double precision floating point number in little endian format
class DoubleLe < BinData::Single
register(self.name, self)
Float.create_float_methods(self, false, :little)
end
# Double precision floating point number in big endian format
class DoubleBe < BinData::Single
register(self.name, self)
Float.create_float_methods(self, false, :big)
end
end

185
lib/bindata/int.rb Normal file
View File

@ -0,0 +1,185 @@
require 'bindata/single'
module BinData
# Provides a number of classes that contain an integer. The integer
# is defined by endian, signedness and number of bytes.
module Integer #:nodoc: all
def self.create_int_methods(klass, nbits, endian)
max = (1 << (nbits - 1)) - 1
min = -(max + 1)
clamp = "val = (val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"
int2uint = "val = val & #{(1 << nbits) - 1}"
uint2int = "val = ((val & #{1 << (nbits - 1)}).zero?) ? " +
"val & #{max} : -(((~val) & #{max}) + 1)"
read = create_read_code(nbits, endian)
to_s = create_to_s_code(nbits, endian)
define_methods(klass, nbits / 8, clamp, read, to_s, int2uint, uint2int)
end
def self.create_uint_methods(klass, nbits, endian)
min = 0
max = (1 << nbits) - 1
clamp = "val = (val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"
read = create_read_code(nbits, endian)
to_s = create_to_s_code(nbits, endian)
define_methods(klass, nbits / 8, clamp, read, to_s)
end
def self.create_read_code(nbits, endian)
c16 = (endian == :little) ? 'v' : 'n'
c32 = (endian == :little) ? 'V' : 'N'
b1 = (endian == :little) ? 0 : 1
b2 = (endian == :little) ? 1 : 0
case nbits
when 8; "io.readbytes(1).unpack('C').at(0)"
when 16; "io.readbytes(2).unpack('#{c16}').at(0)"
when 32; "io.readbytes(4).unpack('#{c32}').at(0)"
when 64; "(a = io.readbytes(8).unpack('#{c32 * 2}'); " +
"(a.at(#{b2}) << 32) + a.at(#{b1}))"
else
raise "unknown nbits '#{nbits}'"
end
end
def self.create_to_s_code(nbits, endian)
c16 = (endian == :little) ? 'v' : 'n'
c32 = (endian == :little) ? 'V' : 'N'
v1 = (endian == :little) ? 'val' : '(val >> 32)'
v2 = (endian == :little) ? '(val >> 32)' : 'val'
case nbits
when 8; "val.chr"
when 16; "[val].pack('#{c16}')"
when 32; "[val].pack('#{c32}')"
when 64; "[#{v1} & 0xffffffff, #{v2} & 0xffffffff].pack('#{c32 * 2}')"
else
raise "unknown nbits '#{nbits}'"
end
end
def self.define_methods(klass, nbytes, clamp, read, to_s,
int2uint = nil, uint2int = nil)
# define methods in the given class
klass.module_eval <<-END
def value=(val)
#{clamp}
super(val)
end
def _do_num_bytes(ignored)
#{nbytes}
end
#---------------
private
def sensible_default
0
end
def val_to_str(val)
#{clamp}
#{int2uint unless int2uint.nil?}
#{to_s}
end
def read_val(io)
val = #{read}
#{uint2int unless uint2int.nil?}
end
END
end
end
# Unsigned 1 byte integer.
class Uint8 < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 8, :little)
end
# Unsigned 2 byte little endian integer.
class Uint16le < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 16, :little)
end
# Unsigned 2 byte big endian integer.
class Uint16be < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 16, :big)
end
# Unsigned 4 byte little endian integer.
class Uint32le < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 32, :little)
end
# Unsigned 4 byte big endian integer.
class Uint32be < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 32, :big)
end
# Unsigned 8 byte little endian integer.
class Uint64le < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 64, :little)
end
# Unsigned 8 byte big endian integer.
class Uint64be < BinData::Single
register(self.name, self)
Integer.create_uint_methods(self, 64, :big)
end
# Signed 1 byte integer.
class Int8 < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 8, :little)
end
# Signed 2 byte little endian integer.
class Int16le < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 16, :little)
end
# Signed 2 byte big endian integer.
class Int16be < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 16, :big)
end
# Signed 4 byte little endian integer.
class Int32le < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 32, :little)
end
# Signed 4 byte big endian integer.
class Int32be < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 32, :big)
end
# Signed 8 byte little endian integer.
class Int64le < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 64, :little)
end
# Signed 8 byte big endian integer.
class Int64be < BinData::Single
register(self.name, self)
Integer.create_int_methods(self, 64, :big)
end
end

192
lib/bindata/io.rb Normal file
View File

@ -0,0 +1,192 @@
module BinData
# A wrapper around an IO object. The wrapper provides a consistent
# interface for BinData objects to use when accessing the IO.
class IO
# Create a new IO wrapper around +io+. +io+ must support #read if used
# for reading, #write if used for writing, #pos if reading the current
# stream position and #seek if setting the current stream position. If
# +io+ is a string it will be automatically wrapped in an StringIO object.
#
# The IO can handle bitstreams in either big or little endian format.
#
# M byte1 L M byte2 L
# S 76543210 S S fedcba98 S
# B B B B
#
# In big endian format:
# readbits(6), readbits(5) #=> [765432, 10fed]
#
# In little endian format:
# readbits(6), readbits(5) #=> [543210, a9876]
#
def initialize(io)
raise ArgumentError, "io must not be a BinData::IO" if BinData::IO === io
# wrap strings in a StringIO
if io.respond_to?(:to_str)
io = StringIO.new(io)
end
@raw_io = io
# initial stream position if stream supports positioning
@initial_pos = io.respond_to?(:pos) ? io.pos : 0
# bits when reading
@rnbits = 0
@rval = 0
@rendian = nil
# bits when writing
@wnbits = 0
@wval = 0
@wendian = nil
end
# Access to the underlying raw io.
attr_reader :raw_io
# Returns the current offset of the io stream. The exact value of
# the offset when reading bitfields is not defined.
def offset
if @raw_io.respond_to?(:pos)
@raw_io.pos - @initial_pos
else
# stream does not support positioning
0
end
end
# Seek +n+ bytes from the current position in the io stream.
def seekbytes(n)
@raw_io.seek(n, ::IO::SEEK_CUR)
end
# Reads exactly +n+ bytes from +io+.
#
# If the data read is nil an EOFError is raised.
#
# If the data read is too short an IOError is raised.
def readbytes(n)
raise "Internal state error nbits = #{@rnbits}" if @rnbits > 8
@rnbits = 0
@rval = 0
str = @raw_io.read(n)
raise EOFError, "End of file reached" if str.nil?
raise IOError, "data truncated" if str.size < n
str
end
# Reads exactly +nbits+ bits from +io+. +endian+ specifies whether
# the bits are stored in :big or :little endian format.
def readbits(nbits, endian = :big)
if @rendian != endian
# don't mix bits of differing endian
@rnbits = 0
@rval = 0
@rendian = endian
end
while nbits > @rnbits
byte = @raw_io.read(1)
raise EOFError, "End of file reached" if byte.nil?
byte = byte.unpack('C').at(0) & 0xff
if endian == :big
@rval = (@rval << 8) | byte
else
@rval = @rval | (byte << @rnbits)
end
@rnbits += 8
end
if endian == :big
val = (@rval >> (@rnbits - nbits)) & ((1 << nbits) - 1)
@rnbits -= nbits
@rval &= ((1 << @rnbits) - 1)
else
val = @rval & ((1 << nbits) - 1)
@rnbits -= nbits
@rval >>= nbits
end
val
end
# Writes the given string of bytes to the io stream.
def writebytes(str)
flushbits
@raw_io.write(str)
end
# Reads +nbits+ bits from +val+ to the stream. +endian+ specifies whether
# the bits are to be stored in :big or :little endian format.
def writebits(val, nbits, endian = :big)
# clamp val to range
val = val & ((1 << nbits) - 1)
if @wendian != endian
# don't mix bits of differing endian
flushbits if @wnbits > 0
@wendian = endian
end
if endian == :big
while nbits > 0
bits_req = 8 - @wnbits
if nbits >= bits_req
msb_bits = (val >> (nbits - bits_req)) & ((1 << bits_req) - 1)
nbits -= bits_req
val &= (1 << nbits) - 1
@wval = (@wval << bits_req) | msb_bits
@raw_io.write(@wval.chr)
@wval = 0
@wnbits = 0
else
@wval = (@wval << nbits) | val
@wnbits += nbits
nbits = 0
end
end
else
while nbits > 0
bits_req = 8 - @wnbits
if nbits >= bits_req
lsb_bits = val & ((1 << bits_req) - 1)
nbits -= bits_req
val >>= bits_req
@wval |= (lsb_bits << @wnbits)
@raw_io.write(@wval.chr)
@wval = 0
@wnbits = 0
else
@wval |= (val << @wnbits)
@wnbits += nbits
nbits = 0
end
end
end
end
# To be called after all +writebits+ have been applied.
def flushbits
if @wnbits > 8
raise "Internal state error nbits = #{@wnbits}" if @wnbits > 8
elsif @wnbits > 0
writebits(0, 8 - @wnbits, @wendian)
else
# do nothing
end
end
alias_method :flush, :flushbits
end
end

111
lib/bindata/lazy.rb Normal file
View File

@ -0,0 +1,111 @@
module BinData
# The enviroment in which a lazily evaluated lamba is called. These lambdas
# are those that are passed to data objects as parameters. Each lambda
# has access to the following:
#
# parent:: the environment of the parent data object
# params:: any extra parameters that have been passed to the data object.
# The value of a parameter is either a lambda, a symbol or a
# literal value (such as a Fixnum).
#
# Unknown methods are resolved in the context of the parent environment,
# first as keys in the extra parameters, and secondly as methods in the
# parent data object. This makes the lambda easier to read as we just write
# <tt>field</tt> instead of <tt>obj.field</tt>.
class LazyEvalEnv
# An empty hash shared by all instances
@@empty_hash = Hash.new.freeze
@@variables_cache = {}
# Creates a new environment. +parent+ is the environment of the
# parent data object.
def initialize(parent = nil)
@parent = parent
@variables = @@empty_hash
@overrides = @@empty_hash
@params = @@empty_hash
end
attr_reader :parent, :params
attr_accessor :data_object
# only accessible by another LazyEvalEnv
protected :data_object
# Set the parameters for this environment.
def params=(p)
@params = (p.nil? or p.empty?) ? @@empty_hash : p
end
# Add a variable with a pre-assigned value to this environment. +sym+
# will be accessible as a variable for any lambda evaluated
# with #lazy_eval.
def add_variable(sym, value)
sym = sym.to_sym
if @variables.equal?(@@empty_hash)
# optimise the case where only 1 variable is added as this
# is the most common occurance (BinData::Arrays adding index)
key = [sym, value]
@variables = @@variables_cache[key]
if @variables.nil?
# cache this variable and value so it can be shared with
# other LazyEvalEnvs to keep memory usage down
@variables = {sym => value}.freeze
@@variables_cache[key] = @variables
end
else
if @variables.length == 1
key = @variables.keys[0]
@variables = {key => @variables[key]}
end
@variables[sym] = value
end
end
# TODO: offset_of needs to be better thought out
def offset_of(sym)
@parent.data_object.offset_of(sym)
rescue
nil
end
# Returns the data_object for the parent environment.
def parent_data_object
@parent.nil? ? nil : @parent.data_object
end
# Evaluates +obj+ in the context of this environment. Evaluation
# recurses until it yields a value that is not a symbol or lambda.
# +overrides+ is an optional +params+ like hash
def lazy_eval(obj, overrides = nil)
result = obj
@overrides = overrides if overrides
if obj.is_a? Symbol
# treat :foo as lambda { foo }
result = __send__(obj)
elsif obj.respond_to? :arity
result = instance_eval(&obj)
end
@overrides = @@empty_hash
result
end
def method_missing(symbol, *args)
if @overrides.include?(symbol)
@overrides[symbol]
elsif @variables.include?(symbol)
@variables[symbol]
elsif @parent
obj = symbol
if @parent.params and @parent.params.has_key?(symbol)
obj = @parent.params[symbol]
elsif @parent.data_object and @parent.data_object.respond_to?(symbol)
obj = @parent.data_object.__send__(symbol, *args)
end
@parent.lazy_eval(obj)
else
super
end
end
end
end

134
lib/bindata/multi_value.rb Normal file
View File

@ -0,0 +1,134 @@
require 'bindata/struct'
module BinData
# A MultiValue is a declarative wrapper around Struct.
#
# require 'bindata'
#
# class Tuple < BinData::MultiValue
# int8 :x
# int8 :y
# int8 :z
# end
#
# class SomeDataType < BinData::MultiValue
# hide 'a'
#
# int32le :a
# int16le :b
# tuple :s
# end
#
# obj = SomeDataType.new
# obj.field_names =># ["b", "s"]
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# <tt>:fields</tt>:: An array specifying the fields for this struct.
# Each element of the array is of the form [type, name,
# params]. Type is a symbol representing a registered
# type. Name is the name of this field. Params is an
# optional hash of parameters to pass to this field
# when instantiating it.
# <tt>:hide</tt>:: A list of the names of fields that are to be hidden
# from the outside world. Hidden fields don't appear
# in #snapshot or #field_names but are still accessible
# by name.
# <tt>:endian</tt>:: Either :little or :big. This specifies the default
# endian of any numerics in this struct, or in any
# nested data objects.
class MultiValue < BinData::Struct
class << self
# Register the names of all subclasses of this class.
def inherited(subclass) #:nodoc:
register(subclass.name, subclass)
end
# Returns or sets the endianess of numerics used in this stucture.
# Endianess is applied to the fields of this structure.
# Valid values are :little and :big.
def endian(endian = nil)
@endian ||= nil
if [:little, :big].include?(endian)
@endian = endian
elsif endian != nil
raise ArgumentError, "unknown value for endian '#{endian}'"
end
@endian
end
# Returns the names of any hidden fields in this struct. Any given args
# are appended to the hidden list.
def hide(*args)
# note that fields are stored in an instance variable not a class var
@hide ||= []
@hide.concat(args.collect { |name| name.to_s })
@hide
end
# Returns all stored fields.
# Should only be called by #sanitize_parameters
def fields
@fields ||= []
end
# Used to define fields for this structure.
def method_missing(symbol, *args)
name, params = args
type = symbol
name = name.to_s
params ||= {}
# note that fields are stored in an instance variable not a class var
@fields ||= []
# check that type is known
unless Sanitizer.type_exists?(type, endian)
raise TypeError, "unknown type '#{type}' for #{self}", caller
end
# check for duplicate names
@fields.each do |t, n, p|
if n == name
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
end
end
# check that name doesn't shadow an existing method
if self.instance_methods.include?(name)
raise NameError.new("", name),
"field '#{name}' shadows an existing method", caller
end
# check that name isn't reserved
if self::RESERVED.include?(name)
raise NameError.new("", name),
"field '#{name}' is a reserved name", caller
end
# remember this field. These fields will be recalled upon creating
# an instance of this class
@fields.push([type, name, params])
end
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
endian = params[:endian] || self.endian
fields = params[:fields] || self.fields
hide = params[:hide] || self.hide
params[:endian] = endian unless endian.nil?
params[:fields] = fields
params[:hide] = hide
super(sanitizer, params)
end
end
end
end

341
lib/bindata/my_array.rb Normal file
View File

@ -0,0 +1,341 @@
require 'bindata/base'
require 'bindata/sanitize'
module BinData
# An Array is a list of data objects of the same type.
#
# require 'bindata'
#
# data = "\x03\x04\x05\x06\x07\x08\x09"
#
# obj = BinData::Array.new(:type => :int8, :initial_length => 6)
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7, 8]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { index == 1 })
# obj.read(data)
# obj.snapshot #=> [3, 4]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { element >= 6 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6]
#
# obj = BinData::Array.new(:type => :int8,
# :read_until => lambda { array[index] + array[index - 1] == 13 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7]
#
# obj = BinData::Array.new(:type => :int8, :read_until_eof => true)
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# <tt>:type</tt>:: The symbol representing the data type of the
# array elements. If the type is to have params
# passed to it, then it should be provided as
# <tt>[type_symbol, hash_params]</tt>.
# <tt>:initial_length</tt>:: The initial length of the array.
# <tt>:read_until</tt>:: While reading, elements are read until this
# condition is true. This is typically used to
# read an array until a sentinel value is found.
# The variables +index+, +element+ and +array+
# are made available to any lambda assigned to
# this parameter.
#
# Each data object in an array has the variable +index+ made available
# to any lambda evaluated as a parameter of that data object.
class Array < BinData::Base
include Enumerable
# Register this class
register(self.name, self)
# These are the parameters used by this class.
mandatory_parameter :type
optional_parameters :initial_length, :read_until, :read_until_eof
mutually_exclusive_parameters :initial_length, :read_until, :read_until_eof
class << self
# Returns a sanitized +params+ that is of the form expected
# by #initialize.
def sanitize_parameters(sanitizer, params)
params = params.dup
unless params.has_key?(:initial_length) or params.has_key?(:read_until) or params.has_key?(:read_until_eof)
# ensure one of :initial_length, :read_until, or :read_until_eof exists
params[:initial_length] = 0
end
if params.has_key?(:read_length)
warn ":read_length is not used with arrays. You probably want to change this to :initial_length"
end
if params.has_key?(:type)
type, el_params = params[:type]
params[:type] = sanitizer.sanitize(type, el_params)
end
super(sanitizer, params)
end
end
# Creates a new Array
def initialize(params = {}, env = nil)
super(params, env)
klass, el_params = param(:type)
@element_list = nil
@element_klass = klass
@element_params = el_params
end
# Returns if the element at position +index+ is clear?. If +index+
# is not given, then returns whether all fields are clear.
def clear?(index = nil)
if @element_list.nil?
true
elsif index.nil?
elements.each { |f| return false if not f.clear? }
true
else
(index < elements.length) ? elements[index].clear? : true
end
end
# Clears the element at position +index+. If +index+ is not given, then
# the internal state of the array is reset to that of a newly created
# object.
def clear(index = nil)
if @element_list.nil?
# do nothing as the array is already clear
elsif index.nil?
@element_list = nil
elsif index < elements.length
elements[index].clear
end
end
# Returns whether this data object contains a single value. Single
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
def single_value?
return false
end
# To be called after calling #do_read.
def done_read
elements.each { |f| f.done_read }
end
# Appends a new element to the end of the array. If the array contains
# single_values then the +value+ may be provided to the call.
# Returns the appended object, or value in the case of single_values.
def append(value = nil)
# TODO: deprecate #append as it can be replaced with #push
append_new_element
self[-1] = value unless value.nil?
self.last
end
# Pushes the given object(s) on to the end of this array.
# This expression returns the array itself, so several appends may
# be chained together.
def push(*args)
args.each do |arg|
if @element_klass == arg.class
# TODO: need to modify arg.env to add_variable(:index) and
# to link arg.env to self.env
elements.push(arg)
else
append(arg)
end
end
self
end
# Returns the element at +index+. If the element is a single_value
# then the value of the element is returned instead.
def [](*args)
if args.length == 1 and ::Integer === args[0]
# extend array automatically
while args[0] >= elements.length
append_new_element
end
end
data = elements[*args]
if data.respond_to?(:each)
data.collect { |el| (el && el.single_value?) ? el.value : el }
else
(data && data.single_value?) ? data.value : data
end
end
alias_method :slice, :[]
# Sets the element at +index+. If the element is a single_value
# then the value of the element is set instead.
def []=(index, value)
# extend array automatically
while index >= elements.length
append_new_element
end
obj = elements[index]
unless obj.single_value?
# TODO: allow setting objects, not just values
raise NoMethodError, "undefined method `[]=' for #{self}", caller
end
obj.value = value
end
# Iterate over each element in the array. If the elements are
# single_values then the values of the elements are iterated instead.
def each
elements.each do |el|
yield(el.single_value? ? el.value : el)
end
end
# Returns the first element, or the first +n+ elements, of the array.
# If the array is empty, the first form returns nil, and the second
# form returns an empty array.
def first(n = nil)
if n.nil?
if elements.empty?
# explicitly return nil as arrays grow automatically
nil
else
self[0]
end
else
self[0, n]
end
end
# Returns the last element, or the last +n+ elements, of the array.
# If the array is empty, the first form returns nil, and the second
# form returns an empty array.
def last(n = nil)
if n.nil?
self[-1]
else
n = length if n > length
self[-n, n]
end
end
# The number of elements in this array.
def length
elements.length
end
alias_method :size, :length
# Returns true if self array contains no elements.
def empty?
length.zero?
end
# Allow this object to be used in array context.
def to_ary
snapshot
end
#---------------
private
# Reads the values for all fields in this object from +io+.
def _do_read(io)
if has_param?(:initial_length)
elements.each { |f| f.do_read(io) }
elsif has_param?(:read_until)
@element_list = nil
loop do
element = append_new_element
element.do_read(io)
variables = { :index => self.length - 1, :element => self.last,
:array => self }
finished = eval_param(:read_until, variables)
break if finished
end
else # :read_until_eof
loop do
element = append_new_element
begin
element.do_read(io)
rescue EOFError
finished = true
remove_last_element
end
variables = { :index => self.length - 1, :element => self.last,
:array => self }
break if finished
end
end
end
# Writes the values for all fields in this object to +io+.
def _do_write(io)
elements.each { |f| f.do_write(io) }
end
# Returns the number of bytes it will take to write the element at
# +index+. If +index+, then returns the number of bytes required
# to write all fields.
def _do_num_bytes(index)
if index.nil?
(elements.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
else
elements[index].do_num_bytes
end
end
# Returns a snapshot of the data in this array.
def _snapshot
elements.collect { |e| e.snapshot }
end
# Returns the list of all elements in the array. The elements
# will be instantiated on the first call to this method.
def elements
if @element_list.nil?
@element_list = []
if has_param?(:initial_length)
# create the desired number of instances
eval_param(:initial_length).times do
append_new_element
end
end
end
@element_list
end
# Creates a new element and appends it to the end of @element_list.
# Returns the newly created element
def append_new_element
# ensure @element_list is initialised
elements()
env = create_env
env.add_variable(:index, @element_list.length)
element = @element_klass.new(@element_params, env)
@element_list << element
element
end
# Pops the last element off the end of @element_list.
# Returns the popped element.
# This is important for the :read_until_eof option to properly close
# the do_read io handle.
def remove_last_element
elements()
@element_list.pop
end
end
end

92
lib/bindata/new.diff Normal file
View File

@ -0,0 +1,92 @@
Index: lib/bindata/array.rb
===================================================================
--- lib/bindata/array.rb (revision 94)
+++ lib/bindata/array.rb (working copy)
@@ -27,6 +27,10 @@
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7]
#
+ # obj = BinData::Array.new(:type => :int8, :read_until => :eof)
+ # obj.read(data)
+ # obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
+ #
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
@@ -42,7 +46,9 @@
# read an array until a sentinel value is found.
# The variables +index+, +element+ and +array+
# are made available to any lambda assigned to
- # this parameter.
+ # this parameter. If the value of this parameter
+ # is the symbol :eof, then the array will read
+ # as much data from the stream as possible.
#
# Each data object in an array has the variable +index+ made available
# to any lambda evaluated as a parameter of that data object.
@@ -249,15 +255,28 @@
def _do_read(io)
if has_param?(:initial_length)
elements.each { |f| f.do_read(io) }
- else # :read_until
- @element_list = nil
- loop do
- element = append_new_element
- element.do_read(io)
- variables = { :index => self.length - 1, :element => self.last,
- :array => self }
- finished = eval_param(:read_until, variables)
- break if finished
+ elsif has_param?(:read_until)
+ if param(:read_until) == :eof
+ @element_list = nil
+ loop do
+ element = append_new_element
+ begin
+ element.do_read(io)
+ rescue
+ @element_list.pop
+ break
+ end
+ end
+ else
+ @element_list = nil
+ loop do
+ element = append_new_element
+ element.do_read(io)
+ variables = { :index => self.length - 1, :element => self.last,
+ :array => self }
+ finished = eval_param(:read_until, variables)
+ break if finished
+ end
end
end
end
Index: spec/array_spec.rb
===================================================================
--- spec/array_spec.rb (revision 92)
+++ spec/array_spec.rb (working copy)
@@ -264,6 +264,22 @@
end
end
+describe BinData::Array, "with :read_until => :eof" do
+ it "should read records until eof" do
+ obj = BinData::Array.new(:type => :int8, :read_until => :eof)
+ data = "\x01\x02\x03"
+ obj.read(data)
+ obj.snapshot.should == [1, 2, 3]
+ end
+
+ it "should read records until eof, ignoring partial records" do
+ obj = BinData::Array.new(:type => :int16be, :read_until => :eof)
+ data = "\x00\x01\x00\x02\x03"
+ obj.read(data)
+ obj.snapshot.should == [1, 2]
+ end
+end
+
describe BinData::Array, "of bits" do
before(:each) do
@data = BinData::Array.new(:type => :bit1, :initial_length => 15)

86
lib/bindata/out.txt Normal file
View File

@ -0,0 +1,86 @@
Index: array.rb
===================================================================
--- array.rb (revision 98)
+++ array.rb (working copy)
@@ -26,7 +26,12 @@
# :read_until => lambda { array[index] + array[index - 1] == 13 })
# obj.read(data)
# obj.snapshot #=> [3, 4, 5, 6, 7]
- #
+ #
+ # obj = BinData::Array.new(:type => :int8, :read_until_eof => true)
+ # obj.read(data)
+ # obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
+ #
+ #
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
@@ -54,8 +59,8 @@
# These are the parameters used by this class.
mandatory_parameter :type
- optional_parameters :initial_length, :read_until
- mutually_exclusive_parameters :initial_length, :read_until
+ optional_parameters :initial_length, :read_until, :read_until_eof
+ mutually_exclusive_parameters :initial_length, :read_until, :read_until_eof
class << self
# Returns a sanitized +params+ that is of the form expected
@@ -63,8 +68,8 @@
def sanitize_parameters(sanitizer, params)
params = params.dup
- unless params.has_key?(:initial_length) or params.has_key?(:read_until)
- # ensure one of :initial_length and :read_until exists
+ unless params.has_key?(:initial_length) or params.has_key?(:read_until) or params.has_key?(:read_until_eof)
+ # ensure one of :initial_length, :read_until, or :read_until_eof exists
params[:initial_length] = 0
end
@@ -249,7 +254,7 @@
def _do_read(io)
if has_param?(:initial_length)
elements.each { |f| f.do_read(io) }
- else # :read_until
+ elsif has_param?(:read_until)
@element_list = nil
loop do
element = append_new_element
@@ -259,7 +264,20 @@
finished = eval_param(:read_until, variables)
break if finished
end
- end
+ else # :read_until_eof
+ loop do
+ element = append_new_element
+ begin
+ element.do_read(io)
+ rescue EOFError
+ finished = true
+ remove_last_element
+ end
+ variables = { :index => self.length - 1, :element => self.last,
+ :array => self }
+ break if finished
+ end
+ end
end
# Writes the values for all fields in this object to +io+.
@@ -310,5 +328,14 @@
@element_list << element
element
end
+
+ # Pops the last element off the end of @element_list.
+ # Returns the popped element.
+ # This is important for the :read_until_eof option to properly close
+ # the do_read io handle.
+ def remove_last_element
+ elements()
+ @element_list.pop
+ end
end
end

42
lib/bindata/registry.rb Normal file
View File

@ -0,0 +1,42 @@
require 'singleton'
module BinData
# This registry contains a register of name -> class mappings.
class Registry
include Singleton
def initialize
@registry = {}
end
# Convert camelCase +name+ to underscore style.
def underscore_name(name)
name.to_s.sub(/.*::/, "").
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
# Registers the mapping of +name+ to +klass+. +name+ is converted
# from camelCase to underscore style.
# Returns the converted name
def register(name, klass)
# convert camelCase name to underscore style
key = underscore_name(name)
# warn if replacing an existing class
if $VERBOSE and (existing = @registry[key])
warn "warning: replacing registered class #{existing} with #{klass}"
end
@registry[key] = klass
key.dup
end
# Returns the class matching a previously registered +name+.
def lookup(name)
@registry[name.to_s]
end
end
end

41
lib/bindata/rest.rb Normal file
View File

@ -0,0 +1,41 @@
require "bindata/single"
module BinData
# Rest will consume the input stream from the current position to the end of
# the stream. This will mainly be useful for debugging and developing.
#
# require 'bindata'
#
# class A < BinData::MultiValue
# string :a, :read_length => 5
# rest :rest
# end
#
# obj = A.read("abcdefghij")
# obj.a #=> "abcde"
# obj.rest #=" "fghij"
#
class Rest < BinData::Single
# Register this class
register(self.name, self)
#---------------
private
# Return the string representation that +val+ will take when written.
def val_to_str(val)
val
end
# Read a number of bytes from +io+ and return the value they represent.
def read_val(io)
io.raw_io.read
end
# Returns an empty string as default.
def sensible_default
""
end
end
end

121
lib/bindata/sanitize.rb Normal file
View File

@ -0,0 +1,121 @@
require 'forwardable'
module BinData
class Sanitizer
class << self
# Sanitize +params+ for +obj+.
# Returns sanitized parameters.
def sanitize(obj, params)
sanitizer = self.new
klass, new_params = sanitizer.sanitize(obj.class, params)
new_params
end
# Returns true if +type+ is registered.
def type_exists?(type, endian = nil)
lookup(type, endian) != nil
end
# Returns the class matching a previously registered +name+.
def lookup(name, endian)
name = name.to_s
klass = Registry.instance.lookup(name)
if klass.nil? and endian != nil
# lookup failed so attempt endian lookup
if /^u?int\d{1,3}$/ =~ name
new_name = name + ((endian == :little) ? "le" : "be")
klass = Registry.instance.lookup(new_name)
elsif ["float", "double"].include?(name)
new_name = name + ((endian == :little) ? "_le" : "_be")
klass = Registry.instance.lookup(new_name)
end
end
klass
end
end
# Create a new Sanitizer.
def initialize
@seen = []
@endian = nil
end
# Executes the given block with +endian+ set as the current endian.
def with_endian(endian, &block)
if endian != nil
saved_endian = @endian
@endian = endian
yield
@endian = saved_endian
else
yield
end
end
# Sanitizes +params+ for +type+.
# Returns [klass, sanitized_params]
def sanitize(type, params)
if Class === type
klass = type
else
klass = self.class.lookup(type, @endian)
raise TypeError, "unknown type '#{type}'" if klass.nil?
end
new_params = params.nil? ? {} : params.dup
if @seen.include?(klass)
# This klass is defined recursively. Remember the current endian
# and delay sanitizing the parameters until later.
if @endian != nil and klass.accepted_parameters.include?(:endian) and
not new_params.has_key?(:endian)
new_params[:endian] = @endian
end
else
# subclasses of MultiValue may be defined recursively
# TODO: define a class field instead
possibly_recursive = (BinData.const_defined?(:MultiValue) and
klass.ancestors.include?(BinData.const_get(:MultiValue)))
@seen.push(klass) if possibly_recursive
klass.sanitize_parameters!(self, new_params)
new_params = SanitizedParameters.new(klass, new_params)
end
[klass, new_params]
end
end
# A BinData object accepts arbitrary parameters. This class ensures that
# the parameters have been sanitized, and categorizes them according to
# whether they are BinData::Base.accepted_parameters or are extra.
class SanitizedParameters
extend Forwardable
# Sanitize the given parameters.
def initialize(klass, params)
@hash = params
@accepted_parameters = {}
@extra_parameters = {}
# partition parameters into known and extra parameters
@hash.each do |k,v|
k = k.to_sym
if v.nil?
raise ArgumentError, "parameter :#{k} has nil value in #{klass}"
end
if klass.accepted_parameters.include?(k)
@accepted_parameters[k] = v
else
@extra_parameters[k] = v
end
end
end
attr_reader :accepted_parameters, :extra_parameters
def_delegators :@hash, :[], :has_key?, :include?, :keys
end
end

177
lib/bindata/single.rb Normal file
View File

@ -0,0 +1,177 @@
require 'bindata/base'
module BinData
# A BinData::Single object is a container for a value that has a particular
# binary representation. A value corresponds to a primitive type such as
# as integer, float or string. Only one value can be contained by this
# object. This value can be read from or written to an IO stream.
#
# require 'bindata'
#
# obj = BinData::Uint8.new(:initial_value => 42)
# obj.value #=> 42
# obj.value = 5
# obj.value #=> 5
# obj.clear
# obj.value #=> 42
#
# obj = BinData::Uint8.new(:value => 42)
# obj.value #=> 42
# obj.value = 5
# obj.value #=> 42
#
# obj = BinData::Uint8.new(:check_value => 3)
# obj.read("\005") #=> BinData::ValidityError: value is '5' but expected '3'
#
# obj = BinData::Uint8.new(:check_value => lambda { value < 5 })
# obj.read("\007") #=> BinData::ValidityError: value not as expected
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params include those for BinData::Base as well as:
#
# [<tt>:initial_value</tt>] This is the initial value to use before one is
# either #read or explicitly set with #value=.
# [<tt>:value</tt>] The object will always have this value.
# Explicitly calling #value= is prohibited when
# using this param. In the interval between
# calls to #do_read and #done_read, #value
# will return the value of the data read from the
# IO, not the result of the <tt>:value</tt> param.
# [<tt>:check_value</tt>] Raise an error unless the value read in meets
# this criteria. The variable +value+ is made
# available to any lambda assigned to this
# parameter. A boolean return indicates success
# or failure. Any other return is compared to
# the value just read in.
class Single < BinData::Base
# These are the parameters used by this class.
optional_parameters :initial_value, :value, :check_value
mutually_exclusive_parameters :initial_value, :value
def initialize(params = {}, env = nil)
super(params, env)
clear
end
# Resets the internal state to that of a newly created object.
def clear
@value = nil
@in_read = false
end
# Returns if the value of this data has been read or explicitly set.
def clear?
@value.nil?
end
# Single objects are single_values
def single_value?
true
end
# To be called after calling #do_read.
def done_read
@in_read = false
end
# Returns the current value of this data.
def value
_value
end
# Sets the value of this data.
def value=(v)
# only allow modification if the value isn't predefined
unless has_param?(:value)
raise ArgumentError, "can't set a nil value" if v.nil?
@value = v
# Note that this doesn't do anything in ruby 1.8.x so ignore for now
# # explicitly return the output of #value as v may be different
# self.value
end
end
#---------------
private
# Reads the value for this data from +io+.
def _do_read(io)
@in_read = true
@value = read_val(io)
# does the value meet expectations?
if has_param?(:check_value)
current_value = self.value
expected = eval_param(:check_value, :value => current_value)
if not expected
raise ValidityError, "value '#{current_value}' not as expected"
elsif current_value != expected and expected != true
raise ValidityError, "value is '#{current_value}' but " +
"expected '#{expected}'"
end
end
end
# Writes the value for this data to +io+.
def _do_write(io)
raise "can't write whilst reading" if @in_read
io.writebytes(val_to_str(_value))
end
# Returns the number of bytes it will take to write this data.
def _do_num_bytes(ignored)
val_to_str(_value).length
end
# Returns a snapshot of this data object.
def _snapshot
value
end
# The unmodified value of this data object. Note that #value calls this
# method. This is so that #value can be overridden in subclasses to
# modify the value.
def _value
# Table of possible preconditions and expected outcome
# 1. :value and !in_read -> :value
# 2. :value and in_read -> @value
# 3. :initial_value and clear? -> :initial_value
# 4. :initial_value and !clear? -> @value
# 5. clear? -> sensible_default
# 6. !clear? -> @value
if not @in_read and (evaluated_value = eval_param(:value))
# rule 1 above
evaluated_value
else
# combining all other rules gives this simplified expression
@value || eval_param(:value) ||
eval_param(:initial_value) || sensible_default()
end
end
###########################################################################
# To be implemented by subclasses
# Return the string representation that +val+ will take when written.
def val_to_str(val)
raise NotImplementedError
end
# Read a number of bytes from +io+ and return the value they represent.
def read_val(io)
raise NotImplementedError
end
# Return a sensible default for this data.
def sensible_default
raise NotImplementedError
end
# To be implemented by subclasses
###########################################################################
end
end

194
lib/bindata/single_value.rb Normal file
View File

@ -0,0 +1,194 @@
require 'bindata/single'
require 'bindata/struct'
module BinData
# A SingleValue is a declarative way to define a new BinData data type.
# The data type must contain a single value only. For new data types
# that contain multiple values see BinData::MultiValue.
#
# To define a new data type, set fields as if for MultiValue and add a
# #get and #set method to extract / convert the data between the fields
# and the #value of the object.
#
# require 'bindata'
#
# class PascalString < BinData::SingleValue
# uint8 :len, :value => lambda { data.length }
# string :data, :read_length => :len
#
# def get
# self.data
# end
#
# def set(v)
# self.data = v
# end
# end
#
# ps = PascalString.new(:initial_value => "hello")
# ps.to_s #=> "\005hello"
# ps.read("\003abcde")
# ps.value #=> "abc"
#
# # Unsigned 24 bit big endian integer
# class Uint24be < BinData::SingleValue
# uint8 :byte1
# uint8 :byte2
# uint8 :byte3
#
# def get
# (self.byte1 << 16) | (self.byte2 << 8) | self.byte3
# end
#
# def set(v)
# v = 0 if v < 0
# v = 0xffffff if v > 0xffffff
#
# self.byte1 = (v >> 16) & 0xff
# self.byte2 = (v >> 8) & 0xff
# self.byte3 = v & 0xff
# end
# end
#
# u24 = Uint24be.new
# u24.read("\x12\x34\x56")
# "0x%x" % u24.value #=> 0x123456
#
# == Parameters
#
# SingleValue objects accept all the parameters that BinData::Single do.
#
class SingleValue < Single
class << self
# Register the names of all subclasses of this class.
def inherited(subclass) #:nodoc:
register(subclass.name, subclass)
end
# Returns or sets the endianess of numerics used in this stucture.
# Endianess is applied to the fields of this structure.
# Valid values are :little and :big.
def endian(endian = nil)
@endian ||= nil
if [:little, :big].include?(endian)
@endian = endian
elsif endian != nil
raise ArgumentError, "unknown value for endian '#{endian}'"
end
@endian
end
# Returns all stored fields. Should only be called by
# #sanitize_parameters
def fields
@fields || []
end
# Used to define fields for the internal structure.
def method_missing(symbol, *args)
name, params = args
type = symbol
name = (name.nil? or name == "") ? nil : name.to_s
params ||= {}
# note that fields are stored in an instance variable not a class var
@fields ||= []
# check that type is known
unless Sanitizer.type_exists?(type, endian)
raise TypeError, "unknown type '#{type}' for #{self}", caller
end
# check that name is okay
if name != nil
# check for duplicate names
@fields.each do |t, n, p|
if n == name
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
end
end
# check that name doesn't shadow an existing method
if self.instance_methods.include?(name)
raise NameError.new("", name),
"field '#{name}' shadows an existing method", caller
end
end
# remember this field. These fields will be recalled upon creating
# an instance of this class
@fields.push([type, name, params])
end
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
struct_params = {}
struct_params[:fields] = self.fields
struct_params[:endian] = self.endian unless self.endian.nil?
params[:struct_params] = struct_params
super(sanitizer, params)
end
end
# These are the parameters used by this class.
mandatory_parameter :struct_params
def initialize(params = {}, env = nil)
super(params, env)
@struct = BinData::Struct.new(param(:struct_params), create_env)
end
# Forward method calls to the internal struct.
def method_missing(symbol, *args, &block)
if @struct.respond_to?(symbol)
@struct.__send__(symbol, *args, &block)
else
super
end
end
#---------------
private
# Retrieve a sensible default from the internal struct.
def sensible_default
get
end
# Read data into the fields of the internal struct then return the value.
def read_val(io)
@struct.read(io)
get
end
# Sets +val+ into the fields of the internal struct then returns the
# string representation.
def val_to_str(val)
set(val)
@struct.to_s
end
###########################################################################
# To be implemented by subclasses
# Extracts the value for this data object from the fields of the
# internal struct.
def get
raise NotImplementedError
end
# Sets the fields of the internal struct to represent +v+.
def set(v)
raise NotImplementedError
end
# To be implemented by subclasses
###########################################################################
end
end

115
lib/bindata/string.rb Normal file
View File

@ -0,0 +1,115 @@
require "bindata/single"
module BinData
# A String is a sequence of bytes. This is the same as strings in Ruby.
# The issue of character encoding is ignored by this class.
#
# require 'bindata'
#
# data = "abcdefghij"
#
# obj = BinData::String.new(:read_length => 5)
# obj.read(data)
# obj.value #=> "abcde"
#
# obj = BinData::String.new(:length => 6)
# obj.read(data)
# obj.value #=> "abcdef"
# obj.value = "abcdefghij"
# obj.value #=> "abcdef"
# obj.value = "abcd"
# obj.value #=> "abcd\000\000"
#
# obj = BinData::String.new(:length => 6, :trim_value => true)
# obj.value = "abcd"
# obj.value #=> "abcd"
# obj.to_s #=> "abcd\000\000"
#
# obj = BinData::String.new(:length => 6, :pad_char => 'A')
# obj.value = "abcd"
# obj.value #=> "abcdAA"
# obj.to_s #=> "abcdAA"
#
# == Parameters
#
# String objects accept all the params that BinData::Single
# does, as well as the following:
#
# <tt>:read_length</tt>:: The length to use when reading a value.
# <tt>:length</tt>:: The fixed length of the string. If a shorter
# string is set, it will be padded to this length.
# <tt>:pad_char</tt>:: The character to use when padding a string to a
# set length. Valid values are Integers and
# Strings of length 1. "\0" is the default.
# <tt>:trim_value</tt>:: Boolean, default false. If set, #value will
# return the value with all pad_chars trimmed
# from the end of the string. The value will
# not be trimmed when writing.
class String < BinData::Single
# Register this class
register(self.name, self)
# These are the parameters used by this class.
optional_parameters :read_length, :length, :trim_value
default_parameters :pad_char => "\0"
mutually_exclusive_parameters :read_length, :length
mutually_exclusive_parameters :length, :value
class << self
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
# warn about deprecated param - remove before releasing 1.0
if params[:initial_length]
warn ":initial_length is deprecated. Replacing with :read_length"
params[:read_length] = params.delete(:initial_length)
end
# set :pad_char to be a single length character string
if params.has_key?(:pad_char)
ch = params[:pad_char]
ch = ch.respond_to?(:chr) ? ch.chr : ch.to_s
if ch.length > 1
raise ArgumentError, ":pad_char must not contain more than 1 char"
end
params[:pad_char] = ch
end
super(sanitizer, params)
end
end
# Overrides value to return the value padded to the desired length or
# trimmed as required.
def value
v = val_to_str(_value)
v.sub!(/#{eval_param(:pad_char)}*$/, "") if param(:trim_value) == true
v
end
#---------------
private
# Returns +val+ ensuring that it is padded to the desired length.
def val_to_str(val)
# trim val if necessary
len = eval_param(:length) || val.length
str = val.slice(0, len)
# then pad to length if str is short
str << (eval_param(:pad_char) * (len - str.length))
end
# Read a number of bytes from +io+ and return the value they represent.
def read_val(io)
len = eval_param(:read_length) || eval_param(:length) || 0
io.readbytes(len)
end
# Returns an empty string as default.
def sensible_default
""
end
end
end

98
lib/bindata/stringz.rb Normal file
View File

@ -0,0 +1,98 @@
require "bindata/single"
module BinData
# A BinData::Stringz object is a container for a zero ("\0") terminated
# string.
#
# For convenience, the zero terminator is not necessary when setting the
# value. Likewise, the returned value will not be zero terminated.
#
# require 'bindata'
#
# data = "abcd\x00efgh"
#
# obj = BinData::Stringz.new
# obj.read(data)
# obj.snapshot #=> "abcd"
# obj.value #=> "abcd"
# obj.num_bytes #=> 5
# obj.to_s #=> "abcd\000"
#
# == Parameters
#
# Stringz objects accept all the params that BinData::Single
# does, as well as the following:
#
# <tt>:max_length</tt>:: The maximum length of the string including the zero
# byte.
class Stringz < BinData::Single
# Register this class
register(self.name, self)
# These are the parameters used by this class.
optional_parameters :max_length
# Overrides value to return the value of this data excluding the trailing
# zero byte.
def value
v = super
val_to_str(v).chomp("\0")
end
#---------------
private
# Returns +val+ ensuring it is zero terminated and no longer
# than <tt>:max_length</tt> bytes.
def val_to_str(val)
zero_terminate(val, eval_param(:max_length))
end
# Read a number of bytes from +io+ and return the value they represent.
def read_val(io)
max_length = eval_param(:max_length)
str = ""
i = 0
ch = nil
# read until zero byte or we have read in the max number of bytes
while ch != "\0" and i != max_length
ch = io.readbytes(1)
str << ch
i += 1
end
zero_terminate(str, max_length)
end
# Returns an empty string as default.
def sensible_default
""
end
# Returns +str+ after it has been zero terminated. The returned string
# will not be longer than +max_length+.
def zero_terminate(str, max_length = nil)
# str must not be empty
result = (str == "") ? "\0" : str
# remove anything after the first \0
result = result.sub(/([^\0]*\0).*/, '\1')
# trim string to be no longer than max_length including zero byte
if max_length
max_length = 1 if max_length < 1
result = result[0, max_length]
if result.length == max_length and result[-1, 1] != "\0"
result[-1, 1] = "\0"
end
end
# ensure last byte in the string is a zero byte
result << "\0" if result[-1, 1] != "\0"
result
end
end
end

383
lib/bindata/struct.rb Normal file
View File

@ -0,0 +1,383 @@
require 'bindata/base'
require 'bindata/sanitize'
module BinData
# A Struct is an ordered collection of named data objects.
#
# require 'bindata'
#
# class Tuple < BinData::MultiValue
# int8 :x
# int8 :y
# int8 :z
# end
#
# obj = BinData::Struct.new(:hide => :a,
# :fields => [ [:int32le, :a],
# [:int16le, :b],
# [:tuple, :s] ])
# obj.field_names =># ["b", "s"]
#
#
# == Parameters
#
# Parameters may be provided at initialisation to control the behaviour of
# an object. These params are:
#
# <tt>:fields</tt>:: An array specifying the fields for this struct.
# Each element of the array is of the form [type, name,
# params]. Type is a symbol representing a registered
# type. Name is the name of this field. Params is an
# optional hash of parameters to pass to this field
# when instantiating it.
# <tt>:hide</tt>:: A list of the names of fields that are to be hidden
# from the outside world. Hidden fields don't appear
# in #snapshot or #field_names but are still accessible
# by name.
# <tt>:endian</tt>:: Either :little or :big. This specifies the default
# endian of any numerics in this struct, or in any
# nested data objects.
class Struct < BinData::Base
# These reserved words may not be used as field names
RESERVED = (::Hash.instance_methods +
%w{alias and begin break case class def defined do else elsif
end ensure false for if in module next nil not or redo
rescue retry return self super then true undef unless until
when while yield }).uniq
# Register this class
register(self.name, self)
# A hash that can be accessed via attributes.
class Snapshot < Hash #:nodoc:
def method_missing(symbol, *args)
self[symbol.id2name] || super
end
end
class << self
#### DEPRECATION HACK to allow inheriting from BinData::Struct
#
def inherited(subclass) #:nodoc:
if subclass != MultiValue
# warn about deprecated method - remove before releasing 1.0
warn "warning: inheriting from BinData::Struct in deprecated. Inherit from BinData::MultiValue instead."
register(subclass.name, subclass)
end
end
def endian(endian = nil)
@endian ||= nil
if [:little, :big].include?(endian)
@endian = endian
elsif endian != nil
raise ArgumentError, "unknown value for endian '#{endian}'"
end
@endian
end
def hide(*args)
# note that fields are stored in an instance variable not a class var
@hide ||= []
args.each do |name|
@hide << name.to_s
end
@hide
end
def fields
@fields || []
end
def method_missing(symbol, *args)
name, params = args
type = symbol
name = name.to_s
params ||= {}
# note that fields are stored in an instance variable not a class var
@fields ||= []
# check that type is known
unless Sanitizer.type_exists?(type, endian)
raise TypeError, "unknown type '#{type}' for #{self}", caller
end
# check for duplicate names
@fields.each do |t, n, p|
if n == name
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
end
end
# check that name doesn't shadow an existing method
if self.instance_methods.include?(name)
raise NameError.new("", name),
"field '#{name}' shadows an existing method", caller
end
# check that name isn't reserved
if self::RESERVED.include?(name)
raise NameError.new("", name),
"field '#{name}' is a reserved name", caller
end
# remember this field. These fields will be recalled upon creating
# an instance of this class
@fields.push([type, name, params])
end
def deprecated_hack!(params)
# possibly override endian
endian = params[:endian] || self.endian
params[:endian] = endian unless endian.nil?
params[:fields] = params[:fields] || self.fields
params[:hide] = params[:hide] || self.hide
end
#
#### DEPRECATION HACK to allow inheriting from BinData::Struct
# Ensures that +params+ is of the form expected by #initialize.
def sanitize_parameters!(sanitizer, params)
#### DEPRECATION HACK to allow inheriting from BinData::Struct
#
deprecated_hack!(params)
#
#### DEPRECATION HACK to allow inheriting from BinData::Struct
# possibly override endian
endian = params[:endian]
if endian != nil
unless [:little, :big].include?(endian)
raise ArgumentError, "unknown value for endian '#{endian}'"
end
params[:endian] = endian
end
if params.has_key?(:fields)
sanitizer.with_endian(endian) do
# ensure names of fields are strings and that params is sanitized
all_fields = params[:fields].collect do |ftype, fname, fparams|
fname = fname.to_s
klass, sanitized_fparams = sanitizer.sanitize(ftype, fparams)
[klass, fname, sanitized_fparams]
end
params[:fields] = all_fields
end
# now params are sanitized, check that parameter names are okay
field_names = []
instance_methods = self.instance_methods
reserved_names = RESERVED
params[:fields].each do |fklass, fname, fparams|
# check that name doesn't shadow an existing method
if instance_methods.include?(fname)
raise NameError.new("Rename field '#{fname}' in #{self}, " +
"as it shadows an existing method.", fname)
end
# check that name isn't reserved
if reserved_names.include?(fname)
raise NameError.new("Rename field '#{fname}' in #{self}, " +
"as it is a reserved name.", fname)
end
# check for multiple definitions
if field_names.include?(fname)
raise NameError.new("field '#{fname}' in #{self}, " +
"is defined multiple times.", fname)
end
field_names << fname
end
# collect all hidden names that correspond to a field name
hide = []
if params.has_key?(:hide)
hidden = (params[:hide] || []).collect { |h| h.to_s }
all_field_names = params[:fields].collect { |k,n,p| n }
hide = hidden & all_field_names
end
params[:hide] = hide
end
super(sanitizer, params)
end
end
# These are the parameters used by this class.
mandatory_parameter :fields
optional_parameters :endian, :hide
# Creates a new Struct.
def initialize(params = {}, env = nil)
super(params, env)
# extract field names but don't instantiate the fields
@field_names = param(:fields).collect { |k, n, p| n }
@field_objs = []
end
# Clears the field represented by +name+. If no +name+
# is given, clears all fields in the struct.
def clear(name = nil)
if name.nil?
@field_objs.each { |f| f.clear unless f.nil? }
else
obj = find_obj_for_name(name.to_s)
obj.clear unless obj.nil?
end
end
# Returns if the field represented by +name+ is clear?. If no +name+
# is given, returns whether all fields are clear.
def clear?(name = nil)
if name.nil?
@field_objs.each do |f|
return false unless f.nil? or f.clear?
end
true
else
obj = find_obj_for_name(name.to_s)
obj.nil? ? true : obj.clear?
end
end
# Returns whether this data object contains a single value. Single
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
def single_value?
return false
end
# Returns a list of the names of all fields accessible through this
# object. +include_hidden+ specifies whether to include hidden names
# in the listing.
def field_names(include_hidden = false)
# collect field names
names = []
hidden = param(:hide)
@field_names.each do |name|
if include_hidden or not hidden.include?(name)
names << name
end
end
names
end
# To be called after calling #read.
def done_read
@field_objs.each { |f| f.done_read unless f.nil? }
end
# Returns the data object that stores values for +name+.
def find_obj_for_name(name)
idx = @field_names.index(name)
if idx
instantiate_obj(idx)
@field_objs[idx]
else
nil
end
end
def offset_of(field)
idx = @field_names.index(field.to_s)
if idx
instantiate_all
offset = 0
(0...idx).each do |i|
this_offset = @field_objs[i].do_num_bytes
if ::Float === offset and ::Integer === this_offset
offset = offset.ceil
end
offset += this_offset
end
offset
else
nil
end
end
# Override to include field names
alias_method :orig_respond_to?, :respond_to?
def respond_to?(symbol, include_private = false)
orig_respond_to?(symbol, include_private) ||
field_names(true).include?(symbol.id2name.chomp("="))
end
def method_missing(symbol, *args, &block)
name = symbol.id2name
is_writer = (name[-1, 1] == "=")
name.chomp!("=")
# find the object that is responsible for name
if (obj = find_obj_for_name(name))
# pass on the request
if obj.single_value? and is_writer
obj.value = *args
elsif obj.single_value?
obj.value
else
obj
end
else
super
end
end
#---------------
private
# Instantiates all fields.
def instantiate_all
(0...@field_names.length).each { |idx| instantiate_obj(idx) }
end
# Instantiates the field object at position +idx+.
def instantiate_obj(idx)
if @field_objs[idx].nil?
fklass, fname, fparams = param(:fields)[idx]
@field_objs[idx] = fklass.new(fparams, create_env)
end
end
# Reads the values for all fields in this object from +io+.
def _do_read(io)
instantiate_all
@field_objs.each { |f| f.do_read(io) }
end
# Writes the values for all fields in this object to +io+.
def _do_write(io)
instantiate_all
@field_objs.each { |f| f.do_write(io) }
end
# Returns the number of bytes it will take to write the field represented
# by +name+. If +name+ is nil then returns the number of bytes required
# to write all fields.
def _do_num_bytes(name)
if name.nil?
instantiate_all
(@field_objs.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
else
obj = find_obj_for_name(name.to_s)
obj.nil? ? 0 : obj.do_num_bytes
end
end
# Returns a snapshot of this struct as a hash.
def _snapshot
hash = Snapshot.new
field_names.each do |name|
ss = find_obj_for_name(name).snapshot
hash[name] = ss unless ss.nil?
end
hash
end
end
end

70
lib/packetfu.rb Normal file
View File

@ -0,0 +1,70 @@
if VERSION < "1.8.6"
raise RuntimeError, "Ruby not at a minimum version of 1.8.6"
end
require 'bindata'
# This version requirement is a bit of a lie; we need svn version
# 99 or later, so we can make use of this commit:
# r99 | dmendel | 2008-07-24 23:45:49 -0500 (Thu, 24 Jul 2008) | 1 line
#
# Allow arrays to read until eof
#
# So, for now, PacketFu will distribute with a slightly forked BinData.
# We'll unfork when 0.9.3 is released and all will be right with the world.
if BinData::VERSION < "0.9.2-eofpatch"
raise LoadError, "BinData not at version 0.9.2-eofpatch"
end
require 'ipaddr'
require 'singleton'
module PacketFu
@@pcaprub_loaded = false
begin
require 'pcaprub'
if Pcap.version < "0.8-dev"
@@pcaprub_loaded = false # Don't bother with broken versions
raise LoadError, "PcapRub not at a minimum version of 0.8-dev"
end
require 'packetfu/capture'
require 'packetfu/read'
require 'packetfu/inject'
rescue LoadError
end
end
# Doesn't require PcapRub
require 'packetfu/pcap'
require 'packetfu/write'
# Packet crafting/parsing goodness.
require 'packetfu/packet'
require 'packetfu/invalid'
require 'packetfu/eth'
require 'packetfu/ip'
require 'packetfu/arp'
require 'packetfu/icmp'
require 'packetfu/udp'
require 'packetfu/tcp'
require 'packetfu/ipv6'
# Various often-used utilities.
require 'packetfu/utils'
# A place to keep defaults.
require 'packetfu/config'
#:main:PacketFu
#
#:include:../README
#:include:../LICENSE
module PacketFu
# Returns the version.
def self.version
"0.1.0" # September 13, 2008
end
end

26
lib/packetfu/LICENSE Normal file
View File

@ -0,0 +1,26 @@
Copyright (c) 2008, Tod Beardsley
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Tod Beardsley nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY TOD BEARDSLEY ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL TOD BEARDSLEY BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

28
lib/packetfu/README Normal file
View File

@ -0,0 +1,28 @@
= PacketFu
A library for reading a writing packets to an interface or to a libpcap-formatted file.
It is maintained at http://code.google.com/p/packetfu
== Installation
PacketFu should live somewhere in your path. I haven't decided yet on packaging, will probably succumb to gems; your best bet is to just drop it into /usr/local/lib/site_ruby/1.8 or where ever you install bindata to.
== Requirements
BinData: http://bindata.rubyforge.org
Dion Mendel's BinData is absolutely critical for PacketFu. Specifically, BinData's subversion r99 or later is required, in order to make use of the :eof patch. So, BinData r101 is included in this distribution until 0.9.3 or later is released.
PcapRub: www.metasploit.com/svn/framework3/trunk/external/pcaprub
Marshall Beddoe's PcapRub is required only for packet reading and writing from a network interfaces (which is a pretty big only). PcapRub itself relies on libpcap 0.9.8 or later for packet injection. It also requires root privilieges to access the interface directly. Perhaps most noteworthy, PcapRub is <b>not</b> included in this distribution, as the vagaries of libpcap driver install can lead to some unexpected results. You are on your own for that.
== Examples
Wouldn't that be nice? The best way to learn right now is to pore over the documentation, and mess around with packetfu-shell.rb
== Author
PacketFu is maintained primarily by Tod Beardsley <todb@planb-security.net>
== License

177
lib/packetfu/arp.rb Normal file
View File

@ -0,0 +1,177 @@
module PacketFu
# ARPHeader is a complete ARP struct, used in ARPPacket.
#
# ARP is used to discover the machine address of nearby devices.
#
# See http://www.networksorcery.com/enp/protocol/arp.htm for details.
#
# ==== Header Definition
#
# uint16be :arp_hw, :initial_value => 1 # Ethernet
# uint16be :arp_proto, :initial_value => 0x0800 # IP
# uint8 :arp_hw_len, :initial_value => 6
# uint8 :arp_proto_len, :initial_value => 4
# uint16be :arp_opcode, :initial_value => 1 # 1: Request, 2: Reply, 3: Request-Reverse, 4: Reply-Reverse
# eth_mac :arp_src_mac # From eth.rb
# octets :arp_src_ip # From ip.rb
# eth_mac :arp_dst_mac # From eth.rb
# octets :arp_dst_ip # From ip.rb
# rest :body
#
class ARPHeader < BinData::MultiValue
uint16be :arp_hw, :initial_value => 1 # Ethernet
uint16be :arp_proto, :initial_value => 0x0800 # IP
uint8 :arp_hw_len, :initial_value => 6
uint8 :arp_proto_len, :initial_value => 4
uint16be :arp_opcode, :initial_value => 1 # 1: Request, 2: Reply, 3: Request-Reverse, 4: Reply-Reverse
eth_mac :arp_src_mac # From eth.rb
octets :arp_src_ip # From ip.rb
eth_mac :arp_dst_mac # From eth.rb
octets :arp_dst_ip # From ip.rb
rest :body
# Set the source MAC address in a more readable way.
def arp_saddr_mac=(mac)
mac = EthHeader.mac2str(mac)
self.arp_src_mac.read(mac)
self.arp_src_mac
end
# Returns a more readable source MAC address.
def arp_saddr_mac
EthHeader.str2mac(self.arp_src_mac.to_s)
end
# Set the destination MAC address in a more readable way.
def arp_daddr_mac=(mac)
mac = EthHeader.mac2str(mac)
self.arp_dst_mac.read(mac)
self.arp_dst_mac
end
# Returns a more readable source MAC address.
def arp_daddr_mac
EthHeader.str2mac(self.arp_dst_mac.to_s)
end
# Sets a more readable source IP address.
def arp_saddr_ip=(addr)
addr = IPHeader.octet_array(addr)
arp_src_ip.o1 = addr[0]
arp_src_ip.o2 = addr[1]
arp_src_ip.o3 = addr[2]
arp_src_ip.o4 = addr[3]
end
# Returns a more readable source IP address.
def arp_saddr_ip
[arp_src_ip.o1,arp_src_ip.o2,arp_src_ip.o3,arp_src_ip.o4].join('.')
end
# Sets a more readable destination IP address.
def arp_daddr_ip=(addr)
addr = IPHeader.octet_array(addr)
arp_dst_ip.o1 = addr[0]
arp_dst_ip.o2 = addr[1]
arp_dst_ip.o3 = addr[2]
arp_dst_ip.o4 = addr[3]
end
# Returns a more readable destination IP address.
def arp_daddr_ip
[arp_dst_ip.o1,arp_dst_ip.o2,arp_dst_ip.o3,arp_dst_ip.o4].join('.')
end
end # class ARPHeader
# ARPPacket is used to construct ARP packets. They contain an EthHeader and an ARPHeader.
# == Example
#
# require 'packetfu'
# arp_pkt = PacketFu::ARPPacket.new(:flavor => "Windows")
# arp_pkt.arp_saddr_mac="00:1c:23:44:55:66" # Your hardware address
# arp_pkt.arp_saddr_ip="10.10.10.17" # Your IP address
# arp_pkt.arp_daddr_ip="10.10.10.1" # Target IP address
# arp_pkt.arp_opcode=1 # Request
#
# arp_pkt.to_w('eth0') # Inject on the wire. (requires root)
# arp_pkt.to_f('/tmp/arp.pcap') # Write to a file.
#
# == Parameters
#
# :flavor
# Sets the "flavor" of the ARP packet. Choices are currently:
# :windows, :linux, :hp_deskjet
# :eth
# A pre-generated EthHeader object. If not specified, a new one will be created.
# :arp
# A pre-generated ARPHeader object. If not specificed, a new one will be created.
# :config
# A hash of return address details, often the output of Utils.whoami?
class ARPPacket < Packet
attr_accessor :eth_header, :arp_header
def ethernet?; true; end
def arp?; true; end
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@arp_header = (args[:arp] || ARPHeader.new)
@eth_header.eth_proto = 0x806
@eth_header.body=@arp_header
# Please send more flavors to todb-packetfu@planb-security.net.
# Most of these initial fingerprints come from one (1) sample.
case (args[:flavor].nil?) ? :nil : args[:flavor].to_s.downcase.intern
when :windows; @arp_header.body = "\x00" * 64 # 64 bytes of padding
when :linux; @arp_header.body = "\x00" * 4 + # 32 bytes of padding
"\x00\x07\x5c\x14" + "\x00" * 4 +
"\x00\x0f\x83\x34" + "\x00\x0f\x83\x74" +
"\x01\x11\x83\x78" + "\x00\x00\x00\x0c" +
"\x00\x00\x00\x00"
when :hp_deskjet; # Pads up to 60 bytes.
@arp_header.body = "\xe0\x90\x0d\x6c" +
"\xff\xff\xee\xee" + "\x00" * 4 +
"\xe0\x8f\xfa\x18\x00\x20"
else; @arp_header.body = "\x00" * 18 # Pads up to 60 bytes.
end
@headers = [@eth_header, @arp_header]
super
end
# Used to generate summary data for ARP packets.
def peek(args={})
peek_data = ["A "]
peek_data << "%-5d" % self.to_s.size
peek_data << self.arp_saddr_mac
peek_data << "(#{self.arp_saddr_ip})"
peek_data << "->"
peek_data << case self.arp_daddr_mac
when "00:00:00:00:00:00"; "Bcast00"
when "ff:ff:ff:ff:ff:ff"; "BcastFF"
else; self.arp_daddr_mac
end
peek_data << "(#{self.arp_daddr_ip})"
peek_data << ":"
peek_data << case self.arp_opcode
when 1; "Requ"
when 2; "Repl"
when 3; "RReq"
when 4; "RRpl"
when 5; "IReq"
when 6; "IRpl"
else; "0x%02x" % self.opcode
end
peek_data.join
end
end # class ARPPacket
end # module PacketFu

165
lib/packetfu/capture.rb Normal file
View File

@ -0,0 +1,165 @@
module PacketFu
# The Capture class is used to construct PcapRub objects in order to collect
# packets from an interface.
#
# This class requires PcapRub. In addition, you will need root (or root-like) privileges
# in order to capture from the interface.
#
# Note, on some wireless cards, setting :promisc => true will disable capturing.
#
# == Example
#
# # Typical use
# cap = PacketFu::Capture.new(:iface => 'eth0', :promisc => true)
# cap.start
# sleep 10
# cap.save
# first_packet = cap.array[0]
#
# # Tcpdump-like use
# cap = PacketFu::Capture.new(:start => true)
# cap.show_live(:save => true, :filter => 'tcp and not port 22')
#
# == See Also
#
# Read, Write
class Capture
attr_accessor :array, :stream # Leave these public and open.
attr_reader :iface, :snaplen, :promisc, :timeout # Cant change after the init.
def initialize(args={})
@array = [] # Where the packet array goes.
@stream = [] # Where the stream goes.
@iface = args[:iface] || Pcap.lookupdev
@snaplen = args[:snaplen] || 0xffff
@promisc = args[:promisc] || false # Sensible for some Intel wifi cards
@timeout = args[:timeout] || 1
setup_params(args)
end
def setup_params(args={})
filter = args[:filter] # Not global; filter criteria can change.
start = args[:start] || false
capture if start
bpf(:filter=>filter) if filter
end
# capture() initializes the @stream varaible. Valid arguments are:
#
# :filter
# Provide a bpf filter to enable for the capture. For example, 'ip and not tcp'
# :start
# When true, start capturing packets to the @stream variable. Defaults to true
def capture(args={})
if Process.euid.zero?
filter = args[:filter]
start = args[:start] || true
if start
begin
@stream = Pcap.open_live(@iface,@snaplen,@promisc,@timeout)
rescue RuntimeError
$stderr.print "Are you sure you're root? Error: "
raise
end
bpf(:filter=>filter) if filter
else
@stream = []
end
@stream
else
raise RuntimeError,"Not root, so can't capture packets. Error: "
end
end
# start() is equivalent to capture().
def start(args={})
capture(args)
end
# clear() clears the @stream and @array variables, essentially starting the
# capture session over. Valid arguments are:
#
# :array
# If true, the @array is cleared.
# :stream
# If true, the @stream is cleared.
def clear(args={})
array = args[:array] || true
stream = args[:stream] || true
@array = [] if array
@stream = [] if stream
end
# bpf() sets a bpf filter on a capture session. Valid arugments are:
#
# :filter
# Provide a bpf filter to enable for the capture. For example, 'ip and not tcp'
def bpf(args={})
filter = args[:filter]
capture if @stream.class == Array
@stream.setfilter(filter)
end
# wire_to_array() saves a packet stream as an array of binary strings. From here,
# packets may accessed by other functions. Note that the wire_to_array empties
# the stream, so multiple calls will append new packets to @array.
# Valid arguments are:
#
# :filter
# Provide a bpf filter to apply to packets moving from @stream to @array.
def wire_to_array(args={})
filter = args[:filter]
bpf(:filter=>filter) if filter
while this_pkt = @stream.next
@array << this_pkt
end
@array.size
end
# w2a() is a equivalent to wire_to_array()
def w2a(args={})
wire_to_array(args)
end
# save() is a equivalent to wire_to_array()
def save(args={})
wire_to_array(args)
end
# show_live() is a method to capture packets and display peek() data to stdout. Valid arguments are:
#
# :filter
# Provide a bpf filter to captured packets.
# :save
# Save the capture in @array
# :verbose
# TODO: Not implemented yet; do more than just peek() at the packets.
# :quiet
# TODO: Not implemented yet; do less than peek() at the packets.
def show_live(args={})
filter = args[:filter]
save = args[:save]
verbose = args[:verbose] || args[:v] || false
quiet = args[:quiet] || args[:q] || false # Setting q and v doesn't make a lot of sense but hey.
# Ensure the capture's started.
if @stream.class == Array
capture
end
@stream.setfilter(filter) if filter
while true
@stream.each do |pkt|
puts Packet.parse(pkt).peek
@array << pkt if args[:save]
end
end
end
end # class Capture
end # module PacketFu

54
lib/packetfu/config.rb Normal file
View File

@ -0,0 +1,54 @@
module PacketFu
# The Config class holds various bits of useful default information for packet creation.
# If initialized without arguments, the @iface and @pcapfile instance variables are
# set to the (pcaprub-believed) default interface and "/tmp/out.pcap", respectively.
#
# Any number of instance variables can be passed in to the intialize function (as a
# hash), though only the expected network-related variables will be readable and
# writeable directly.
#
# == Examples
#
# PacketFu::Config.new(:ip_saddr => "1.2.3.4").ip_saddr #=> "1.2.3.4"
# PacketFu::Config.new(:foo=>"bar").foo #=> NomethodError: undefined method `foo'...
#
# The config() function, however, does provide access to custom variables:
#
# PacketFu::Config.new(:foo=>"bar").config[:foo] #=> "bar"
# obj = PacketFu::Config.new(:foo=>"bar")
# obj.config(:baz => "bat")
# obj.config #=> {:iface=>"eth0", :baz=>"bat", :pcapfile=>"/tmp/out.pcap", :foo=>"bar"}
class Config
attr_accessor :eth_saddr, # The discovered eth_saddr
:eth_daddr, # The discovered eth_daddr (ie, the gateway)
:eth_src, # The discovered eth_src in binary form.
:eth_dst, # The discovered eth_dst (gateway) in binary form.
:ip_saddr, # The discovered ip_saddr
:ip_src, # The discovered ip_src in binary form.
:iface, # The declared interface.
:pcapfile # A declared default file to write to.
def initialize(args={})
if Process.euid.zero?
@iface = Pcap.lookupdev || "lo" # In case there aren't any...
end
@pcapfile = "/tmp/out.pcap"
args.each_pair { |k,v| self.instance_variable_set(("@" + k.to_s).intern,v) }
end
# Returns all instance variables as a hash (including custom variables set at initialization).
def config(arg=nil)
if arg.nil?
config_hash = {}
self.instance_variables.each { |v| config_hash[v.delete("@").intern] = self.instance_variable_get(v) }
config_hash
else
arg.each_pair {|k,v| self.instance_variable_set(("@" + k.to_s).intern, v)}
end
end
end
end

160
lib/packetfu/eth.rb Normal file
View File

@ -0,0 +1,160 @@
module PacketFu
# EthOui is the Organizationally Unique Identifier portion of a MAC address, used in EthHeader.
#
# See the OUI list at http://standards.ieee.org/regauth/oui/oui.txt
#
# ==== Header Definition
#
# bit1 :b0
# bit1 :b1
# bit1 :b2
# bit1 :b3
# bit1 :b4
# bit1 :b5
# bit1 :local
# bit1 :multicast
# uint16be :oui, :initial_value => 0x1ac5 # :)
class EthOui < BinData::MultiValue
bit1 :b0
bit1 :b1
bit1 :b2
bit1 :b3
bit1 :b4
bit1 :b5
bit1 :local
bit1 :multicast
uint16be :oui, :initial_value => 0x1ac5 # :)
end
# EthNic is the Network Interface Controler portion of a MAC address, used in EthHeader.
#
# ==== Header Definition
#
# unit8 :n1
# unit8 :n2
# unit8 :n3
#
class EthNic < BinData::MultiValue
uint8 :n1
uint8 :n2
uint8 :n3
end
# EthMac is the combination of an EthOui and EthNic, used in EthHeader.
#
# ==== Header Definition
#
# eth_oui :oui # See EthOui
# eth_nic :nic # See EthOui
class EthMac < BinData::MultiValue
eth_oui :oui # See EthOui
eth_nic :nic # See EthOui
end
# EthHeader is a complete Ethernet struct, used in EthPacket.
# It's the base header for all other protocols, such as IPHeader, TCPHeader, etc.
#
# For more on the construction on MAC addresses, see http://en.wikipedia.org/wiki/MAC_address
#
# ==== Header Definition
#
# eth_mac :eth_dst # See EthMac
# eth_mac :eth_src # See EthMac
# uint16be :eth_proto, :initial_value => 0x0800 # IP 0x0800, Arp 0x0806
# rest :body
class EthHeader < BinData::MultiValue
eth_mac :eth_dst # See EthMac
eth_mac :eth_src # See EthMac
uint16be :eth_proto, :initial_value => 0x0800 # IP 0x0800, Arp 0x0806
rest :body
# Set the source MAC address in a more readable way.
def eth_saddr=(mac)
mac = EthHeader.mac2str(mac)
self.eth_src.read(mac)
self.eth_src
end
# Returns a more readable source MAC address.
def eth_saddr
EthHeader.str2mac(self.eth_src.to_s)
end
# Set the destination MAC address in a more readable way.
def eth_daddr=(mac)
mac = EthHeader.mac2str(mac)
self.eth_dst.read(mac)
self.eth_dst
end
# Returns a more readable source MAC address.
def eth_daddr
EthHeader.str2mac(self.eth_dst.to_s)
end
# Converts a readable MAC (11:22:33:44:55:66) to a binary string. Readable MAC's may be split on colons, dots,
# spaces, or underscores.
#
# irb> PacketFu::EthHeader.mac2str("11:22:33:44:55:66")
#
# #=> "\021\"3DUf"
def self.mac2str(mac)
if mac.split(/[:\x2d\x2e\x5f]/).size == 6
ret = mac.split(/[:\x2d\x2e\x20\x5f]/).collect {|x| x.to_i(16)}.pack("C6")
else
raise ArgumentError, "Unkown format for mac address."
end
return ret
end
# Converts a binary string to a readable MAC (11:22:33:44:55:66).
#
# irb> PacketFu::EthHeader.str2mac("\x11\x22\x33\x44\x55\x66")
#
# #=> "11:22:33:44:55:66"
def self.str2mac(mac)
if mac.size == 6 && mac.class == String
ret = mac.unpack("C6").collect {|x| sprintf("%02x",x)}.join(":")
end
end
end
# EthPacket is used to construct Ethernet packets. They contain an EthHeader, and usually
# other packet types.
#
# == Example
#
# require 'packetfu'
# eth_pkt = PacketFu::EthPacket.new(:flavor => :apple)
# eth_pkt.eth_saddr="01:02:03:04:05:06"
# eth_pkt.eth_daddr="0a:0b:0c:0d:0e:0f"
# eth_pkt.payload="I'm a lonely little eth packet with no real protocol information to speak of."
# puts eth_pkt.to_f('/tmp/eth.pcap').inspect
#
# == Parameters
#
# :eth
# A pre-generated EthHeader object. If not specified, a new one will be created.
# :flavor
# TODO: not implemented. Will generate EthPacket objects based on the OUI list.
# :config
# A hash of return address details, often the output of Utils.whoami?
class EthPacket < Packet
attr_accessor :eth_header
def ethernet?; true; end
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@headers = [@eth_header]
super
end
end
end # module PacketFu

112
lib/packetfu/icmp.rb Normal file
View File

@ -0,0 +1,112 @@
module PacketFu
# ICMPHeader is a complete ICMP struct, used in ICMPPacket. ICMP is typically used for network
# administration and connectivity testing.
#
# For more on ICMP packets, see http://www.networksorcery.com/enp/protocol/icmp.htm
#
# ==== Header Definition
#
# uint8 :icmp_type
# uint8 :icmp_code
# uint16be :icmp_sum, :initial_value => lambda { icmp_calc_sum }
# rest :body
class ICMPHeader < BinData::MultiValue
uint8 :icmp_type
uint8 :icmp_code
uint16be :icmp_sum, :initial_value => lambda { icmp_calc_sum }
rest :body
def icmp_calc_sum
checksum = (icmp_type << 8) + icmp_code
chk_body = (body.size % 2 == 0 ? body : body + "\x00")
chk_body.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
end
def icmp_recalc(arg=:all)
case arg.intern
when :icmp_sum
self.icmp_sum=icmp_calc_sum
when :all
self.icmp_sum=icmp_calc_sum
else
raise ArgumentError, "No such field `#{arg}'"
end
end
end
# ICMPPacket is used to construct ICMP Packets. They contain an EthHeader, an IPHeader, and a ICMPHeader.
#
# == Example
#
# icmp_pkt.new
# icmp_pkt.icmp_type = 8
# icmp_pkt.icmp_code = 0
# icmp_pkt.payload = "ABC, easy as 123. As simple as do-re-mi. ABC, 123, baby, you and me!"
#
# icmp_pkt.ip_saddr="1.2.3.4"
# icmp_pkt.ip_daddr="5.6.7.8"
#
# icmp_pkt.recalc
# icmp_pkt.to_f('/tmp/icmp.pcap')
#
# == Parameters
#
# :eth
# A pre-generated EthHeader object.
# :ip
# A pre-generated IPHeader object.
# :flavor
# TODO: Sets the "flavor" of the ICMP packet. Pings, in particular, often betray their true
# OS.
# :config
# A hash of return address details, often the output of Utils.whoami?
class ICMPPacket < Packet
attr_accessor :eth_header, :ip_header, :icmp_header
def ethernet?; true; end
def ip?; true; end
def icmp?; true; end
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@ip_header = (args[:ip] || IPHeader.new)
@icmp_header = (args[:icmp] || ICMPHeader.new)
@ip_header.body = @icmp_header
@eth_header.body = @ip_header
@headers = [@eth_header, @ip_header, @icmp_header]
super
end
# Peek provides summary data on packet contents.
def peek(args={})
peek_data = ["C "] # I is taken by IP
peek_data << "%-5d" % self.to_s.size
type = case self.icmp_type
when 8
"ping"
when 0
"pong"
else
"%02x-%02x" % [self.icmp_type, self.icmp_code]
end
peek_data << "%-21s" % "#{self.ip_saddr}:#{type}"
peek_data << "->"
peek_data << "%21s" % "#{self.ip_daddr}"
peek_data << "%23s" % "I:"
peek_data << "%04x" % self.ip_id
peek_data.join
end
end
end # module PacketFu

67
lib/packetfu/inject.rb Normal file
View File

@ -0,0 +1,67 @@
module PacketFu
# The Inject class handles injecting arrays of binary data on the wire.
#
# To inject single packets, use PacketFu::Packet.to_w() instead.
class Inject
attr_accessor :array, :stream, :show_live # Leave these public and open.
attr_reader :iface, :snaplen, :promisc, :timeout # Cant change after the init.
def initialize(args={})
@array = [] # Where the packet array goes.
@stream = [] # Where the stream goes.
@iface = args[:iface] || Pcap.lookupdev || 'lo'
@snaplen = args[:snaplen] || 0xffff
@promisc = args[:promisc] || false # Sensible for some Intel wifi cards
@timeout = args[:timeout] || 1
@show_live = nil
end
# Takes an array, and injects them onto an interface. Note that
# complete packets (Ethernet headers on down) are expected.
#
# === Parameters
#
# :array || arr
# An array of binary data (usually packet.to_s style).
# :int || sleep
# Number of seconds to sleep between injections (in float format)
# :show_live || :live
# If true, puts data about what was injected to stdout.
#
# === Example
#
# inj = PacketFu::Inject.new
# inj.array_to_wire(:array => [pkt1, pkt2, pkt3], :sleep => 0.1)
#
def array_to_wire(args={})
pkt_array = args[:array] || args[:arr] || @array
interval = args[:int] || args[:sleep]
show_live = args[:show_live] || args[:live] || @show_live
@stream = Pcap.open_live(@iface,@snaplen,@promisc,@timeout)
pkt_count = 0
pkt_array.each do |pkt|
@stream.inject(pkt)
sleep interval if interval
pkt_count +=1
puts "Sent Packet \##{pkt_count} (#{pkt.size})" if show_live
end
# Return # of packets sent, array size, and array total size
[pkt_count, pkt_array.size, pkt_array.join.size]
end
# Equivalent to array_to_wire
def a2w(args={})
array_to_wire(args)
end
# Equivalent to array_to_wire
def inject(args={})
array_to_wire(args)
end
end
end

23
lib/packetfu/invalid.rb Normal file
View File

@ -0,0 +1,23 @@
module PacketFu
# InvalidHeader catches all packets that we don't already have a struct for, or
# for whatever reason, violates some basic packet rules for other packet types.
class InvalidHeader < BinData::MultiValue
rest :body # No idea how big this will be or what it will look like; it's invalid!
end
# You probably don't want to write invalid packets on purpose.
class InvalidPacket < Packet
attr_accessor :invalid_header
def invalid?; true; end
def initialize(args={})
@invalid_header = (args[:invalid] || InvalidHeader.new)
@headers = [@invalid_header]
end
end
end # module PacketFu

229
lib/packetfu/ip.rb Normal file
View File

@ -0,0 +1,229 @@
module PacketFu
# Octets implements the addressing scheme for IP.
#
# ==== Header Definition
#
# uint8 :o1
# uint8 :o2
# uint8 :o3
# uint8 :o4
class Octets < BinData::MultiValue
uint8 :o1
uint8 :o2
uint8 :o3
uint8 :o4
# Returns an address in dotted-quad format.
def to_x
ip_str = [o1, o2, o3, o4].join('.')
IPAddr.new(ip_str).to_s
end
# Returns an address in numerical format.
def to_i
ip_str = [o1, o2, o3, o4].join('.')
IPAddr.new(ip_str).to_i
end
# Returns an address as an array of numbers.
def to_ary
[o1,o2,o3,o4]
end
alias to_a to_ary
end
# IPHeader is a complete IP struct, used in IPPacket. Most traffic on most networks today is IP-based.
#
# For more on IP packets, see http://www.networksorcery.com/enp/protocol/ip.htm
#
# ==== Header Definition
#
# bit4 :ip_v, :initial_value => 4
# bit4 :ip_hl, :initial_value => 5
# uint8 :ip_tos, :initial_value => 0 # TODO: Break out the bits
# uint16be :ip_len, :initial_value => lambda { ip_calc_len }
# uint16be :ip_id, :initial_value => lambda { ip_calc_id } # IRL, hardly random.
# uint16be :ip_frag, :initial_value => 0 # TODO: Break out the bits
# uint8 :ip_ttl, :initial_value => 0xff # Changes per flavor
# uint8 :ip_proto, :initial_value => 0x01 # TCP: 0x06, UDP 0x11, ICMP 0x01
# uint16be :ip_sum, :initial_value => lambda { ip_calc_sum }
# octets :ip_src # No value as this is a MultiValue
# octets :ip_dst # Ditto.
# rest :body
class IPHeader < BinData::MultiValue
bit4 :ip_v, :initial_value => 4
bit4 :ip_hl, :initial_value => 5
uint8 :ip_tos, :initial_value => 0 # TODO: Break out the bits
uint16be :ip_len, :initial_value => lambda { ip_calc_len }
uint16be :ip_id, :initial_value => lambda { ip_calc_id } # IRL, hardly random.
uint16be :ip_frag, :initial_value => 0 # TODO: Break out the bits
uint8 :ip_ttl, :initial_value => 0xff # Changes per flavor
uint8 :ip_proto,:initial_value => 0x01 # TCP: 0x06, UDP 0x11, ICMP 0x01
uint16be :ip_sum, :initial_value => lambda { ip_calc_sum }
octets :ip_src # No value as this is a MultiValue
octets :ip_dst # Ditto.
rest :body
# Creates a new IPHeader object, and intialize with a random IPID.
def initialize(*args)
@random_id = rand(0xffff)
super
end
# Calulcate the true length of the packet.
def ip_calc_len
(ip_hl * 4) + body.to_s.length
end
# Calculate the true checksum of the packet.
# (Yes, this is the long way to do it, but it's e-z-2-read for mathtards like me.)
def ip_calc_sum
checksum = (((ip_v << 4) + ip_hl) << 8) +ip_tos
checksum += ip_len
checksum += ip_id
checksum += ip_frag
checksum += (ip_ttl << 8) + ip_proto
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 = checksum % 0xffff
checksum = 0xffff - checksum
checksum == 0 ? 0xffff : checksum
end
# Retrieve the IP ID
def ip_calc_id
@random_id
end
# Sets a more readable IP address. If you wants to manipulate individual octets,
# (eg, for host scanning in one network), it would be better use ip_src.o1 through
# ip_src.o4 instead.
def ip_saddr=(addr)
addr = IPHeader.octet_array(addr)
ip_src.o1 = addr[0]
ip_src.o2 = addr[1]
ip_src.o3 = addr[2]
ip_src.o4 = addr[3]
end
# Returns a more readable IP source address.
def ip_saddr
[ip_src.o1,ip_src.o2,ip_src.o3,ip_src.o4].join('.')
end
# Sets a more readable IP address.
def ip_daddr=(addr)
addr = IPHeader.octet_array(addr)
ip_dst.o1 = addr[0]
ip_dst.o2 = addr[1]
ip_dst.o3 = addr[2]
ip_dst.o4 = addr[3]
end
# Returns a more readable IP destination address.
def ip_daddr
[ip_dst.o1,ip_dst.o2,ip_dst.o3,ip_dst.o4].join('.')
end
# Translate various formats of IPv4 Addresses to an array of digits.
def self.octet_array(addr)
if addr.class == String
oa = addr.split('.').collect {|x| x.to_i}
elsif addr.class == Fixnum
oa = IPAddr.new(addr, Socket::AF_INET).to_s.split('.')
elsif addr.class == Bignum
oa = IPAddr.new(addr, Socket::AF_INET).to_s.split('.')
elsif addr.class == Array
oa = addr
else
raise ArgumentError, "IP Address should be a dotted quad string, an array of ints, or a bignum"
end
end
# Recalculate the calculated IP fields. Valid arguments are:
# :all :ip_len :ip_sum :ip_id
def ip_recalc(arg=:all)
case arg
when :ip_len
self.ip_len=ip_calc_len
when :ip_sum
self.ip_sum=ip_calc_sum
when :ip_id
@random_id = rand(0xffff)
when :all
self.ip_id= ip_calc_id
self.ip_len= ip_calc_len
self.ip_sum= ip_calc_sum
else
raise ArgumentError, "No such field `#{arg}'"
end
end
end # class IPHeader
# IPPacket is used to construct IP packets. They contain an EthHeader, an IPHeader, and usually
# a transport-layer protocol such as UDPHeader, TCPHeader, or ICMPHeader.
#
# == Example
#
# require 'packetfu'
# ip_pkt = PacketFu::IPPacket.new
# ip_pkt.ip_saddr="10.20.30.40"
# ip_pkt.ip_daddr="192.168.1.1"
# ip_pkt.ip_proto=1
# ip_pkt.ip_ttl=64
# ip_pkt.ip_payload="\x00\x00\x12\x34\x00\x01\x00\x01"+
# "Lovingly hand-crafted echo responses delivered directly to your door."
# ip_pkt.recalc
# ip_pkt.to_f('/tmp/ip.pcap')
#
# == Parameters
#
# :eth
# A pre-generated EthHeader object.
# :ip
# A pre-generated IPHeader object.
# :flavor
# TODO: Sets the "flavor" of the IP packet. This might include known sets of IP options, and
# certainly known starting TTLs.
# :config
# A hash of return address details, often the output of Utils.whoami?
class IPPacket < Packet
attr_accessor :eth_header, :ip_header
def ethernet?; true; end
def ip?; true; end
# Creates a new IPPacket object.
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@ip_header = (args[:ip] || IPHeader.new)
@eth_header.body=@ip_header
@headers = [@eth_header, @ip_header]
super
end
# Peek provides summary data on packet contents.
def peek(args={})
peek_data = ["I "]
peek_data << "%-5d" % self.to_s.size
peek_data << "%-21s" % "#{self.ip_saddr}"
peek_data << "->"
peek_data << "%21s" % "#{self.ip_daddr}"
peek_data << "%23s" % "I:"
peek_data << "%04x" % self.ip_id
peek_data.join
end
end
end # module PacketFu

138
lib/packetfu/ipv6.rb Normal file
View File

@ -0,0 +1,138 @@
module PacketFu
# AddrIpv6 handles addressing for IPv6Header
#
# ==== Header Definition
#
#
# uint32be :a1
# uint32be :a2
# uint32be :a3
# uint32be :a4
class AddrIpv6 < BinData::MultiValue
uint32be :a1
uint32be :a2
uint32be :a3
uint32be :a4
end
# IPv6Header is complete IPv6 struct, used in IPv6Packet.
#
# ==== Header Definition
#
# bit4 :ipv6_v, :initial_value => 6 # Versiom
# bit8 :ipv6_class # Class
# bit20 :ipv6_label # Label
# uint16be :ipv6_len, :initial_value => lambda { ipv6_calc_len } # Payload length
# uint8 :ipv6_next # Next Header
# uint8 :ipv6_hop, :initial_value => 0xff # Hop limit
# addr_ipv6 :ipv6_src
# addr_ipv6 :ipv6_dst
# rest :body
class IPv6Header < BinData::MultiValue
bit4 :ipv6_v, :initial_value => 6 # Versiom
bit8 :ipv6_class # Class
bit20 :ipv6_label # Label
uint16be :ipv6_len, :initial_value => lambda { ipv6_calc_len } # Payload length
uint8 :ipv6_next # Next Header
uint8 :ipv6_hop, :initial_value => 0xff # Hop limit
addr_ipv6 :ipv6_src
addr_ipv6 :ipv6_dst
rest :body
def ipv6_calc_len
ipv6_len = self.body.size
end
def ipv6_recalc(arg=:all)
case arg
when :ipv6_len
ipv6_calc_len
when :all
ipv6_recalc(:len)
end
end
# Presents in a more readable form.
def ipv6_saddr
addr = [self.ipv6_src.a1,self.ipv6_src.a2,self.ipv6_src.a3,self.ipv6_src.a4].pack("NNNN")
addr.unpack("H*")[0].scan(/.{8}/).collect {|x| x.sub(/^0*([0-9a-f])/,"\\1")}.join(":")
end
# Takes in a more readable form. Leading zero compression is fine, but that's it. :(
def ipv6_saddr=(str)
arr = str.split(':').collect {|x| x.to_i(16)}
self.ipv6_src.a1 = arr[0]
self.ipv6_src.a2 = arr[1]
self.ipv6_src.a3 = arr[2]
self.ipv6_src.a4 = arr[3]
end
# Presents in a more readable form.
def ipv6_daddr
addr = [self.ipv6_dst.a1,self.ipv6_dst.a2,self.ipv6_dst.a3,self.ipv6_dst.a4].pack("NNNN")
addr.unpack("H*")[0].scan(/.{8}/).collect {|x| x.sub(/^0*([0-9a-f])/,"\\1")}.join(":")
end
# Takes in a more readable form. Leading zero compression is fine, but that's it. :(
def ipv6_daddr=(str)
arr = str.split(':').collect {|x| x.to_i(16)}
self.ipv6_dst.a1 = arr[0]
self.ipv6_dst.a2 = arr[1]
self.ipv6_dst.a3 = arr[2]
self.ipv6_dst.a4 = arr[3]
end
end # class IPv6Header
# IPv6Packet is used to construct IPv6 Packets. They contain an EthHeader and an IPv6Header, and in
# the distant, unknowable future, will take interesting IPv6ish payloads.
#
# This mostly complete, but not very useful. It's intended primarily as an example protocol.
#
# == Parameters
#
# :eth
# A pre-generated EthHeader object.
# :ip
# A pre-generated IPHeader object.
# :flavor
# TODO: Sets the "flavor" of the IPv6 packet. No idea what this will look like, haven't done much IPv6 fingerprinting.
# :config
# A hash of return address details, often the output of Utils.whoami?
class IPv6Packet < Packet
attr_accessor :eth_header, :ipv6_header
def ethernet?; true; end
def ipv6?; true; end
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@ipv6_header = (args[:ipv6] || IPv6Header.new)
@eth_header.eth_proto = 0x86dd
@eth_header.body=@ipv6_header
@headers = [@eth_header, @arp_header]
super
end
# Peek provides summary data on packet contents.
def peek(args={})
peek_data = ["6 "]
peek_data << "%-5d" % self.to_s.size
peek_data << self.ipv6_saddr
peek_data << "->"
peek_data << self.ipv6_daddr
peek_data << " N:"
peek_data << self.ipv6_next.to_s(16)
peek_data.join
end
end # class IPv6Packet
end # module PacketFu

336
lib/packetfu/packet.rb Normal file
View File

@ -0,0 +1,336 @@
module PacketFu
# Packet is the parent class of EthPacket, IPPacket, UDPPacket, TCPPacket, and all
# other packets.
class Packet
attr_reader :flavor # Packet Headers are responsible for their own specific flavor methods.
# Parse() creates the correct packet type based on the data, and returns the apporpiate
# Packet subclass.
#
# There is an assumption here that all incoming packets are either EthPacket
# or InvalidPacket types.
#
# New packet types should get an entry here.
def self.parse(packet,args={})
if packet.size >= 14 # Min size for Ethernet. No check for max size, yet.
case packet[12,2] # Check the Eth protocol field.
when "\x08\x00" # It's IP.
case (packet[14,1][0] >> 4) # Check the IP version field.
when 4; # It's IPv4.
case packet[23,1] # Check the IP protocol field.
when "\x06"; p = TCPPacket.new # Returns a TCPPacket.
when "\x11"; p = UDPPacket.new # Returns a UDPPacket.
when "\x01"; p = ICMPPacket.new # Returns an ICMPPacket.
else; p = IPPacket.new # Returns an IPPacket since we can't tell the transport layer.
end
else; p = EthPacket.new # Returns an EthPacket since we don't know any other IP version.
end
when "\x08\x06" # It's arp
if packet.size >= 28 # Min size for complete arp
p = ARPPacket.new
else; p = EthPacket.new # Returns an EthPacket since we can't deal with tiny arps.
end
when "\x86\xdd" # It's IPv6
if packet.size >= 54 # Min size for a complete IPv6 packet.
p = IPv6Packet.new
else; p = EthPacket.new # Returns an EthPacket since we can't deal with tiny Ipv6.
end
else; p = EthPacket.new # Returns an EthPacket since we can't tell the network layer.
end
else
p = InvalidPacket.new # Not the right size for Ethernet (jumbo frames are okay)
end
p.read(packet,args)
return p
end
#
# These methods are overridden for specific types of packets
# This allows easy identification of the packet type
#
def ip?; false; end
def tcp?; false; end
def udp?; false; end
def icmp?; false; end
def arp?; false; end
def ipv6?; false; end
def ethernet?; false; end
def invalid?; false; end
#method_missing() delegates protocol-specific field actions to the apporpraite
#class variable (which contains the associated packet type)
#This register-of-protocols style switch will work for the
#forseeable future (there aren't /that/ many packet types), and it's a handy
#way to know at a glance what packet types are supported.
def method_missing(sym, *args)
case sym.to_s
when /^invalid_/
@invalid_header.send(sym,*args)
when /^eth_/
@eth_header.send(sym,*args)
when /^arp_/
@arp_header.send(sym,*args)
when /^ip_/
@ip_header.send(sym,*args)
when /^icmp_/
@icmp_header.send(sym,*args)
when /^udp_/
@udp_header.send(sym,*args)
when /^tcp_/
@tcp_header.send(sym,*args)
when /^ipv6_/
@ipv6_header.send(sym,*args)
else
raise NoMethodError, "Unknown method `#{sym}' for this packet object."
end
end
# Get the binary string of the entire packet.
def to_s
@headers[0].to_s
end
# In the event of no proper decoding, at least send it to the inner-most header.
def read(io)
@headers[0].read(io)
end
# In the event of no proper decoding, at least send it to the inner-most header.
def write(io)
@headers[0].write(io)
end
# Get the outermost payload (body) of the packet; this is why all packet headers
# should have a body type.
def payload
@headers.last.body
end
# Set the outermost payload (body) of the packet.
def payload=(args)
@headers.last.body=(args)
end
# Put the entire packet into a libpcap file.
def to_f(filename=nil)
PacketFu::Write.a2f(:file=> filename || PacketFu::Config.new.config[:pcapfile],
:arr=>[@headers[0].to_s])
end
# Put the entire packet on the wire by creating a temporary PacketFu::Inject object.
# TODO: Do something with auto-checksumming?
def to_w(iface=nil)
inj = PacketFu::Inject.new(:iface => (iface || PacketFu::Config.new.config[:iface]))
inj.array = [@headers[0].to_s]
inj.inject
end
# Recalculates all the calcuated fields for all headers in the packet.
# This is important since read() wipes out all the calculated fields
# such as length and checksum and what all.
# TODO: Is there a better way to ensure I get the correct checksum?
# This way is pretty easy; third time is, indeed, the charm.
def recalc(arg=:all)
3.times do # XXX: This is a silly fix, surely there's a better way.
case arg
when :ip
ip_recalc(:all)
when :udp
udp_recalc(:all)
when :tcp
tcp_recalc(:all)
when :all
ip_recalc(:all) if @ip_header
udp_recalc(:all) if @udp_header
tcp_recalc(:all) if @tcp_header
else
raise ArgumentError, "Recalculating `#{arg}' unsupported. Try :all"
end
end
@headers[0]
end
# Read() takes (and trusts) the io input and shoves it all into a well-formed Packet.
# Note that read is a destructive process, so any existing data will be lost.
#
# TODO: This giant if tree is a mess, and worse, is decieving. You need to define
# actions both here and in parse(). All read() does is make a (good) guess as to
# what @headers to expect, and reads data to them.
#
# To take strings and turn them into packets without knowing ahead of time what kind of
# packet it is, use Packet.parse instead; parse() handles the figuring-out part.
#
# A note on the :strip => true argument: If :strip is set, defined lengths of data will
# be believed, and any trailers (such as frame check sequences) will be chopped off. This
# helps to ensure well-formed packets, at the cost of losing perhaps important FCS data.
#
# If :strip is false, header lengths are /not/ believed, and all data will be piped in.
# When capturing from the wire, this is usually fine, but recalculating the length before
# saving or re-transmitting will absolutely change the data payload; FCS data will become
# part of the TCP data as far as tcp_len is concerned. Some effort has been made to preserve
# the "real" payload for the purposes of checksums, but currently, it's impossible to seperate
# new payload data from old trailers, so things like pkt.payload += "some data" will not work
# correctly.
#
# So, to summarize; if you intend to alter the data, use :strip. If you don't, don't.
def read(io,args={})
if io.size >= 14
@eth_header.read(io[0,14])
eth_proto_num = io[12,2].unpack("n")[0]
if eth_proto_num == 0x0800 # It's IP.
ip_hlen=(io[14] & 0x0f) * 4
ip_proto_num = io[23,1].unpack("C")[0]
@ip_header.read(io[14,ip_hlen])
@eth_header.body = @ip_header
if ip_proto_num == 0x06 # It's TCP.
tcp_len = io[16,2].unpack("n")[0] - 20
if args[:strip] # Drops trailers like frame check sequence (FCS). Often desired for cleaner packets.
tcp_all = io[ip_hlen+14,tcp_len] # Believe the tcp_len value; chop off anything that's not in range.
else
tcp_all = io[ip_hlen+14,0xffff] # Don't believe the tcp_len value; suck everything up.
end
tcp_hlen = ((tcp_all[12,1].unpack("C")[0]) >> 4) * 4
tcp_opts = tcp_all[20,tcp_hlen-20]
tcp_body = tcp_all[tcp_hlen,0xffff]
@tcp_header.read(tcp_all[0,20])
@tcp_header.tcp_opts=tcp_opts
@tcp_header.body=tcp_body
@ip_header.body = @tcp_header
elsif ip_proto_num == 0x11 # It's UDP.
udp_len = io[16,2].unpack("n")[0] - 20
if args[:strip] # Same deal as with TCP. We might have stuff at the end of the packet that's not part of the payload.
@udp_header.read(io[ip_hlen+14,udp_len])
else # ... Suck it all up. BTW, this will change the lengths if they are ever recalc'ed. Bummer.
@udp_header.read(io[ip_hlen+14,0xffff])
end
@ip_header.body = @udp_header
elsif ip_proto_num == 1 # It's ICMP
@icmp_header.read(io[ip_hlen+14,0xffff])
@ip_header.body = @icmp_header
else # It's an IP packet for a protocol we don't have a decoder for.
@ip_header.body = io[16,io.size-16]
end
@eth_header.body = @ip_header
elsif eth_proto_num == 0x0806 # It's ARP
@arp_header.read(io[14,0xffff]) # You'll nearly have a trailer and you'll never know what size.
@eth_header.body=@arp_header
elsif eth_proto_num == 0x86dd # It's IPv6
@ipv6_header.read(io[14,0xffff])
@eth_header.body=@ipv6_header
else # It's an Ethernet packet for a protocol we don't have a decoder for
@eth_header.body = io[14,io.size-14]
end
if (args[:fix] || args[:recalc])
# Unfortunately, we cannot simply recalc with abandon, since
# we may have unaccounted trailers that will sneak into the checksum.
# The better way to handle this is to put trailers in their own
# BinData field, but I'm not a-gonna right now. :/
ip_recalc(:ip_sum) if respond_to? :ip_header
recalc(:tcp) if respond_to? :tcp_header
recalc(:udp) if respond_to? :udp_header
end
else # You're not big enough for Ethernet.
@invalid_header.read(io)
end
@headers[0]
end
# Peek provides summary data on packet contents.
# Each packet type should provide its own peek method, and shouldn't exceed 80 characters wide (for
# easy reading in normal irb shells). If they don't, this default summary will step in.
def peek(args={})
peek_data = ["? "]
peek_data << "%-5d" % self.to_s.size
peek_data << "%68s" % self.to_s[0,34].unpack("H*")[0]
peek_data.join
end
# Hexify provides a neatly-formatted dump of binary data, familar to hex readers.
def hexify(str)
hexascii_lines = str.to_s.unpack("H*")[0].scan(/.{1,32}/)
chars = str.to_s.gsub(/[\x00-\x1f\x7f-\xff]/,'.')
chars_lines = chars.scan(/.{1,16}/)
ret = []
hexascii_lines.size.times {|i| ret << "%-48s %s" % [hexascii_lines[i].gsub(/(.{2})/,"\\1 "),chars_lines[i]]}
ret.join("\n")
end
# Returns a hex-formatted representation of the packet.
#
# ==== Arguments
#
# 0..9 : If a number is given only the layer in @header[arg] will be displayed. Note that this will include all @headers included in that header.
# :layers : If :layers is specified, the dump will return an array of headers by layer level.
# :all : An alias for arg=0.
#
# ==== Examples
#
# irb(main):003:0> pkt = TCPPacket.new
# irb(main):003:0> puts pkt.inspect_hex(:layers)
# 00 1a c5 00 00 00 00 1a c5 00 00 00 08 00 45 00 ..............E.
# 00 28 83 ce 00 00 ff 06 38 02 00 00 00 00 00 00 .(......8.......
# 00 00 a6 0f 00 00 ac 89 7b 26 00 00 00 00 50 00 ........{&....P.
# 40 00 a2 25 00 00 @..%..
# 45 00 00 28 83 ce 00 00 ff 06 38 02 00 00 00 00 E..(......8.....
# 00 00 00 00 a6 0f 00 00 ac 89 7b 26 00 00 00 00 ..........{&....
# 50 00 40 00 a2 25 00 00 P.@..%..
# a6 0f 00 00 ac 89 7b 26 00 00 00 00 50 00 40 00 ......{&....P.@.
# a2 25 00 00 .%..
# => nil
# irb(main):004:0> puts pkt.inspect_hex(:layers)[2]
# a6 0f 00 00 ac 89 7b 26 00 00 00 00 50 00 40 00 ......{&....P.@.
# a2 25 00 00 .%..
# => nil
#
def inspect_hex(arg=0)
case arg
when :layers
ret = []
@headers.size.times do |i|
ret << hexify(@headers[i])
end
ret
when (0..9)
if @headers[arg]
hexify(@headers[arg])
else
nil
end
when :all
inspect_hex(0)
end
end
# For packets, inspect is overloaded as inspect_hex(0).
# Not sure if this is a great idea yet, but it sure makes
# the irb output more sane.
def inspect
self.inspect_hex
end
# Returns the size of the packet (as a binary string)
def size
self.to_s.size
end
alias_method :length, :size
def initialize(args={})
if args[:config]
args[:config].each_pair do |k,v|
case k
when :eth_daddr; @eth_header.eth_daddr=v if @eth_header
when :eth_saddr; @eth_header.eth_saddr=v if @eth_header
when :ip_saddr; @ip_header.ip_saddr=v if @ip_header
end
end
end
end
end # class Packet
end # module PacketFu

39
lib/packetfu/pcap.rb Normal file
View File

@ -0,0 +1,39 @@
module PacketFu
# PcapHeader describes the libpcap file header format, and is used in PcapFile.
class PcapHeader < BinData::MultiValue
string :magic, :length => 4, :initial_value => "\xd4\xc3\xb2\xa1"
uint16le :ver_major, :initial_value => 2
uint16le :ver_minor, :initial_value => 4
int32le :thiszone, :initial_value => 0
uint32le :sigfigs, :initial_value => 0
uint32le :snaplen, :initial_value => 0xffff
uint32le :network, :initial_value => 1
end
# PcapPacket describes a complete libpcap-formatted packet, which includes timestamp
# and length information. It is used in PcapPackets class.
class PcapPacket < BinData::MultiValue
uint32le :ts_sec
uint32le :ts_usec
uint32le :incl_len, :value => lambda {data.length}
uint32le :orig_len
string :data, :read_length => :incl_len
end
# PcapPackets is an BinData array type, used to collect packets and their associated
# frame data. It is part of the PcapFile class.
class PcapPackets < BinData::MultiValue
array :data, :type => :pcap_packet, :read_until => :eof
end
# PcapFile is a complete libpcap file struct, made up of a PcapHeader and PcapPackets.
#
# See http://wiki.wireshark.org/Development/LibpcapFileFormat
class PcapFile < BinData::MultiValue
pcap_header :head
pcap_packets :body
end
end

57
lib/packetfu/read.rb Normal file
View File

@ -0,0 +1,57 @@
module PacketFu
# The Read class facilitates reading from libpcap files, which is the native file format
# for packet capture utilities such as tcpdump, Wireshark, and PacketFu::PcapFile.
#
# This class requires PcapRub to be loaded (for now).
#
# == Example
#
# pkt_array = PacketFu::Read.f2a(:file => 'pcaps/my_capture.pcap')
#
# === file_to_array() Arguments
#
# :filename | :file | :out
# The file to read from.
#
# == See Also
#
# Write, Capture
class Read
# file_to_array() translates a libpcap file into an array of packets.
def self.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)
p = Pcap.open_offline(filename) #Using HD's patch instead of parsing it myself.
pcap_arr = []
while this_packet = p.next
pcap_arr << this_packet
end
pcap_arr
end
# f2a() is equivalent to file_to_array
def self.f2a(args={})
self.file_to_array(args)
end
# IRB tab-completion hack.
#--
# This silliness is so IRB's tab-completion works for my class methods
# when those methods are called without first instantiating. (I like
# tab completion a lot). The alias_methods make sure they show up
# as instance methods, but but when you call them, you're really
# calling the class methods. Tricksy!
def truth
"You can't handle the truth" ; true
end
#:stopdoc:
alias_method :file_to_array, :truth
alias_method :f2a, :truth
#:startdoc:
end
end

362
lib/packetfu/tcp.rb Normal file
View File

@ -0,0 +1,362 @@
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 reciever 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 ethernet?; true; end
def ip?; true; end
def tcp?; true; end
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

213
lib/packetfu/tcpopts.rb Normal file
View File

@ -0,0 +1,213 @@
module PacketFu
# TcpOpts handles the translation of TCP option strings to human-readable form,
# and vice versa. It is nearly certain to be completely rewritten, though the
# syntax will remain the same.
#
# == Example
#
# tcp_pkt = PacketFu::TCPPacket.new
# tcp_pkt.tcp_options="nop,nop,sack.ok,ws:7,eol"
#
# It's usable now, but you should not trust attacker data, since certain
# combinations of malformed options will raise execeptions.
#
# See http://www.iana.org/assignments/tcp-parameters/ for the rules on sizes and such.
class TcpOpts
include Singleton
# XXX: Here be dragons!
#
# Like decode, encode requires fairly strict adherance to the various
# RFCs when it comes to lengths and expected data types. If you're
# trying to enter well-formed data, it should all work fine. If you're
# trying to something like fuzzing, then you'll probably have better
# luck using tcp_opts= instead.
#
# There are a few opportunities for attackers get get bad data passed
# from decode over to encode to produce unexpected results (SACK
# and timestamp manipulation seeming to be the most disasterous).
# So don't make any life-critical decisions based on these options
# remaining the same between the decode and encode functions.
def self.encode(str)
opts = str.split(/\s*,\s*/)
binary_opts = ''
opts.each do |opt|
binary_opts << case opt
when /^EOL$/i; "\x00"
when /^NOP$/i; "\x01"
when /^MSS\s*:/i; tcp_opts_short(2,opt)
when /^WS\s*:/i; tcp_opts_char(3,opt)
when /^SACK\.OK$/i; "\x04\x02"
when /^SACK\s*:/i
sack_opts = opt.split(/\s*:\s*/)[1].split(/\s*;\s*/).collect {|i| i.to_i}.pack("N*")
[0x05,sack_opts.size+2,sack_opts].pack("CCa*")
when /^ECHO\s*:/i; tcp_opts_long(6,opt)
when /^ECHO.REPLY\s*:/i; tcp_opts_long(7,opt)
when /^TS\s*:/i
ts_opts = opt.split(/\s*:\s*/)[1].split(/\s*;\s*/).collect {|i| i.to_i}.pack("N*")
[0x08,ts_opts.size+2,ts_opts].pack("CCa*")
when /^POCP$/i; "\x09\x02"
when /^POSP\s*:/i
posp = [10,3,(opt.split(/\s*:\s*/)[1].to_i << 6)].pack("C3")
when /^CC\s*:/i; tcp_opts_long(11,opt)
when /^CC.NEW\s*:/i; tcp_opts_long(12,opt)
when /^CC.ECHO\s*:/i; tcp_opts_long(13,opt)
when /^ALT.CRC\s*:/i; tcp_opts_char(14,opt)
when /^ALT.DATA\s*:/i; tcp_opts_variable(15,opt)
when /^Skeeter\s*:/i; tcp_opts_variable(16,opt)
when /^Bubba\s*:/i; tcp_opts_variable(17,opt)
when /^TCO\s*:/i; tcp_opts_char(18,opt)
when /^MD5\s*:/i; tcp_opts_variable(19,opt)
when /^QSR\s*:/i; tcp_opts_variable(27,opt) # TODO: Do the bitwise math to match the decode.
when /^0x[0-9a-f][0-9a-f]\s*:/i; tcp_opts_variable(opt[0,4].to_i(16),opt)
else
raise ArgumentError, "Invalid tcp_options format or entry. Perhaps you want tcp_opts?"
end
end
binary_opts
end
def self.tcp_opts_variable(optnum,optstr)
ret = [optnum,0,optstr.split(/\s*:\s*/)[1]]
ret[1] = ret.pack("CCH*").size
ret.pack("CCH*")
end
def self.tcp_opts_char(optnum,optstr)
[optnum,3,optstr.split(/\s*:\s*/)[1][0,3].to_i].pack("C3")
end
def self.tcp_opts_short(optnum,optstr)
[optnum,4,optstr.split(/\s*:\s*/)[1].to_i].pack("CCn")
end
def self.tcp_opts_long(optnum,optstr)
[optnum,6,optstr.split(/\s*:\s*/)[1].to_i].pack("CCN")
end
# XXX: Here be dragons!
#
# There are a few opportunities for attackers get get bad data passed
# from decode over to encode to produce unexpected results (SACK
# and timestamp manipulation seeming to be the most disasterous).
# So don't make any life-critical decisions based on these options
# remaining the same between the decode and encode functions.
def self.decode(str)
bare_opts = []
invalid_opts = false
invalid_opts = true if str.size > 44
opts = StringIO.new(str)
while opts.pos < opts.size
bare_opts << case opts.read(1)
when "\x00"; "\x00"
when "\x01"; "\x01"
else
arr = []
opts.seek(opts.pos-1,0) # No StringIO.read(-1)? Lame.
arr << opts.read(1)
sz = opts.read(1)
if sz.nil? # Every option needs a size.
invalid_opts = true
else
sz = sz.unpack("C")[0]
if sz <= 1 # Every option's size is 2 or greater.
invalid_opts = true
else
arr << sz
arr << opts.read(sz-2)
end
end
end
end
if invalid_opts
"INVALID:#{str}"
else
TcpOpts.translate(bare_opts)
end
end
# Get an array of TCP options as produced by decode() and translate it into
# something passing for human readable.
def self.translate(arr)
translated_opts = arr.collect do |opt|
case opt
when "\x00"; "EOL"
when "\x01"; "NOP"
else
if opt[2].class == String
TcpOpts.option_to_s(opt[0].unpack("C")[0],opt[2])
else
"INVALID:#{opt.pack("aC")}"
end
end
end
translated_opts.join(",")
end
# Option_to_s translates TCP option strings into a human-readable
# form (really, a nerd-readable form, and only if that nerd has
# a copy of the various RFC's handy). It makes big assumptions
# that the sizes are RFC correct by this point, what with the
# unpacks and other presentations; for example, SACK-OK options
# are always two bytes, and no payload, so if you get a SACK-OK
# with a payload, it will be invisible when you process it with
# option_to_s.
#
# Underruns and other argument errors /should/ be impossible.
# If you find one, please file a bug!
#
# If you require more precision than this (eg, to
# account for malformed options), you should probably do
# your own opts processing using the bare tcp_opts values.
#
# Eventually, option_to_s should get a lot smarter about
# weirdly-formed options. And it at least shouldn't raise
# exceptions.
def self.option_to_s(optnum,value)
case optnum
when 2; "MSS:#{value.unpack("n")}" # Max Segment Size
when 3; "WS:#{value.unpack("C")}" # Window Scale
when 4; "SACK.OK" # SACK Permitted
when 5; sack_opt = "SACK:" # SACK values
if value.size % 4 == 0 # Well formed or not.
edges = value.scan(/[\x00-\xff]{4}/).collect {|h| h.unpack("N")}.join(';')
else
edges = value.unpack("H*")[0]
end
sack_opt + edges
when 6; "ECHO:#{value.unpack("N")}" # Echo
when 7; "ECHO.REPLY:#{value.unpack("N")}" # Echo Reply
when 8; ts_opt = "TS:" # Timestamp and TS-echo reply
ts_opt << value[0,4].unpack("N")[0].to_s
ts_opt << ";"
ts_opt << value[4,4].unpack("N")[0].to_s
when 9; "POCP" # Partial Order Connection Permitted
when 10; "POSP:" + # Partial Order Service Profile
"%02d" % ((value.unpack("C")[0] >> 6).to_s(2)) # Partial Order bits
when 11; "CC:#{value.unpack("N")}" # Connection Count. RFC 1644 is hi-larious, btw.
when 12; "CC.NEW:#{value.unpack("N")}" # Connection Count New.
when 13; "CC.EHCO:#{value.unpack("N")}" # Conn. Count Echo.
when 14: "ALT.CRC:#{value.unpack("C")}" # Alt Checksum request
when 15: "ALT.DATA:#{value.unpack("H*")}" # Alt checksum data. I'm too dumb for this.
when 16: "Skeeter:#{value.unpack("H*")}" # Skeeter crypto.
when 17: "Bubba:#{value.unpack("H*")}" # Bubba crypto.
when 18: "TCO:#{value.unpack("C")}" # Trailer Checksum Option. Nobody knows what this is.
when 19: "MD5:#{value.unpack("H*")}" # MD5 Signature Option. Hash-signed TCP? Outrageous!
when 27: qsr_opt = "QSR:" # Quick-Start Request. Experimental in Jan 2007.
qsr_val = []
qsr_val << (value[0,1].unpack("C")[0] >> 4)
qsr_val << (value[0,1].unpack("C")[0] & 0x0f)
qsr_val << value[1,1].unpack("C")[0]
qsr_val << value[2,4].unpack("N")[0] # Note bits 30,31 RFC-SHOULD be zero.
qsr_opt + qsr_val.join(';')
# Pretty much everything else is obsolete, experiemental, unused, or undoc'ed.
else; "0x#{[optnum].pack("C").unpack("H*")[0].upcase}:#{value.unpack("H*")}"
end
end
end
end

152
lib/packetfu/udp.rb Normal file
View File

@ -0,0 +1,152 @@
module PacketFu
# UDPHeader is a complete UDP struct, used in UDPPacket. Many Internet-critical protocols
# rely on UDP, such as DNS and World of Warcraft.
#
# For more on UDP packets, see http://www.networksorcery.com/enp/protocol/udp.htm
#
# ==== Header Definition
# uint16be :udp_src
# uint16be :udp_dst
# uint16be :udp_len, :initial_value => lambda {udp_calc_len}
# uint16be :udp_sum, :initial_value => 0 # Checksum off
# rest :body
class UDPHeader < BinData::MultiValue
uint16be :udp_src
uint16be :udp_dst
uint16be :udp_len, :initial_value => lambda {udp_calc_len}
uint16be :udp_sum, :initial_value => 0 # Checksum off
rest :body
# Returns the true length of the UDP packet.
def udp_calc_len
body.to_s.size + 8
end
# Recalculates calculated fields for UDP.
def udp_recalc(args=:all)
case args
when :udp_len
self.udp_len = udp_calc_len
when :all
self.udp_recalc(:udp_len)
else
raise ArgumentError, "No such field `#{arg}'"
end
end
end
# UDPPacket is used to construct UDP Packets. They contain an EthHeader, an IPHeader, and a UDPHeader.
#
# == Example
#
# udp_pkt = PacketFu::UDPPacket.new
# udp_pkt.udp_src=rand(0xffff-1024) + 1024
# udp_pkt.udp_dst=53
#
# udp_pkt.ip_saddr="1.2.3.4"
# udp_pkt.ip_daddr="10.20.30.40"
#
# udp_pkt.recalc
# udp_pkt.to_f('/tmp/udp.pcap')
#
# == Parameters
#
# :eth
# A pre-generated EthHeader object.
# :ip
# A pre-generated IPHeader object.
# :flavor
# TODO: Sets the "flavor" of the UDP packet. UDP packets don't tend have a lot of
# flavor, but their underlying ip headers do.
# :config
# A hash of return address details, often the output of Utils.whoami?
class UDPPacket < Packet
attr_accessor :eth_header, :ip_header, :udp_header
def ethernet?; true; end
def ip?; true; end
def udp?; true; end
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@ip_header = (args[:ip] || IPHeader.new)
@udp_header = (args[:udp] || UDPHeader.new)
@ip_header.body = @udp_header
@eth_header.body = @ip_header
@headers = [@eth_header, @ip_header, @udp_header]
@ip_header.ip_proto=0x11
super
udp_calc_sum
end
# udp_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.
def udp_calc_sum
# This is /not/ delegated down to @udp_header since we need info
# from the IP header, too.
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 += 0x11
checksum += udp_len
checksum += udp_src
checksum += udp_dst
checksum += udp_len
if udp_len >= 8
real_udp_payload = payload[0,(udp_len-8)] # For IP trailers. This isn't very reliable, though. :/
else
real_udp_payload = payload # I'm not going to mess with this right now.
end
chk_payload = (real_udp_payload.size % 2 == 0 ? real_udp_payload : real_udp_payload + "\x00")
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
@udp_header.udp_sum = checksum
end
# udp_recalc() recalculates various fields of the TCP packet. Valid arguments are:
#
# :all
# Recomputes all calculated fields.
# :udp_sum
# Recomputes the UDP checksum.
# :udp_len
# Recomputes the UDP length.
def udp_recalc(args=:all)
case args
when :udp_len
@udp_header.udp_recalc
when :udp_sum
udp_calc_sum
when :all
@udp_header.udp_recalc
udp_calc_sum
else
raise ArgumentError, "No such field `#{arg}'"
end
end
# Peek provides summary data on packet contents.
def peek(args={})
peek_data = ["U "]
peek_data << "%-5d" % self.to_s.size
peek_data << "%-21s" % "#{self.ip_saddr}:#{self.udp_src}"
peek_data << "->"
peek_data << "%21s" % "#{self.ip_daddr}:#{self.udp_dst}"
peek_data << "%23s" % "I:"
peek_data << "%04x" % self.ip_id
peek_data.join
end
end
end # module PacketFu

128
lib/packetfu/utils.rb Normal file
View File

@ -0,0 +1,128 @@
require 'singleton'
module PacketFu
# Utils is a collection of various and sundry network utilities that are useful for packet
# manipulation.
class Utils
include Singleton
# Returns the MAC address of an IP address, or nil if it's not responsive to arp. Takes
# a dotted-octect notation of the target IP address, as well as a number of parameters:
#
# === Parameters
# :eth_saddr
# Source MAC address. Defaults to "00:00:00:00:00:00".
# :ip_saddr
# Source IP address. Defaults to "0.0.0.0"
# :flavor
# The flavor of the ARP request. Defaults to :none.
# :timeout
# Timeout in seconds. Defaults to 3.
#
# === Example
# PacketFu::Utils::arp("192.168.1.1") #=> "00:18:39:01:33:70"
# PacketFu::Utils::arp("192.168.1.1", :timeout => 5, :flavor => :hp_deskjet)
#
# === Warning
#
# It goes without saying, spewing forged ARP packets on your network is a great way to really
# irritate your co-workers.
def self.arp(target_ip,args={})
arp_pkt = PacketFu::ARPPacket.new(:flavor => (args[:flavor] || :none))
arp_pkt.eth_saddr = arp_pkt.arp_saddr_mac = (args[:eth_saddr] || ($packetfu_default.config[:eth_saddr] if $packetfu_default) || "00:00:00:00:00:00" )
arp_pkt.eth_daddr = "ff:ff:ff:ff:ff:ff"
arp_pkt.arp_daddr_mac = "00:00:00:00:00:00"
arp_pkt.arp_saddr_ip = (args[:ip_saddr] || ($packetfu_default.config[:ip_saddr] if $packetfu_default) || "0.0.0.0")
arp_pkt.arp_daddr_ip = target_ip
iface = (args[:iface] || ($packetfu_default.iface if $packetfu_default) || "eth0")
# Stick the Capture object in its own thread.
cap_thread = Thread.new do
target_mac = nil
cap = PacketFu::Capture.new(:iface => iface, :start => true,
:filter => "arp src #{target_ip} and ether dst #{arp_pkt.eth_saddr}")
arp_pkt.to_w(iface) # Shorthand for sending single packets to the default interface.
timeout = 0
while target_mac.nil? && timeout <= (args[:timeout] || 3)
if cap.save > 0
arp_response = PacketFu::Packet.parse(cap.array[0])
target_mac = arp_response.arp_saddr_mac if arp_response.arp_saddr_ip = target_ip
end
timeout += 0.1
sleep 0.1 # Check for a response ten times per second.
end
target_mac
end # cap_thread
cap_thread.value
end # def self.arp
# Discovers the local IP and Ethernet address, which is useful for writing
# packets you expect to get a response to. Note, this is a noisy
# operation; a UDP packet is generated and dropped on to the default (or named)
# interface, and then captured (which means you need to be root to do this).
#
# whoami? returns a hash of :eth_saddr, :eth_src, :ip_saddr, :ip_src,
# :eth_dst, and :eth_daddr (the last two are usually suitable for a
# gateway mac address). It's most useful as an argument to PacketFu::Config.new.
#
# === Parameters
# :iface => "eth0"
# An interface to listen for packets on. Note that since we rely on the OS to send the probe packet,
# you will need to specify a target which will use this interface.
# :target => "1.2.3.4"
# A target IP address. By default, a packet will be sent to a random address in the 177/8 network.
# Since this network is IANA reserved (for now), this network should be handled by your default gateway
# and default interface.
def self.whoami?(args={})
if args[:iface] =~ /^lo/ # Linux loopback more or less. Need a switch for windows loopback, too.
dst_host = "127.0.0.1"
else
dst_host = (args[:target] || IPAddr.new((rand(16777216) + 2969567232), Socket::AF_INET).to_s)
end
dst_port = rand(0xffff-1024)+1024
msg = "PacketFu whoami? packet #{(Time.now.to_i + rand(0xffffff)+1)}"
cap = Capture.new(:iface => (args[:iface] || Pcap.lookupdev), :start => true, :filter => "udp and dst host #{dst_host} and dst port #{dst_port}")
UDPSocket.open.send(msg,0,dst_host,dst_port)
cap.save
pkt = Packet.parse(cap.array[0]) unless cap.save.zero?
timeout = 0
while timeout < 1 # Sometimes packet generation can be a little pokey.
if pkt
timeout = 1.1 # Cancel the timeout
if pkt.payload == msg
my_data = {
:iface => args[:iface] || Pcap.lookupdev || 'lo',
:pcapfile => args[:pcapfile] || "/tmp/out.pcap",
:eth_saddr => pkt.eth_saddr,
:eth_src => pkt.eth_src.to_s,
:ip_saddr => pkt.ip_saddr,
:ip_src => pkt.ip_src.to_s,
:eth_dst => pkt.eth_dst.to_s,
:eth_daddr => pkt.eth_daddr
}
else raise SecurityError,
"whoami() packet doesn't match sent data. Something fishy's going on."
end
else
sleep 0.1; timeout += 0.1
cap.save
pkt = Packet.parse(cap.array[0]) unless cap.save.zero?
end
raise SocketError, "Didn't recieve the whomi() packet." if !pkt
cap = nil
end
my_data
end
# This is a brute-force approach at trying to find a suitable interface with an IP address.
def self.lookupdev
# XXX cycle through eth0-9 and wlan0-9, and if a cap start throws a RuntimeErorr (and we're
# root), it's not a good interface. Boy, really ought to fix lookupdev directly with another
# method that returns an array rather than just the first candidate.
end
end # class Utils
end # module PacketFu

91
lib/packetfu/write.rb Normal file
View File

@ -0,0 +1,91 @@
module PacketFu
# The Write class facilitates writing to libpcap files, which is the native file format
# for packet capture utilities such as tcpdump, Wireshark, and PacketFu::PcapFile.
#
# == Example
#
# cap = PacketFu::Capture.new(:start => true)
# sleep 10
# cap.save
# pkt_array = cap.array
# PacketFu::Write.a2f(:file => 'pcaps/my_capture.pcap', :array => pkt_array)
#
# === array_to_file() Arguments
#
# :filename | :file | :out
# The file to write to. If it exists, it will be overwritten. By default, no file will be written.
#
# :array | arr
# The array to read packet data from. Note, these should be strings, and not packet objects!
#
# :ts | :timestamp
# The starting timestamp. By default, it is the result of Time.now.to_i
#
# :ts_inc | :timestamp_increment
# The timestamp increment, in seconds. (Sorry, no usecs yet)
#
# == See Also
#
# Read, Capture
class Write
# Writes an array of binary data to a libpcap file.
def self.array_to_file(args={})
filename = args[:filename] || args[:file] || args[:out] || :nowrite
arr = args[:arr] || args[:array] || []
ts = args[:ts] || args[:timestamp] || Time.now.to_i
ts_inc = args[:ts_inc] || args[:timestamp_increment] || 1
if arr.class != Array
raise ArgumentError, "This needs to be an array."
end
formatted_packets = []
arr.each do |pkt|
this_pkt = PcapPacket.new
this_pkt.data = pkt[0,0xffff]
this_pkt.orig_len = pkt.size # orig_len isn't calc'ed already.
this_pkt.ts_sec = ts += decimal_to_usecs(ts_inc)[0]
formatted_packets << this_pkt.to_s
end
filedata = PcapFile.new
filedata.read(PcapFile.new.to_s + formatted_packets.join) # Like a cat playing the bass.
if filename != :nowrite
out = File.new(filename.to_s, 'w')
out.print filedata
out.close
# Return [filename, file size, # of packets, initial timestamp, timestamp increment]
ret = [filename,filedata.to_s.size,arr.size,ts,ts_inc]
else
ret = [nil,filedata.to_s.size,arr.size,ts,ts_inc]
end
end
# A synonym for array_to_file()
def self.a2f(args={})
self.array_to_file(args)
end
# TODO: Wire this in later to enable incrementing by microseconds.
def self.decimal_to_usecs(decimal)
secs = decimal.to_i
usecs = decimal.to_f.to_s.split('.')[1]
[secs,usecs]
end
# IRB tab-completion hack.
def truth
"Stranger than fiction" ; true
end
#:stopdoc:
alias_method :array_to_file, :truth
alias_method :a2f, :truth
#:startdoc:
end
end