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