ws_discovery 0.0.1
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/Gemfile +2 -0
- data/History.rdoc +3 -0
- data/README.rdoc +77 -0
- data/Rakefile +16 -0
- data/lib/ws_discovery/core_ext/socket_patch.rb +16 -0
- data/lib/ws_discovery/error.rb +4 -0
- data/lib/ws_discovery/multicast_connection.rb +77 -0
- data/lib/ws_discovery/network_constants.rb +13 -0
- data/lib/ws_discovery/response.rb +95 -0
- data/lib/ws_discovery/searcher.rb +91 -0
- data/lib/ws_discovery/version.rb +3 -0
- data/lib/ws_discovery.rb +61 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/application.css +1110 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/application.js +626 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/favicon_green.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/favicon_red.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/favicon_yellow.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/loading.gif +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/magnify.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/spec/ws_discovery/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/spec/ws_discovery/coverage/index.html +72 -0
- data/spec/ws_discovery/multicast_connection_spec.rb +108 -0
- data/spec/ws_discovery/response_spec.rb +118 -0
- data/spec/ws_discovery/searcher_spec.rb +85 -0
- data/spec/ws_discovery_spec.rb +43 -0
- data/ws_discovery.gemspec +32 -0
- metadata +296 -0
data/Gemfile
ADDED
data/History.rdoc
ADDED
data/README.rdoc
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
= ws_discovery
|
|
2
|
+
|
|
3
|
+
* {Homepage}[https://github.com/pelco-automation/ws-discovery]
|
|
4
|
+
* {WS-Discovery 1.0 Specification}[http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf]
|
|
5
|
+
* {SOAP-over-UDP Specification}[http://specs.xmlsoap.org/ws/2004/09/soap-over-udp/soap-over-udp.pdf]
|
|
6
|
+
|
|
7
|
+
== Description
|
|
8
|
+
|
|
9
|
+
This gem aims to provide the ability to search for WS-Discovery compatible target
|
|
10
|
+
services.
|
|
11
|
+
|
|
12
|
+
This uses EventMachine[http://github.com/eventmachine/eventmachine], so if
|
|
13
|
+
you're not already, getting familiar with its concepts will be helpful here.
|
|
14
|
+
|
|
15
|
+
== Features
|
|
16
|
+
|
|
17
|
+
* Search for WS-Discovery compatible target services.
|
|
18
|
+
|
|
19
|
+
== Examples
|
|
20
|
+
|
|
21
|
+
=== WS-Discovery Searches
|
|
22
|
+
|
|
23
|
+
A WS-Discovery search simply sends the probe out to the multicast group and
|
|
24
|
+
listens for responses for a given (or default of 5 seconds) amount of time. The
|
|
25
|
+
return from this depends on if you're running it within an EventMachine reactor
|
|
26
|
+
or not. If not, it returns an Array of responses as WSDiscovery::Responses.
|
|
27
|
+
Take a look at the WSDiscovery#search docs for more on the options here.
|
|
28
|
+
|
|
29
|
+
require 'ws_discovery'
|
|
30
|
+
|
|
31
|
+
# Search for all devices (do a probe with Types left unspecified)
|
|
32
|
+
all_devices = WSDiscovery.search # this is default
|
|
33
|
+
|
|
34
|
+
# Search for devices of a specific Type
|
|
35
|
+
network_video_transmitters = WSDiscovery.search(
|
|
36
|
+
env_namespaces: { "xmlns:dn" => "http://www.onvif.org/ver10/network/wsdl" },
|
|
37
|
+
types: "dn:NetworkVideoTransmitter")
|
|
38
|
+
|
|
39
|
+
# These searches will return an Array of WSDiscovery::Responses. See the
|
|
40
|
+
# WSDiscovery::Response documentation for more information.
|
|
41
|
+
|
|
42
|
+
If you do the search inside of an EventMachine reactor, as the
|
|
43
|
+
WSDiscovery::Searcher receives and parses responses, it adds them to the accessor
|
|
44
|
+
#discovery_responses, which is an EventMachine::Channel. This lets you subscribe
|
|
45
|
+
to the responses and do what you want with them.
|
|
46
|
+
|
|
47
|
+
== Requirements
|
|
48
|
+
|
|
49
|
+
* Ruby
|
|
50
|
+
* 1.9.3
|
|
51
|
+
* Gems
|
|
52
|
+
* builder
|
|
53
|
+
* eventmachine
|
|
54
|
+
* log_switch
|
|
55
|
+
* nokogiri
|
|
56
|
+
* nori
|
|
57
|
+
* uuid
|
|
58
|
+
* Gems (development)
|
|
59
|
+
* bundler
|
|
60
|
+
* rake
|
|
61
|
+
* rspec
|
|
62
|
+
* simplecov
|
|
63
|
+
* simplecov-rcov
|
|
64
|
+
* yard
|
|
65
|
+
|
|
66
|
+
== Install
|
|
67
|
+
|
|
68
|
+
$ gem install ws_discovery
|
|
69
|
+
|
|
70
|
+
== THANKS
|
|
71
|
+
|
|
72
|
+
The initial core of this gem came from https://github.com/turboladen/upnp due to
|
|
73
|
+
the similarities in how SSDP and WS-Discovery searches are performed.
|
|
74
|
+
|
|
75
|
+
The WSDiscovery::Response class reuses parts of https://github.com/savonrb/savon.
|
|
76
|
+
It made sense to me that WSDiscovery::Responses would behave similarly to
|
|
77
|
+
Savon::SOAP::Responses.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
require 'yard'
|
|
4
|
+
|
|
5
|
+
YARD::Rake::YardocTask.new
|
|
6
|
+
RSpec::Core::RakeTask.new
|
|
7
|
+
|
|
8
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
|
9
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
|
10
|
+
spec.rcov = true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :install
|
|
14
|
+
|
|
15
|
+
# Alias for rubygems-test
|
|
16
|
+
task test: :spec
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
|
|
3
|
+
# Workaround for missing constants on Windows
|
|
4
|
+
module Socket::Constants
|
|
5
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
|
6
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
|
7
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
|
8
|
+
IP_TTL = 4 unless defined? IP_TTL
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Socket
|
|
12
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
|
13
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
|
14
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
|
15
|
+
IP_TTL = 4 unless defined? IP_TTL
|
|
16
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require_relative 'core_ext/socket_patch'
|
|
2
|
+
require_relative 'network_constants'
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
require 'ipaddr'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'eventmachine'
|
|
7
|
+
|
|
8
|
+
module WSDiscovery
|
|
9
|
+
class MulticastConnection < EventMachine::Connection
|
|
10
|
+
include WSDiscovery::NetworkConstants
|
|
11
|
+
|
|
12
|
+
# @param [Fixnum] ttl The TTL value to use when opening the UDP socket
|
|
13
|
+
# required for WSDiscovery actions.
|
|
14
|
+
def initialize ttl=TTL
|
|
15
|
+
@ttl = ttl
|
|
16
|
+
@discovery_responses = EM::Channel.new
|
|
17
|
+
|
|
18
|
+
setup_multicast_socket
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Gets the IP and port from the peer that just sent data.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<String,Fixnum>] The IP and port.
|
|
26
|
+
def peer_info
|
|
27
|
+
peer_bytes = get_peername[2, 6].unpack("nC4")
|
|
28
|
+
port = peer_bytes.first.to_i
|
|
29
|
+
ip = peer_bytes[1, 4].join(".")
|
|
30
|
+
|
|
31
|
+
[ip, port]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Sets Socket options to allow for multicasting. If ENV["RUBY_TESTING_ENV"]
|
|
35
|
+
# is equal to "testing", then it doesn't turn off multicast looping.
|
|
36
|
+
def setup_multicast_socket
|
|
37
|
+
set_membership(IPAddr.new(MULTICAST_IP).hton + IPAddr.new('0.0.0.0').hton)
|
|
38
|
+
set_multicast_ttl(@ttl)
|
|
39
|
+
set_ttl(@ttl)
|
|
40
|
+
|
|
41
|
+
unless ENV["RUBY_TESTING_ENV"] == "testing"
|
|
42
|
+
switch_multicast_loop :off
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param [String] membership The network byte ordered String that represents
|
|
47
|
+
# the IP(s) that should join the membership group.
|
|
48
|
+
def set_membership(membership)
|
|
49
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param [Fixnum] ttl TTL to set IP_MULTICAST_TTL to.
|
|
53
|
+
def set_multicast_ttl(ttl)
|
|
54
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, [ttl].pack('i'))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param [Fixnum] ttl TTL to set IP_TTL to.
|
|
58
|
+
def set_ttl(ttl)
|
|
59
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_TTL, [ttl].pack('i'))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param [Symbol] on_off Turn on/off multicast looping. Supply :on or :off.
|
|
63
|
+
# @raise [WSDiscovery::Error] If invalid option is given.
|
|
64
|
+
def switch_multicast_loop(on_off)
|
|
65
|
+
hex_value = case on_off
|
|
66
|
+
when :on, "\001"
|
|
67
|
+
"\001"
|
|
68
|
+
when :off, "\000"
|
|
69
|
+
"\000"
|
|
70
|
+
else
|
|
71
|
+
raise WSDiscovery::Error, "Can't switch IP_MULTICAST_LOOP to '#{on_off}'"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
set_sock_opt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, hex_value)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'nori'
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
|
|
5
|
+
Nori.configure do |config|
|
|
6
|
+
config.strip_namespaces = true
|
|
7
|
+
config.convert_tags_to { |tag| tag.snakecase.to_sym }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module WSDiscovery
|
|
11
|
+
|
|
12
|
+
# Represents the probe response.
|
|
13
|
+
class Response
|
|
14
|
+
|
|
15
|
+
attr_accessor :response
|
|
16
|
+
|
|
17
|
+
# @param [String] response Text of the response to a WSDiscovery probe.
|
|
18
|
+
def initialize(response)
|
|
19
|
+
@response = response
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Shortcut accessor for the SOAP response body Hash.
|
|
23
|
+
#
|
|
24
|
+
# @param [Symbol] key The key to access in the body Hash.
|
|
25
|
+
# @return [Hash,String] The accessed value.
|
|
26
|
+
def [](key)
|
|
27
|
+
body[key]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the SOAP response header as a Hash.
|
|
31
|
+
#
|
|
32
|
+
# @return [Hash] SOAP response header.
|
|
33
|
+
# @raise [WSDiscovery::Error] If unable to parse response.
|
|
34
|
+
def header
|
|
35
|
+
unless hash.has_key? :envelope
|
|
36
|
+
raise WSDiscovery::Error, "Unable to parse response body '#{to_xml}'"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
hash[:envelope][:header]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the SOAP response body as a Hash.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash] SOAP response body.
|
|
45
|
+
# @raise [WSDiscovery::Error] If unable to parse response.
|
|
46
|
+
def body
|
|
47
|
+
unless hash.has_key? :envelope
|
|
48
|
+
raise WSDiscovery::Error, "Unable to parse response body '#{to_xml}'"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
hash[:envelope][:body]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias to_hash body
|
|
55
|
+
|
|
56
|
+
# Returns the complete SOAP response XML without normalization.
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] Complete SOAP response Hash.
|
|
59
|
+
def hash
|
|
60
|
+
@hash ||= Nori.parse(to_xml)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the SOAP response XML.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] Raw SOAP response XML.
|
|
66
|
+
def to_xml
|
|
67
|
+
response
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns a Nokogiri::XML::Document for the SOAP response XML.
|
|
71
|
+
#
|
|
72
|
+
# @return [Nokogiri::XML::Document] Document for the SOAP response.
|
|
73
|
+
def doc
|
|
74
|
+
@doc ||= Nokogiri::XML(to_xml)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns an Array of Nokogiri::XML::Node objects retrieved with the given +path+.
|
|
78
|
+
# Automatically adds all of the document's namespaces unless a +namespaces+ hash is provided.
|
|
79
|
+
#
|
|
80
|
+
# @param [String] path XPath to search.
|
|
81
|
+
# @param [Hash<String>] namespaces Namespaces to append.
|
|
82
|
+
def xpath(path, namespaces = nil)
|
|
83
|
+
doc.xpath(path, namespaces || xml_namespaces)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# XML Namespaces from the Document.
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash] Namespaces from the Document.
|
|
91
|
+
def xml_namespaces
|
|
92
|
+
@xml_namespaces ||= doc.collect_namespaces
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require_relative 'multicast_connection'
|
|
2
|
+
require_relative 'response'
|
|
3
|
+
require 'builder'
|
|
4
|
+
require 'log_switch'
|
|
5
|
+
require 'uuid'
|
|
6
|
+
|
|
7
|
+
class WSDiscovery::Searcher < WSDiscovery::MulticastConnection
|
|
8
|
+
extend LogSwitch
|
|
9
|
+
self.logger.datetime_format = "%Y-%m-%d %H:%M:%S "
|
|
10
|
+
|
|
11
|
+
# @return [EventMachine::Channel] Provides subscribers with responses from
|
|
12
|
+
# their search request.
|
|
13
|
+
attr_reader :discovery_responses
|
|
14
|
+
|
|
15
|
+
# @param [Hash] options The options for the probe.
|
|
16
|
+
# @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
|
|
17
|
+
# @option options [Hash<String>] :type_attributes Type attributes.
|
|
18
|
+
# @option options [String] :types Types.
|
|
19
|
+
# @option options [Hash<String>] :scope_attributes Scope attributes.
|
|
20
|
+
# @option options [String] :scopes Scopes.
|
|
21
|
+
# @option options [Fixnum] :ttl TTL for the probe.
|
|
22
|
+
def initialize(options={})
|
|
23
|
+
options[:ttl] ||= TTL
|
|
24
|
+
|
|
25
|
+
@search = probe(options)
|
|
26
|
+
|
|
27
|
+
super options[:ttl]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# This is the callback called by EventMachine when it receives data on the
|
|
31
|
+
# socket that's been opened for this connection. In this case, the method
|
|
32
|
+
# parses the probe matches into WSDiscovery::Responses and adds them to the
|
|
33
|
+
# appropriate EventMachine::Channel (provided as accessor methods). This
|
|
34
|
+
# effectively means that in each Channel, you get a WSDiscovery::Response
|
|
35
|
+
# for each response that comes in on the socket.
|
|
36
|
+
#
|
|
37
|
+
# @param [String] response The data received on this connection's socket.
|
|
38
|
+
def receive_data(response)
|
|
39
|
+
ip, port = peer_info
|
|
40
|
+
WSDiscovery::Searcher.log "<#{self.class}> Response from #{ip}:#{port}:\n#{response}\n"
|
|
41
|
+
parsed_response = parse(response)
|
|
42
|
+
@discovery_responses << parsed_response
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Converts the headers to a set of key-value pairs.
|
|
46
|
+
#
|
|
47
|
+
# @param [String] data The data to convert.
|
|
48
|
+
# @return [WSDiscovery::Response] The converted data.
|
|
49
|
+
def parse(data)
|
|
50
|
+
WSDiscovery::Response.new(data)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Sends the probe that was built during init. Logs what was sent if the
|
|
54
|
+
# send was successful.
|
|
55
|
+
def post_init
|
|
56
|
+
if send_datagram(@search, MULTICAST_IP, MULTICAST_PORT) > 0
|
|
57
|
+
WSDiscovery::Searcher.log("Sent datagram search:\n#{@search}")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Probe for target services supporting WS-Discovery.
|
|
62
|
+
#
|
|
63
|
+
# @param [Hash] options The options for the probe.
|
|
64
|
+
# @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
|
|
65
|
+
# @option options [Hash<String>] :type_attributes Type attributes.
|
|
66
|
+
# @option options [String] :types Types.
|
|
67
|
+
# @option options [Hash<String>] :scope_attributes Scope attributes.
|
|
68
|
+
# @option options [String] :scopes Scopes.
|
|
69
|
+
# @return [String] Probe SOAP message.
|
|
70
|
+
def probe(options={})
|
|
71
|
+
namespaces = {
|
|
72
|
+
'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
|
|
73
|
+
'xmlns:d' => 'http://schemas.xmlsoap.org/ws/2005/04/discovery',
|
|
74
|
+
'xmlns:s' => 'http://www.w3.org/2003/05/soap-envelope'
|
|
75
|
+
}
|
|
76
|
+
namespaces.merge options[:env_namespaces] if options[:env_namespaces]
|
|
77
|
+
|
|
78
|
+
Builder::XmlMarkup.new.s(:Envelope, namespaces) do |xml|
|
|
79
|
+
xml.s(:Header) do |xml|
|
|
80
|
+
xml.a(:Action, 'http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe')
|
|
81
|
+
xml.a(:MessageID, "uuid:#{UUID.generate}")
|
|
82
|
+
xml.a(:To, 'urn:schemas-xmlsoap-org:ws:2005:04:discovery')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
xml.s(:Body) do |xml|
|
|
86
|
+
xml.d(:Types, options[:type_attributes], options[:types])
|
|
87
|
+
xml.d(:Scopes, options[:scope_attributes], options[:scopes])
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/ws_discovery.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require_relative 'ws_discovery/core_ext/socket_patch'
|
|
2
|
+
require 'eventmachine'
|
|
3
|
+
|
|
4
|
+
require_relative 'ws_discovery/error'
|
|
5
|
+
require_relative 'ws_discovery/network_constants'
|
|
6
|
+
require_relative 'ws_discovery/searcher'
|
|
7
|
+
|
|
8
|
+
module WSDiscovery
|
|
9
|
+
include NetworkConstants
|
|
10
|
+
|
|
11
|
+
DEFAULT_WAIT_TIME = 5
|
|
12
|
+
|
|
13
|
+
# Opens a UDP socket on 0.0.0.0, on an ephemeral port, has WSDiscovery::Searcher
|
|
14
|
+
# build and send the search request, then receives the responses. The search
|
|
15
|
+
# will stop after +response_wait_time+.
|
|
16
|
+
#
|
|
17
|
+
# @param [Hash] options The options for the probe.
|
|
18
|
+
# @option options [Hash<String>] :env_namespaces Additional envelope namespaces.
|
|
19
|
+
# @option options [Hash<String>] :type_attributes Type attributes.
|
|
20
|
+
# @option options [String] :types Types.
|
|
21
|
+
# @option options [Hash<String>] :scope_attributes Scope attributes.
|
|
22
|
+
# @option options [String] :scopes Scopes.
|
|
23
|
+
# @return [Array<WSDiscovery::Response>,WSDiscovery::Searcher] Returns an
|
|
24
|
+
# Array of probe responses. If the reactor is already running this will return
|
|
25
|
+
# a WSDiscovery::Searcher which will make its accessors available so you can
|
|
26
|
+
# get responses in real time.
|
|
27
|
+
def self.search(options={})
|
|
28
|
+
response_wait_time = options[:response_wait_time] || DEFAULT_WAIT_TIME
|
|
29
|
+
responses = []
|
|
30
|
+
|
|
31
|
+
multicast_searcher = proc do
|
|
32
|
+
EM.open_datagram_socket('0.0.0.0', 0, WSDiscovery::Searcher, options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if EM.reactor_running?
|
|
36
|
+
return multicast_searcher.call
|
|
37
|
+
else
|
|
38
|
+
EM.run do
|
|
39
|
+
ms = multicast_searcher.call
|
|
40
|
+
|
|
41
|
+
ms.discovery_responses.subscribe do |notification|
|
|
42
|
+
responses << notification
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
EM.add_timer(response_wait_time) { EM.stop }
|
|
46
|
+
trap_signals
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
responses.flatten
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Traps INT, TERM, and HUP signals and stops the reactor.
|
|
56
|
+
def self.trap_signals
|
|
57
|
+
trap('INT') { EM.stop }
|
|
58
|
+
trap('TERM') { EM.stop }
|
|
59
|
+
trap('HUP') { EM.stop } if RUBY_PLATFORM !~ /mswin|mingw/
|
|
60
|
+
end
|
|
61
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'simplecov'
|
|
2
|
+
require 'simplecov-rcov'
|
|
3
|
+
|
|
4
|
+
class SimpleCov::Formatter::MergedFormatter
|
|
5
|
+
def format(result)
|
|
6
|
+
SimpleCov::Formatter::HTMLFormatter.new.format(result)
|
|
7
|
+
SimpleCov::Formatter::RcovFormatter.new.format(result)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
|
|
12
|
+
|
|
13
|
+
SimpleCov.start do
|
|
14
|
+
add_filter "/spec"
|
|
15
|
+
add_filter "/lib/deps"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
19
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
20
|
+
require 'rspec'
|
|
21
|
+
|
|
22
|
+
# Requires supporting files with custom matchers and macros, etc,
|
|
23
|
+
# in ./support/ and its subdirectories.
|
|
24
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |file| require file }
|
|
25
|
+
|
|
26
|
+
ENV["RUBY_TESTING_ENV"] = "testing"
|