rupnp 0.1.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.
- data/MIT-LICENSE +34 -0
- data/README.md +45 -0
- data/Rakefile +3 -0
- data/bin/discover +4 -0
- data/lib/rupnp/constants.rb +31 -0
- data/lib/rupnp/control_point.rb +173 -0
- data/lib/rupnp/cp/base.rb +67 -0
- data/lib/rupnp/cp/event_server.rb +52 -0
- data/lib/rupnp/cp/event_subscriber.rb +49 -0
- data/lib/rupnp/cp/remote_device.rb +271 -0
- data/lib/rupnp/cp/remote_service.rb +304 -0
- data/lib/rupnp/discover.rb +55 -0
- data/lib/rupnp/event.rb +32 -0
- data/lib/rupnp/log_mixin.rb +30 -0
- data/lib/rupnp/ssdp/http.rb +45 -0
- data/lib/rupnp/ssdp/listener.rb +45 -0
- data/lib/rupnp/ssdp/msearch_responder.rb +19 -0
- data/lib/rupnp/ssdp/multicast_connection.rb +48 -0
- data/lib/rupnp/ssdp/notifier.rb +93 -0
- data/lib/rupnp/ssdp/search_responder.rb +115 -0
- data/lib/rupnp/ssdp/searcher.rb +69 -0
- data/lib/rupnp/ssdp/usearch_responder.rb +29 -0
- data/lib/rupnp/ssdp.rb +47 -0
- data/lib/rupnp/tools.rb +66 -0
- data/lib/rupnp.rb +62 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/ssdp/listener_spec.rb +102 -0
- data/spec/ssdp/notifier_spec.rb +61 -0
- data/spec/ssdp/searcher_spec.rb +53 -0
- data/tasks/gem.rake +39 -0
- data/tasks/spec.rake +3 -0
- data/tasks/yard.rake +6 -0
- metadata +209 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module RUPNP
|
2
|
+
module SSDP
|
3
|
+
|
4
|
+
# HTTP module to provide some helper methods
|
5
|
+
# @author Sylvain Daubert
|
6
|
+
module SSDP::HTTP
|
7
|
+
|
8
|
+
# Return status from HTTP response
|
9
|
+
# @param [IO] sock
|
10
|
+
# @return [Booelan]
|
11
|
+
def is_http_status_ok?(sock)
|
12
|
+
sock.readline =~ /\s*HTTP\/1.1 200 OK\r\n\z/i
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get HTTP headers from response
|
16
|
+
# @param [IO] sock
|
17
|
+
# @return [Hash] keys are downcase header name strings
|
18
|
+
def get_http_headers(sock)
|
19
|
+
headers = {}
|
20
|
+
sock.each_line do |l|
|
21
|
+
l =~ /([\w\.-]+):\s*(.*)/
|
22
|
+
if $1
|
23
|
+
headers[$1.downcase] = $2.strip
|
24
|
+
end
|
25
|
+
end
|
26
|
+
headers
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get HTTP verb from HTTP request
|
30
|
+
# @param [IO] sock
|
31
|
+
# @return [nil,Hash] keys are +:verb+, +:path+, +:http_version+ and
|
32
|
+
# +:cmd+ (all line)
|
33
|
+
def get_http_verb(sock)
|
34
|
+
str = sock.readline
|
35
|
+
if str =~ /([\w-]+)\s+(.*)\s+HTTP\/(\d\.\d)/
|
36
|
+
{:verb => $1, :path => $2, :http_version => $3, :cmd => str}
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# Listener class for listening for devices' notifications
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class SSDP::Listener < SSDP::MulticastConnection
|
6
|
+
include SSDP::HTTP
|
7
|
+
|
8
|
+
# Channel to receive notifications
|
9
|
+
# @return [EM::Channel]
|
10
|
+
attr_reader :notifications
|
11
|
+
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Integer] :ttl
|
14
|
+
def initialize(options={})
|
15
|
+
@notifications = EM::Channel.new
|
16
|
+
|
17
|
+
super options[:ttl]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @private
|
21
|
+
def receive_data(data)
|
22
|
+
port, ip = peer_info
|
23
|
+
log :info, "Receive notification from #{ip}:#{port}"
|
24
|
+
log :debug, data
|
25
|
+
|
26
|
+
io = StringIO.new(data)
|
27
|
+
h = get_http_verb(io)
|
28
|
+
|
29
|
+
if h.nil?
|
30
|
+
log :warn, "No HTTP command"
|
31
|
+
return
|
32
|
+
elsif h[:verb] == 'M-SEARCH'
|
33
|
+
return
|
34
|
+
elsif !(h[:verb].upcase == 'NOTIFY' and h[:path] == '*' and
|
35
|
+
h[:http_version] == '1.1')
|
36
|
+
log :warn, "Unknown HTTP command: #{h[:cmd]}"
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
@notifications << get_http_headers(io)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# M-SEARCH responder for M-SEARCH multicast requests from control points.
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class SSDP::MSearchResponder < SSDP::MulticastConnection
|
6
|
+
include HTTP
|
7
|
+
include SSDP::SearchResponder
|
8
|
+
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option options [Integer] :ttl
|
11
|
+
def initialize(device, options={})
|
12
|
+
@device = device
|
13
|
+
@options = options
|
14
|
+
super options[:ttl]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ipaddr'
|
3
|
+
|
4
|
+
module RUPNP
|
5
|
+
|
6
|
+
# Base class for multicast connections (mainly SSDP search and listen)
|
7
|
+
# @abstract
|
8
|
+
class SSDP::MulticastConnection < EM::Connection
|
9
|
+
include LogMixin
|
10
|
+
|
11
|
+
# @param [Integer] ttl
|
12
|
+
def initialize(ttl=nil)
|
13
|
+
@ttl = ttl || DEFAULT_TTL
|
14
|
+
setup_multicast_socket
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get peer info
|
18
|
+
# @return [Array] [port, hostname]
|
19
|
+
def peer_info
|
20
|
+
Socket.unpack_sockaddr_in(get_peername)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def setup_multicast_socket
|
27
|
+
set_membership IPAddr.new(MULTICAST_IP).hton + IPAddr.new('0.0.0.0').hton
|
28
|
+
set_ttl
|
29
|
+
set_reuse_addr
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_membership(value)
|
33
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, value
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_ttl
|
37
|
+
value = [@ttl].pack('i')
|
38
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, value
|
39
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_TTL, value
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_reuse_addr
|
43
|
+
set_sock_opt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
module RUPNP
|
3
|
+
|
4
|
+
# Searcher class for searching devices
|
5
|
+
# @author Sylvain Daubert
|
6
|
+
class SSDP::Notifier < SSDP::MulticastConnection
|
7
|
+
include SSDP::HTTP
|
8
|
+
|
9
|
+
# Number of SEARCH datagrams to send
|
10
|
+
DEFAULT_NOTIFY_TRY = 2
|
11
|
+
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Integer] :try_number
|
14
|
+
# @option options [Integer] :ttl
|
15
|
+
# @option options [String] :ip
|
16
|
+
def initialize(type, subtype, options={})
|
17
|
+
@type = (type == :root) ? 'upnp:rootdevice' : type
|
18
|
+
@subtype = subtype
|
19
|
+
@notify_count = options[:try_number] || DEFAULT_NOTIFY_TRY
|
20
|
+
@options = options
|
21
|
+
|
22
|
+
super options.delete(:ttl)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
26
|
+
def post_init
|
27
|
+
notify = notify_request
|
28
|
+
@notify_count.times do
|
29
|
+
send_datagram notify, MULTICAST_IP, DISCOVERY_PORT
|
30
|
+
log :debug, "#{self.class}: send datagram:\n#{notify}"
|
31
|
+
end
|
32
|
+
close_connection_after_writing
|
33
|
+
end
|
34
|
+
|
35
|
+
# @private
|
36
|
+
def receive_data(data)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def notify_request
|
43
|
+
usn = if @type[0..3] == 'uuid'
|
44
|
+
@type
|
45
|
+
else
|
46
|
+
"uuid:#{@options[:uuid]}::#@type"
|
47
|
+
end
|
48
|
+
case @subtype
|
49
|
+
when :alive
|
50
|
+
<<EOD
|
51
|
+
NOTIFY * HTTP/1.1\r
|
52
|
+
HOST: #{MULTICAST_IP}:#{DISCOVERY_PORT}\r
|
53
|
+
CACHE-CONTROL: max-age = #{@options[:max_age]}\r
|
54
|
+
LOCATION: http://#{@options[:ip]}:#{@options[:port]}/root_description.xml\r
|
55
|
+
NT: #@type\r
|
56
|
+
NTS: ssdp:#@subtype\r
|
57
|
+
SERVER: #{USER_AGENT}\r
|
58
|
+
USN: #{usn}\r
|
59
|
+
BOOTID.UPNP.ORG: #{@options[:boot_id]}\r
|
60
|
+
CONFIGID.UPNP.ORG: #{@options[:config_id]}\r
|
61
|
+
SEARCHPORT.UPNP.ORG: #{@options[:u_search_port]}\r
|
62
|
+
\r
|
63
|
+
EOD
|
64
|
+
when :byebye
|
65
|
+
<<EOD
|
66
|
+
NOTIFY * HTTP/1.1\r
|
67
|
+
HOST: #{MULTICAST_IP}:#{DISCOVERY_PORT}\r
|
68
|
+
NT: #@type\r
|
69
|
+
NTS: ssdp:#@subtype\r
|
70
|
+
USN: #{usn}\r
|
71
|
+
BOOTID.UPNP.ORG: #{@options[:boot_id]}\r
|
72
|
+
CONFIGID.UPNP.ORG: #{@options[:config_id]}\r
|
73
|
+
\r
|
74
|
+
EOD
|
75
|
+
when :update
|
76
|
+
<<EOD
|
77
|
+
NOTIFY * HTTP/1.1\r
|
78
|
+
HOST: #{MULTICAST_IP}:#{DISCOVERY_PORT}\r
|
79
|
+
LOCATION: http://#{@options[:ip]}:#{@options[:port]}/root_description.xml\r
|
80
|
+
NT: #@type\r
|
81
|
+
NTS: ssdp:#@subtype\r
|
82
|
+
USN: #{usn}\r
|
83
|
+
BOOTID.UPNP.ORG: #{(@options[:boot_id] - 1) % 2**31}\r
|
84
|
+
CONFIGID.UPNP.ORG: #{@options[:config_id]}\r
|
85
|
+
NEXTBOOTID.UPNP.ORG: #{@options[:boot_id]}\r
|
86
|
+
SEARCHPORT.UPNP.ORG: #{@options[:u_search_port]}\r
|
87
|
+
\r
|
88
|
+
EOD
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# M-SEARCH responder for M-SEARCH multicast requests from control points.
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
module SSDP::SearchResponder
|
6
|
+
include SSDP::HTTP
|
7
|
+
|
8
|
+
def receive_data(data)
|
9
|
+
port, ip = peer_info
|
10
|
+
log :debug, "#{self.class}: Receive data from #{ip}:#{port}"
|
11
|
+
log :debug, data
|
12
|
+
|
13
|
+
io = StringIO.new(data)
|
14
|
+
h = get_http_verb(io)
|
15
|
+
|
16
|
+
if h[:verb].upcase == 'NOTIFY'
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
if h.nil? or !(h[:verb].upcase == 'M-SEARCH' and h[:path] == '*' and
|
21
|
+
h[:http_version] == '1.1')
|
22
|
+
log :warn, "#{self.class}: Unknown HTTP command: #{h[:cmd]}"
|
23
|
+
return
|
24
|
+
end
|
25
|
+
|
26
|
+
h = get_http_headers(io)
|
27
|
+
if h['man'] != '"ssdp:discover"'
|
28
|
+
log :warn, "#{self.class}: Unknown MAN field: #{h['man']}"
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
log :info, "#{self.class}: Receive M-SEARCH request from #{ip}:#{port}"
|
33
|
+
|
34
|
+
callback = nil
|
35
|
+
case h['st']
|
36
|
+
when 'ssdp:all'
|
37
|
+
callback = Proc.new do
|
38
|
+
send_response 'upnp:rootdevice'
|
39
|
+
send_response "uuid:#{@device.uuid}"
|
40
|
+
send_response "urn:#{@device.urn}"
|
41
|
+
@device.services.each do |s|
|
42
|
+
send_response "urn:#{s.urn}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
when 'upnp:rootdevice'
|
46
|
+
callback = Proc.new { send_response 'upnp:rootdevice' }
|
47
|
+
when /^uuid:([0-9a-fA-F-]+)/
|
48
|
+
if $1 and $1 == @device.uuid
|
49
|
+
callback = Proc.new { send_response "uuid:#{@device.uuid}" }
|
50
|
+
end
|
51
|
+
when /^urn:schemas-upnp-org:(\w+):(\w+):(\w+)/
|
52
|
+
case $1
|
53
|
+
when 'device'
|
54
|
+
if urn_are_equivalent?(h['st'], @device.urn)
|
55
|
+
callback = Proc.new { send_response "urn:#{@device.urn}" }
|
56
|
+
end
|
57
|
+
when 'service'
|
58
|
+
if @device.services.one? { |s| urn_are_equivalent? h['st'], s.urn }
|
59
|
+
callback = Proc.new { send_response h['st'] }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if callback
|
65
|
+
if self.is_a? SSDP::MulticastConnection
|
66
|
+
if h['mx']
|
67
|
+
mx = h['mx'].to_i
|
68
|
+
# MX MUST not be greater than 5
|
69
|
+
mx = 5 if mx > 5
|
70
|
+
# Wait for a random time less than MX
|
71
|
+
wait_time = rand(mx)
|
72
|
+
EM.add_timer wait_time, &callback
|
73
|
+
else
|
74
|
+
log :warn, "#{self.class}: Multicast M-SEARCH request with no MX" +
|
75
|
+
" field. Discarded."
|
76
|
+
end
|
77
|
+
else
|
78
|
+
# Unicast request. Don't bother for MX field.
|
79
|
+
callback.call
|
80
|
+
end
|
81
|
+
else
|
82
|
+
log :debug, 'No response sent'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def send_response(st)
|
88
|
+
usn = "uuid:#{@device.uuid}"
|
89
|
+
usn += case st
|
90
|
+
when 'upnp:rootdevice', /^urn/
|
91
|
+
"::#{st}"
|
92
|
+
else
|
93
|
+
''
|
94
|
+
end
|
95
|
+
response =<<EOR
|
96
|
+
HTTP/1.1 200 OK\r
|
97
|
+
CACHE-CONTROL: max-age = #{@options[:max_age]}\r
|
98
|
+
DATE: #{Time.now.httpdate}\r
|
99
|
+
EXT:\r
|
100
|
+
LOCATION: http://#{@options[:ip]}/root_description.xml\r
|
101
|
+
SERVER: #{USER_AGENT}\r
|
102
|
+
ST: #{st}
|
103
|
+
USN: #{usn}\r
|
104
|
+
BOOTID.UPNP.ORG: #{@options[:boot_id]}\r
|
105
|
+
CONFIGID.UPNP.ORG: #{@options[:config_id]}\r
|
106
|
+
SEARCHPORT.UPNP.ORG: #{@options[:u_search_port]}\r
|
107
|
+
\r
|
108
|
+
EOR
|
109
|
+
|
110
|
+
send_data response
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
module RUPNP
|
3
|
+
|
4
|
+
# Searcher class for searching devices
|
5
|
+
# @author Sylvain Daubert
|
6
|
+
class SSDP::Searcher < SSDP::MulticastConnection
|
7
|
+
include SSDP::HTTP
|
8
|
+
|
9
|
+
# Number of SEARCH datagrams to send
|
10
|
+
DEFAULT_M_SEARCH_TRY = 2
|
11
|
+
|
12
|
+
# Channel to receive discovery responses
|
13
|
+
# @return [EM::Channel]
|
14
|
+
attr_reader :discovery_responses
|
15
|
+
|
16
|
+
|
17
|
+
# @param [Hash] options
|
18
|
+
# @option options [String] :search_target
|
19
|
+
# @option options [Integer] :response_wait_time
|
20
|
+
# @option options [Integer] :try_number
|
21
|
+
# @option options [Integer] :ttl
|
22
|
+
def initialize(options={})
|
23
|
+
@options = options
|
24
|
+
@options[:try_number] ||= DEFAULT_M_SEARCH_TRY
|
25
|
+
@discovery_responses = EM::Channel.new
|
26
|
+
|
27
|
+
super options[:ttl]
|
28
|
+
end
|
29
|
+
|
30
|
+
# @private
|
31
|
+
def post_init
|
32
|
+
search = search_request
|
33
|
+
@options[:try_number].times do
|
34
|
+
send_datagram search, MULTICAST_IP, DISCOVERY_PORT
|
35
|
+
log :debug, "send datagram:\n#{search}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @private
|
40
|
+
def receive_data(data)
|
41
|
+
port, ip = peer_info
|
42
|
+
log :debug, "Response from #{ip}:#{port}"
|
43
|
+
|
44
|
+
response = StringIO.new(data)
|
45
|
+
if !is_http_status_ok?(response)
|
46
|
+
log :error, "bad HTTP response:\n #{data}"
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
@discovery_responses << get_http_headers(response)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def search_request
|
57
|
+
<<EOR
|
58
|
+
M-SEARCH * HTTP/1.1\r
|
59
|
+
HOST: #{MULTICAST_IP}:#{DISCOVERY_PORT}\r
|
60
|
+
MAN: "ssdp:discover"\r
|
61
|
+
MX: #{@options[:response_wait_time]}\r
|
62
|
+
ST: #{@options[:search_target]}\r
|
63
|
+
USER-AGENT: #{USER_AGENT}\r
|
64
|
+
\r
|
65
|
+
EOR
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module RUPNP
|
4
|
+
|
5
|
+
# M-SEARCH responder for M-SEARCH unicast requests from control points.
|
6
|
+
# @author Sylvain Daubert
|
7
|
+
class SSDP::USearchResponder < EM::Connection
|
8
|
+
include HTTP
|
9
|
+
include SSDP::SearchResponder
|
10
|
+
|
11
|
+
# @param [Hash] options
|
12
|
+
# @option options [Integer] :ttl
|
13
|
+
def initialize(device, options={})
|
14
|
+
@device = device
|
15
|
+
@options = options
|
16
|
+
set_ttl options[:ttl] || DEFAULT_TTL
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def set_ttl(ttl)
|
23
|
+
value = [ttl].pack('i')
|
24
|
+
set_sock_opt Socket::IPPROTO_IP, Socket::IP_TTL, value
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/rupnp/ssdp.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'ssdp/http'
|
2
|
+
require_relative 'ssdp/multicast_connection'
|
3
|
+
require_relative 'ssdp/searcher'
|
4
|
+
require_relative 'ssdp/listener'
|
5
|
+
require_relative 'ssdp/notifier'
|
6
|
+
require_relative 'ssdp/search_responder.rb'
|
7
|
+
require_relative 'ssdp/msearch_responder.rb'
|
8
|
+
require_relative 'ssdp/usearch_responder.rb'
|
9
|
+
|
10
|
+
|
11
|
+
module RUPNP
|
12
|
+
|
13
|
+
# SSDP module. This a discovery part of UPnP.
|
14
|
+
# @author Sylvain Daubert
|
15
|
+
module SSDP
|
16
|
+
|
17
|
+
# Some shorcut for common targets
|
18
|
+
KNOWN_TARGETS = {
|
19
|
+
:all => 'ssdp:all',
|
20
|
+
:root => 'upnp:rootdevice'
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
# Search devices
|
25
|
+
# @param [Symbol,String] target
|
26
|
+
# @param [Hash] options see {SSDP::Searcher#initialize}
|
27
|
+
def self.search(target=:all, options={})
|
28
|
+
options[:search_target] = KNOWN_TARGETS[target] || target
|
29
|
+
EM.open_datagram_socket '0.0.0.0', 0, SSDP::Searcher, options
|
30
|
+
end
|
31
|
+
|
32
|
+
# Listen for devices' announces
|
33
|
+
# @param [Hash] options see {SSDP::Listener#initialize}
|
34
|
+
def self.listen(options={})
|
35
|
+
EM.open_datagram_socket(MULTICAST_IP, DISCOVERY_PORT,
|
36
|
+
SSDP::Listener, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Notify announces
|
40
|
+
# @param [Hash] options see {SSDP::Notifier#initialize}
|
41
|
+
def self.notify(type, stype, options={})
|
42
|
+
EM.open_datagram_socket(MULTICAST_IP, DISCOVERY_PORT,
|
43
|
+
SSDP::Notifier, type, stype, options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/lib/rupnp/tools.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# Helper module
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
module Tools
|
6
|
+
|
7
|
+
# Build an url from a base and a relative url
|
8
|
+
# @param [String] base
|
9
|
+
# @param [String] rest
|
10
|
+
# @return [String]
|
11
|
+
def build_url(base, rest)
|
12
|
+
url = base + (base.end_with?('/') ? '' : '/')
|
13
|
+
url + (rest.start_with?('/') ? rest[1..-1] : rest)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Convert a camel cased string to a snake cased one
|
17
|
+
# snake_case("iconList") # => "icon_list"
|
18
|
+
# snake_case("eventSubURL") # => "event_sub_url"
|
19
|
+
# @param [String] str
|
20
|
+
# @return [String]
|
21
|
+
def snake_case(str)
|
22
|
+
g = str.gsub(/([^A-Z_])([A-Z])/,'\1_\2')
|
23
|
+
g.downcase || str.downcase
|
24
|
+
end
|
25
|
+
|
26
|
+
# Check if two URN are equivalent. They are equivalent if they have
|
27
|
+
# the same name and the same major version.
|
28
|
+
# @param [String] urn1
|
29
|
+
# @param [String] urn2
|
30
|
+
# @return [Boolean]
|
31
|
+
def urn_are_equivalent?(urn1, urn2)
|
32
|
+
u1 = urn1
|
33
|
+
if urn1[0..3] == 'urn:'
|
34
|
+
u1 = urn1[4..-1]
|
35
|
+
end
|
36
|
+
u2 = urn2
|
37
|
+
if urn2[0..3] == 'urn:'
|
38
|
+
u2 = urn2[4..-1]
|
39
|
+
end
|
40
|
+
|
41
|
+
m1 = u1.match(/(\w+):(\w+):(\w+):([\d-]+)/)
|
42
|
+
m2 = u2.match(/(\w+):(\w+):(\w+):([\d-]+)/)
|
43
|
+
if m1[1] == m2[1]
|
44
|
+
if m1[2] == m2[2]
|
45
|
+
if m1[3] == m2[3]
|
46
|
+
v1_major = m1[4].split(/-/).first
|
47
|
+
v2_major = m2[4].split(/-/).first
|
48
|
+
if v1_major == v2_major
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
# Retrieve UDN from USN
|
58
|
+
# @param [String] usn
|
59
|
+
# @return [String]
|
60
|
+
def usn2udn(usn)
|
61
|
+
rex = '([A-Fa-f0-9]{8,8}(-[A-Fa-f0-9]{4,4}){3,3}-[A-Fa-f0-9]{12,12})'
|
62
|
+
usn.match(/#{rex}/)[1]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/lib/rupnp.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'awesome_print'
|
2
|
+
require 'eventmachine-le'
|
3
|
+
|
4
|
+
# Module for RUPNP namespace
|
5
|
+
# @author Sylvain Daubert
|
6
|
+
module RUPNP
|
7
|
+
|
8
|
+
# RUPNP version
|
9
|
+
VERSION = '0.1.0'
|
10
|
+
|
11
|
+
@logdev = STDERR
|
12
|
+
@log_level = :info
|
13
|
+
|
14
|
+
# Set log device
|
15
|
+
# @param [IO,String] io_or_string io or filename to log to
|
16
|
+
# @return [IO]
|
17
|
+
def self.logdev=(io_or_string)
|
18
|
+
if io_or_string.is_a? String
|
19
|
+
@logdev = File.open(io_or_string, 'w')
|
20
|
+
else
|
21
|
+
@logdev = io_or_string
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get log device
|
26
|
+
# @return [IO] io used to log
|
27
|
+
def self.logdev
|
28
|
+
@logdev
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set log level
|
32
|
+
# @param [:debug,:info,:warn,:error] lvl
|
33
|
+
def self.log_level=(lvl)
|
34
|
+
@log_level = lvl
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get log level
|
38
|
+
# @return [Symbol]
|
39
|
+
def self.log_level
|
40
|
+
@log_level
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Base class for RUPNP errors.
|
45
|
+
class Error < StandardError; end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
require_relative 'rupnp/constants'
|
51
|
+
require_relative 'rupnp/tools'
|
52
|
+
require_relative 'rupnp/log_mixin'
|
53
|
+
require_relative 'rupnp/event'
|
54
|
+
require_relative 'rupnp/control_point'
|
55
|
+
require_relative 'rupnp/cp/base'
|
56
|
+
require_relative 'rupnp/cp/remote_service'
|
57
|
+
require_relative 'rupnp/cp/remote_device'
|
58
|
+
require_relative 'rupnp/cp/event_server'
|
59
|
+
require_relative 'rupnp/cp/event_subscriber'
|
60
|
+
require_relative 'rupnp/ssdp'
|
61
|
+
|
62
|
+
#require_relative 'rupnp/device'
|