druzy-upnp 1.0.0 → 2.0.0
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.
- checksums.yaml +4 -4
- data/lib/druzy/upnp/ssdp.rb +52 -182
- data/lib/druzy/upnp/upnp_device.rb +73 -0
- data/lib/druzy/upnp/upnp_service.rb +74 -0
- data/lib/druzy/upnp/version.rb +2 -2
- metadata +17 -187
- data/lib/core_ext/hash_patch.rb +0 -5
- data/lib/core_ext/socket_patch.rb +0 -16
- data/lib/core_ext/to_upnp_s.rb +0 -65
- data/lib/druzy/upnp/control_point/base.rb +0 -70
- data/lib/druzy/upnp/control_point/device.rb +0 -465
- data/lib/druzy/upnp/control_point/error.rb +0 -15
- data/lib/druzy/upnp/control_point/service.rb +0 -387
- data/lib/druzy/upnp/control_point.rb +0 -161
- data/lib/druzy/upnp/device.rb +0 -31
- data/lib/druzy/upnp/ssdp/broadcast_searcher.rb +0 -110
- data/lib/druzy/upnp/ssdp/error.rb +0 -8
- data/lib/druzy/upnp/ssdp/listener.rb +0 -36
- data/lib/druzy/upnp/ssdp/multicast_connection.rb +0 -109
- data/lib/druzy/upnp/ssdp/network_constants.rb +0 -19
- data/lib/druzy/upnp/ssdp/notifier.rb +0 -39
- data/lib/druzy/upnp/ssdp/searcher.rb +0 -85
- data/lib/rack/upnp_control_point.rb +0 -70
@@ -1,36 +0,0 @@
|
|
1
|
-
require_relative 'multicast_connection'
|
2
|
-
|
3
|
-
|
4
|
-
class Druzy::Upnp::SSDP::Listener < Druzy::Upnp::SSDP::MulticastConnection
|
5
|
-
|
6
|
-
# @return [EventMachine::Channel] Provides subscribers with notifications
|
7
|
-
# from devices that have come online (sent +ssdp:alive+ notifications).
|
8
|
-
attr_reader :alive_notifications
|
9
|
-
|
10
|
-
# @return [EventMachine::Channel] Provides subscribers with notifications
|
11
|
-
# from devices that have gone offline (sent +ssd:byebye+ notifications).
|
12
|
-
attr_reader :byebye_notifications
|
13
|
-
|
14
|
-
# This is the callback called by EventMachine when it receives data on the
|
15
|
-
# socket that's been opened for this connection. In this case, the method
|
16
|
-
# parses the SSDP notifications into Hashes and adds them to the
|
17
|
-
# appropriate EventMachine::Channel (provided as accessor methods). This
|
18
|
-
# effectively means that in each Channel, you get a Hash that represents
|
19
|
-
# the headers for each notification that comes in on the socket.
|
20
|
-
#
|
21
|
-
# @param [String] response The data received on this connection's socket.
|
22
|
-
def receive_data(response)
|
23
|
-
ip, port = peer_info
|
24
|
-
parsed_response = parse(response)
|
25
|
-
|
26
|
-
return unless parsed_response.has_key? :nts
|
27
|
-
|
28
|
-
if parsed_response[:nts] == 'ssdp:alive'
|
29
|
-
@alive_notifications << parsed_response
|
30
|
-
elsif parsed_response[:nts] == 'ssdp:byebye'
|
31
|
-
@byebye_notifications << parsed_response
|
32
|
-
else
|
33
|
-
raise "Unknown NTS value: #{parsed_response[:nts]}"
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,109 +0,0 @@
|
|
1
|
-
require 'core_ext/socket_patch'
|
2
|
-
require_relative 'network_constants'
|
3
|
-
require_relative 'error'
|
4
|
-
require 'ipaddr'
|
5
|
-
require 'socket'
|
6
|
-
require 'eventmachine'
|
7
|
-
require 'em-synchrony'
|
8
|
-
|
9
|
-
module Druzy
|
10
|
-
module Upnp
|
11
|
-
class SSDP
|
12
|
-
class MulticastConnection < EventMachine::Connection
|
13
|
-
include Druzy::Upnp::SSDP::NetworkConstants
|
14
|
-
|
15
|
-
# @param [Fixnum] ttl The TTL value to use when opening the UDP socket
|
16
|
-
# required for SSDP actions.
|
17
|
-
def initialize(ttl=TTL)
|
18
|
-
@ttl = ttl
|
19
|
-
|
20
|
-
@discovery_responses = EM::Channel.new
|
21
|
-
@alive_notifications = EM::Channel.new
|
22
|
-
@byebye_notifications = EM::Channel.new
|
23
|
-
|
24
|
-
setup_multicast_socket
|
25
|
-
end
|
26
|
-
|
27
|
-
# Gets the IP and port from the peer that just sent data.
|
28
|
-
#
|
29
|
-
# @return [Array<String,Fixnum>] The IP and port.
|
30
|
-
def peer_info
|
31
|
-
peer_bytes = get_peername[2, 6].unpack('nC4')
|
32
|
-
port = peer_bytes.first.to_i
|
33
|
-
ip = peer_bytes[1, 4].join('.')
|
34
|
-
|
35
|
-
[ip, port]
|
36
|
-
end
|
37
|
-
|
38
|
-
# Converts the headers to a set of key-value pairs.
|
39
|
-
#
|
40
|
-
# @param [String] data The data to convert.
|
41
|
-
# @return [Hash] The converted data. Returns an empty Hash if it didn't
|
42
|
-
# know how to parse.
|
43
|
-
def parse(data)
|
44
|
-
new_data = {}
|
45
|
-
|
46
|
-
unless data =~ /\n/
|
47
|
-
return new_data
|
48
|
-
end
|
49
|
-
|
50
|
-
data.each_line do |line|
|
51
|
-
line =~ /(\S+):(.*)/
|
52
|
-
|
53
|
-
unless $1.nil?
|
54
|
-
key = $1
|
55
|
-
value = $2
|
56
|
-
key = key.gsub('-', '_').downcase.to_sym
|
57
|
-
new_data[key] = value.strip
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
new_data
|
62
|
-
end
|
63
|
-
|
64
|
-
# Sets Socket options to allow for multicasting. If ENV["RUBY_UPNP_ENV"] is
|
65
|
-
# equal to "testing", then it doesn't turn off multicast looping.
|
66
|
-
def setup_multicast_socket
|
67
|
-
set_membership(IPAddr.new(MULTICAST_IP).hton +
|
68
|
-
IPAddr.new('0.0.0.0').hton)
|
69
|
-
set_multicast_ttl(@ttl)
|
70
|
-
set_ttl(@ttl)
|
71
|
-
|
72
|
-
unless ENV['RUBY_UPNP_ENV'] == 'testing'
|
73
|
-
switch_multicast_loop :off
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# @param [String] membership The network byte ordered String that represents
|
78
|
-
# the IP(s) that should join the membership group.
|
79
|
-
def set_membership(membership)
|
80
|
-
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
|
81
|
-
end
|
82
|
-
|
83
|
-
# @param [Fixnum] ttl TTL to set IP_MULTICAST_TTL to.
|
84
|
-
def set_multicast_ttl(ttl)
|
85
|
-
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL,
|
86
|
-
[ttl].pack('i'))
|
87
|
-
end
|
88
|
-
|
89
|
-
# @param [Fixnum] ttl TTL to set IP_TTL to.
|
90
|
-
def set_ttl(ttl)
|
91
|
-
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_TTL, [ttl].pack('i'))
|
92
|
-
end
|
93
|
-
|
94
|
-
# @param [Symbol] on_off Turn on/off multicast looping. Supply :on or :off.
|
95
|
-
def switch_multicast_loop(on_off)
|
96
|
-
hex_value = case on_off
|
97
|
-
when :on then "\001"
|
98
|
-
when "\001" then "\001"
|
99
|
-
when :off then "\000"
|
100
|
-
when "\000" then "\000"
|
101
|
-
else raise SSDP::Error, "Can't switch IP_MULTICAST_LOOP to '#{on_off}'"
|
102
|
-
end
|
103
|
-
|
104
|
-
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, hex_value)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module Druzy
|
2
|
-
module Upnp
|
3
|
-
class SSDP
|
4
|
-
module NetworkConstants
|
5
|
-
|
6
|
-
BROADCAST_IP = '255.255.255.255'
|
7
|
-
|
8
|
-
# Default multicast IP address
|
9
|
-
MULTICAST_IP = '239.255.255.250'
|
10
|
-
|
11
|
-
# Default multicast port
|
12
|
-
MULTICAST_PORT = 1900
|
13
|
-
|
14
|
-
# Default TTL
|
15
|
-
TTL = 4
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
require_relative 'multicast_connection'
|
2
|
-
|
3
|
-
|
4
|
-
class Druzy::Upnp::SSDP::Notifier < Druzy::Upnp::SSDP::MulticastConnection
|
5
|
-
|
6
|
-
def initialize(nt, usn, ddf_url, valid_for_duration)
|
7
|
-
@os = RbConfig::CONFIG['host_vendor'].capitalize + '/' +
|
8
|
-
RbConfig::CONFIG['host_os']
|
9
|
-
@upnp_version = '1.0'
|
10
|
-
@notification = notification(nt, usn, ddf_url, valid_for_duration)
|
11
|
-
end
|
12
|
-
|
13
|
-
def post_init
|
14
|
-
if send_datagram(@notification, MULTICAST_IP, MULTICAST_PORT) > 0
|
15
|
-
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# @param [String] nt "Notification Type"; a potential search target. Used in
|
20
|
-
# +NT+ header.
|
21
|
-
# @param [String] usn "Unique Service Name"; a composite identifier for the
|
22
|
-
# advertisement. Used in +USN+ header.
|
23
|
-
# @param [String] ddf_url Device Description File URL for the root device.
|
24
|
-
# @param [Fixnum] valid_for_duration Duration in seconds for which the
|
25
|
-
# advertisement is valid. Used in +CACHE-CONTROL+ header.
|
26
|
-
def notification(nt, usn, ddf_url, valid_for_duration)
|
27
|
-
<<-NOTIFICATION
|
28
|
-
NOTIFY * HTTP/1.1\r
|
29
|
-
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
30
|
-
CACHE-CONTROL: max-age=#{valid_for_duration}\r
|
31
|
-
LOCATION: #{ddf_url}\r
|
32
|
-
NT: #{nt}\r
|
33
|
-
NTS: ssdp:alive\r
|
34
|
-
SERVER: #{@os} UPnP/#{@upnp_version} Playful/#{Playful::VERSION}\r
|
35
|
-
USN: #{usn}\r
|
36
|
-
\r
|
37
|
-
NOTIFICATION
|
38
|
-
end
|
39
|
-
end
|
@@ -1,85 +0,0 @@
|
|
1
|
-
require_relative 'multicast_connection'
|
2
|
-
|
3
|
-
module Druzy
|
4
|
-
module Upnp
|
5
|
-
|
6
|
-
# A subclass of an EventMachine::Connection, this handles doing M-SEARCHes.
|
7
|
-
#
|
8
|
-
# Search types:
|
9
|
-
# ssdp:all
|
10
|
-
# upnp:rootdevice
|
11
|
-
# uuid:[device-uuid]
|
12
|
-
# urn:schemas-upnp-org:device:[deviceType-version]
|
13
|
-
# urn:schemas-upnp-org:service:[serviceType-version]
|
14
|
-
# urn:[custom-schema]:device:[deviceType-version]
|
15
|
-
# urn:[custom-schema]:service:[serviceType-version]
|
16
|
-
class SSDP::Searcher < SSDP::MulticastConnection
|
17
|
-
|
18
|
-
DEFAULT_RESPONSE_WAIT_TIME = 5
|
19
|
-
DEFAULT_M_SEARCH_COUNT = 2
|
20
|
-
|
21
|
-
# @return [EventMachine::Channel] Provides subscribers with responses from
|
22
|
-
# their search request.
|
23
|
-
attr_reader :discovery_responses
|
24
|
-
|
25
|
-
# @param [String] search_target
|
26
|
-
# @param [Hash] options
|
27
|
-
# @option options [Fixnum] response_wait_time
|
28
|
-
# @option options [Fixnum] ttl
|
29
|
-
# @option options [Fixnum] m_search_count The number of times to send the
|
30
|
-
# M-SEARCH. UPnP 1.0 suggests to send the request more than once.
|
31
|
-
def initialize(search_target, options = {})
|
32
|
-
options[:ttl] ||= TTL
|
33
|
-
options[:response_wait_time] ||= DEFAULT_RESPONSE_WAIT_TIME
|
34
|
-
@m_search_count = options[:m_search_count] ||= DEFAULT_M_SEARCH_COUNT
|
35
|
-
|
36
|
-
@search = m_search(search_target, options[:response_wait_time])
|
37
|
-
|
38
|
-
super options[:ttl]
|
39
|
-
end
|
40
|
-
|
41
|
-
# This is the callback called by EventMachine when it receives data on the
|
42
|
-
# socket that's been opened for this connection. In this case, the method
|
43
|
-
# parses the SSDP responses/notifications into Hashes and adds them to the
|
44
|
-
# appropriate EventMachine::Channel (provided as accessor methods). This
|
45
|
-
# effectively means that in each Channel, you get a Hash that represents
|
46
|
-
# the headers for each response/notification that comes in on the socket.
|
47
|
-
#
|
48
|
-
# @param [String] response The data received on this connection's socket.
|
49
|
-
def receive_data(response)
|
50
|
-
ip, port = peer_info
|
51
|
-
parsed_response = parse(response)
|
52
|
-
|
53
|
-
return if parsed_response.has_key? :nts
|
54
|
-
return if parsed_response[:man] &&
|
55
|
-
parsed_response[:man] =~ /ssdp:discover/
|
56
|
-
|
57
|
-
@discovery_responses << parsed_response
|
58
|
-
end
|
59
|
-
|
60
|
-
# Sends the M-SEARCH that was built during init. Logs what was sent if the
|
61
|
-
# send was successful.
|
62
|
-
def post_init
|
63
|
-
@m_search_count.times do
|
64
|
-
send_datagram(@search, MULTICAST_IP, MULTICAST_PORT)
|
65
|
-
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# Builds the M-SEARCH request string.
|
70
|
-
#
|
71
|
-
# @param [String] search_target
|
72
|
-
# @param [Fixnum] response_wait_time
|
73
|
-
def m_search(search_target, response_wait_time)
|
74
|
-
<<-MSEARCH
|
75
|
-
M-SEARCH * HTTP/1.1\r
|
76
|
-
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
77
|
-
MAN: "ssdp:discover"\r
|
78
|
-
MX: #{response_wait_time}\r
|
79
|
-
ST: #{search_target}\r
|
80
|
-
\r
|
81
|
-
MSEARCH
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
@@ -1,70 +0,0 @@
|
|
1
|
-
require 'rack'
|
2
|
-
require 'druzy/upnp/control_point'
|
3
|
-
|
4
|
-
module Rack
|
5
|
-
|
6
|
-
# Middleware that allows your Rack app to keep tabs on devices that the
|
7
|
-
# {Playful::ControlPoint} has found. UPnP devices that match the +search_type+
|
8
|
-
# are discovered and added to the list, then removed as those devices send out
|
9
|
-
# +ssdp:byebye+ notifications. All of this depends on +EventMachine::Channel+s,
|
10
|
-
# and thus requires that an EventMachine reactor is running. If you don't
|
11
|
-
# have one running, the {Playful::ControlPoint} will start one for you.
|
12
|
-
#
|
13
|
-
# @example Control all root devices
|
14
|
-
#
|
15
|
-
# Thin::Server.start('0.0.0.0', 3000) do
|
16
|
-
# use Rack::UPnPControlPoint, search_type: :root
|
17
|
-
#
|
18
|
-
# map "/devices" do
|
19
|
-
# run lambda { |env|
|
20
|
-
# devices = env['upnp.devices']
|
21
|
-
# friendly_names = devices.map(&:friendly_name).join("\n")
|
22
|
-
# [200, {'Content-Type' => 'text/plain'}, [friendly_names]]
|
23
|
-
# }
|
24
|
-
# end
|
25
|
-
# end
|
26
|
-
#
|
27
|
-
class UPnPControlPoint
|
28
|
-
|
29
|
-
# @param [Rack::Builder] app Your Rack application.
|
30
|
-
# @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
|
31
|
-
# @see Playful::SSDP::Searcher
|
32
|
-
def initialize(app, options = {})
|
33
|
-
@app = app
|
34
|
-
@devices = []
|
35
|
-
options[:search_type] ||= :root
|
36
|
-
EM.next_tick { start_control_point(options[:search_type], options) }
|
37
|
-
end
|
38
|
-
|
39
|
-
# Creates and starts the {Playful::ControlPoint}, then manages the list of
|
40
|
-
# devices using the +EventMachine::Channel+ objects yielded in.
|
41
|
-
#
|
42
|
-
# @param [Symbol,String] search_type The device(s) you want to search for
|
43
|
-
# and control.
|
44
|
-
# @param [Hash] options Options to pass to the Playful::SSDP::Searcher.
|
45
|
-
# @see Playful::SSDP::Searcher
|
46
|
-
def start_control_point(search_type, options)
|
47
|
-
@cp = ::Playful::ControlPoint.new(search_type, options)
|
48
|
-
|
49
|
-
@cp.start do |new_device_channel, old_device_channel|
|
50
|
-
new_device_channel.subscribe do |notification|
|
51
|
-
@devices << notification
|
52
|
-
end
|
53
|
-
|
54
|
-
old_device_channel.subscribe do |old_device|
|
55
|
-
@devices.reject! { |d| d.usn == old_device[:usn] }
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
end
|
60
|
-
|
61
|
-
# Adds the whole list of devices to <tt>env['upnp.devices']</tt> so that
|
62
|
-
# that list can be accessed from within your app.
|
63
|
-
#
|
64
|
-
# @param [Hash] env The Rack environment.
|
65
|
-
def call(env)
|
66
|
-
env['upnp.devices'] = @devices
|
67
|
-
@app.call(env)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|