ether_shell 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.project ADDED
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>ether_shell</name>
4
+ <comment></comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>com.aptana.ide.core.unifiedBuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>com.aptana.ruby.core.rubynature</nature>
16
+ </natures>
17
+ </projectDescription>
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.5.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.5.2"
12
+ gem "rcov", ">= 0"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.2)
5
+ git (1.2.5)
6
+ jeweler (1.5.2)
7
+ bundler (~> 1.0.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rake (0.8.7)
11
+ rcov (0.9.9)
12
+ rspec (2.5.0)
13
+ rspec-core (~> 2.5.0)
14
+ rspec-expectations (~> 2.5.0)
15
+ rspec-mocks (~> 2.5.0)
16
+ rspec-core (2.5.1)
17
+ rspec-expectations (2.5.0)
18
+ diff-lcs (~> 1.1.2)
19
+ rspec-mocks (2.5.0)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bundler (~> 1.0.0)
26
+ jeweler (~> 1.5.2)
27
+ rcov
28
+ rspec (~> 2.5.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Victor Costan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,54 @@
1
+ = ether_shell
2
+
3
+ ether_shell installs an IRB session with a DSL for testing Ethernet devices.
4
+
5
+ == Usage
6
+
7
+ Installing the gem will give you an ether_shell binary. The binary can be
8
+ launched standalone to get to the IRB session, or it can be given a script that
9
+ it will execute. Here is how I use it.
10
+
11
+ ether_shell < example.esh
12
+
13
+ Here is an example.esh that describes the DSL by example.
14
+
15
+ # All commands will log to standard output.
16
+ verbose
17
+ # IP's Ethernet II protocol number is 0x0800
18
+ connect 'eth0', 0x0800, '0x112233445566'
19
+ disconnect
20
+ # The device's Ethernet MAC can be specified in a number of ways.
21
+ connect 'eth0', 0x0800, 'BADMAC'
22
+ disconnect
23
+ connect 'eth0', 0x0800, '112233445566'
24
+ disconnect
25
+ # Sends a packet to the device.
26
+ out "This is short and will be padded"
27
+ # Again, multiple ways of specifying a packet are supported.
28
+ out "0xCAFEBABE"
29
+ out [0xDE, 0xAD, 0xBE, 0xAF]
30
+ # Receives a packet and compares it with a golden value.
31
+ # If the device echoed packets, these would be valid expectations.
32
+ expect "This is short and will be padded"
33
+ expect "0xCAFEBABE000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
34
+ expect [0xDE, 0xAD, 0xBE, 0xAF] + [0x00] * 42
35
+ disconnect
36
+ exit
37
+
38
+ The code should have full RSpec coverage, and the RSpecs can be used for
39
+ documetation. You're most likely interested in the spec for ShellDsl.
40
+
41
+ == Contributing to ether_shell
42
+
43
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
44
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
45
+ * Fork the project
46
+ * Start a feature/bugfix branch
47
+ * Commit and push until you are happy with your contribution
48
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
49
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
50
+
51
+ == Copyright
52
+
53
+ Copyright (c) 2011 Massachusetts Institute of Technology. See LICENSE.txt for
54
+ further details.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "ether_shell"
16
+ gem.homepage = "http://github.com/pwnall/ether_shell"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{IRB session specialized for testing Ethernet devices}
19
+ gem.description = %Q{IRB session specialized for testing Ethernet devices}
20
+ gem.email = "victor@costan.us"
21
+ gem.authors = ["Victor Costan"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "ether_shell #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
data/bin/ether_shell ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'ether_shell'
4
+
5
+ require 'irb'
6
+
7
+ include EtherShell::ShellDsl
8
+ IRB.start __FILE__
@@ -0,0 +1,85 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{ether_shell}
8
+ s.version = "0.9.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Victor Costan"]
12
+ s.date = %q{2011-03-22}
13
+ s.default_executable = %q{ether_shell}
14
+ s.description = %q{IRB session specialized for testing Ethernet devices}
15
+ s.email = %q{victor@costan.us}
16
+ s.executables = ["ether_shell"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".project",
24
+ ".rspec",
25
+ "Gemfile",
26
+ "Gemfile.lock",
27
+ "LICENSE.txt",
28
+ "README.rdoc",
29
+ "Rakefile",
30
+ "VERSION",
31
+ "bin/ether_shell",
32
+ "ether_shell.gemspec",
33
+ "lib/ether_shell.rb",
34
+ "lib/ether_shell/expectation_error.rb",
35
+ "lib/ether_shell/high_socket.rb",
36
+ "lib/ether_shell/raw_socket.rb",
37
+ "lib/ether_shell/shell_dsl.rb",
38
+ "spec/ether_shell/high_socket_spec.rb",
39
+ "spec/ether_shell/raw_socket_spec.rb",
40
+ "spec/ether_shell/shell_dsl_spec.rb",
41
+ "spec/ether_shell_spec.rb",
42
+ "spec/spec_helper.rb",
43
+ "spec/support/raw_socket_stub.rb",
44
+ "spec/support/shell_stub.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/pwnall/ether_shell}
47
+ s.licenses = ["MIT"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.6.0}
50
+ s.summary = %q{IRB session specialized for testing Ethernet devices}
51
+ s.test_files = [
52
+ "spec/ether_shell/high_socket_spec.rb",
53
+ "spec/ether_shell/raw_socket_spec.rb",
54
+ "spec/ether_shell/shell_dsl_spec.rb",
55
+ "spec/ether_shell_spec.rb",
56
+ "spec/spec_helper.rb",
57
+ "spec/support/raw_socket_stub.rb",
58
+ "spec/support/shell_stub.rb"
59
+ ]
60
+
61
+ if s.respond_to? :specification_version then
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
65
+ s.add_development_dependency(%q<rspec>, ["~> 2.5.0"])
66
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
67
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
68
+ s.add_development_dependency(%q<rcov>, [">= 0"])
69
+ s.add_development_dependency(%q<rspec>, ["> 1.2.3"])
70
+ else
71
+ s.add_dependency(%q<rspec>, ["~> 2.5.0"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
74
+ s.add_dependency(%q<rcov>, [">= 0"])
75
+ s.add_dependency(%q<rspec>, ["> 1.2.3"])
76
+ end
77
+ else
78
+ s.add_dependency(%q<rspec>, ["~> 2.5.0"])
79
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
80
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
81
+ s.add_dependency(%q<rcov>, [">= 0"])
82
+ s.add_dependency(%q<rspec>, ["> 1.2.3"])
83
+ end
84
+ end
85
+
@@ -0,0 +1,8 @@
1
+ # Documentation here.
2
+ module EtherShell
3
+ end
4
+
5
+ require 'ether_shell/expectation_error.rb'
6
+ require 'ether_shell/high_socket.rb'
7
+ require 'ether_shell/raw_socket.rb'
8
+ require 'ether_shell/shell_dsl.rb'
@@ -0,0 +1,9 @@
1
+ # :nodoc: namespace
2
+ module EtherShell
3
+
4
+ # Raised by ShellDsl#expect if the received packet doesn't match the pattern.
5
+ class ExpectationError < StandardError
6
+
7
+ end # class EtherShell::ExpectationError
8
+
9
+ end # namespace EtherShell
@@ -0,0 +1,122 @@
1
+ # :nodoc: namespace
2
+ module EtherShell
3
+
4
+ # Wraps an Ethernet socket and abstracts away the Ethernet II frame.
5
+ class HighSocket
6
+ # Creates a wrapper around a raw Ethernet socket.
7
+ #
8
+ # Args:
9
+ # raw_socket_or_device:: a raw Ethernet socket or a string containing an
10
+ # Ethernet device name
11
+ # ether_type:: 2-byte Ethernet packet type number
12
+ # mac_address:: 6-byte MAC address for the Ethernet socket (optional if
13
+ # raw_socket_or_device is an Ethernet device name)
14
+ #
15
+ # Raises:
16
+ # RuntimeError:: if mac isn't exactly 6-bytes long
17
+ def initialize(raw_socket_or_device, ether_type, mac_address = nil)
18
+ check_mac mac_address if mac_address
19
+
20
+ if raw_socket_or_device.respond_to? :to_str
21
+ @source_mac = mac_address || RawSocket.mac(raw_socket_or_device)
22
+ @socket = RawSocket.socket raw_socket_or_device, ether_type
23
+ else
24
+ raise 'MAC address needed with raw socket' unless mac_address
25
+ @source_mac = mac_address.dup
26
+ @socket = raw_socket_or_device
27
+ end
28
+
29
+ @dest_mac = nil
30
+ @ether_type = [ether_type].pack('n')
31
+ end
32
+
33
+ # Sets the destination MAC address for future calls to send.
34
+ #
35
+ # Args:
36
+ # mac:: 6-byte MAC address for the Ethernet socket
37
+ #
38
+ # Raises:
39
+ # RuntimeError:: if mac isn't exactly 6-bytes long
40
+ def connect(mac_address)
41
+ check_mac mac_address
42
+ @dest_mac = mac_address
43
+ end
44
+
45
+ # Closes the underlying socket.
46
+ def close
47
+ @socket.close
48
+ end
49
+
50
+ # Sends an Ethernet II frame.
51
+ #
52
+ # Args:
53
+ # data:: the data bytes to be sent
54
+ #
55
+ # Raises:
56
+ # RuntimeError:: if connect wasn' previously called
57
+ def send(data, send_flags = 0)
58
+ raise "Not connected" unless @dest_mac
59
+ send_to @dest_mac, data, send_flags
60
+ end
61
+
62
+ # Sends an Ethernet II frame.
63
+ #
64
+ # Args:
65
+ # mac_address:: the destination MAC address
66
+ # data:: the data bytes to be sent
67
+ #
68
+ # Raises:
69
+ # RuntimeError:: if connect wasn' previously called
70
+ def send_to(mac_address, data, send_flags = 0)
71
+ check_mac mac_address
72
+
73
+ padding = (data.length < 46) ? "\0" * (46 - data.length) : ''
74
+ packet = [mac_address, @source_mac, @ether_type, data, padding].join
75
+ @socket.send packet, send_flags
76
+ end
77
+
78
+ # Receives an Ethernet II frame.
79
+ #
80
+ # Args:
81
+ # buffer_size:: optional maximum packet size argument passed to the raw
82
+ # socket's recv method
83
+ #
84
+ # Returns the data and the source MAC address in the frame.
85
+ #
86
+ # This will discard incoming frames that don't match the MAC address that the
87
+ # socket is connected to, or the Ethernet packet type.
88
+ def recv(buffer_size = 8192)
89
+ raise "Not connected" unless @dest_mac
90
+ loop do
91
+ data, mac_address = recv_from buffer_size
92
+ return data if @dest_mac == mac_address
93
+ end
94
+ end
95
+
96
+ # Receives an Ethernet II frame.
97
+ #
98
+ # Args:
99
+ # buffer_size:: optional maximum packet size argument passed to the raw
100
+ # socket's recv method
101
+ #
102
+ # Returns the data in the frame.
103
+ #
104
+ # This will discard incoming frames that don't match the MAC address that the
105
+ # socket is connected to, or the Ethernet packet type.
106
+ def recv_from(buffer_size = 8192)
107
+ loop do
108
+ packet = @socket.recv buffer_size
109
+ next unless packet[12, 2] == @ether_type
110
+ next unless packet[0, 6] == @source_mac
111
+ return packet[14..-1], packet[6, 6]
112
+ end
113
+ end
114
+
115
+ # Raises an exception if the given MAC address is invalid.
116
+ def check_mac(mac_address)
117
+ raise "Invalid MAC address" unless mac_address.length == 6
118
+ end
119
+ private :check_mac
120
+ end # class EtherShell::HighSocket
121
+
122
+ end # namespace EtherShell
@@ -0,0 +1,102 @@
1
+ require 'socket'
2
+
3
+ # :nodoc: namespace
4
+ module EtherShell
5
+
6
+ # Low-level socket creation functionality.
7
+ module RawSocket
8
+ # A raw socket will receive all Ethernet frames, and send raw frames.
9
+ #
10
+ # Args:
11
+ # eth_device:: device name for the Ethernet card, e.g. 'eth0'
12
+ # ether_type:: Ethernet protocol number
13
+ def self.socket(eth_device = nil, ether_type = nil)
14
+ ether_type ||= all_ethernet_protocols
15
+ socket = Socket.new raw_address_family, Socket::SOCK_RAW, htons(ether_type)
16
+ socket.setsockopt Socket::SOL_SOCKET, Socket::SO_BROADCAST, true
17
+ set_socket_eth_device(socket, eth_device, ether_type) if eth_device
18
+ socket
19
+ end
20
+
21
+ # The MAC address for an Ethernet card.
22
+ #
23
+ # Args:
24
+ # eth_device:: device name for the Ethernet card, e.g. 'eth0'
25
+ def self.mac(eth_device)
26
+ case RUBY_PLATFORM
27
+ when /linux/
28
+ # /usr/include/net/if.h, structure ifreq
29
+ ifreq = [eth_device].pack 'a32'
30
+ # 0x8927 is SIOCGIFHWADDR in /usr/include/bits/ioctls.h
31
+ socket.ioctl 0x8927, ifreq
32
+ ifreq[18, 6]
33
+ else
34
+ raise "Unsupported platform #{RUBY_PLATFORM}"
35
+ end
36
+ end
37
+
38
+ class <<self
39
+ # Sets the Ethernet interface and protocol type for a socket.
40
+ def set_socket_eth_device(socket, eth_device, ether_type)
41
+ case RUBY_PLATFORM
42
+ when /linux/
43
+ if_number = get_interface_number eth_device
44
+ # struct sockaddr_ll in /usr/include/linux/if_packet.h
45
+ socket_address = [raw_address_family, htons(ether_type), if_number,
46
+ 0xFFFF, 0, 0, ""].pack 'SSISCCa8'
47
+ socket.bind socket_address
48
+ else
49
+ raise "Unsupported platform #{RUBY_PLATFORM}"
50
+ end
51
+ socket
52
+ end
53
+ private :set_socket_eth_device
54
+
55
+ # The interface number for an Ethernet interface.
56
+ def get_interface_number(eth_device)
57
+ case RUBY_PLATFORM
58
+ when /linux/
59
+ # /usr/include/net/if.h, structure ifreq
60
+ ifreq = [eth_device].pack 'a32'
61
+ # 0x8933 is SIOCGIFINDEX in /usr/include/bits/ioctls.h
62
+ socket.ioctl 0x8933, ifreq
63
+ ifreq[16, 4].unpack('I').first
64
+ else
65
+ raise "Unsupported platform #{RUBY_PLATFORM}"
66
+ end
67
+ end
68
+ private :get_interface_number
69
+
70
+ # The protocol number for listening to all ethernet protocols.
71
+ def all_ethernet_protocols
72
+ case RUBY_PLATFORM
73
+ when /linux/
74
+ 3
75
+ else
76
+ raise "Unsupported platform #{RUBY_PLATFORM}"
77
+ end
78
+ end
79
+ private :all_ethernet_protocols
80
+
81
+ # The AF / PF number for raw sockets.
82
+ def raw_address_family
83
+ case RUBY_PLATFORM
84
+ when /linux/
85
+ 17 # cat /usr/include/bits/socket.h | grep PF_PACKET
86
+ when /darwin/
87
+ 18 # cat /usr/include/sys/socket.h | grep AF_LINK
88
+ else
89
+ raise "Unsupported platform #{RUBY_PLATFORM}"
90
+ end
91
+ end
92
+ private :raw_address_family
93
+
94
+ # Converts a 16-bit integer from host-order to network-order.
95
+ def htons(short_integer)
96
+ [short_integer].pack('n').unpack('S').first
97
+ end
98
+ private :htons
99
+ end
100
+ end # module EtherShell::RawSocket
101
+
102
+ end # namespace EtherShell
@@ -0,0 +1,151 @@
1
+ # :nodoc: namespace
2
+ module EtherShell
3
+
4
+ # Provides the Ethernet shell DSL.
5
+ #
6
+ # Include this in the evaluation context where you want the Ethernet shell DSL.
7
+ module ShellDsl
8
+ # Creates a Ethernet socket for this shell.
9
+ #
10
+ # Args:
11
+ # eth_device:: an Ethernet device name, e.g. 'eth0'
12
+ # ether_type:: 2-byte Ethernet packet type number
13
+ # dest_mac:: MAC address of the endpoint to be tested; it can be a raw
14
+ # 6-byte string,
15
+ def connect(eth_device, ether_type, dest_mac)
16
+ raise "Already connected. did you forget to call disconnect?" if @_socket
17
+ mac_bytes = EtherShell::ShellDsl.parse_mac_data dest_mac
18
+ @_socket = EtherShell::HighSocket.new eth_device, ether_type
19
+ @_socket.connect mac_bytes
20
+ if @_verbose
21
+ print ['Connected to ', mac_bytes.unpack('H*').first, ' using ',
22
+ '%04x' % ether_type, ' via ', eth_device, "\n"].join
23
+ end
24
+ @_nothing = ''
25
+ class <<@_nothing
26
+ def inspect
27
+ ''
28
+ end
29
+ end
30
+ EtherShell::ShellDsl.nothing
31
+ end
32
+
33
+ # Disconnects this shell's Ethernet socket.
34
+ #
35
+ # A socket should have been connected previously, using connect or socket. The
36
+ # shell can take further connect and socket calls.
37
+ def disconnect
38
+ raise "Not connected. did you forget to call connect?" unless @_socket
39
+ @_socket.close
40
+ @_socket = nil
41
+ print "Disconnected\n" if @_verbose
42
+ EtherShell::ShellDsl.nothing
43
+ end
44
+
45
+ # Connects this shell to a pre-created socket
46
+ #
47
+ # Args:
48
+ # high_socket:: socket that behaves like an EtherShell::HighSocket
49
+ def socket(high_socket)
50
+ raise "Already connected. did you forget to call disconnect?" if @_socket
51
+ @_socket = high_socket
52
+ print "Connected directly to socket\n" if @_verbose
53
+ EtherShell::ShellDsl.nothing
54
+ end
55
+
56
+ # Enables or disables the console output in out and expect.
57
+ #
58
+ # Args:
59
+ # true_or_false:: if true, out and expect will produce console output
60
+ def verbose(true_or_false = true)
61
+ @_verbose = true_or_false
62
+ EtherShell::ShellDsl.nothing
63
+ end
64
+
65
+ # Outputs a packet.
66
+ #
67
+ # Args:
68
+ # packet_data:: an Array of integers (bytes), a hex string starting with 0x,
69
+ # or a string of raw bytes
70
+ #
71
+ # Raises:
72
+ # RuntimeError:: if the shell was not connected to a socket by a call to
73
+ # connect or socket
74
+ def out(packet_data)
75
+ raise "Not connected. did you forget to call connect?" unless @_socket
76
+ bytes = EtherShell::ShellDsl.parse_packet_data packet_data
77
+
78
+
79
+ print "Sending #{bytes.unpack('H*').first}... " if @_verbose
80
+ @_socket.send bytes
81
+ print "OK\n" if @_verbose
82
+ EtherShell::ShellDsl.nothing
83
+ end
84
+
85
+ # Receives a packet and matches it against an expected value.
86
+ #
87
+ # Args:
88
+ # packet_data:: an Array of integers (bytes), a hex string starting with 0x,
89
+ # or a string of raw bytes
90
+ #
91
+ # Raises:
92
+ # RuntimeError:: if the shell was not connected to a socket by a call to
93
+ # connect or socket
94
+ # RuntimeError:: if the received packet doesn't match the expected value
95
+ def expect(packet_data)
96
+ raise "Not connected. did you forget to call connect?" unless @_socket
97
+ expected_bytes = EtherShell::ShellDsl.parse_packet_data packet_data
98
+
99
+ print "Receiving... " if @_verbose
100
+ bytes = @_socket.recv
101
+ print " #{bytes.unpack('H*').first} " if @_verbose
102
+ if bytes == expected_bytes
103
+ print "OK\n" if @_verbose
104
+ else
105
+ print "!= #{expected_bytes.unpack('H*').first} ERROR\n" if @_verbose
106
+ raise EtherShell::ExpectationError,
107
+ "#{bytes.unpack('H*').first} != #{expected_bytes.unpack('H*').first}"
108
+ end
109
+ EtherShell::ShellDsl.nothing
110
+ end
111
+
112
+ # :nodoc: turns a packet pattern into a string of raw bytes
113
+ def self.parse_packet_data(packet_data)
114
+ if packet_data.kind_of? Array
115
+ # Array of integers.
116
+ packet_data.pack('C*')
117
+ elsif packet_data.kind_of? String
118
+ if packet_data[0, 2] == '0x'
119
+ [packet_data[2..-1]].pack('H*')
120
+ else
121
+ packet_data
122
+ end
123
+ end
124
+ end
125
+
126
+ # :nodoc: turns a packet pattern into a string of raw bytes
127
+ def self.parse_mac_data(mac_data)
128
+ if mac_data.length == 12
129
+ [mac_data].pack('H*')
130
+ elsif mac_data.length == 14 && mac_data[0, 2] == '0x'
131
+ [mac_data[2, 12]].pack('H*')
132
+ elsif mac_data.kind_of? Array
133
+ mac_data.pack('C*')
134
+ else
135
+ mac_data
136
+ end
137
+ end
138
+
139
+ # :nodoc: value that doesn't show up in irb
140
+ def self.nothing
141
+ @nothing ||= Nothing.new
142
+ end
143
+
144
+ class Nothing
145
+ def inspect
146
+ ''
147
+ end
148
+ end
149
+ end # module EtherShell::ShellDsl
150
+
151
+ end # namespace EtherShell
@@ -0,0 +1,102 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe 'HighSocket' do
4
+ let(:eth_device) { 'eth0' }
5
+ let(:eth_type) { 0x0800 }
6
+ let(:mac) { EtherShell::RawSocket.mac eth_device }
7
+ let(:dest_mac) { "\x00\x11\x22\x33\x44\x55" }
8
+ let(:bcast_mac) { "\xff" * 6 }
9
+
10
+ shared_examples_for 'a real socket' do
11
+ it 'should output a packet' do
12
+ @socket.send_to dest_mac, "\r\n"
13
+ end
14
+
15
+ it 'should receive some network noise' do
16
+ @socket.recv_from.first.should_not be_empty
17
+ end
18
+ end
19
+
20
+ describe 'on eth0' do
21
+ before { @socket = EtherShell::HighSocket.new eth_device, eth_type }
22
+ after { @socket.close }
23
+
24
+ it_should_behave_like 'a real socket'
25
+ end
26
+
27
+ describe 'from raw socket' do
28
+ before do
29
+ raw_socket = EtherShell::RawSocket.socket eth_device, eth_type
30
+ @socket = EtherShell::HighSocket.new raw_socket, eth_type, mac
31
+ end
32
+ after { @socket.close }
33
+
34
+ it_should_behave_like 'a real socket'
35
+ end
36
+
37
+ describe 'stubbed' do
38
+ let(:socket_stub) do
39
+ RawSocketStub.new([
40
+ [mac, dest_mac, "\x88\xB7", 'Wrong Ethernet type'].join,
41
+ [bcast_mac, dest_mac, [eth_type].pack('n'), 'Wrong dest MAC'].join,
42
+ [mac, bcast_mac, [eth_type].pack('n'), 'Bcast'].join,
43
+ [mac, dest_mac, [eth_type].pack('n'), 'Correct'].join,
44
+ ])
45
+ end
46
+ let(:socket) { EtherShell::HighSocket.new socket_stub, eth_type, mac }
47
+
48
+ shared_examples_for 'after a small send call' do
49
+ it 'should send a single packet' do
50
+ socket_stub.sends.length.should == 1
51
+ end
52
+ it 'should pad the packet' do
53
+ socket_stub.sends.first.length.should == 60
54
+ end
55
+ it 'should assemble packet correctly in send' do
56
+ gold = [dest_mac, mac, [eth_type].pack('n'), 'Send data'].join
57
+ socket_stub.sends.first[0, gold.length].should == gold
58
+ end
59
+ end
60
+
61
+ describe 'send_to' do
62
+ before { socket.send_to dest_mac, 'Send data' }
63
+ it_should_behave_like 'after a small send call'
64
+ end
65
+
66
+ describe 'recv_from' do
67
+ it 'should filter down to the correct packet' do
68
+ socket.recv_from.should == ['Bcast', bcast_mac]
69
+ end
70
+ end
71
+
72
+ describe 'unconnected' do
73
+ it 'should complain in recv' do
74
+ lambda { socket.recv }.should raise_error(RuntimeError)
75
+ end
76
+
77
+ it 'should complain in send' do
78
+ lambda { socket.send 'Send data' }.should raise_error(RuntimeError)
79
+ end
80
+ end
81
+
82
+ describe 'connected' do
83
+ before { socket.connect dest_mac }
84
+
85
+ describe 'send' do
86
+ before { socket.send 'Send data' }
87
+ it_should_behave_like 'after a small send call'
88
+ end
89
+
90
+ describe 'recv' do
91
+ it 'should filter down to the correct packet' do
92
+ socket.recv.should == 'Correct'
93
+ end
94
+ end
95
+ end
96
+
97
+ it 'should delegate close' do
98
+ socket_stub.should_receive(:close).once
99
+ socket.close
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,41 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe 'RawSocket' do
4
+ let(:eth_device) { 'eth0' }
5
+ let(:mac) { EtherShell::RawSocket.mac eth_device }
6
+
7
+ describe 'mac' do
8
+ let(:golden_mac) do
9
+ hex_mac = `ifconfig #{eth_device}`[/HWaddr .*$/][7..-1]
10
+ [hex_mac.gsub(':', '').strip].pack('H*')
11
+ end
12
+
13
+ it 'should have 6 bytes' do
14
+ mac.length.should == 6
15
+ end
16
+
17
+ it 'should match ifconfig output' do
18
+ mac.should == golden_mac
19
+ end
20
+ end
21
+
22
+ describe 'socket' do
23
+ let(:eth_type) { 0x88B7 }
24
+
25
+ before { @socket = EtherShell::RawSocket.socket eth_device }
26
+ after { @socket.close }
27
+
28
+ it 'should be able to receive data' do
29
+ @socket.should respond_to(:recv)
30
+ end
31
+
32
+ it 'should output a packet' do
33
+ packet = [mac, mac, [eth_type].pack('n'), "\r\n" * 32].join
34
+ @socket.send packet, 0
35
+ end
36
+
37
+ it 'should receive some network noise' do
38
+ @socket.recv(8192).should_not be_empty
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,198 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe 'ShellDsl' do
4
+ let(:eth_device) { 'eth0' }
5
+ let(:eth_type) { 0x0800 }
6
+ let(:eth_type_hex) { '0800' }
7
+ let(:mac) { EtherShell::RawSocket.mac eth_device }
8
+ let(:dest_mac) { "\x00\x11\x22\x33\x44\x55" }
9
+ let(:dest_mac_hex) { '001122334455' }
10
+ let(:bcast_mac) do
11
+ string = "\xff" * 6
12
+ # Awful hack so the MAC matches any packet.
13
+ class <<string
14
+ def ==(other)
15
+ other.respond_to?(:length) && other.length == 6
16
+ end
17
+ end
18
+ string
19
+ end
20
+
21
+ let(:shell) { ShellStub.new }
22
+
23
+ shared_examples_for 'a connected shell' do
24
+ it 'should be able to send a packet' do
25
+ shell.out 'Shell test packet'
26
+ end
27
+
28
+ it 'should be able to receive noise' do
29
+ lambda {
30
+ shell.expect 'Impossible pattern'
31
+ }.should raise_error(EtherShell::ExpectationError)
32
+ end
33
+
34
+ it 'should not connect again' do
35
+ lambda {
36
+ shell.connect eth_device, eth_type, bcast_mac
37
+ }.should raise_error(RuntimeError)
38
+ end
39
+
40
+ it 'should not accept a socket again' do
41
+ raw_socket = EtherShell::HighSocket.new eth_device, eth_type
42
+ lambda {
43
+ shell.socket raw_socket
44
+ }.should raise_error(RuntimeError)
45
+ raw_socket.close
46
+ end
47
+ end
48
+
49
+ describe 'connected to new socket' do
50
+ before { shell.connect eth_device, eth_type, bcast_mac }
51
+ after { shell.disconnect }
52
+
53
+ it_should_behave_like 'a connected shell'
54
+ end
55
+
56
+ describe 'connected to a live socket' do
57
+ let(:live_socket) do
58
+ socket = EtherShell::HighSocket.new eth_device, eth_type
59
+ socket.connect bcast_mac
60
+ socket
61
+ end
62
+ before { shell.socket live_socket }
63
+ after { shell.disconnect }
64
+
65
+ it_should_behave_like 'a connected shell'
66
+ end
67
+
68
+
69
+ describe 'disconnected' do
70
+ it 'should not send packets' do
71
+ lambda {
72
+ shell.out 'Will never go out'
73
+ }.should raise_error(RuntimeError)
74
+ end
75
+
76
+ it 'should not expect packets' do
77
+ lambda {
78
+ shell.expect 'Impossible pattern'
79
+ }.should raise_error(RuntimeError)
80
+ end
81
+ end
82
+
83
+ describe 'connected to stubs' do
84
+ let(:socket_stub) do
85
+ RawSocketStub.new([
86
+ [mac, dest_mac, "\x88\xB7", 'Wrong Ethernet type'].join,
87
+ [bcast_mac, dest_mac, [eth_type].pack('n'), 'Wrong dest MAC'].join,
88
+ [mac, bcast_mac, [eth_type].pack('n'), 'Bcast'].join,
89
+ [mac, dest_mac, [eth_type].pack('n'), "\xC0\xDE\xAA\x13\x37"].join,
90
+ ])
91
+ end
92
+ let(:stubbed_shell_socket) do
93
+ socket = EtherShell::HighSocket.new socket_stub, eth_type, mac
94
+ socket.connect dest_mac
95
+ socket
96
+ end
97
+ before { shell.socket stubbed_shell_socket }
98
+
99
+ let(:golden_sends) do
100
+ [
101
+ [dest_mac, mac, [eth_type].pack('n'), "\x13\x37\xAA\xC0\xDE",
102
+ "\0" * 41].join
103
+ ]
104
+ end
105
+
106
+ it 'should send raw packet' do
107
+ shell.out "\x13\x37\xAA\xC0\xDE"
108
+ socket_stub.sends.should == golden_sends
109
+ end
110
+
111
+ it 'should send array packet' do
112
+ shell.out [0x13, 0x37, 0xAA, 0xC0, 0xDE]
113
+ socket_stub.sends.should == golden_sends
114
+ end
115
+
116
+ it 'should send hex packet' do
117
+ shell.out '0x1337AAC0DE'
118
+ socket_stub.sends.should == golden_sends
119
+ end
120
+
121
+ it 'should expect raw packet' do
122
+ lambda {
123
+ shell.expect "\xC0\xDE\xAA\x13\x37"
124
+ }.should_not raise_error
125
+ end
126
+
127
+ it 'should expect array packet' do
128
+ lambda {
129
+ shell.expect [0xC0, 0xDE, 0xAA, 0x13, 0x37]
130
+ }.should_not raise_error
131
+ end
132
+
133
+ it 'should expect hex packet' do
134
+ lambda {
135
+ shell.expect '0xC0DEAA1337'
136
+ }.should_not raise_error
137
+ end
138
+
139
+ it 'should raise on expectation mismatch' do
140
+ lambda {
141
+ shell.expect '0xC0DEAA1338'
142
+ }.should raise_error(EtherShell::ExpectationError)
143
+ end
144
+
145
+ describe 'with verbosity enabled' do
146
+ before do
147
+ shell.verbose
148
+ shell.allow_console
149
+ end
150
+
151
+ it 'should log disconnects' do
152
+ shell.disconnect
153
+ shell.console.should include('Disconnect')
154
+ end
155
+
156
+ it 'should log direct connects' do
157
+ shell.disconnect
158
+ shell.socket stubbed_shell_socket
159
+ shell.console.should include('Connected')
160
+ shell.console.should include('directly to socket')
161
+ end
162
+
163
+ it 'should log socket connects' do
164
+ shell.disconnect
165
+ shell.connect eth_device, eth_type, dest_mac
166
+ shell.console.should include('Connected')
167
+ shell.console.should include(eth_device)
168
+ shell.console.should include(eth_type_hex)
169
+ shell.console.should include(dest_mac_hex)
170
+ end
171
+
172
+ it 'should log packet transmissions' do
173
+ shell.out '0x1337AAC0DE'
174
+ shell.console.should include('Sending')
175
+ shell.console.should include('OK')
176
+ shell.console.should include('1337aac0de')
177
+ end
178
+
179
+ it 'should log successful expectations' do
180
+ shell.expect '0xC0DEAA1337'
181
+ shell.console.should include('Receiving')
182
+ shell.console.should include('OK')
183
+ shell.console.should include('c0deaa1337')
184
+ end
185
+
186
+ it 'should log failed expectations' do
187
+ lambda {
188
+ shell.expect '0xC0DEAA1338'
189
+ }.should raise_error(EtherShell::ExpectationError)
190
+ shell.console.should include('Receiving')
191
+ shell.console.should include('ERROR')
192
+ shell.console.should include('c0deaa1337')
193
+ shell.console.should include('c0deaa1338')
194
+ shell.console.should include('!=')
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "EtherShell" do
4
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'ether_shell'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
@@ -0,0 +1,23 @@
1
+ class RawSocketStub
2
+ def initialize(recv_data)
3
+ @packets = recv_data
4
+ @sends = []
5
+ end
6
+
7
+ def recv(buffer_size)
8
+ raise 'recv called too many times' if @packets.empty?
9
+ @packets.shift
10
+ end
11
+
12
+ def send(data, flags)
13
+ raise 'Weird flags' if flags != 0
14
+ @sends << data
15
+ end
16
+
17
+ def close
18
+ @packets = nil
19
+ @sends = nil
20
+ end
21
+
22
+ attr_reader :sends
23
+ end
@@ -0,0 +1,17 @@
1
+ class ShellStub
2
+ include EtherShell::ShellDsl
3
+
4
+ def initialize()
5
+ @console = nil
6
+ end
7
+ attr_reader :console
8
+
9
+ def print(output)
10
+ raise "Console output not allowed" unless @console
11
+ @console << output
12
+ end
13
+
14
+ def allow_console
15
+ @console ||= ''
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ether_shell
3
+ version: !ruby/object:Gem::Version
4
+ hash: 59
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 9
9
+ - 0
10
+ version: 0.9.0
11
+ platform: ruby
12
+ authors:
13
+ - Victor Costan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-22 00:00:00 -04:00
19
+ default_executable: ether_shell
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ type: :development
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 27
29
+ segments:
30
+ - 2
31
+ - 5
32
+ - 0
33
+ version: 2.5.0
34
+ version_requirements: *id001
35
+ name: rspec
36
+ prerelease: false
37
+ - !ruby/object:Gem::Dependency
38
+ type: :development
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 23
45
+ segments:
46
+ - 1
47
+ - 0
48
+ - 0
49
+ version: 1.0.0
50
+ version_requirements: *id002
51
+ name: bundler
52
+ prerelease: false
53
+ - !ruby/object:Gem::Dependency
54
+ type: :development
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 7
61
+ segments:
62
+ - 1
63
+ - 5
64
+ - 2
65
+ version: 1.5.2
66
+ version_requirements: *id003
67
+ name: jeweler
68
+ prerelease: false
69
+ - !ruby/object:Gem::Dependency
70
+ type: :development
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ version_requirements: *id004
81
+ name: rcov
82
+ prerelease: false
83
+ - !ruby/object:Gem::Dependency
84
+ type: :development
85
+ requirement: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">"
89
+ - !ruby/object:Gem::Version
90
+ hash: 25
91
+ segments:
92
+ - 1
93
+ - 2
94
+ - 3
95
+ version: 1.2.3
96
+ version_requirements: *id005
97
+ name: rspec
98
+ prerelease: false
99
+ description: IRB session specialized for testing Ethernet devices
100
+ email: victor@costan.us
101
+ executables:
102
+ - ether_shell
103
+ extensions: []
104
+
105
+ extra_rdoc_files:
106
+ - LICENSE.txt
107
+ - README.rdoc
108
+ files:
109
+ - .document
110
+ - .project
111
+ - .rspec
112
+ - Gemfile
113
+ - Gemfile.lock
114
+ - LICENSE.txt
115
+ - README.rdoc
116
+ - Rakefile
117
+ - VERSION
118
+ - bin/ether_shell
119
+ - ether_shell.gemspec
120
+ - lib/ether_shell.rb
121
+ - lib/ether_shell/expectation_error.rb
122
+ - lib/ether_shell/high_socket.rb
123
+ - lib/ether_shell/raw_socket.rb
124
+ - lib/ether_shell/shell_dsl.rb
125
+ - spec/ether_shell/high_socket_spec.rb
126
+ - spec/ether_shell/raw_socket_spec.rb
127
+ - spec/ether_shell/shell_dsl_spec.rb
128
+ - spec/ether_shell_spec.rb
129
+ - spec/spec_helper.rb
130
+ - spec/support/raw_socket_stub.rb
131
+ - spec/support/shell_stub.rb
132
+ has_rdoc: true
133
+ homepage: http://github.com/pwnall/ether_shell
134
+ licenses:
135
+ - MIT
136
+ post_install_message:
137
+ rdoc_options: []
138
+
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ hash: 3
147
+ segments:
148
+ - 0
149
+ version: "0"
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ none: false
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ hash: 3
156
+ segments:
157
+ - 0
158
+ version: "0"
159
+ requirements: []
160
+
161
+ rubyforge_project:
162
+ rubygems_version: 1.6.0
163
+ signing_key:
164
+ specification_version: 3
165
+ summary: IRB session specialized for testing Ethernet devices
166
+ test_files:
167
+ - spec/ether_shell/high_socket_spec.rb
168
+ - spec/ether_shell/raw_socket_spec.rb
169
+ - spec/ether_shell/shell_dsl_spec.rb
170
+ - spec/ether_shell_spec.rb
171
+ - spec/spec_helper.rb
172
+ - spec/support/raw_socket_stub.rb
173
+ - spec/support/shell_stub.rb