packetfu 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. data/.gitignore +3 -0
  2. data/INSTALL.rdoc +40 -0
  3. data/LICENSE.txt +25 -0
  4. data/examples/100kpackets.rb +41 -0
  5. data/examples/ackscan.rb +38 -0
  6. data/examples/arp.rb +60 -0
  7. data/examples/arphood.rb +59 -0
  8. data/examples/dissect_thinger.rb +22 -0
  9. data/examples/ethernet.rb +10 -0
  10. data/examples/examples.rb +3 -0
  11. data/examples/ids.rb +4 -0
  12. data/examples/idsv2.rb +6 -0
  13. data/examples/new-simple-stats.rb +52 -0
  14. data/examples/oui.txt +84177 -0
  15. data/examples/packetfu-shell.rb +113 -0
  16. data/examples/simple-sniffer.rb +40 -0
  17. data/examples/simple-stats.rb +50 -0
  18. data/examples/slammer.rb +33 -0
  19. data/examples/uniqpcap.rb +15 -0
  20. data/lib/packetfu.rb +147 -0
  21. data/lib/packetfu/capture.rb +169 -0
  22. data/lib/packetfu/config.rb +58 -0
  23. data/lib/packetfu/inject.rb +65 -0
  24. data/lib/packetfu/packet.rb +533 -0
  25. data/lib/packetfu/pcap.rb +594 -0
  26. data/lib/packetfu/protos/arp.rb +268 -0
  27. data/lib/packetfu/protos/eth.rb +296 -0
  28. data/lib/packetfu/protos/hsrp.rb +206 -0
  29. data/lib/packetfu/protos/icmp.rb +179 -0
  30. data/lib/packetfu/protos/invalid.rb +55 -0
  31. data/lib/packetfu/protos/ip.rb +378 -0
  32. data/lib/packetfu/protos/ipv6.rb +250 -0
  33. data/lib/packetfu/protos/tcp.rb +1127 -0
  34. data/lib/packetfu/protos/udp.rb +240 -0
  35. data/lib/packetfu/structfu.rb +294 -0
  36. data/lib/packetfu/utils.rb +194 -0
  37. data/lib/packetfu/version.rb +50 -0
  38. data/packetfu.gemspec +21 -0
  39. data/setup.rb +1586 -0
  40. data/test/all_tests.rb +41 -0
  41. data/test/ethpacket_spec.rb +74 -0
  42. data/test/packet_spec.rb +73 -0
  43. data/test/packet_subclasses_spec.rb +13 -0
  44. data/test/packetfu_spec.rb +90 -0
  45. data/test/ptest.rb +16 -0
  46. data/test/sample-ipv6.pcap +0 -0
  47. data/test/sample.pcap +0 -0
  48. data/test/sample2.pcap +0 -0
  49. data/test/sample_hsrp_pcapr.cap +0 -0
  50. data/test/structfu_spec.rb +335 -0
  51. data/test/tcp_spec.rb +101 -0
  52. data/test/test_arp.rb +135 -0
  53. data/test/test_eth.rb +91 -0
  54. data/test/test_hsrp.rb +20 -0
  55. data/test/test_icmp.rb +54 -0
  56. data/test/test_inject.rb +31 -0
  57. data/test/test_invalid.rb +28 -0
  58. data/test/test_ip.rb +69 -0
  59. data/test/test_ip6.rb +68 -0
  60. data/test/test_octets.rb +37 -0
  61. data/test/test_packet.rb +174 -0
  62. data/test/test_pcap.rb +209 -0
  63. data/test/test_structfu.rb +112 -0
  64. data/test/test_tcp.rb +327 -0
  65. data/test/test_udp.rb +73 -0
  66. data/test/vlan-pcapr.cap +0 -0
  67. metadata +85 -6
