ssdp 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +12 -0
- data/lib/ssdp.rb +64 -0
- data/lib/ssdp/consumer.rb +144 -0
- data/lib/ssdp/producer.rb +138 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a650ded600f6688ce2d53aa7185b275d39b279f3
|
4
|
+
data.tar.gz: 65f3d49aa1aae08d792d61424c7891e4ffc4c35d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 428f3aa8fb465cd6407f9ef31723187ea2fe7810bb96da72a807dd8fe18c04e1e9be656e74567a27520b5dffc4f1c5f01448f18673e3ab6656a0017babd83964
|
7
|
+
data.tar.gz: 61af0480b1a12ac1230c7452d3a6d5271748686adf9aa8c11c6852eb8aa2508f8b1f3297442d9b0dc3f10bdb593218df0971791716f3514c1851ff8e89c555d5
|
data/LICENSE
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Copyright © 2015, Dillon Aumiller
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
BSD 2-Clause License
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
11
|
+
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/lib/ssdp.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ipaddr'
|
3
|
+
require_relative 'ssdp/producer'
|
4
|
+
require_relative 'ssdp/consumer'
|
5
|
+
|
6
|
+
module SSDP
|
7
|
+
DEFAULTS = {
|
8
|
+
# Shared
|
9
|
+
:broadcast => '239.255.255.250',
|
10
|
+
:bind => '0.0.0.0',
|
11
|
+
:port => 1900,
|
12
|
+
:maxpack => 65_507,
|
13
|
+
# Producer-Only
|
14
|
+
:interval => 30,
|
15
|
+
:notifier => true,
|
16
|
+
# Consumer-Only
|
17
|
+
:timeout => 30,
|
18
|
+
:first_only => false,
|
19
|
+
:synchronous => true,
|
20
|
+
:no_warnings => false
|
21
|
+
}
|
22
|
+
|
23
|
+
HEADER_MATCH = /^([^:]+):\s*(.+)$/
|
24
|
+
|
25
|
+
def parse_ssdp(message)
|
26
|
+
message.gsub! "\r\n", "\n"
|
27
|
+
header, body = message.split "\n\n"
|
28
|
+
|
29
|
+
header = header.split "\n"
|
30
|
+
status = header.shift
|
31
|
+
params = {}
|
32
|
+
header.each do |line|
|
33
|
+
match = HEADER_MATCH.match line
|
34
|
+
next if match.nil?
|
35
|
+
value = match[2]
|
36
|
+
value = (value[1, value.length - 2] || '') if value.start_with?('"') && value.end_with?('"')
|
37
|
+
params[match[1]] = value
|
38
|
+
end
|
39
|
+
|
40
|
+
{ :status => status, :params => params, :body => body }
|
41
|
+
end
|
42
|
+
module_function :parse_ssdp
|
43
|
+
|
44
|
+
def create_listener(options)
|
45
|
+
listener = UDPSocket.new
|
46
|
+
listener.do_not_reverse_lookup = true
|
47
|
+
membership = IPAddr.new(options[:broadcast]).hton + IPAddr.new(options[:bind]).hton
|
48
|
+
listener.setsockopt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership
|
49
|
+
listener.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
|
50
|
+
listener.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true
|
51
|
+
listener.bind options[:bind], options[:port]
|
52
|
+
listener
|
53
|
+
end
|
54
|
+
module_function :create_listener
|
55
|
+
|
56
|
+
def create_broadcaster
|
57
|
+
broadcaster = UDPSocket.new
|
58
|
+
broadcaster.setsockopt Socket::SOL_SOCKET, Socket::SO_BROADCAST, true
|
59
|
+
broadcaster.setsockopt :IPPROTO_IP, :IP_MULTICAST_TTL, 1
|
60
|
+
broadcaster
|
61
|
+
end
|
62
|
+
module_function :create_broadcaster
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ssdp'
|
3
|
+
|
4
|
+
module SSDP
|
5
|
+
class Consumer
|
6
|
+
def initialize(options = {})
|
7
|
+
@options = SSDP::DEFAULTS.merge options
|
8
|
+
@search_socket = SSDP.create_broadcaster
|
9
|
+
@watch = {
|
10
|
+
:socket => nil,
|
11
|
+
:thread => nil,
|
12
|
+
:services => {}
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def search(options, &block)
|
17
|
+
options = @options.merge options
|
18
|
+
options[:callback] ||= block unless block.nil?
|
19
|
+
fail "SSDP consumer async search missing callback." if (options[:synchronous] == false) && options[:callback].nil?
|
20
|
+
fail "SSDP consumer search accepting multiple responses must specify a timeout value." if (options[:first_only] == false) && (options[:timeout].to_i < 1)
|
21
|
+
warn "Warning: Calling SSDP search without a service specified." if options[:service].nil? && (options[:no_warnings] != true)
|
22
|
+
warn "Warning: Calling SSDP search without a timeout value." if (options[:timeout].to_i < 1) && (options[:no_warnings] != true)
|
23
|
+
|
24
|
+
@search_socket.send compose_search(options), 0, options[:broadcast], options[:port]
|
25
|
+
|
26
|
+
if options[:synchronous]
|
27
|
+
search_sync options
|
28
|
+
else
|
29
|
+
search_async options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_watching_type(type, &block)
|
34
|
+
@watch[:services][type] = block
|
35
|
+
start_watch if @watch[:thread].nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop_watching_type(type)
|
39
|
+
@watch[:services].delete type
|
40
|
+
stop_watch if (@watch[:services].count == 0) && (@watch[:thread] != nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop_watching_all
|
44
|
+
@watch[:services] = {}
|
45
|
+
stop_watch if @watch[:thread] != nil
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def compose_search(options)
|
51
|
+
query = "M-SEARCH * HTTP/1.1\n" \
|
52
|
+
"Host: #{options[:broadcast]}:#{options[:port]}\n" \
|
53
|
+
"Man: \"ssdp:discover\"\n"
|
54
|
+
query += "ST: #{options[:service]}\n" if options[:service]
|
55
|
+
options[:params].each { |key, val| query += "#{key}: #{value}\n" } if options[:params]
|
56
|
+
query + "\n"
|
57
|
+
end
|
58
|
+
|
59
|
+
def search_sync(options)
|
60
|
+
if options[:first_only]
|
61
|
+
search_single options
|
62
|
+
else
|
63
|
+
search_multi options
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def search_async(options)
|
68
|
+
if options[:first_only]
|
69
|
+
Thread.new { search_single options }
|
70
|
+
else
|
71
|
+
Thread.new { search_multi options }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def search_single(options)
|
76
|
+
result = nil
|
77
|
+
|
78
|
+
if options[:timeout]
|
79
|
+
ready = IO::select [@search_socket], nil, nil, options[:timeout]
|
80
|
+
if ready
|
81
|
+
message, producer = @search_socket.recvfrom options[:maxpack]
|
82
|
+
result = process_ssdp_packet message, producer
|
83
|
+
end
|
84
|
+
else
|
85
|
+
message, producer = @search_socket.recvfrom options[:maxpack]
|
86
|
+
result = process_ssdp_packet message, producer
|
87
|
+
end
|
88
|
+
|
89
|
+
if options[:synchronous]
|
90
|
+
result
|
91
|
+
else
|
92
|
+
options[:callback].call result
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def search_multi(options)
|
97
|
+
remaining = options[:timeout]
|
98
|
+
responses = []
|
99
|
+
|
100
|
+
while remaining > 0
|
101
|
+
start_time = Time.now
|
102
|
+
ready = IO::select [@search_socket], nil, nil, remaining
|
103
|
+
if ready
|
104
|
+
message, producer = @search_socket.recvfrom options[:maxpack]
|
105
|
+
responses << process_ssdp_packet(message, producer)
|
106
|
+
end
|
107
|
+
remaining -= (Time.now - start_time).to_i
|
108
|
+
end
|
109
|
+
|
110
|
+
if options[:synchronous]
|
111
|
+
responses
|
112
|
+
else
|
113
|
+
options[:callback].call responses
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def process_ssdp_packet(message, producer)
|
118
|
+
ssdp = SSDP.parse_ssdp message
|
119
|
+
{ :address => producer[3], :port => producer[1] }.merge ssdp
|
120
|
+
end
|
121
|
+
|
122
|
+
def start_watch
|
123
|
+
@watch[:socket] = SSDP.create_listener @options
|
124
|
+
@watch[:thread] = Thread.new do
|
125
|
+
begin
|
126
|
+
while true
|
127
|
+
message, producer = @watch[:socket].recvfrom @options[:maxpack]
|
128
|
+
notification = process_ssdp_packet message, producer
|
129
|
+
notification_type = notification[:params]['NT']
|
130
|
+
@watch[:services][notification_type].call notification if @watch[:services].include? notification_type
|
131
|
+
end
|
132
|
+
ensure
|
133
|
+
@watch[:socket].close
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def stop_watch
|
139
|
+
@watch[:thread].exit
|
140
|
+
@watch[:thread] = nil
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'SecureRandom'
|
3
|
+
require 'ssdp'
|
4
|
+
|
5
|
+
module SSDP
|
6
|
+
class Producer
|
7
|
+
attr_accessor :services
|
8
|
+
attr_accessor :uuid
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@uuid = SecureRandom.uuid
|
12
|
+
@services = {}
|
13
|
+
@listener = { :socket => nil, :thread => nil }
|
14
|
+
@notifier = { :thread => nil }
|
15
|
+
@options = SSDP::DEFAULTS.merge options
|
16
|
+
end
|
17
|
+
|
18
|
+
def running?
|
19
|
+
@listener[:thread] != nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
start_notifier if @options[:notifier]
|
24
|
+
start_listener
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop(bye_bye = true)
|
28
|
+
@services.each { |type, params| send_bye_bye type, params } if bye_bye
|
29
|
+
|
30
|
+
if @listener[:thread] != nil
|
31
|
+
@listener[:thread].exit
|
32
|
+
@listener[:thread] = nil
|
33
|
+
end
|
34
|
+
if @notifier[:thread] != nil
|
35
|
+
@notifier[:thread].exit
|
36
|
+
@notifier[:thread] = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_service(type, location_or_param_hash)
|
41
|
+
params = {}
|
42
|
+
if location_or_param_hash.is_a? Hash
|
43
|
+
params = location_or_param_hash
|
44
|
+
else
|
45
|
+
params['AL'] = location_or_param_hash
|
46
|
+
params['LOCATION'] = location_or_param_hash
|
47
|
+
end
|
48
|
+
|
49
|
+
@services[type] = params
|
50
|
+
send_notification type, params if @options[:notifier]
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_service(type)
|
54
|
+
@services.delete[type]
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def process_ssdp(message, consumer)
|
60
|
+
ssdp = SSDP.parse_ssdp message
|
61
|
+
return unless ssdp[:status].start_with? 'M-SEARCH * HTTP'
|
62
|
+
|
63
|
+
return if ssdp[:params]['ST'].nil?
|
64
|
+
return if @services[ssdp[:params]['ST']].nil?
|
65
|
+
send_response ssdp[:params]['ST'], consumer
|
66
|
+
end
|
67
|
+
|
68
|
+
def send_response(type, consumer)
|
69
|
+
params = @services[type]
|
70
|
+
response_body = "HTTP/1.1 200 OK\r\n" \
|
71
|
+
"ST: #{type}\r\n" \
|
72
|
+
"USN: uuid:#{@uuid}\r\n" +
|
73
|
+
params.map { |k, v| "#{k}: #{v}" }.join("\r\n") +
|
74
|
+
"\r\n\r\n"
|
75
|
+
send_direct_packet response_body, consumer
|
76
|
+
end
|
77
|
+
|
78
|
+
def send_notification(type, params)
|
79
|
+
notify_body = "NOTIFY * HTTP/1.1\r\n" \
|
80
|
+
"Host: #{@options[:broadcast]}:#{@options[:port]}\r\n" \
|
81
|
+
"NTS: ssdp:alive\r\n" \
|
82
|
+
"NT: #{type}\r\n" \
|
83
|
+
"USN: uuid:#{@uuid}\r\n" +
|
84
|
+
params.map { |k, v| "#{k}: #{v}" }.join("\r\n") +
|
85
|
+
"\r\n\r\n"
|
86
|
+
|
87
|
+
send_broadcast_packet notify_body
|
88
|
+
end
|
89
|
+
|
90
|
+
def send_bye_bye(type, params)
|
91
|
+
bye_bye_body = "NOTIFY * HTTP/1.1\r\n" \
|
92
|
+
"Host: #{@options[:broadcast]}:#{@options[:port]}\r\n" \
|
93
|
+
"NTS: ssdp:byebye\r\n" \
|
94
|
+
"NT: #{type}\r\n" \
|
95
|
+
"USN: uuid:#{@uuid}\r\n" +
|
96
|
+
params.map { |k, v| "#{k}: #{v}" }.join("\r\n") +
|
97
|
+
"\r\n\r\n"
|
98
|
+
|
99
|
+
send_broadcast_packet bye_bye_body
|
100
|
+
end
|
101
|
+
|
102
|
+
def send_direct_packet(body, endpoint)
|
103
|
+
udp_socket = UDPSocket.new
|
104
|
+
udp_socket.send body, 0, endpoint[:address], endpoint[:port]
|
105
|
+
udp_socket.close
|
106
|
+
end
|
107
|
+
|
108
|
+
def send_broadcast_packet(body)
|
109
|
+
broadcaster = SSDP.create_broadcaster
|
110
|
+
broadcaster.send body, 0, @options[:broadcast], @options[:port]
|
111
|
+
broadcaster.close
|
112
|
+
end
|
113
|
+
|
114
|
+
def start_listener
|
115
|
+
@listener[:socket] = SSDP.create_listener @options
|
116
|
+
@listener[:thread] = Thread.new do
|
117
|
+
begin
|
118
|
+
while true
|
119
|
+
message, consumer = @listener[:socket].recvfrom @options[:maxpack]
|
120
|
+
process_ssdp message, { :address => consumer[3], :port => consumer[1] } unless @services.count == 0
|
121
|
+
end
|
122
|
+
ensure
|
123
|
+
@listener[:socket].close
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def start_notifier
|
129
|
+
@notifier[:thread] = Thread.new do
|
130
|
+
while true
|
131
|
+
sleep @options[:interval]
|
132
|
+
@services.each { |type, params| send_notification type, params }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ssdp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dillon Aumiller
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-02 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: SSDP client/server library. Server notify/part/respond; client search/listen.
|
14
|
+
email: dillonaumiller@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE
|
20
|
+
- lib/ssdp.rb
|
21
|
+
- lib/ssdp/consumer.rb
|
22
|
+
- lib/ssdp/producer.rb
|
23
|
+
homepage: https://github.com/daumiller/ssdp
|
24
|
+
licenses:
|
25
|
+
- BSD 2-Clause
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 1.9.0
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.2.2
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: SSDP client/server library.
|
47
|
+
test_files: []
|