s-savon 0.8.6
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/.gitignore +9 -0
- data/.rspec +1 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +37 -0
- data/Rakefile +40 -0
- data/lib/savon.rb +14 -0
- data/lib/savon/client.rb +157 -0
- data/lib/savon/core_ext/hash.rb +70 -0
- data/lib/savon/core_ext/object.rb +14 -0
- data/lib/savon/core_ext/string.rb +51 -0
- data/lib/savon/core_ext/time.rb +14 -0
- data/lib/savon/error.rb +6 -0
- data/lib/savon/global.rb +75 -0
- data/lib/savon/http/error.rb +42 -0
- data/lib/savon/soap.rb +24 -0
- data/lib/savon/soap/fault.rb +59 -0
- data/lib/savon/soap/request.rb +61 -0
- data/lib/savon/soap/response.rb +80 -0
- data/lib/savon/soap/xml.rb +187 -0
- data/lib/savon/version.rb +5 -0
- data/lib/savon/wsdl/document.rb +112 -0
- data/lib/savon/wsdl/parser.rb +102 -0
- data/lib/savon/wsdl/request.rb +35 -0
- data/lib/savon/wsse.rb +150 -0
- data/savon.gemspec +29 -0
- data/spec/fixtures/gzip/message.gz +0 -0
- data/spec/fixtures/response/another_soap_fault.xml +14 -0
- data/spec/fixtures/response/authentication.xml +14 -0
- data/spec/fixtures/response/header.xml +13 -0
- data/spec/fixtures/response/list.xml +18 -0
- data/spec/fixtures/response/multi_ref.xml +39 -0
- data/spec/fixtures/response/soap_fault.xml +8 -0
- data/spec/fixtures/response/soap_fault12.xml +18 -0
- data/spec/fixtures/wsdl/authentication.xml +63 -0
- data/spec/fixtures/wsdl/geotrust.xml +156 -0
- data/spec/fixtures/wsdl/namespaced_actions.xml +307 -0
- data/spec/fixtures/wsdl/no_namespace.xml +115 -0
- data/spec/fixtures/wsdl/two_bindings.xml +25 -0
- data/spec/savon/client_spec.rb +346 -0
- data/spec/savon/core_ext/hash_spec.rb +121 -0
- data/spec/savon/core_ext/object_spec.rb +19 -0
- data/spec/savon/core_ext/string_spec.rb +57 -0
- data/spec/savon/core_ext/time_spec.rb +13 -0
- data/spec/savon/http/error_spec.rb +52 -0
- data/spec/savon/savon_spec.rb +85 -0
- data/spec/savon/soap/fault_spec.rb +89 -0
- data/spec/savon/soap/request_spec.rb +45 -0
- data/spec/savon/soap/response_spec.rb +174 -0
- data/spec/savon/soap/xml_spec.rb +335 -0
- data/spec/savon/soap_spec.rb +21 -0
- data/spec/savon/wsdl/document_spec.rb +132 -0
- data/spec/savon/wsdl/parser_spec.rb +99 -0
- data/spec/savon/wsdl/request_spec.rb +15 -0
- data/spec/savon/wsse_spec.rb +213 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/endpoint.rb +25 -0
- data/spec/support/fixture.rb +37 -0
- metadata +251 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
|
3
|
+
require "savon/wsdl/request"
|
4
|
+
require "savon/wsdl/parser"
|
5
|
+
|
6
|
+
module Savon
|
7
|
+
module WSDL
|
8
|
+
|
9
|
+
# = Savon::WSDL::Document
|
10
|
+
#
|
11
|
+
# Represents the WSDL of your service, including information like the namespace URI,
|
12
|
+
# the SOAP endpoint and available SOAP actions.
|
13
|
+
class Document
|
14
|
+
|
15
|
+
# Accepts an <tt>HTTPI::Request</tt> and a +document+.
|
16
|
+
def initialize(request = nil, document = nil)
|
17
|
+
self.request = request
|
18
|
+
self.document = document
|
19
|
+
end
|
20
|
+
|
21
|
+
# Accessor for the <tt>HTTPI::Request</tt> to use.
|
22
|
+
attr_accessor :request
|
23
|
+
|
24
|
+
def present?
|
25
|
+
!!@document
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the namespace URI of the WSDL.
|
29
|
+
def namespace
|
30
|
+
@namespace ||= parser.namespace
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets the SOAP namespace.
|
34
|
+
attr_writer :namespace
|
35
|
+
|
36
|
+
# Returns the SOAP endpoint.
|
37
|
+
def endpoint
|
38
|
+
@endpoint ||= parser.endpoint
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets the SOAP endpoint.
|
42
|
+
attr_writer :endpoint
|
43
|
+
|
44
|
+
# Returns an Array of available SOAP actions.
|
45
|
+
def soap_actions
|
46
|
+
@soap_actions ||= parser.operations.keys
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the SOAP action for a given +key+.
|
50
|
+
def soap_action(key)
|
51
|
+
operations[key][:action] if present? && operations[key]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the SOAP input for a given +key+.
|
55
|
+
def soap_input(key)
|
56
|
+
operations[key][:input].to_sym if present? && operations[key]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns a Hash of SOAP operations.
|
60
|
+
def operations
|
61
|
+
@operations ||= parser.operations
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the elementFormDefault value.
|
65
|
+
def element_form_default
|
66
|
+
@element_form_default ||= parser.element_form_default
|
67
|
+
end
|
68
|
+
|
69
|
+
# Sets the location of the WSDL document to use. This can either be a URL
|
70
|
+
# or a path to a local file.
|
71
|
+
attr_writer :document
|
72
|
+
|
73
|
+
# Returns the raw WSDL document.
|
74
|
+
def document
|
75
|
+
@wsdl_document ||= begin
|
76
|
+
raise ArgumentError, "No WSDL document given" if @document.blank?
|
77
|
+
remote? ? http_request : read_file
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
alias :to_xml :document
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Returns whether the WSDL document is located on the Web.
|
86
|
+
def remote?
|
87
|
+
@document =~ /^http/
|
88
|
+
end
|
89
|
+
|
90
|
+
# Executes an HTTP GET request to retrieve a remote WSDL document.
|
91
|
+
def http_request
|
92
|
+
request.url = @document
|
93
|
+
Request.new(request).response.body
|
94
|
+
end
|
95
|
+
|
96
|
+
# Reads the WSDL document from a local file.
|
97
|
+
def read_file
|
98
|
+
File.read @document
|
99
|
+
end
|
100
|
+
|
101
|
+
# Parses the WSDL document and returns the <tt>Savon::WSDL::Parser</tt>.
|
102
|
+
def parser
|
103
|
+
@parser ||= begin
|
104
|
+
parser = Parser.new
|
105
|
+
REXML::Document.parse_stream document, parser
|
106
|
+
parser
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "savon/core_ext/object"
|
2
|
+
require "savon/core_ext/string"
|
3
|
+
|
4
|
+
module Savon
|
5
|
+
module WSDL
|
6
|
+
|
7
|
+
# = Savon::WSDL::Parser
|
8
|
+
#
|
9
|
+
# Serves as a stream listener for parsing WSDL documents.
|
10
|
+
class Parser
|
11
|
+
|
12
|
+
# The main sections of a WSDL document.
|
13
|
+
Sections = %w(definitions types message portType binding service)
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@path = []
|
17
|
+
@operations = {}
|
18
|
+
@namespaces = {}
|
19
|
+
@element_form_default = :unqualified
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the namespace URI.
|
23
|
+
attr_reader :namespace
|
24
|
+
|
25
|
+
# Returns the SOAP operations.
|
26
|
+
attr_reader :operations
|
27
|
+
|
28
|
+
# Returns the SOAP endpoint.
|
29
|
+
attr_reader :endpoint
|
30
|
+
|
31
|
+
# Returns the elementFormDefault value.
|
32
|
+
attr_reader :element_form_default
|
33
|
+
|
34
|
+
# Hook method called when the stream parser encounters a starting tag.
|
35
|
+
def tag_start(tag, attrs)
|
36
|
+
# read xml namespaces if root element
|
37
|
+
read_namespaces(attrs) if @path.empty?
|
38
|
+
|
39
|
+
tag, namespace = tag.split(":").reverse
|
40
|
+
@path << tag
|
41
|
+
|
42
|
+
if @section == :types && tag == "schema"
|
43
|
+
@element_form_default = attrs["elementFormDefault"].to_sym if attrs["elementFormDefault"]
|
44
|
+
end
|
45
|
+
|
46
|
+
if @section == :binding && tag == "binding"
|
47
|
+
# ensure that we are in an wsdl/soap namespace
|
48
|
+
@section = nil unless @namespaces[namespace].starts_with? "http://schemas.xmlsoap.org/wsdl/soap"
|
49
|
+
end
|
50
|
+
|
51
|
+
@section = tag.to_sym if Sections.include?(tag) && depth <= 2
|
52
|
+
|
53
|
+
@namespace ||= attrs["targetNamespace"] if @section == :definitions
|
54
|
+
@endpoint ||= URI(URI.escape(attrs["location"])) if @section == :service && tag == "address"
|
55
|
+
|
56
|
+
operation_from tag, attrs if @section == :binding && tag == "operation"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns our current depth in the WSDL document.
|
60
|
+
def depth
|
61
|
+
@path.size
|
62
|
+
end
|
63
|
+
|
64
|
+
# Reads namespace definitions from a given +attrs+ Hash.
|
65
|
+
def read_namespaces(attrs)
|
66
|
+
attrs.each do |key, value|
|
67
|
+
@namespaces[key.strip_namespace] = value if key.starts_with? "xmlns:"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Hook method called when the stream parser encounters a closing tag.
|
72
|
+
def tag_end(tag)
|
73
|
+
@path.pop
|
74
|
+
|
75
|
+
if @section == :binding && @input && tag.strip_namespace == "operation"
|
76
|
+
# no soapAction attribute found till now
|
77
|
+
operation_from tag, "soapAction" => @input
|
78
|
+
end
|
79
|
+
|
80
|
+
@section = :definitions if Sections.include?(tag) && depth <= 1
|
81
|
+
end
|
82
|
+
|
83
|
+
# Stores available operations from a given tag +name+ and +attrs+.
|
84
|
+
def operation_from(tag, attrs)
|
85
|
+
@input = attrs["name"] if attrs["name"]
|
86
|
+
|
87
|
+
if attrs["soapAction"]
|
88
|
+
@action = !attrs["soapAction"].blank? ? attrs["soapAction"] : @input
|
89
|
+
@input = @action.split("/").last if !@input || @input.empty?
|
90
|
+
|
91
|
+
@operations[@input.snakecase.to_sym] = { :action => @action, :input => @input }
|
92
|
+
@input, @action = nil, nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Catches calls to unimplemented hook methods.
|
97
|
+
def method_missing(method, *args)
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "httpi"
|
2
|
+
|
3
|
+
module Savon
|
4
|
+
module WSDL
|
5
|
+
|
6
|
+
# = Savon::WSDL::Request
|
7
|
+
#
|
8
|
+
# Executes WSDL requests.
|
9
|
+
class Request
|
10
|
+
|
11
|
+
# Expects an <tt>HTTPI::Request</tt>.
|
12
|
+
def initialize(request)
|
13
|
+
self.request = request
|
14
|
+
end
|
15
|
+
|
16
|
+
# Accessor for the <tt>HTTPI::Request</tt>.
|
17
|
+
attr_accessor :request
|
18
|
+
|
19
|
+
# Executes the request and returns the response.
|
20
|
+
def response
|
21
|
+
@response ||= with_logging { HTTPI.get request }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Logs the HTTP request and yields to a given +block+.
|
27
|
+
def with_logging
|
28
|
+
Savon.log "Retrieving WSDL from: #{request.url}"
|
29
|
+
Savon.log "Using :#{request.auth.type} authentication" if request.auth?
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/savon/wsse.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "digest/sha1"
|
3
|
+
|
4
|
+
require "savon/core_ext/string"
|
5
|
+
require "savon/core_ext/hash"
|
6
|
+
require "savon/core_ext/time"
|
7
|
+
|
8
|
+
module Savon
|
9
|
+
|
10
|
+
# = Savon::WSSE
|
11
|
+
#
|
12
|
+
# Provides WSSE authentication.
|
13
|
+
class WSSE
|
14
|
+
|
15
|
+
# Namespace for WS Security Secext.
|
16
|
+
WSENamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
|
17
|
+
|
18
|
+
# Namespace for WS Security Utility.
|
19
|
+
WSUNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
|
20
|
+
|
21
|
+
# URI for "wsse:Password/@Type" #PasswordText.
|
22
|
+
PasswordTextURI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
|
23
|
+
|
24
|
+
# URI for "wsse:Password/@Type" #PasswordDigest.
|
25
|
+
PasswordDigestURI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
|
26
|
+
|
27
|
+
# Returns a value from the WSSE Hash.
|
28
|
+
def [](key)
|
29
|
+
hash[key]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sets a value on the WSSE Hash.
|
33
|
+
def []=(key, value)
|
34
|
+
hash[key] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets authentication credentials for a wsse:UsernameToken header.
|
38
|
+
# Also accepts whether to use WSSE digest authentication.
|
39
|
+
def credentials(username, password, digest = false)
|
40
|
+
self.username = username
|
41
|
+
self.password = password
|
42
|
+
self.digest = digest
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :username, :password, :created_at, :expires_at
|
46
|
+
|
47
|
+
# Returns whether to use WSSE digest. Defaults to +false+.
|
48
|
+
def digest?
|
49
|
+
!!@digest
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_writer :digest
|
53
|
+
|
54
|
+
# Returns whether to generate a wsse:UsernameToken header.
|
55
|
+
def username_token?
|
56
|
+
username && password
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns whether to generate a wsse:Timestamp header.
|
60
|
+
def timestamp?
|
61
|
+
created_at || expires_at || @wsse_timestamp
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets whether to generate a wsse:Timestamp header.
|
65
|
+
def timestamp=(timestamp)
|
66
|
+
@wsse_timestamp = timestamp
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the XML for a WSSE header.
|
70
|
+
def to_xml
|
71
|
+
if username_token?
|
72
|
+
Gyoku.xml wsse_username_token.merge!(hash)
|
73
|
+
elsif timestamp?
|
74
|
+
Gyoku.xml wsse_timestamp.merge!(hash)
|
75
|
+
else
|
76
|
+
""
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Returns a Hash containing wsse:UsernameToken details.
|
83
|
+
def wsse_username_token
|
84
|
+
if digest?
|
85
|
+
wsse_security "UsernameToken",
|
86
|
+
"wsse:Username" => username,
|
87
|
+
"wsse:Nonce" => nonce,
|
88
|
+
"wsu:Created" => timestamp,
|
89
|
+
"wsse:Password" => digest_password,
|
90
|
+
:attributes! => { "wsse:Password" => { "Type" => PasswordDigestURI } }
|
91
|
+
else
|
92
|
+
wsse_security "UsernameToken",
|
93
|
+
"wsse:Username" => username,
|
94
|
+
"wsse:Password" => password,
|
95
|
+
:attributes! => { "wsse:Password" => { "Type" => PasswordTextURI } }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a Hash containing wsse:Timestamp details.
|
100
|
+
def wsse_timestamp
|
101
|
+
wsse_security "Timestamp",
|
102
|
+
"wsu:Created" => (created_at || Time.now).xs_datetime,
|
103
|
+
"wsu:Expires" => (expires_at || (created_at || Time.now) + 60).xs_datetime
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns a Hash containing wsse:Security details for a given +tag+ and +hash+.
|
107
|
+
def wsse_security(tag, hash)
|
108
|
+
{
|
109
|
+
"wsse:Security" => {
|
110
|
+
"wsse:#{tag}" => hash,
|
111
|
+
:attributes! => { "wsse:#{tag}" => { "wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSUNamespace } }
|
112
|
+
},
|
113
|
+
:attributes! => { "wsse:Security" => { "xmlns:wsse" => WSENamespace } }
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the WSSE password, encrypted for digest authentication.
|
118
|
+
def digest_password
|
119
|
+
token = nonce + timestamp + password
|
120
|
+
Base64.encode64(Digest::SHA1.hexdigest(token)).chomp!
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns a WSSE nonce.
|
124
|
+
def nonce
|
125
|
+
@nonce ||= Digest::SHA1.hexdigest random_string + timestamp
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a random String of 100 characters.
|
129
|
+
def random_string
|
130
|
+
(0...100).map { ("a".."z").to_a[rand(26)] }.join
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns a WSSE timestamp.
|
134
|
+
def timestamp
|
135
|
+
@timestamp ||= Time.now.xs_datetime
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns a new number with every call.
|
139
|
+
def count
|
140
|
+
@count ||= 0
|
141
|
+
@count += 1
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns a memoized and autovivificating Hash.
|
145
|
+
def hash
|
146
|
+
@hash ||= Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
data/savon.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$:.unshift lib unless $:.include? lib
|
3
|
+
|
4
|
+
require "savon/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "s-savon"
|
8
|
+
s.version = Savon::Version
|
9
|
+
s.authors = "Daniel Harrington"
|
10
|
+
s.email = "me@rubiii.com"
|
11
|
+
s.homepage = "http://github.com/rubiii/#{s.name}"
|
12
|
+
s.summary = "Heavy metal Ruby SOAP client"
|
13
|
+
s.description = "Savon is the heavy metal Ruby SOAP client."
|
14
|
+
|
15
|
+
s.rubyforge_project = s.name
|
16
|
+
|
17
|
+
s.add_dependency "builder", ">= 2.1.2"
|
18
|
+
s.add_dependency "crack", "~> 0.1.8"
|
19
|
+
s.add_dependency "httpi", ">= 0.7.8"
|
20
|
+
s.add_dependency "gyoku", ">= 0.3.0"
|
21
|
+
|
22
|
+
s.add_development_dependency "rspec", "~> 2.4.0"
|
23
|
+
s.add_development_dependency "autotest"
|
24
|
+
s.add_development_dependency "mocha", "~> 0.9.8"
|
25
|
+
s.add_development_dependency "timecop", "~> 0.3.5"
|
26
|
+
|
27
|
+
s.files = `git ls-files`.split("\n")
|
28
|
+
s.require_path = "lib"
|
29
|
+
end
|
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
|
2
|
+
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
|
3
|
+
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
|
4
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
5
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
6
|
+
<SOAP-ENV:Body>
|
7
|
+
<SOAP-ENV:Fault>
|
8
|
+
<faultcode>ERR_NO_SESSION</faultcode>
|
9
|
+
<faultfactor>doGetItemsInfo - Wrong session</faultfactor>
|
10
|
+
<faultstring>Wrong session message</faultstring>
|
11
|
+
<detail><soapVal><ERRNO xsi:type="xsd:string">80:1289245853:55</ERRNO></soapVal></detail>
|
12
|
+
</SOAP-ENV:Fault>
|
13
|
+
</SOAP-ENV:Body>
|
14
|
+
</SOAP-ENV:Envelope>
|