data/test/tcp_spec.rb ADDED
@@ -0,0 +1,101 @@
1
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
2
+ require 'packetfu'
3
+
4
+ include PacketFu
5
+
6
+ def unusual_numeric_handling_headers(header,i)
7
+ camelized_header = header.to_s.split("_").map {|x| x.capitalize}.join
8
+ header_class = PacketFu.const_get camelized_header
9
+ specify { subject.send(header).should == i }
10
+ specify { subject.send(header).should be_kind_of Integer }
11
+ specify { subject.headers.last[header].should be_kind_of header_class }
12
+ end
13
+
14
+ def tcp_hlen_numeric(i)
15
+ unusual_numeric_handling_headers(:tcp_hlen,i)
16
+ end
17
+
18
+ def tcp_reserved_numeric(i)
19
+ unusual_numeric_handling_headers(:tcp_reserved,i)
20
+ end
21
+
22
+ def tcp_ecn_numeric(i)
23
+ unusual_numeric_handling_headers(:tcp_ecn,i)
24
+ end
25
+
26
+
27
+ describe TCPPacket do
28
+
29
+ subject do
30
+ bytes = PcapFile.file_to_array("sample2.pcap")[2]
31
+ packet = Packet.parse(bytes)
32
+ end
33
+
34
+ context "TcpHlen reading and setting" do
35
+ context "TcpHlen set via #read" do
36
+ tcp_hlen_numeric(8)
37
+ end
38
+ context "TcpHlen set via an Integer for the setter" do
39
+ (0..15).each do |i|
40
+ context "i is #{i}" do
41
+ before { subject.tcp_hlen = i }
42
+ tcp_hlen_numeric(i)
43
+ end
44
+ end
45
+ end
46
+ context "TcpHlen set via a String for the setter" do
47
+ before { subject.tcp_hlen = "\x60" }
48
+ tcp_hlen_numeric(6)
49
+ end
50
+ context "TcpHlen set via a TcpHlen for the setter" do
51
+ before { subject.tcp_hlen = TcpHlen.new(:hlen => 7) }
52
+ tcp_hlen_numeric(7)
53
+ end
54
+ end
55
+
56
+ context "TcpReserved reading and setting" do
57
+ context "TcpReserved set via #read" do
58
+ tcp_reserved_numeric(0)
59
+ end
60
+ context "TcpReserved set via an Integer for the setter" do
61
+ (0..7).each do |i|
62
+ context "i is #{i}" do
63
+ before { subject.tcp_reserved = i }
64
+ tcp_reserved_numeric(i)
65
+ end
66
+ end
67
+ end
68
+ context "TcpReserved set via a String for the setter" do
69
+ before { subject.tcp_reserved = "\x03" }
70
+ tcp_reserved_numeric(3)
71
+ end
72
+ context "TcpReserved set via a TcpReserved for the setter" do
73
+ before { subject.tcp_reserved = TcpReserved.new(:r1 => 1, :r2 => 0, :r3 => 1) }
74
+ tcp_reserved_numeric(5)
75
+ end
76
+ end
77
+
78
+ context "TcpEcn reading and setting" do
79
+ context "TcpEcn set via #read" do
80
+ tcp_ecn_numeric(0)
81
+ end
82
+ context "TcpEcn set via an Integer for the setter" do
83
+ (0..7).each do |i|
84
+ context "i is #{i}" do
85
+ before { subject.tcp_ecn = i }
86
+ tcp_ecn_numeric(i)
87
+ end
88
+ end
89
+ end
90
+ context "TcpEcn set via a String for the setter" do
91
+ before { subject.tcp_ecn = "\x00\xc0" }
92
+ tcp_ecn_numeric(3)
93
+ end
94
+ context "TcpEcn set via a TcpEcn for the setter" do
95
+ before { subject.tcp_ecn = TcpEcn.new(:n => 1, :c => 0, :e => 1) }
96
+ tcp_ecn_numeric(5)
97
+ end
98
+ end
99
+
100
+ end
101
+
data/test/test_arp.rb ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
4
+ require 'packetfu'
5
+ class ArpTest < Test::Unit::TestCase
6
+ include PacketFu
7
+
8
+ def test_arp_header
9
+ a = ARPHeader.new
10
+ assert_kind_of ARPHeader, a
11
+ assert_kind_of StructFu::Int16, a[:arp_hw]
12
+ assert_kind_of Fixnum, a.arp_hw
13
+ assert_kind_of Octets, a[:arp_src_ip]
14
+ assert_kind_of String, a.arp_src_ip
15
+ assert_kind_of EthMac, a[:arp_dst_mac]
16
+ assert_kind_of String, a.arp_dst_mac
17
+ assert_kind_of StructFu::String, a.body
18
+ end
19
+
20
+ def test_read_header
21
+ a = ARPHeader.new
22
+ sample_arp = "000108000604000200032f1a74dec0a80102001b1151b7cec0a80169"
23
+ sample_arp = sample_arp.scan(/../).map {|x| x.to_i(16)}.pack("C*")
24
+ a.read(sample_arp)
25
+ assert_equal(sample_arp, a.to_s)
26
+ assert_equal("192.168.1.105", a.arp_daddr_ip)
27
+ assert_equal("192.168.1.2", a.arp_saddr_ip)
28
+ assert_equal("00:1b:11:51:b7:ce", a.arp_daddr_mac)
29
+ assert_equal("00:03:2f:1a:74:de", a.arp_saddr_mac)
30
+ end
31
+
32
+ def test_arp_read
33
+ a = ARPPacket.new
34
+ sample_arp = "001b1151b7ce00032f1a74de0806000108000604000200032f1a74dec0a80102001b1151b7cec0a80169c0a80169"
35
+ sample_arp = sample_arp.scan(/../).map {|x| x.to_i(16)}.pack("C*")
36
+ a.read(sample_arp)
37
+ assert_equal(sample_arp, a.to_s)
38
+ end
39
+
40
+ def test_write_ip
41
+ a = ARPPacket.new
42
+ a.arp_saddr_ip="1.2.3.4"
43
+ a.arp_daddr_ip="5.6.7.8"
44
+ assert_equal("1.2.3.4",a.arp_saddr_ip)
45
+ assert_equal("5.6.7.8",a.arp_daddr_ip)
46
+ assert_equal("\x01\x02\x03\x04",a.arp_src_ip)
47
+ assert_equal("\x05\x06\x07\x08",a.arp_dst_ip)
48
+ end
49
+
50
+ def test_write_mac
51
+ a = ARPPacket.new
52
+ a.arp_saddr_mac = "00:01:02:03:04:05"
53
+ a.arp_daddr_mac = "00:06:07:08:09:0a"
54
+ assert_equal("00:01:02:03:04:05",a.arp_saddr_mac)
55
+ assert_equal("00:06:07:08:09:0a",a.arp_daddr_mac)
56
+ assert_equal("\x00\x01\x02\x03\x04\x05",a.arp_src_mac)
57
+ assert_equal("\x00\x06\x07\x08\x09\x0a",a.arp_dst_mac)
58
+ end
59
+
60
+ def test_arp_flavors
61
+ a = ARPPacket.new(:flavor => "Windows")
62
+ assert_equal("\x00" * 64, a.payload)
63
+ a = ARPPacket.new(:flavor => "Linux")
64
+ assert_equal(32, a.payload.size)
65
+ a = ARPPacket.new(:flavor => :hp_deskjet)
66
+ assert_equal(18, a.payload.size)
67
+ a = ARPPacket.new
68
+ assert_equal("\x00" * 18, a.payload)
69
+ end
70
+
71
+ def test_arp_create
72
+ sample_arp = "000108000604000200032f1a74dec0a80102001b1151b7cec0a80169"
73
+ sample_arp = sample_arp.scan(/../).map {|x| x.to_i(16)}.pack("C*")
74
+ a = ARPPacket.new
75
+ assert_kind_of ARPPacket, a
76
+ a.arp_hw = 1
77
+ a.arp_proto = 0x0800
78
+ a.arp_hw_len = 6
79
+ a.arp_proto_len = 4
80
+ a.arp_opcode = 2
81
+ a.arp_src_mac = "\x00\x03\x2f\x1a\x74\xde"
82
+ a.arp_src_ip = "\xc0\xa8\x01\x02"
83
+ a.arp_dst_mac = "\x00\x1b\x11\x51\xb7\xce"
84
+ a.arp_dst_ip = "\xc0\xa8\x01\x69"
85
+ a.payload = ""
86
+ assert_equal(sample_arp,a.to_s[14,0xffff])
87
+ end
88
+
89
+ def test_arp_new
90
+ sample_arp = "000108000604000200032f1a74dec0a80102001b1151b7cec0a80169c0a80169"
91
+ sample_arp = sample_arp.scan(/../).map {|x| x.to_i(16)}.pack("C*")
92
+ arp = ARPPacket.new(:arp_hw => 1, :arp_proto => 0x0800,
93
+ :arp_opcode => 2, :arp_src_ip => "\xc0\xa8\x01\x02")
94
+ assert_kind_of ARPPacket, arp
95
+ arp.arp_hw_len = 6
96
+ arp.arp_proto_len = 4
97
+ arp.arp_src_mac = "\x00\x03\x2f\x1a\x74\xde"
98
+ arp.arp_dst_mac = "\x00\x1b\x11\x51\xb7\xce"
99
+ arp.arp_dst_ip = "\xc0\xa8\x01\x69"
100
+ arp.payload = "\xc0\xa8\x01\x69"
101
+ assert_equal(sample_arp,arp.to_s[14,0xffff])
102
+ end
103
+
104
+ def test_arp_peek
105
+ a = ARPPacket.new
106
+ puts "\n"
107
+ puts "ARP Peek format: "
108
+ puts a.peek
109
+ puts "\n"
110
+ assert(a.peek.size <= 80)
111
+ end
112
+
113
+ def test_arp_pcap
114
+ a = ARPPacket.new
115
+ assert_kind_of ARPPacket, a
116
+ a.to_f('arp_test.pcap','w')
117
+ a.arp_hw = 1
118
+ a.arp_proto = 0x0800
119
+ a.arp_hw_len = 6
120
+ a.arp_proto_len = 4
121
+ a.arp_opcode = 2
122
+ a.arp_src_mac = "\x00\x03\x2f\x1a\x74\xde"
123
+ a.arp_src_ip = "\xc0\xa8\x01\x02"
124
+ a.arp_dst_mac = "\x00\x1b\x11\x51\xb7\xce"
125
+ a.arp_dst_ip = "\xc0\xa8\x01\x69"
126
+ a.payload = ""
127
+ a.eth_daddr = "00:1b:11:51:b7:ce"
128
+ a.eth_saddr = "00:03:2f:1a:74:de"
129
+ a.to_f('arp_test.pcap','a')
130
+ end
131
+
132
+ end
133
+
134
+
135
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
data/test/test_eth.rb ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
4
+ require 'packetfu'
5
+ puts "Testing #{PacketFu.version}: #{$0}"
6
+
7
+ class EthTest < Test::Unit::TestCase
8
+
9
+ def test_ethmac
10
+ dst = "\x00\x03\x2f\x1a\x74\xde"
11
+ e = PacketFu::EthMac.new
12
+ e.read dst
13
+ assert_equal(dst, e.to_s)
14
+ assert_equal(0x32f, e.oui.oui)
15
+ assert_equal("\x1a\x74\xde", e.nic.to_s)
16
+ assert_equal(222, e.nic.n2)
17
+ end
18
+
19
+ def test_ethmac_ipad
20
+ dst = "\x7c\x6d\x62\x01\x02\x03"
21
+ e = PacketFu::EthMac.new
22
+ e.read dst
23
+ assert_equal(dst, e.to_s)
24
+ assert_equal(0x6d62, e.oui.oui)
25
+ end
26
+
27
+ def test_ethmac_class
28
+ src = "\x00\x1b\x11\x51\xb7\xce"
29
+ e = PacketFu::EthMac.new
30
+ e.read src
31
+ assert_instance_of(PacketFu::EthMac, e)
32
+ end
33
+
34
+ def test_eth
35
+ header = "00032f1a74de001b1151b7ce0800".scan(/../).map { |x| x.to_i(16) }.pack("C*")
36
+ src = "\x00\x1b\x11\x51\xb7\xce"
37
+ dst = "\x00\x03\x2f\x1a\x74\xde"
38
+ e = PacketFu::EthHeader.new
39
+ e.eth_dst = dst
40
+ e.eth_src = src
41
+ e.eth_proto = "\x08\x00"
42
+ assert_equal(header, e.to_s)
43
+ assert_equal(header, PacketFu::EthHeader.new.read(header).to_s)
44
+ end
45
+
46
+ def test_macaddr
47
+ dst = "\x00\x03\x2f\x1a\x74\xde"
48
+ dstmac = "00:03:2f:1a:74:de"
49
+ assert_equal(dstmac,PacketFu::EthHeader.str2mac(dst))
50
+ assert_equal(dst, PacketFu::EthHeader.mac2str(dstmac))
51
+ end
52
+
53
+ end
54
+
55
+ class EthPacketTest < Test::Unit::TestCase
56
+ include PacketFu
57
+
58
+ def test_eth_create
59
+ sample_packet = PcapFile.new.file_to_array(:f => 'sample.pcap')[0]
60
+ e = EthPacket.new
61
+ header = "00032f1a74de001b1151b7ce0800".scan(/../).map { |x| x.to_i(16) }.pack("C*")
62
+ assert_kind_of EthPacket, e
63
+ assert_kind_of EthHeader, e.headers[0]
64
+ assert e.is_eth?
65
+ assert !e.is_tcp?
66
+ e.eth_dst = "\x00\x03\x2f\x1a\x74\xde"
67
+ e.eth_src = "\x00\x1b\x11\x51\xb7\xce"
68
+ e.eth_proto = 0x0800
69
+ assert_equal header, e.to_s[0,14]
70
+ end
71
+
72
+ def test_eth_new
73
+ p = EthPacket.new(
74
+ :eth_dst => "\x00\x03\x2f\x1a\x74\xde",
75
+ :eth_src => "\x00\x1b\x11\x51\xb7\xce",
76
+ :eth_proto => 0x0800)
77
+ header = "00032f1a74de001b1151b7ce0800".scan(/../).map { |x| x.to_i(16) }.pack("C*")
78
+ assert_equal header, p.to_s[0,14]
79
+ end
80
+
81
+ def test_eth_write
82
+ p = EthPacket.new(
83
+ :eth_dst => "\x00\x03\x2f\x1a\x74\xde",
84
+ :eth_src => "\x00\x1b\x11\x51\xb7\xce",
85
+ :eth_proto => 0x0800)
86
+ p.to_f('eth_test.pcap')
87
+ end
88
+
89
+ end
90
+
91
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
data/test/test_hsrp.rb ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
4
+ require 'packetfu'
5
+
6
+ class HSRPTest < Test::Unit::TestCase
7
+ include PacketFu
8
+
9
+ def test_hsrp_read
10
+ sample_packet = PcapFile.new.file_to_array(:f => 'sample_hsrp_pcapr.cap')[0]
11
+ pkt = Packet.parse(sample_packet)
12
+ assert pkt.is_hsrp?
13
+ assert pkt.is_udp?
14
+ assert_equal(0x2d8d, pkt.udp_sum.to_i)
15
+ # pkt.to_f('udp_test.pcap','a')
16
+ end
17
+
18
+ end
19
+
20
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
data/test/test_icmp.rb ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
4
+ require 'packetfu'
5
+
6
+ class ICMPTest < Test::Unit::TestCase
7
+ include PacketFu
8
+
9
+ def test_icmp_header_new
10
+ i = ICMPHeader.new
11
+ assert_kind_of ICMPHeader, i
12
+ assert_equal("\x00\x00\xff\xff", i.to_s)
13
+ i.icmp_type = 1
14
+ i.icmp_recalc :icmp_sum
15
+ assert_equal("\x01\x00\xfe\xff", i.to_s)
16
+ end
17
+
18
+ def test_icmp_peek
19
+ i = ICMPPacket.new
20
+ i.ip_saddr = "10.20.30.40"
21
+ i.ip_daddr = "50.60.70.80"
22
+ i.payload = "abcdefghijklmnopqrstuvwxyz"
23
+ i.recalc
24
+ puts "\n"
25
+ puts "ICMP Peek format: "
26
+ puts i.peek
27
+ assert (i.peek.size <= 80)
28
+ end
29
+
30
+ def test_icmp_pcap
31
+ i = ICMPPacket.new
32
+ assert_kind_of ICMPPacket, i
33
+ i.recalc
34
+ i.to_f('icmp_test.pcap')
35
+ i.ip_saddr = "10.20.30.40"
36
+ i.ip_daddr = "50.60.70.80"
37
+ i.payload = "\x00\x01\x00\01abcdefghijklmnopqrstuvwxyz"
38
+ i.icmp_code = 8
39
+ i.recalc
40
+ i.to_f('icmp_test.pcap','a')
41
+ assert File.exists?('icmp_test.pcap')
42
+ end
43
+
44
+ def test_icmp_read
45
+ sample_packet = PcapFile.new.file_to_array(:f => 'sample.pcap')[2]
46
+ pkt = Packet.parse(sample_packet)
47
+ assert_kind_of ICMPPacket, pkt
48
+ assert_equal(0x4d58, pkt.icmp_sum.to_i)
49
+ assert_equal(8, pkt.icmp_type.to_i)
50
+ end
51
+
52
+ end
53
+
54
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
4
+ require 'packetfu'
5
+
6
+
7
+ class InjectTest < Test::Unit::TestCase
8
+
9
+ def test_cap
10
+ assert_nothing_raised { PacketFu::Capture }
11
+ end
12
+
13
+ def test_whoami
14
+ assert_nothing_raised { PacketFu::Utils.whoami?(:iface => (ENV['IFACE'] || 'lo')) }
15
+ end
16
+
17
+ def test_to_w
18
+ assert_equal(Process.euid, 0, "TEST FAIL: This test must be run as root")
19
+ conf = PacketFu::Utils.whoami?(:iface => (ENV['IFACE'] || 'lo'))
20
+ p = PacketFu::UDPPacket.new(:config => conf)
21
+ p.udp_dport = 12345
22
+ p.udp_sport = 12345
23
+ p.payload = "PacketFu test packet"
24
+ p.recalc
25
+ assert p.to_w
26
+ end
27
+
28
+ end
29
+
30
+
31
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), "..", "lib")
4
+ require 'packetfu'
5
+
6
+ class InvalidTest < Test::Unit::TestCase
7
+ include PacketFu
8
+
9
+ def test_create_invalid
10
+ p = InvalidPacket.new
11
+ assert_kind_of InvalidPacket, p
12
+ assert_kind_of Packet, p
13
+ assert p.is_invalid?
14
+ assert_equal false, p.is_eth?
15
+ assert_not_equal EthPacket, p.class
16
+ end
17
+
18
+ # Sadly, the only way to generate an "InvalidPacket" is
19
+ # to read a packet that's less than 14 bytes. Otherwise,
20
+ # it's presumed to be an EthPacket. TODO: Fix this assumption!
21
+ def test_parse_invalid
22
+ p = Packet.parse("A" * 13)
23
+ assert_kind_of InvalidPacket, p
24
+ end
25
+
26
+ end
27
+
28
+ # vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby