UPnP 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
@@ -0,0 +1,30 @@
1
+ # vim: filetype=ruby
2
+
3
+ Autotest.add_hook :initialize do |at|
4
+
5
+ at.add_mapping %r%^lib/UPnP/SSDP.rb$% do
6
+ ['test/test_UPnP_SSDP.rb',
7
+ 'test/test_UPnP_SSDP_notification.rb',
8
+ 'test/test_UPnP_SSDP_response.rb',
9
+ ]
10
+ end
11
+
12
+ at.add_mapping %r%^lib/UPnP/control/(\w+).rb$% do |_,m|
13
+ "test/test_UPnP_Control_#{m[1].capitalize}.rb"
14
+ end
15
+
16
+ at.add_mapping %r%^test/utilities.rb$% do
17
+ at.known_files
18
+ end
19
+
20
+ at.extra_class_map["TestUPnPControlDevice"] =
21
+ 'test/test_UPnP_control_device.rb'
22
+ at.extra_class_map["TestUPnPSSDP"] = 'test/test_UPnP_SSDP.rb'
23
+ at.extra_class_map["TestUPnPSSDPNotification"] =
24
+ 'test/test_UPnP_SSDP_notification.rb'
25
+ at.extra_class_map["TestUPnPSSDPResponse"] =
26
+ 'test/test_UPnP_SSDP_response.rb'
27
+ at.extra_class_map["TestUPnPControlService"] =
28
+ 'test/test_UPnP_control_service.rb'
29
+ end
30
+
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-06-25
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,18 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/upnp_discover
7
+ bin/upnp_listen
8
+ lib/UPnP.rb
9
+ lib/UPnP/SSDP.rb
10
+ lib/UPnP/control.rb
11
+ lib/UPnP/control/device.rb
12
+ lib/UPnP/control/service.rb
13
+ test/test_UPnP_SSDP.rb
14
+ test/test_UPnP_SSDP_notification.rb
15
+ test/test_UPnP_SSDP_response.rb
16
+ test/test_UPnP_control_device.rb
17
+ test/test_UPnP_control_service.rb
18
+ test/utilities.rb
@@ -0,0 +1,95 @@
1
+ = UPnP
2
+
3
+ * http://seattlerb.org/UPnP
4
+ * http://upnp.org
5
+ * Bugs: http://rubyforge.org/tracker/?atid=5921&group_id=1513
6
+
7
+ == DESCRIPTION:
8
+
9
+ An implementation of the UPnP protocol
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ * Discovers UPnP devices and services via SSDP, see UPnP::SSDP
14
+ * Creates a SOAP RPC driver for discovered services, see UPnP::Control::Service
15
+ * Creates concrete UPnP device and service classes that may be extended with
16
+ utility methods, see UPnP::Control::Device::create,
17
+ UPnP::Control::Service::create and the UPnP-IGD gem.
18
+ * Eventing not implemented
19
+ * Servers not implemented
20
+
21
+ == SYNOPSIS:
22
+
23
+ Print out information about UPnP devices nearby:
24
+
25
+ upnp_discover
26
+
27
+ Listen for UPnP resource notifications:
28
+
29
+ upnp_listen
30
+
31
+ Search for root UPnP devices and print out their description URLs:
32
+
33
+ require 'UPnP/SSDP'
34
+
35
+ resources = UPnP::SSDP.new.search :root
36
+ locations = resources.map { |resource| resource.location }
37
+ puts locations.join("\n")
38
+
39
+ Create a UPnP::Control::Device from the first discovered root device:
40
+
41
+ require 'UPnP/control/device'
42
+
43
+ device = UPnP::Control::Device.create locations.first
44
+
45
+ Enumerate actions on all services on the device:
46
+
47
+ service_names = device.services.map do |service|
48
+ service.methods(false)
49
+ end
50
+
51
+ puts service_names.sort.join("\n")
52
+
53
+ Assuming the root device is an InternetGatewayDevice with a WANIPConnection
54
+ service, print out the external IP address for the gateway:
55
+
56
+ wic = device.services.find { |service| service.type =~ /WANIPConnection/ }
57
+ puts wic.GetExternalIPAddress
58
+
59
+ == REQUIREMENTS:
60
+
61
+ * UPnP devices
62
+
63
+ == INSTALL:
64
+
65
+ sudo gem install UPnP
66
+
67
+ == LICENSE:
68
+
69
+ All code copyright 2008 Eric Hodel. All rights reserved.
70
+
71
+ Redistribution and use in source and binary forms, with or without
72
+ modification, are permitted provided that the following conditions
73
+ are met:
74
+
75
+ 1. Redistributions of source code must retain the above copyright
76
+ notice, this list of conditions and the following disclaimer.
77
+ 2. Redistributions in binary form must reproduce the above copyright
78
+ notice, this list of conditions and the following disclaimer in the
79
+ documentation and/or other materials provided with the distribution.
80
+ 3. Neither the names of the authors nor the names of their contributors
81
+ may be used to endorse or promote products derived from this software
82
+ without specific prior written permission.
83
+
84
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
85
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
86
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
87
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
88
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
89
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
90
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
91
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
92
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
93
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
94
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
95
+
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/upnp.rb'
6
+
7
+ Hoe.new('UPnP', UPnP::VERSION) do |p|
8
+ p.rubyforge_name = 'seattlerb'
9
+ p.developer('Eric Hodel', 'drbrain@segment7.net')
10
+ end
11
+
12
+ # vim: syntax=Ruby
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'UPnP/SSDP'
4
+ require 'UPnP/control'
5
+
6
+ ssdp = UPnP::SSDP.new
7
+ timeout = ARGV.shift
8
+ timeout = if timeout then
9
+ begin
10
+ timeout = Integer timeout
11
+ rescue
12
+ abort <<-EOF
13
+ Usage: #{$0} [timeout]
14
+
15
+ Prints information about UPnP internet gateway devices
16
+ EOF
17
+ end
18
+ else
19
+ 1
20
+ end
21
+
22
+ ssdp.timeout = timeout
23
+
24
+ devices = ssdp.search(:root).map do |resource|
25
+ UPnP::Control::Device.new resource.location
26
+ end
27
+
28
+ if devices.empty? then
29
+ puts 'No UPnP devices found'
30
+ exit
31
+ end
32
+
33
+ def print_device(device, indent = ' ')
34
+ out = []
35
+
36
+ out << "Friendly name: #{device.friendly_name}"
37
+ out << "Presentation URL: #{device.presentation_url}"
38
+ out << nil
39
+ out << "Unique device name: #{device.name}"
40
+ out << nil
41
+ out << "Manufacturer: #{device.manufacturer}"
42
+ out << "Manufacturer URL: #{device.manufacturer_url}"
43
+ out << nil
44
+ out << "Model name: #{device.model_name}"
45
+ out << "Model description: #{device.model_description}"
46
+ out << "Model URL: #{device.model_url}"
47
+ out << "UPC: #{device.upc}"
48
+ out << "Serial number: #{device.serial_number}"
49
+ out << nil
50
+
51
+ puts indent + out.join("\n#{indent}")
52
+ end
53
+
54
+ devices.each do |device|
55
+ type = device.type.sub "#{UPnP::DEVICE_SCHEMA_PREFIX}:", ''
56
+ puts "#{device.url}: #{type}"
57
+ print_device device
58
+
59
+ device.devices.each do |sub_device|
60
+ type = sub_device.type.sub "#{UPnP::DEVICE_SCHEMA_PREFIX}:", ''
61
+ puts " Sub-device #{type}:"
62
+ print_device sub_device, ' '
63
+ end
64
+
65
+ device.services.each do |service|
66
+ type = service.type.sub("#{UPnP::DEVICE_SCHEMA_PREFIX}:", '')
67
+ puts " Service: #{type}"
68
+ puts " Id: #{service.id}"
69
+ puts " Type: #{service.type}"
70
+ puts " SCPD URL: #{service.scpd_url}"
71
+ puts " Control URL: #{service.control_url}"
72
+ puts " Event subscription URL: #{service.event_sub_url}"
73
+ puts
74
+ puts " Actions:"
75
+ service.driver.methods(false).sort.each do |method|
76
+ puts " #{method}"
77
+ end
78
+
79
+ puts
80
+ end
81
+ end
82
+
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'UPnP/SSDP'
4
+ require 'UPnP/control'
5
+
6
+ ssdp = UPnP::SSDP.new
7
+
8
+ ssdp.discover do |notification|
9
+ schemas = Regexp.union UPnP::DEVICE_SCHEMA_PREFIX, UPnP::SERVICE_SCHEMA_PREFIX
10
+
11
+ type = notification.type.sub(/#{schemas}:/, '')
12
+
13
+ if notification.alive? then
14
+ puts "#{type} is alive"
15
+ puts "Description: #{notification.location}"
16
+ expiration = notification.expiration.strftime '%c'
17
+ puts "Valid until #{expiration}"
18
+ else
19
+ puts "#{type} says byebye"
20
+ end
21
+
22
+ puts "USN: #{notification.name}"
23
+
24
+ puts
25
+ end
26
+
@@ -0,0 +1,35 @@
1
+ $KCODE = 'u'
2
+
3
+ require 'rubygems'
4
+ gem 'soap4r'
5
+
6
+ ##
7
+ # An implementation of the Universal Plug and Play protocol.
8
+ #
9
+ # http://upnp.org/
10
+
11
+ module UPnP
12
+
13
+ ##
14
+ # UPnP device schema prefix
15
+
16
+ DEVICE_SCHEMA_PREFIX = 'urn:schemas-upnp-org:device'
17
+
18
+ ##
19
+ # UPnP service schema prefix
20
+
21
+ SERVICE_SCHEMA_PREFIX = 'urn:schemas-upnp-org:service'
22
+
23
+ ##
24
+ # The version of UPnP you are using
25
+
26
+ VERSION = '1.0.0'
27
+
28
+ ##
29
+ # UPnP error base class
30
+
31
+ class Error < RuntimeError
32
+ end
33
+
34
+ end
35
+
@@ -0,0 +1,495 @@
1
+ require 'ipaddr'
2
+ require 'socket'
3
+ require 'thread'
4
+ require 'time'
5
+ require 'uri'
6
+
7
+ require 'UPnP'
8
+ require 'UPnP/control'
9
+
10
+ ##
11
+ # Simple Service Discovery Protocol for the UPnP Device Architecture.
12
+ #
13
+ # Currently SSDP only handles the discovery portions of SSDP.
14
+ #
15
+ # To listen for SSDP notifications from UPnP devices:
16
+ #
17
+ # ssdp = SSDP.new
18
+ # notifications = ssdp.listen
19
+ #
20
+ # To discover all devices and services:
21
+ #
22
+ # ssdp = SSDP.new
23
+ # resources = ssdp.search
24
+ #
25
+ # After a device has been found you can create a Device object for it:
26
+ #
27
+ # UPnP::Control::Device.create resource.location
28
+ #
29
+ # Based on code by Kazuhiro NISHIYAMA (zn@mbf.nifty.com)
30
+
31
+ class UPnP::SSDP
32
+
33
+ ##
34
+ # SSDP Error class
35
+
36
+ class Error < UPnP::Error
37
+ end
38
+
39
+ ##
40
+ # Abstract class for SSDP advertisements
41
+
42
+ class Advertisement
43
+
44
+ ##
45
+ # Expiration time of this advertisement
46
+
47
+ def expiration
48
+ date + max_age
49
+ end
50
+
51
+ ##
52
+ # True if this advertisement has expired
53
+
54
+ def expired?
55
+ Time.now > expiration
56
+ end
57
+
58
+ end
59
+
60
+ ##
61
+ # Holds information about a NOTIFY message. For an alive notification, all
62
+ # fields will be present. For a byebye notification, location, max_age and
63
+ # server will be nil.
64
+
65
+ class Notification < Advertisement
66
+
67
+ ##
68
+ # Date the notification was received
69
+
70
+ attr_reader :date
71
+
72
+ ##
73
+ # Host the notification was sent from
74
+
75
+ attr_reader :host
76
+
77
+ ##
78
+ # Port the notification was sent from
79
+
80
+ attr_reader :port
81
+
82
+ ##
83
+ # Location of the advertised service or device
84
+
85
+ attr_reader :location
86
+
87
+ ##
88
+ # Maximum age the advertisement is valid for
89
+
90
+ attr_reader :max_age
91
+
92
+ ##
93
+ # Unique Service Name of the advertisement
94
+
95
+ attr_reader :name
96
+
97
+ ##
98
+ # Type of the advertised service or device
99
+
100
+ attr_reader :type
101
+
102
+ ##
103
+ # Server name and version of the advertised service or device
104
+
105
+ attr_reader :server
106
+
107
+ ##
108
+ # \Notification sub-type
109
+
110
+ attr_reader :sub_type
111
+
112
+ ##
113
+ # Parses a NOTIFY advertisement into its component pieces
114
+
115
+ def self.parse(advertisement)
116
+ advertisement = advertisement.gsub "\r", ''
117
+
118
+ advertisement =~ /^host:\s*(\S*)/i
119
+ host, port = $1.split ':'
120
+
121
+ advertisement =~ /^nt:\s*(\S*)/i
122
+ type = $1
123
+
124
+ advertisement =~ /^nts:\s*(\S*)/i
125
+ sub_type = $1
126
+
127
+ advertisement =~ /^usn:\s*(\S*)/i
128
+ name = $1
129
+
130
+ if sub_type == 'ssdp:alive' then
131
+ advertisement =~ /^cache-control:\s*max-age\s*=\s*(\d+)/i
132
+ max_age = Integer $1
133
+
134
+ advertisement =~ /^location:\s*(\S*)/i
135
+ location = URI.parse $1
136
+
137
+ advertisement =~ /^server:\s*(.*)/i
138
+ server = $1.strip
139
+ end
140
+
141
+ new Time.now, max_age, host, port, location, type, sub_type, server, name
142
+ end
143
+
144
+ ##
145
+ # Creates a \new Notification
146
+
147
+ def initialize(date, max_age, host, port, location, type, sub_type,
148
+ server, name)
149
+ @date = date
150
+ @max_age = max_age
151
+ @host = host
152
+ @port = port
153
+ @location = location
154
+ @type = type
155
+ @sub_type = sub_type
156
+ @server = server
157
+ @name = name
158
+ end
159
+
160
+ ##
161
+ # Returns true if this is a notification for a resource being alive
162
+
163
+ def alive?
164
+ sub_type == 'ssdp:alive'
165
+ end
166
+
167
+ ##
168
+ # Returns true if this is a notification for a resource going away
169
+
170
+ def byebye?
171
+ sub_type == 'ssdp:byebye'
172
+ end
173
+
174
+ ##
175
+ # A friendlier inspect
176
+
177
+ def inspect
178
+ location = " #{@location}" if @location
179
+ "#<#{self.class}:0x#{object_id.to_s 16} #{@type} #{@sub_type}#{location}>"
180
+ end
181
+
182
+ end
183
+
184
+ ##
185
+ # Holds information about a M-SEARCH response
186
+
187
+ class Response < Advertisement
188
+
189
+ ##
190
+ # Date response was created or received
191
+
192
+ attr_reader :date
193
+
194
+ ##
195
+ # true if MAN header was understood
196
+
197
+ attr_reader :ext
198
+
199
+ ##
200
+ # URI where this device or service is described
201
+
202
+ attr_reader :location
203
+
204
+ ##
205
+ # Maximum age this advertisement is valid for
206
+
207
+ attr_reader :max_age
208
+
209
+ ##
210
+ # Unique Service Name
211
+
212
+ attr_reader :name
213
+
214
+ ##
215
+ # Server version string
216
+
217
+ attr_reader :server
218
+
219
+ ##
220
+ # Search target
221
+
222
+ attr_reader :target
223
+
224
+ ##
225
+ # Creates a new Response by parsing the text in +response+
226
+
227
+ def self.parse(response)
228
+ response =~ /^cache-control:\s*max-age\s*=\s*(\d+)/i
229
+ max_age = Integer $1
230
+
231
+ response =~ /^date:\s*(.*)/i
232
+ date = $1 ? Time.parse($1) : Time.now
233
+
234
+ ext = !!(response =~ /^ext:/i)
235
+
236
+ response =~ /^location:\s*(\S*)/i
237
+ location = URI.parse $1.strip
238
+
239
+ response =~ /^server:\s*(.*)/i
240
+ server = $1.strip
241
+
242
+ response =~ /^st:\s*(\S*)/i
243
+ target = $1.strip
244
+
245
+ response =~ /^usn:\s*(\S*)/i
246
+ name = $1.strip
247
+
248
+ new date, max_age, location, server, target, name, ext
249
+ end
250
+
251
+ ##
252
+ # Creates a new Response
253
+
254
+ def initialize(date, max_age, location, server, target, name, ext)
255
+ @date = date
256
+ @max_age = max_age
257
+ @location = location
258
+ @server = server
259
+ @target = target
260
+ @name = name
261
+ @ext = ext
262
+ end
263
+
264
+ ##
265
+ # A friendlier inspect
266
+
267
+ def inspect
268
+ "#<#{self.class}:0x#{object_id.to_s 16} #{target} #{location}>"
269
+ end
270
+
271
+ end
272
+
273
+ ##
274
+ # Default broadcast address
275
+
276
+ BROADCAST = '239.255.255.250'
277
+
278
+ ##
279
+ # Default port
280
+
281
+ PORT = 1900
282
+
283
+ ##
284
+ # Default timeout
285
+
286
+ TIMEOUT = 1
287
+
288
+ ##
289
+ # Default packet time to live (hops)
290
+
291
+ TTL = 4
292
+
293
+ ##
294
+ # Broadcast address to use when sending searches and listening for
295
+ # notifications
296
+
297
+ attr_accessor :broadcast
298
+
299
+ ##
300
+ # Listener accessor for tests.
301
+
302
+ attr_accessor :listener # :nodoc:
303
+
304
+ ##
305
+ # Port to use for SSDP searching and listening
306
+
307
+ attr_accessor :port
308
+
309
+ ##
310
+ # Queue accessor for tests
311
+
312
+ attr_accessor :queue # :nodoc:
313
+
314
+ ##
315
+ # Socket accessor for tests
316
+
317
+ attr_accessor :socket # :nodoc:
318
+
319
+ ##
320
+ # Time to wait for SSDP responses
321
+
322
+ attr_accessor :timeout
323
+
324
+ ##
325
+ # TTL for SSDP packets
326
+
327
+ attr_accessor :ttl
328
+
329
+ ##
330
+ # Creates a new SSDP object. Use the accessors to override broadcast, port,
331
+ # timeout or ttl.
332
+
333
+ def initialize
334
+ @broadcast = BROADCAST
335
+ @port = PORT
336
+ @timeout = TIMEOUT
337
+ @ttl = TTL
338
+
339
+ @listener = nil
340
+ @queue = Queue.new
341
+ end
342
+
343
+ ##
344
+ # Discovers UPnP devices sending NOTIFY broadcasts.
345
+ #
346
+ # If given a block, yields each Notification as it is received and never
347
+ # returns. Otherwise, discover waits for timeout seconds and returns all
348
+ # notifications received in that time.
349
+
350
+ def discover
351
+ membership = IPAddr.new(@broadcast).hton + IPAddr.new('0.0.0.0').hton
352
+
353
+ @socket ||= UDPSocket.new
354
+
355
+ @socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
356
+ @socket.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership
357
+
358
+ @socket.bind Socket::INADDR_ANY, @port
359
+
360
+ listen
361
+
362
+ if block_given? then
363
+ loop do
364
+ notification = @queue.pop
365
+
366
+ yield notification
367
+ end
368
+ else
369
+ sleep @timeout
370
+
371
+ notifications = []
372
+ notifications << @queue.pop until @queue.empty?
373
+ notifications
374
+ end
375
+ ensure
376
+ stop_listening
377
+ @socket.close if @socket and not @socket.closed?
378
+ @socket = nil
379
+ end
380
+
381
+ ##
382
+ # Listens for UDP packets from devices in a Thread and enqueues them for
383
+ # processing. Requires a socket from search or discover.
384
+
385
+ def listen
386
+ return @listener if @listener and @listener.alive?
387
+
388
+ @listener = Thread.start do
389
+ loop do
390
+ response = @socket.recvfrom(1024).first
391
+
392
+ begin
393
+ @queue << parse(response)
394
+ rescue
395
+ puts $!.message
396
+ puts $!.backtrace
397
+ end
398
+ end
399
+ end
400
+ end
401
+
402
+ ##
403
+ # Returns a Notification or Response created from +response+.
404
+
405
+ def parse(response)
406
+ case response
407
+ when /\ANOTIFY/ then
408
+ Notification.parse response
409
+ when /\AHTTP/ then
410
+ Response.parse response
411
+ else
412
+ raise Error, "Unknown response #{response[/\A.*$/]}"
413
+ end
414
+ end
415
+
416
+ ##
417
+ # Broadcasts M-SEARCH requests looking for +targets+. Waits timeout seconds
418
+ # for responses then returns the collected responses.
419
+ #
420
+ # Supply no arguments to search for all devices and services.
421
+ #
422
+ # Supply <tt>:root</tt> to search for root devices only.
423
+ #
424
+ # Supply <tt>[:device, 'device_type:version']</tt> to search for a specific
425
+ # device type.
426
+ #
427
+ # Supply <tt>[:service, 'service_type:version']</tt> to search for a
428
+ # specific service type.
429
+ #
430
+ # Supply <tt>"uuid:..."</tt> to search for a UUID.
431
+ #
432
+ # Supply <tt>"urn:..."</tt> to search for a URN.
433
+
434
+ def search(*targets)
435
+ @socket ||= UDPSocket.new
436
+
437
+ @socket.setsockopt Socket::IPPROTO_IP, Socket::IP_TTL, [@ttl].pack('i')
438
+
439
+ if targets.empty? then
440
+ send_search 'ssdp:all'
441
+ else
442
+ targets.each do |target|
443
+ if target == :root then
444
+ send_search 'upnp:rootdevice'
445
+ elsif Array === target and target.first == :device then
446
+ target = [UPnP::DEVICE_SCHEMA_PREFIX, target.last]
447
+ send_search target.join(':')
448
+ elsif Array === target and target.first == :service then
449
+ target = [UPnP::SERVICE_SCHEMA_PREFIX, target.last]
450
+ send_search target.join(':')
451
+ elsif String === target and target =~ /\A(urn|uuid|ssdp):/ then
452
+ send_search target
453
+ end
454
+ end
455
+ end
456
+
457
+ listen
458
+ sleep @timeout
459
+
460
+ responses = []
461
+ responses << @queue.pop until @queue.empty?
462
+ responses
463
+ ensure
464
+ stop_listening
465
+ @socket.close if @socket and not @socket.closed?
466
+ @socket = nil
467
+ end
468
+
469
+ ##
470
+ # Builds and sends an M-SEARCH request looking for +search_target+.
471
+
472
+ def send_search(search_target)
473
+ http_request = <<HTTP_REQUEST
474
+ M-SEARCH * HTTP/1.1\r
475
+ HOST: #{@broadcast}:#{@port}\r
476
+ MAN: "ssdp:discover"\r
477
+ MX: #{@timeout}\r
478
+ ST: #{search_target}\r
479
+ \r
480
+ HTTP_REQUEST
481
+
482
+ @socket.send http_request, 0, @broadcast, @port
483
+ end
484
+
485
+ ##
486
+ # Stops and clears the listen thread.
487
+
488
+ def stop_listening
489
+ @listener.kill if @listener
490
+ @queue = Queue.new
491
+ @listener = nil
492
+ end
493
+
494
+ end
495
+