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.
@@ -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