frisky 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +23 -0
- data/History.md +3 -0
- data/LICENSE.md +23 -0
- data/README.md +185 -0
- data/Rakefile +16 -0
- data/features/device_discovery.feature +9 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/support/env.rb +21 -0
- data/features/support/world_extensions.rb +7 -0
- data/frisky.gemspec +28 -0
- data/lib/core_ext/hash_patch.rb +5 -0
- data/lib/core_ext/socket_patch.rb +16 -0
- data/lib/core_ext/to_upnp_s.rb +65 -0
- data/lib/frisky.rb +5 -0
- data/lib/frisky/logger.rb +8 -0
- data/lib/frisky/ssdp.rb +188 -0
- data/lib/frisky/ssdp/broadcast_searcher.rb +114 -0
- data/lib/frisky/ssdp/error.rb +6 -0
- data/lib/frisky/ssdp/listener.rb +38 -0
- data/lib/frisky/ssdp/multicast_connection.rb +112 -0
- data/lib/frisky/ssdp/network_constants.rb +17 -0
- data/lib/frisky/ssdp/notifier.rb +41 -0
- data/lib/frisky/ssdp/searcher.rb +87 -0
- data/lib/frisky/version.rb +3 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/search_responses.rb +134 -0
- data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
- data/spec/unit/frisky/ssdp/listener_spec.rb +29 -0
- data/spec/unit/frisky/ssdp/multicast_connection_spec.rb +157 -0
- data/spec/unit/frisky/ssdp/notifier_spec.rb +76 -0
- data/spec/unit/frisky/ssdp/searcher_spec.rb +110 -0
- data/spec/unit/frisky/ssdp_spec.rb +214 -0
- data/tasks/search.thor +35 -0
- metadata +181 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Frisky
|
2
|
+
class SSDP
|
3
|
+
module NetworkConstants
|
4
|
+
|
5
|
+
BROADCAST_IP = '255.255.255.255'
|
6
|
+
|
7
|
+
# Default multicast IP address
|
8
|
+
MULTICAST_IP = '239.255.255.250'
|
9
|
+
|
10
|
+
# Default multicast port
|
11
|
+
MULTICAST_PORT = 1900
|
12
|
+
|
13
|
+
# Default TTL
|
14
|
+
TTL = 4
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../logger'
|
2
|
+
require_relative 'multicast_connection'
|
3
|
+
|
4
|
+
|
5
|
+
class Frisky::SSDP::Notifier < Frisky::SSDP::MulticastConnection
|
6
|
+
include LogSwitch
|
7
|
+
|
8
|
+
def initialize(nt, usn, ddf_url, valid_for_duration)
|
9
|
+
@os = RbConfig::CONFIG['host_vendor'].capitalize + '/' +
|
10
|
+
RbConfig::CONFIG['host_os']
|
11
|
+
@upnp_version = '1.0'
|
12
|
+
@notification = notification(nt, usn, ddf_url, valid_for_duration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def post_init
|
16
|
+
if send_datagram(@notification, MULTICAST_IP, MULTICAST_PORT) > 0
|
17
|
+
log "Sent notification:\n#{@notification}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [String] nt "Notification Type"; a potential search target. Used in
|
22
|
+
# +NT+ header.
|
23
|
+
# @param [String] usn "Unique Service Name"; a composite identifier for the
|
24
|
+
# advertisement. Used in +USN+ header.
|
25
|
+
# @param [String] ddf_url Device Description File URL for the root device.
|
26
|
+
# @param [Fixnum] valid_for_duration Duration in seconds for which the
|
27
|
+
# advertisement is valid. Used in +CACHE-CONTROL+ header.
|
28
|
+
def notification(nt, usn, ddf_url, valid_for_duration)
|
29
|
+
<<-NOTIFICATION
|
30
|
+
NOTIFY * HTTP/1.1\r
|
31
|
+
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
32
|
+
CACHE-CONTROL: max-age=#{valid_for_duration}\r
|
33
|
+
LOCATION: #{ddf_url}\r
|
34
|
+
NT: #{nt}\r
|
35
|
+
NTS: ssdp:alive\r
|
36
|
+
SERVER: #{@os} UPnP/#{@upnp_version} Frisky/#{Frisky::VERSION}\r
|
37
|
+
USN: #{usn}\r
|
38
|
+
\r
|
39
|
+
NOTIFICATION
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative '../logger'
|
2
|
+
require_relative 'multicast_connection'
|
3
|
+
|
4
|
+
module Frisky
|
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
|
+
include LogSwitch
|
18
|
+
|
19
|
+
DEFAULT_RESPONSE_WAIT_TIME = 5
|
20
|
+
DEFAULT_M_SEARCH_COUNT = 2
|
21
|
+
|
22
|
+
# @return [EventMachine::Channel] Provides subscribers with responses from
|
23
|
+
# their search request.
|
24
|
+
attr_reader :discovery_responses
|
25
|
+
|
26
|
+
# @param [String] search_target
|
27
|
+
# @param [Hash] options
|
28
|
+
# @option options [Fixnum] response_wait_time
|
29
|
+
# @option options [Fixnum] ttl
|
30
|
+
# @option options [Fixnum] m_search_count The number of times to send the
|
31
|
+
# M-SEARCH. UPnP 1.0 suggests to send the request more than once.
|
32
|
+
def initialize(search_target, options = {})
|
33
|
+
options[:ttl] ||= TTL
|
34
|
+
options[:response_wait_time] ||= DEFAULT_RESPONSE_WAIT_TIME
|
35
|
+
@m_search_count = options[:m_search_count] ||= DEFAULT_M_SEARCH_COUNT
|
36
|
+
|
37
|
+
@search = m_search(search_target, options[:response_wait_time])
|
38
|
+
|
39
|
+
super options[:ttl]
|
40
|
+
end
|
41
|
+
|
42
|
+
# This is the callback called by EventMachine when it receives data on the
|
43
|
+
# socket that's been opened for this connection. In this case, the method
|
44
|
+
# parses the SSDP responses/notifications into Hashes and adds them to the
|
45
|
+
# appropriate EventMachine::Channel (provided as accessor methods). This
|
46
|
+
# effectively means that in each Channel, you get a Hash that represents
|
47
|
+
# the headers for each response/notification that comes in on the socket.
|
48
|
+
#
|
49
|
+
# @param [String] response The data received on this connection's socket.
|
50
|
+
def receive_data(response)
|
51
|
+
ip, port = peer_info
|
52
|
+
log "Response from #{ip}:#{port}:\n#{response}\n"
|
53
|
+
parsed_response = parse(response)
|
54
|
+
|
55
|
+
return if parsed_response.has_key? :nts
|
56
|
+
return if parsed_response[:man] &&
|
57
|
+
parsed_response[:man] =~ /ssdp:discover/
|
58
|
+
|
59
|
+
@discovery_responses << parsed_response
|
60
|
+
end
|
61
|
+
|
62
|
+
# Sends the M-SEARCH that was built during init. Logs what was sent if the
|
63
|
+
# send was successful.
|
64
|
+
def post_init
|
65
|
+
@m_search_count.times do
|
66
|
+
if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
|
67
|
+
log "Sent datagram search:\n#{@search}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Builds the M-SEARCH request string.
|
73
|
+
#
|
74
|
+
# @param [String] search_target
|
75
|
+
# @param [Fixnum] response_wait_time
|
76
|
+
def m_search(search_target, response_wait_time)
|
77
|
+
<<-MSEARCH
|
78
|
+
M-SEARCH * HTTP/1.1\r
|
79
|
+
HOST: #{MULTICAST_IP}:#{MULTICAST_PORT}\r
|
80
|
+
MAN: "ssdp:discover"\r
|
81
|
+
MX: #{response_wait_time}\r
|
82
|
+
ST: #{search_target}\r
|
83
|
+
\r
|
84
|
+
MSEARCH
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
SimpleCov.start
|
4
|
+
|
5
|
+
require 'coveralls'
|
6
|
+
Coveralls.wear!
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
9
|
+
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
|
11
|
+
|
12
|
+
ENV['RUBY_UPNP_ENV'] = 'testing'
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
#config.raise_errors_for_deprecations!
|
16
|
+
end
|
17
|
+
|
18
|
+
require_relative "../lib/frisky/logger"
|
@@ -0,0 +1,134 @@
|
|
1
|
+
SSDP_SEARCH_RESPONSES_PARSED = {
|
2
|
+
root_device1: {
|
3
|
+
cache_control: 'max-age=1200',
|
4
|
+
date: 'Mon, 26 Sep 2011 06:40:19 GMT',
|
5
|
+
location: 'http://192.168.10.3:5001/description/fetch',
|
6
|
+
server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
|
7
|
+
st: 'upnp:rootdevice',
|
8
|
+
ext: nil,
|
9
|
+
usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice',
|
10
|
+
content_length: 0
|
11
|
+
},
|
12
|
+
root_device2: {
|
13
|
+
cache_control: 'max-age=1200',
|
14
|
+
date: 'Mon, 26 Sep 2011 06:40:20 GMT',
|
15
|
+
location: 'http://192.168.10.4:5001/description/fetch',
|
16
|
+
server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
|
17
|
+
st: 'upnp:rootdevice',
|
18
|
+
ext: nil,
|
19
|
+
usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice',
|
20
|
+
content_length: 0
|
21
|
+
},
|
22
|
+
media_server: {
|
23
|
+
cache_control: 'max-age=1200',
|
24
|
+
date: 'Mon, 26 Sep 2011 06:40:21 GMT',
|
25
|
+
location: 'http://192.168.10.3:5001/description/fetch',
|
26
|
+
server: 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1',
|
27
|
+
st: 'urn:schemas-upnp-org:device:MediaServer:1',
|
28
|
+
ext: nil,
|
29
|
+
usn: 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:1',
|
30
|
+
content_length: 0
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
SSDP_DESCRIPTIONS = {
|
35
|
+
root_device1: {
|
36
|
+
'root' => {
|
37
|
+
'specVersion' => {
|
38
|
+
'major' => '1',
|
39
|
+
'minor' => '0'
|
40
|
+
},
|
41
|
+
'URLBase' => 'http://192.168.10.3:5001/',
|
42
|
+
'device' => {
|
43
|
+
'dlna:X_DLNADOC' => %w[DMS-1.50 M-DMS-1.50],
|
44
|
+
'deviceType' => 'urn:schemas-upnp-org:device:MediaServer:1',
|
45
|
+
'friendlyName' => 'PS3 Media Server [gutenberg]',
|
46
|
+
'manufacturer' => 'PMS',
|
47
|
+
'manufacturerURL' => 'http://ps3mediaserver.blogspot.com',
|
48
|
+
'modelDescription' => 'UPnP/AV 1.0 Compliant Media Server',
|
49
|
+
'modelName' => 'PMS',
|
50
|
+
'modelNumber' => '01',
|
51
|
+
'modelURL' => 'http://ps3mediaserver.blogspot.com',
|
52
|
+
'serialNumber' => nil,
|
53
|
+
'UPC' => nil,
|
54
|
+
'UDN' => 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d',
|
55
|
+
'iconList' => {
|
56
|
+
'icon' => {
|
57
|
+
'mimetype' => 'image/jpeg',
|
58
|
+
'width' => '120',
|
59
|
+
'height' => '120',
|
60
|
+
'depth' => '24',
|
61
|
+
'url' => '/images/icon-256.png'
|
62
|
+
}
|
63
|
+
},
|
64
|
+
'presentationURL' => 'http://192.168.10.3:5001/console/index.html',
|
65
|
+
'serviceList' => {
|
66
|
+
'service' => [
|
67
|
+
{
|
68
|
+
'serviceType' => 'urn:schemas-upnp-org:service:ContentDirectory:1',
|
69
|
+
'serviceId' => 'urn:upnp-org:serviceId:ContentDirectory',
|
70
|
+
'SCPDURL' => '/UPnP_AV_ContentDirectory_1.0.xml',
|
71
|
+
'controlURL' => '/upnp/control/content_directory',
|
72
|
+
'eventSubURL' => '/upnp/event/content_directory'
|
73
|
+
},
|
74
|
+
{
|
75
|
+
'serviceType' => 'urn:schemas-upnp-org:service:ConnectionManager:1',
|
76
|
+
'serviceId' => 'urn:upnp-org:serviceId:ConnectionManager',
|
77
|
+
'SCPDURL' => '/UPnP_AV_ConnectionManager_1.0.xml',
|
78
|
+
'controlURL' => '/upnp/control/connection_manager',
|
79
|
+
'eventSubURL' => '/upnp/event/connection_manager'
|
80
|
+
}
|
81
|
+
]
|
82
|
+
}
|
83
|
+
},
|
84
|
+
'@xmlns:dlna' => 'urn:schemas-dlna-org:device-1-0',
|
85
|
+
'@xmlns' => 'urn:schemas-upnp-org:device-1-0'
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
ROOT_DEVICE1 = <<-RD
|
91
|
+
HTTP/1.1 200 OK
|
92
|
+
CACHE-CONTROL: max-age=1200
|
93
|
+
DATE: Mon, 26 Sep 2011 06:40:19 GMT
|
94
|
+
LOCATION: http://1.2.3.4:5678/description/fetch
|
95
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
|
96
|
+
ST: upnp:rootdevice
|
97
|
+
EXT:
|
98
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice
|
99
|
+
Content-Length: 0
|
100
|
+
|
101
|
+
RD
|
102
|
+
|
103
|
+
ROOT_DEVICE2 = <<-RD
|
104
|
+
HTTP/1.1 200 OK
|
105
|
+
CACHE-CONTROL: max-age=1200
|
106
|
+
DATE: Mon, 26 Sep 2011 06:40:20 GMT
|
107
|
+
LOCATION: http://1.2.3.4:5678/description/fetch
|
108
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
|
109
|
+
ST: upnp:rootdevice
|
110
|
+
EXT:
|
111
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136e::upnp:rootdevice
|
112
|
+
Content-Length: 0
|
113
|
+
|
114
|
+
RD
|
115
|
+
|
116
|
+
MEDIA_SERVER = <<-MD
|
117
|
+
HTTP/1.1 200 OK
|
118
|
+
CACHE-CONTROL: max-age=1200
|
119
|
+
DATE: Mon, 26 Sep 2011 06:40:21 GMT
|
120
|
+
LOCATION: http://1.2.3.4:5678/description/fetch
|
121
|
+
SERVER: Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1
|
122
|
+
ST: urn:schemas-upnp-org:device:MediaServer:1
|
123
|
+
EXT:
|
124
|
+
USN: uuid:3c202906-992d-3f0f-b94c-90e1902a136d::urn:schemas-upnp-org:device:MediaServer:
|
125
|
+
Content-Length: 0
|
126
|
+
|
127
|
+
MD
|
128
|
+
|
129
|
+
RESPONSES = {
|
130
|
+
root_device1: ROOT_DEVICE1,
|
131
|
+
root_device2: ROOT_DEVICE2,
|
132
|
+
media_server: MEDIA_SERVER
|
133
|
+
}
|
134
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'core_ext/to_upnp_s'
|
3
|
+
|
4
|
+
|
5
|
+
describe Hash do
|
6
|
+
describe '#to_upnp_s' do
|
7
|
+
context ':uuid as key' do
|
8
|
+
it "returns a String like 'uuid:[Hash value]'" do
|
9
|
+
expect({ uuid: '12345' }.to_upnp_s).to eq 'uuid:12345'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "doesn't check if the Hash value is legit" do
|
13
|
+
expect({ uuid: '' }.to_upnp_s).to eq 'uuid:'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context ':device_type as key' do
|
18
|
+
context 'domain name not given' do
|
19
|
+
it "returns a String like 'urn:schemas-upnp-org:device:[device type]'" do
|
20
|
+
expect({ device_type: '12345' }.to_upnp_s).
|
21
|
+
to eq 'urn:schemas-upnp-org:device:12345'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "doesn't check if the Hash value is legit" do
|
25
|
+
expect({ device_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:device:'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'domain name given' do
|
30
|
+
it "returns a String like 'urn:[domain name]:device:[device type]'" do
|
31
|
+
expect({ device_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
|
32
|
+
to eq 'urn:my-domain:device:12345'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "doesn't check if the Hash value is legit" do
|
36
|
+
expect({ device_type: '', domain_name: 'stuff' }.to_upnp_s).
|
37
|
+
to eq 'urn:stuff:device:'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context ':service_type as key' do
|
43
|
+
context 'domain name not given' do
|
44
|
+
it "returns a String like 'urn:schemas-upnp-org:service:[service type]'" do
|
45
|
+
expect({ service_type: '12345' }.to_upnp_s).
|
46
|
+
to eq 'urn:schemas-upnp-org:service:12345'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "doesn't check if the Hash value is legit" do
|
50
|
+
expect({ service_type: '' }.to_upnp_s).to eq 'urn:schemas-upnp-org:service:'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'domain name given' do
|
55
|
+
it "returns a String like 'urn:[domain name]:service:[service type]'" do
|
56
|
+
expect({ service_type: '12345', domain_name: 'my-domain' }.to_upnp_s).
|
57
|
+
to eq 'urn:my-domain:service:12345'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "doesn't check if the Hash value is legit" do
|
61
|
+
expect({ service_type: '', domain_name: 'my-domain' }.to_upnp_s).
|
62
|
+
to eq 'urn:my-domain:service:'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'some other Hash key' do
|
68
|
+
context 'domain name not given' do
|
69
|
+
it 'returns self.to_s' do
|
70
|
+
expect({ firestorm: 12345 }.to_upnp_s).to eq '{:firestorm=>12345}'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe Symbol do
|
78
|
+
context ':all' do
|
79
|
+
describe '#to_upnp_s' do
|
80
|
+
it "returns 'ssdp:all'" do
|
81
|
+
expect(:all.to_upnp_s).to eq 'ssdp:all'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context ':root' do
|
87
|
+
describe '#to_upnp_s' do
|
88
|
+
it "returns 'upnp:rootdevice'" do
|
89
|
+
expect(:root.to_upnp_s).to eq 'upnp:rootdevice'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it "returns itself if one of the defined shortcuts wasn't given" do
|
95
|
+
expect(:firestorm.to_upnp_s).to eq :firestorm
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe String do
|
100
|
+
it 'returns itself' do
|
101
|
+
expect('Stuff and things'.to_upnp_s).to eq 'Stuff and things'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'frisky/ssdp/listener'
|
3
|
+
|
4
|
+
|
5
|
+
describe Frisky::SSDP::Listener do
|
6
|
+
around(:each) do |example|
|
7
|
+
EM.synchrony do
|
8
|
+
example.run
|
9
|
+
EM.stop
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
allow_any_instance_of(Frisky::SSDP::Listener).to receive(:setup_multicast_socket)
|
15
|
+
end
|
16
|
+
|
17
|
+
subject { Frisky::SSDP::Listener.new(1) }
|
18
|
+
|
19
|
+
describe '#receive_data' do
|
20
|
+
it 'logs the IP and port from which the request came from' do
|
21
|
+
expect(subject).to receive(:peer_info).and_return %w[ip port]
|
22
|
+
expect(subject).to receive(:log).
|
23
|
+
with("Response from ip:port:\nmessage\n")
|
24
|
+
allow(subject).to receive(:parse).and_return({})
|
25
|
+
|
26
|
+
subject.receive_data('message')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'frisky/ssdp/multicast_connection'
|
3
|
+
|
4
|
+
|
5
|
+
describe Frisky::SSDP::MulticastConnection do
|
6
|
+
around(:each) do |example|
|
7
|
+
EM.synchrony do
|
8
|
+
example.run
|
9
|
+
EM.stop
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
subject { Frisky::SSDP::MulticastConnection.new(1) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
Frisky.logging_enabled = false
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#peer_info' do
|
20
|
+
before do
|
21
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:setup_multicast_socket)
|
22
|
+
subject.stub_chain(:get_peername, :[], :unpack).
|
23
|
+
and_return(%w[1234 1 2 3 4])
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns an Array with IP and port' do
|
27
|
+
expect(subject.peer_info).to eq ['1.2.3.4', 1234]
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'returns IP as a String' do
|
31
|
+
expect(subject.peer_info.first).to be_a String
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns port as a Fixnum' do
|
35
|
+
expect(subject.peer_info.last).to be_a Fixnum
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#parse' do
|
40
|
+
before do
|
41
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:setup_multicast_socket)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'turns headers into Hash keys' do
|
45
|
+
result = subject.parse ROOT_DEVICE1
|
46
|
+
expect(result).to have_key :cache_control
|
47
|
+
expect(result).to have_key :date
|
48
|
+
expect(result).to have_key :location
|
49
|
+
expect(result).to have_key :server
|
50
|
+
expect(result).to have_key :st
|
51
|
+
expect(result).to have_key :ext
|
52
|
+
expect(result).to have_key :usn
|
53
|
+
expect(result).to have_key :content_length
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'turns header values into Hash values' do
|
57
|
+
result = subject.parse ROOT_DEVICE1
|
58
|
+
expect(result[:cache_control]).to eq 'max-age=1200'
|
59
|
+
expect(result[:date]).to eq 'Mon, 26 Sep 2011 06:40:19 GMT'
|
60
|
+
expect(result[:location]).to eq 'http://1.2.3.4:5678/description/fetch'
|
61
|
+
expect(result[:server]).to eq 'Linux-i386-2.6.38-10-generic-pae, UPnP/1.0, PMS/1.25.1'
|
62
|
+
expect(result[:st]).to eq 'upnp:rootdevice'
|
63
|
+
expect(result[:ext]).to be_empty
|
64
|
+
expect(result[:usn]).to eq 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice'
|
65
|
+
expect(result[:content_length]).to eq '0'
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'single line String as response data' do
|
69
|
+
before { @data = ROOT_DEVICE1.gsub("\n", ' ') }
|
70
|
+
|
71
|
+
it 'returns an empty Hash' do
|
72
|
+
expect(subject.parse(@data)).to eq({ })
|
73
|
+
end
|
74
|
+
|
75
|
+
it "logs the 'bad' response" do
|
76
|
+
subject.should_receive(:log).twice
|
77
|
+
subject.parse @data
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#setup_multicast_socket' do
|
83
|
+
before do
|
84
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:set_membership)
|
85
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:switch_multicast_loop)
|
86
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:set_multicast_ttl)
|
87
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:set_ttl)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'adds 0.0.0.0 and 239.255.255.250 to the membership group' do
|
91
|
+
expect(subject).to receive(:set_membership).with(
|
92
|
+
IPAddr.new('239.255.255.250').hton + IPAddr.new('0.0.0.0').hton
|
93
|
+
)
|
94
|
+
subject.setup_multicast_socket
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'sets multicast TTL to 4' do
|
98
|
+
expect(subject).to receive(:set_multicast_ttl).with(4)
|
99
|
+
subject.setup_multicast_socket
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'sets TTL to 4' do
|
103
|
+
expect(subject).to receive(:set_ttl).with(4)
|
104
|
+
subject.setup_multicast_socket
|
105
|
+
end
|
106
|
+
|
107
|
+
context "ENV['RUBY_UPNP_ENV'] != testing" do
|
108
|
+
after { ENV['RUBY_UPNP_ENV'] = 'testing' }
|
109
|
+
|
110
|
+
it 'turns multicast loop off' do
|
111
|
+
ENV['RUBY_UPNP_ENV'] = 'development'
|
112
|
+
expect(subject).to receive(:switch_multicast_loop).with(:off)
|
113
|
+
subject.setup_multicast_socket
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#switch_multicast_loop' do
|
119
|
+
before do
|
120
|
+
allow_any_instance_of(Frisky::SSDP::MulticastConnection).to receive(:setup_multicast_socket)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "passes '\\001' to the socket option call when param == :on" do
|
124
|
+
expect(subject).to receive(:set_sock_opt).with(
|
125
|
+
Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\001"
|
126
|
+
)
|
127
|
+
subject.switch_multicast_loop :on
|
128
|
+
end
|
129
|
+
|
130
|
+
it "passes '\\001' to the socket option call when param == '\\001'" do
|
131
|
+
expect(subject).to receive(:set_sock_opt).with(
|
132
|
+
Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\001"
|
133
|
+
)
|
134
|
+
subject.switch_multicast_loop "\001"
|
135
|
+
end
|
136
|
+
|
137
|
+
it "passes '\\000' to the socket option call when param == :off" do
|
138
|
+
expect(subject).to receive(:set_sock_opt).with(
|
139
|
+
Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000"
|
140
|
+
)
|
141
|
+
subject.switch_multicast_loop :off
|
142
|
+
end
|
143
|
+
|
144
|
+
it "passes '\\000' to the socket option call when param == '\\000'" do
|
145
|
+
expect(subject).to receive(:set_sock_opt).with(
|
146
|
+
Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000"
|
147
|
+
)
|
148
|
+
subject.switch_multicast_loop "\000"
|
149
|
+
end
|
150
|
+
|
151
|
+
it "raises when not :on, :off, '\\000', or '\\001'" do
|
152
|
+
expect { subject.switch_multicast_loop 12312312 }.
|
153
|
+
to raise_error(Frisky::SSDP::Error)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|