ffi-pcap 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/ChangeLog.rdoc +27 -0
- data/LICENSE.txt +23 -0
- data/README.rdoc +30 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/examples/ipfw_divert.rb +49 -0
- data/examples/print_bytes.rb +17 -0
- data/lib/ffi-pcap.rb +1 -0
- data/lib/ffi/pcap.rb +42 -0
- data/lib/ffi/pcap/addr.rb +21 -0
- data/lib/ffi/pcap/bpf.rb +106 -0
- data/lib/ffi/pcap/bsd.rb +98 -0
- data/lib/ffi/pcap/capture_wrapper.rb +289 -0
- data/lib/ffi/pcap/common_wrapper.rb +175 -0
- data/lib/ffi/pcap/copy_handler.rb +38 -0
- data/lib/ffi/pcap/crt.rb +15 -0
- data/lib/ffi/pcap/data_link.rb +173 -0
- data/lib/ffi/pcap/dead.rb +37 -0
- data/lib/ffi/pcap/dumper.rb +55 -0
- data/lib/ffi/pcap/error_buffer.rb +44 -0
- data/lib/ffi/pcap/exceptions.rb +21 -0
- data/lib/ffi/pcap/file_header.rb +26 -0
- data/lib/ffi/pcap/in_addr.rb +9 -0
- data/lib/ffi/pcap/interface.rb +29 -0
- data/lib/ffi/pcap/live.rb +303 -0
- data/lib/ffi/pcap/offline.rb +53 -0
- data/lib/ffi/pcap/packet.rb +164 -0
- data/lib/ffi/pcap/packet_header.rb +24 -0
- data/lib/ffi/pcap/pcap.rb +252 -0
- data/lib/ffi/pcap/stat.rb +57 -0
- data/lib/ffi/pcap/time_val.rb +48 -0
- data/lib/ffi/pcap/typedefs.rb +27 -0
- data/lib/ffi/pcap/version.rb +6 -0
- data/spec/data_link_spec.rb +65 -0
- data/spec/dead_spec.rb +34 -0
- data/spec/dumps/http.pcap +0 -0
- data/spec/dumps/simple_tcp.pcap +0 -0
- data/spec/error_buffer_spec.rb +17 -0
- data/spec/file_header_spec.rb +28 -0
- data/spec/live_spec.rb +87 -0
- data/spec/offline_spec.rb +61 -0
- data/spec/packet_behaviors.rb +68 -0
- data/spec/packet_injection_spec.rb +38 -0
- data/spec/packet_spec.rb +111 -0
- data/spec/pcap_spec.rb +149 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/wrapper_behaviors.rb +124 -0
- data/tasks/rcov.rb +6 -0
- data/tasks/rdoc.rb +17 -0
- data/tasks/spec.rb +9 -0
- data/tasks/yard.rb +21 -0
- metadata +157 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
# Generic per-packet information, as supplied by libpcap. This structure
|
4
|
+
# is used to track only the libpcap header and just contains the timestamp
|
5
|
+
# and length information used by libpcap.
|
6
|
+
#
|
7
|
+
# See pcap_pkthdr struct in pcap.h
|
8
|
+
class PacketHeader < FFI::Struct
|
9
|
+
include FFI::DRY::StructHelper
|
10
|
+
|
11
|
+
dsl_layout do
|
12
|
+
struct :ts, ::FFI::PCap::TimeVal, :desc => 'time stamp'
|
13
|
+
field :caplen, :bpf_uint32, :desc => 'length of portion present'
|
14
|
+
field :len, :bpf_uint32, :desc => 'length of packet (off wire)'
|
15
|
+
end
|
16
|
+
|
17
|
+
alias timestamp ts
|
18
|
+
alias captured caplen
|
19
|
+
alias length len
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module PCap
|
5
|
+
DEFAULT_SNAPLEN = 65535 # Default snapshot length for packets
|
6
|
+
|
7
|
+
attach_function :pcap_lookupdev, [:pointer], :string
|
8
|
+
|
9
|
+
# Find the default device on which to capture.
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
# Name of default device
|
13
|
+
#
|
14
|
+
# @raise [LibError]
|
15
|
+
# On failure, an exception is raised with the relevant error
|
16
|
+
# message from libpcap.
|
17
|
+
#
|
18
|
+
def PCap.lookupdev
|
19
|
+
e = ErrorBuffer.create()
|
20
|
+
unless name = FFI::PCap.pcap_lookupdev(e)
|
21
|
+
raise(LibError, "pcap_lookupdev(): #{e.to_s}")
|
22
|
+
end
|
23
|
+
return name
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
attach_function :pcap_lookupnet, [:string, :pointer, :pointer, :pointer], :int
|
28
|
+
|
29
|
+
# Determine the IPv4 network number and mask relevant with a network
|
30
|
+
# device.
|
31
|
+
#
|
32
|
+
# @param [String] device
|
33
|
+
# The name of the device to look up.
|
34
|
+
#
|
35
|
+
# @yield [netp, maskp]
|
36
|
+
#
|
37
|
+
# @yieldparam [FFI::MemoryPointer] netp
|
38
|
+
# A pointer to the network return value.
|
39
|
+
#
|
40
|
+
# @yieldparam [FFI::MemoryPointer] maskp
|
41
|
+
# A pointer to the netmask return value.
|
42
|
+
#
|
43
|
+
# @return [nil, String]
|
44
|
+
# The IPv4 network number and mask presented as "n.n.n.n/m.m.m.m".
|
45
|
+
# nil is returned when a block is specified.
|
46
|
+
#
|
47
|
+
# @raise [LibError]
|
48
|
+
# On failure, an exception is raised with the relevant error message
|
49
|
+
# from libpcap.
|
50
|
+
#
|
51
|
+
def PCap.lookupnet(device)
|
52
|
+
netp = FFI::MemoryPointer.new(find_type(:bpf_uint32))
|
53
|
+
maskp = FFI::MemoryPointer.new(find_type(:bpf_uint32))
|
54
|
+
errbuf = ErrorBuffer.create()
|
55
|
+
unless FFI::PCap.pcap_lookupnet(device, netp, maskp, errbuf) == 0
|
56
|
+
raise(LibError, "pcap_lookupnet(): #{errbuf.to_s}")
|
57
|
+
end
|
58
|
+
if block_given?
|
59
|
+
yield(netp, maskp)
|
60
|
+
else
|
61
|
+
return( netp.get_array_of_uchar(0,4).join('.') << "/" <<
|
62
|
+
maskp.get_array_of_uchar(0,4).join('.') )
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Opens a new Live device for capturing from the network. See Live.new()
|
68
|
+
# for arguments.
|
69
|
+
#
|
70
|
+
# If passed a block, the block is passed to Live.new() and the Live
|
71
|
+
# object is closed after completion of the block
|
72
|
+
def PCap.open_live(opts={},&block)
|
73
|
+
ret = Live.new(opts, &block)
|
74
|
+
return block_given? ? ret.close : ret
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Opens a new Dead pcap interface for compiling filters or opening
|
79
|
+
# a capture for output. See Dead.new() for arguments.
|
80
|
+
def PCap.open_dead(opts={}, &block)
|
81
|
+
ret = Dead.new(opts, &block)
|
82
|
+
return block_given? ? ret.close : ret
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Opens a saved capture file for reading. See Offline.new for arguments.
|
87
|
+
def PCap.open_offline(path, opts={}, &block)
|
88
|
+
ret = Offline.new(path, opts={}, &block)
|
89
|
+
return block_given? ? ret.close : ret
|
90
|
+
end
|
91
|
+
|
92
|
+
# Same as open_offline
|
93
|
+
def PCap.open_file(path, opts={}, &block)
|
94
|
+
open_offline(path, opts, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
attach_function :pcap_findalldevs, [:pointer, :pointer], :int
|
98
|
+
attach_function :pcap_freealldevs, [Interface], :void
|
99
|
+
|
100
|
+
# List all capture devices and yield them each to a block
|
101
|
+
#
|
102
|
+
# @yield [dev]
|
103
|
+
#
|
104
|
+
# @yieldparam [Interface] dev
|
105
|
+
# An Interface structure for each device.
|
106
|
+
#
|
107
|
+
# @return [nil]
|
108
|
+
#
|
109
|
+
# @raise [LibError]
|
110
|
+
# On failure, an exception is raised with the relevant error
|
111
|
+
# message from libpcap.
|
112
|
+
#
|
113
|
+
def PCap.each_device
|
114
|
+
devices = ::FFI::MemoryPointer.new(:pointer)
|
115
|
+
errbuf = ErrorBuffer.create()
|
116
|
+
|
117
|
+
FFI::PCap.pcap_findalldevs(devices, errbuf)
|
118
|
+
node = devices.get_pointer(0)
|
119
|
+
|
120
|
+
if node.null?
|
121
|
+
raise(LibError, "pcap_findalldevs(): #{errbuf.to_s}")
|
122
|
+
end
|
123
|
+
|
124
|
+
device = Interface.new(node)
|
125
|
+
|
126
|
+
while device
|
127
|
+
yield(device)
|
128
|
+
device = device.next
|
129
|
+
end
|
130
|
+
|
131
|
+
FFI::PCap.pcap_freealldevs(node)
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Returns an array of device name and network/netmask pairs for
|
137
|
+
# each interface found on the system.
|
138
|
+
#
|
139
|
+
# If an interface does not have an address assigned, its network/netmask
|
140
|
+
# value is returned as a nil value.
|
141
|
+
def PCap.dump_devices
|
142
|
+
FFI::PCap.enum_for(:each_device).map do |dev|
|
143
|
+
net = begin; FFI::PCap.lookupnet(dev.name); rescue LibError; end
|
144
|
+
[dev.name, net]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Returns an array of device names for each interface found on the system.
|
150
|
+
def PCap.device_names
|
151
|
+
FFI::PCap.enum_for(:each_device).map {|dev| dev.name }
|
152
|
+
end
|
153
|
+
|
154
|
+
attach_function :pcap_lib_version, [], :string
|
155
|
+
|
156
|
+
|
157
|
+
# Get the version information for libpcap.
|
158
|
+
#
|
159
|
+
# @return [String]
|
160
|
+
# Information about the version of the libpcap library being used;
|
161
|
+
# note that it contains more information than just a version number.
|
162
|
+
#
|
163
|
+
def PCap.lib_version
|
164
|
+
FFI::PCap.pcap_lib_version
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# Extract just the version number from the lib_version string.
|
169
|
+
#
|
170
|
+
# @return [String]
|
171
|
+
# Version number.
|
172
|
+
#
|
173
|
+
def PCap.lib_version_number
|
174
|
+
if lib_version() =~ /libpcap version (\d+\.\d+.\d+)/
|
175
|
+
return $1
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# Unix Only:
|
181
|
+
begin
|
182
|
+
|
183
|
+
attach_function :pcap_get_selectable_fd, [:pcap_t], :int
|
184
|
+
|
185
|
+
|
186
|
+
# Drops privileges back to the uid of the SUDO_USER environment
|
187
|
+
# variable.
|
188
|
+
#
|
189
|
+
# Only available on Unix.
|
190
|
+
#
|
191
|
+
# This is useful for the paranoid when sudo is used to run a
|
192
|
+
# ruby pcap program as root.
|
193
|
+
#
|
194
|
+
# This method can generally be called right after a call to
|
195
|
+
# open_live() has returned a pcap handle or another privileged
|
196
|
+
# call has completed. Note, however, that once privileges are
|
197
|
+
# dropped, pcap functions that a require higher privilege will
|
198
|
+
# no longer work.
|
199
|
+
#
|
200
|
+
# @raise [StandardError]
|
201
|
+
# An error is raised if privileges cannot be dropped for
|
202
|
+
# some reason. This may be because the SUDO_USER environment
|
203
|
+
# variable is not set, because we already have a lower
|
204
|
+
# privilige and the SUDO_USER id is not the current uid,
|
205
|
+
# or because the SUDO_USER environment variable is not
|
206
|
+
# a valid user.
|
207
|
+
#
|
208
|
+
def PCap.drop_sudo_privs
|
209
|
+
if ENV["SUDO_USER"] and pwent=Etc.getpwnam(ENV["SUDO_USER"])
|
210
|
+
Process::Sys.setgid(pwent.gid)
|
211
|
+
Process::Sys.setegid(pwent.gid)
|
212
|
+
Process::Sys.setuid(pwent.uid)
|
213
|
+
Process::Sys.seteuid(pwent.uid)
|
214
|
+
return true if (
|
215
|
+
Process::Sys.getuid == pwent.uid and
|
216
|
+
Process::Sys.geteuid == pwent.uid and
|
217
|
+
Process::Sys.getgid == pwent.gid and
|
218
|
+
Process::Sys.getegid == pwent.gid )
|
219
|
+
end
|
220
|
+
raise(StandardError, "Unable to drop privileges")
|
221
|
+
end
|
222
|
+
|
223
|
+
rescue FFI::NotFoundError
|
224
|
+
$pcap_not_unix=true
|
225
|
+
end
|
226
|
+
|
227
|
+
# Win32 only:
|
228
|
+
begin
|
229
|
+
attach_function :pcap_setbuff, [:pcap_t, :int], :int
|
230
|
+
attach_function :pcap_setmode, [:pcap_t, :pcap_w32_modes_enum], :int
|
231
|
+
attach_function :pcap_setmintocopy, [:pcap_t, :int], :int
|
232
|
+
rescue FFI::NotFoundError
|
233
|
+
$pcap_not_win32=true
|
234
|
+
end if $pcap_not_unix
|
235
|
+
|
236
|
+
# MSDOS only???:
|
237
|
+
begin
|
238
|
+
attach_function :pcap_stats_ex, [:pcap_t, StatEx], :int
|
239
|
+
attach_function :pcap_set_wait, [:pcap_t, :pointer, :int], :void
|
240
|
+
attach_function :pcap_mac_packets, [], :ulong
|
241
|
+
rescue FFI::NotFoundError
|
242
|
+
end if $pcap_not_win32
|
243
|
+
|
244
|
+
|
245
|
+
#### XXX not sure if we even want FILE io stuff yet (or ever).
|
246
|
+
|
247
|
+
#attach_function :pcap_fopen_offline, [:FILE, :pointer], :pcap_t
|
248
|
+
#attach_function :pcap_file, [:pcap_t], :FILE
|
249
|
+
#attach_function :pcap_dump_fopen, [:pcap_t, :FILE], :pcap_dumper_t
|
250
|
+
#attach_function :pcap_fileno, [:pcap_t], :int
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
# As returned by pcap_stats()
|
4
|
+
#
|
5
|
+
# See pcap_stat struct in pcap.h.
|
6
|
+
class Stat < FFI::Struct
|
7
|
+
include FFI::DRY::StructHelper
|
8
|
+
|
9
|
+
dsl_layout do
|
10
|
+
field :ps_recv, :uint, :desc => "number of packets received"
|
11
|
+
field :ps_drop, :uint, :desc => "numer of packets dropped"
|
12
|
+
field :ps_ifdrop, :uint, :desc => "drops by interface (not yet supported)"
|
13
|
+
# bs_capt field intentionally left off (WIN32 only)
|
14
|
+
end
|
15
|
+
|
16
|
+
alias received ps_recv
|
17
|
+
alias dropped ps_drop
|
18
|
+
alias interface_dropped ps_ifdrop
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
# As returned by pcap_stats_ex() (MSDOS only)
|
23
|
+
#
|
24
|
+
# See pcap_stat_ex struct in pcap.h
|
25
|
+
class StatEx < FFI::Struct
|
26
|
+
include FFI::DRY::StructHelper
|
27
|
+
|
28
|
+
dsl_layout do
|
29
|
+
field :rx_packets, :ulong, :desc => "total packets received"
|
30
|
+
field :tx_packets, :ulong, :desc => "total packets transmitted"
|
31
|
+
field :rx_bytes, :ulong, :desc => "total bytes received"
|
32
|
+
field :tx_bytes, :ulong, :desc => "total bytes transmitted"
|
33
|
+
field :rx_errors, :ulong, :desc => "bad packets received"
|
34
|
+
field :tx_errors, :ulong, :desc => "packet transmit problems"
|
35
|
+
field :rx_dropped, :ulong, :desc => "no space in Rx buffers"
|
36
|
+
field :tx_dropped, :ulong, :desc => "no space available for Tx"
|
37
|
+
field :multicast, :ulong, :desc => "multicast packets received"
|
38
|
+
field :collisions, :ulong
|
39
|
+
|
40
|
+
# detailed rx errors
|
41
|
+
field :rx_length_errors, :ulong
|
42
|
+
field :rx_over_errors, :ulong, :desc => "ring buff overflow"
|
43
|
+
field :rx_crc_errors, :ulong, :desc => "pkt with crc error"
|
44
|
+
field :rx_frame_errors, :ulong, :desc => "frame alignment errors"
|
45
|
+
field :rx_fifo_errors, :ulong, :desc => "fifo overrun"
|
46
|
+
field :rx_missed_errors, :ulong, :desc => "missed packet"
|
47
|
+
|
48
|
+
# detailed tx_errors
|
49
|
+
field :tx_aborted_errors, :ulong
|
50
|
+
field :tx_carrier_errors, :ulong
|
51
|
+
field :tx_fifo_errors, :ulong
|
52
|
+
field :tx_heartbeat_errors, :ulong
|
53
|
+
field :tx_window_errors, :ulong
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
class TimeVal < FFI::Struct
|
4
|
+
include FFI::DRY::StructHelper
|
5
|
+
|
6
|
+
dsl_layout do
|
7
|
+
field :tv_sec, :time_t
|
8
|
+
field :tv_usec, :suseconds_t
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
if args.size == 1 and (t=args[0]).kind_of?(Time)
|
13
|
+
self.time = t
|
14
|
+
else
|
15
|
+
super(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
alias sec tv_sec
|
20
|
+
alias usec tv_usec
|
21
|
+
|
22
|
+
# Returns the time value as a ruby Time object.
|
23
|
+
#
|
24
|
+
# @return [Time]
|
25
|
+
# A ruby time object derived from this TimeVal.
|
26
|
+
def time
|
27
|
+
Time.at(self.tv_sec, self.tv_usec)
|
28
|
+
end
|
29
|
+
|
30
|
+
alias to_time time
|
31
|
+
|
32
|
+
# Sets the time value from a ruby Time object
|
33
|
+
#
|
34
|
+
# @param [Time] t
|
35
|
+
# A ruby time object from which to set the time.
|
36
|
+
#
|
37
|
+
# @return [Time]
|
38
|
+
# Returns the same Time object supplied per convention.
|
39
|
+
#
|
40
|
+
def time=(t)
|
41
|
+
self.tv_sec = t.tv_sec
|
42
|
+
self.tv_usec = t.tv_usec
|
43
|
+
return t
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FFI
|
2
|
+
module PCap
|
3
|
+
typedef :pointer, :FILE
|
4
|
+
|
5
|
+
typedef :int, :bpf_int32
|
6
|
+
typedef :uint, :bpf_uint32
|
7
|
+
|
8
|
+
enum :pcap_direction_t, [
|
9
|
+
:pcap_d_inout,
|
10
|
+
:pcap_d_in,
|
11
|
+
:pcap_d_out
|
12
|
+
]
|
13
|
+
|
14
|
+
# For Win32-only pcap_setmode()
|
15
|
+
enum :pcap_w32_modes_enum, [ :capt, :stat, :mon ]
|
16
|
+
|
17
|
+
typedef :pointer, :pcap_t
|
18
|
+
typedef :pointer, :pcap_dumper_t
|
19
|
+
typedef :pointer, :pcap_addr_t
|
20
|
+
|
21
|
+
# add some of the more temperamental FFI types if needed
|
22
|
+
[ [:long, :time_t],
|
23
|
+
[:uint32, :suseconds_t],
|
24
|
+
].each {|t, d| begin; find_type(d); rescue TypeError; typedef t,d; end }
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DataLink do
|
4
|
+
before(:all) do
|
5
|
+
@datalink = DataLink.new(0)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should map datalink names to datalink layer type values" do
|
9
|
+
DataLink.name_to_val(:en10mb).should == 1
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should map datalink layer type values to datalink names" do
|
13
|
+
DataLink.val_to_name(1).should == "EN10MB"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be initialized from a pcap datalink value" do
|
17
|
+
@datalink.name.should == 'NULL'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should support initialization from a pcap datalink name symbol" do
|
21
|
+
@datalink = DataLink.new(:null)
|
22
|
+
DataLink.should === @datalink
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should support initialization from a pcap datalink name string" do
|
26
|
+
dl = DataLink.new('en10mb')
|
27
|
+
DataLink.should === dl
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should allow equality comparison against numeric values" do
|
31
|
+
(@datalink == 0).should == true
|
32
|
+
(@datalink == 1).should == false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should allow equality comparison against String names" do
|
36
|
+
(@datalink == "null").should == true
|
37
|
+
(@datalink == "en10mb").should == false
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should allow equality comparison against Symbol names" do
|
41
|
+
(@datalink == :null).should == true
|
42
|
+
(@datalink == :en10mb).should == false
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should allow comparison against another DataLink" do
|
46
|
+
(@datalink == DataLink.new(0)).should == true
|
47
|
+
(@datalink == DataLink.new(1)).should == false
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should still compare correctly against any other object" do
|
51
|
+
(@datalink == Object.new).should == false
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have a description" do
|
55
|
+
@datalink.description.should_not be_empty
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should convert to an Integer for the DLT value" do
|
59
|
+
@datalink.to_i.should == 0
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should convert to a String for the DLT name" do
|
63
|
+
@datalink.to_s.should == 'NULL'
|
64
|
+
end
|
65
|
+
end
|