druzy-upnp 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|