ssdp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []