frisky 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.
- 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
|
+
|