rupnp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|