Skip to content

Commit

Permalink
changed Savon::WSSE to be based on a Hash instead of using builder, w…
Browse files Browse the repository at this point in the history
…hich allows custom WSSE header tags (issue #69) and wsse:Timestamp headers (issue #122)
  • Loading branch information
rubiii committed Jan 4, 2011
1 parent 818b164 commit 4cebc34
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 36 deletions.
10 changes: 10 additions & 0 deletions lib/savon/core_ext/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ module Savon
module CoreExt
module Hash

# Returns a new Hash with +self+ and +other_hash+ merged recursively.
# Modifies the receiver in place.
def deep_merge!(other_hash)
other_hash.each_pair do |k,v|
tv = self[k]
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
end
self
end unless defined? deep_merge!

# Returns the values from the soap:Body element or an empty Hash in case the soap:Body tag could
# not be found.
def find_soap_body
Expand Down
14 changes: 14 additions & 0 deletions lib/savon/core_ext/time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Savon
module CoreExt
module Time

# Returns an xs:dateTime formatted String.
def xs_datetime
strftime "%Y-%m-%dT%H:%M:%S%Z"
end

end
end
end

Time.send :include, Savon::CoreExt::Time
3 changes: 0 additions & 3 deletions lib/savon/soap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ module SOAP
2 => "http://www.w3.org/2003/05/soap-envelope"
}

# SOAP xs:dateTime format.
DateTimeFormat = "%Y-%m-%dT%H:%M:%S%Z"

# SOAP xs:dateTime Regexp.
DateTimeRegexp = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/

Expand Down
97 changes: 73 additions & 24 deletions lib/savon/wsse.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
require "base64"
require "digest/sha1"
require "builder"

require "savon/core_ext/string"
require "savon/soap"
require "savon/core_ext/hash"
require "savon/core_ext/time"

module Savon

Expand All @@ -24,14 +24,25 @@ class WSSE
# URI for "wsse:Password/@Type" #PasswordDigest.
PasswordDigestURI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"

# Sets the authentication credentials. Also accepts whether to use WSSE digest.
# Returns a value from the WSSE Hash.
def [](key)
hash[key]
end

# Sets a value on the WSSE Hash.
def []=(key, value)
hash[key] = value
end

# Sets authentication credentials for a wsse:UsernameToken header.
# Also accepts whether to use WSSE digest authentication.
def credentials(username, password, digest = false)
self.username = username
self.password = password
self.digest = digest
end

attr_accessor :username, :password
attr_accessor :username, :password, :created_at, :expires_at

# Returns whether to use WSSE digest. Defaults to +false+.
def digest?
Expand All @@ -40,26 +51,64 @@ def digest?

attr_writer :digest

# Returns the XML for a WSSE header or an empty String unless authentication
# credentials were specified.
# Returns whether to generate a wsse:UsernameToken header.
def username_token?
username && password
end

# Returns whether to generate a wsse:Timestamp header.
def timestamp?
created_at || expires_at || @wsse_timestamp
end

# Sets whether to generate a wsse:Timestamp header.
def timestamp=(timestamp)
@wsse_timestamp = timestamp
end

# Returns the XML for a WSSE header.
def to_xml
return "" unless username && password

builder = Builder::XmlMarkup.new
builder.wsse :Security, "xmlns:wsse" => WSENamespace do |xml|
xml.wsse :UsernameToken, "wsu:Id" => wsu_id, "xmlns:wsu" => WSUNamespace do
xml.wsse :Username, username
xml.wsse :Nonce, nonce
xml.wsu :Created, timestamp
xml.wsse :Password, password_node, :Type => password_type
end
if username_token?
Gyoku.xml wsse_username_token.merge!(hash)
elsif timestamp?
Gyoku.xml wsse_timestamp.merge!(hash)
else
""
end
end

private

# Returns a Hash containing wsse:UsernameToken details.
def wsse_username_token
wsse_security "UsernameToken",
"wsse:Username" => username,
"wsse:Nonce" => nonce,
"wsu:Created" => timestamp,
"wsse:Password" => password_value,
:attributes! => { "wsse:Password" => { "Type" => password_type } }
end

# Returns a Hash containing wsse:Timestamp details.
def wsse_timestamp
wsse_security "Timestamp",
"wsu:Created" => (created_at || Time.now).xs_datetime,
"wsu:Expires" => (expires_at || (created_at || Time.now) + 60).xs_datetime
end

# Returns a Hash containing wsse:Security details for a given +tag+ and +hash+.
def wsse_security(tag, hash)
{
"wsse:Security" => {
"wsse:#{tag}" => hash,
:attributes! => { "wsse:#{tag}" => { "wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSUNamespace } }
},
:attributes! => { "wsse:Security" => { "xmlns:wsse" => WSENamespace } }
}
end

# Returns the WSSE password. Encrypts the password for digest authentication.
def password_node
def password_value
return password unless digest?

token = nonce + timestamp + password
Expand All @@ -83,19 +132,19 @@ def random_string

# Returns a WSSE timestamp.
def timestamp
@timestamp ||= Time.now.strftime Savon::SOAP::DateTimeFormat
end

# Returns the "wsu:Id" attribute.
def wsu_id
"UsernameToken-#{count}"
@timestamp ||= Time.now.xs_datetime
end

# Simple counter.
# Returns a new number with every call.
def count
@count ||= 0
@count += 1
end

# Returns a memoized and autovivificating Hash.
def hash
@hash ||= Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
end

end
end
1 change: 1 addition & 0 deletions savon.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Gem::Specification.new do |s|
s.add_development_dependency "rspec", "~> 2.0.0"
s.add_development_dependency "autotest"
s.add_development_dependency "mocha", "~> 0.9.7"
s.add_development_dependency "timecop", "~> 0.3.5"

s.files = `git ls-files`.split("\n")
s.require_path = "lib"
Expand Down
9 changes: 9 additions & 0 deletions spec/savon/core_ext/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

describe Hash do

describe "#deep_merge!" do
it "should recursively merge two Hashes" do
hash = { :one => 1, "two" => { "three" => 3 } }
other_hash = { :four => 4, "two" => { "three" => "merge", :five => 5 } }

hash.merge!(other_hash).should == { :one => 1, :four => 4, "two" => { "three" => "merge", :five => 5 } }
end
end

describe "find_soap_body" do
it "should return the content from the 'soap:Body' element" do
soap_body = { "soap:Envelope" => { "soap:Body" => "content" } }
Expand Down
13 changes: 13 additions & 0 deletions spec/savon/core_ext/time_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require "spec_helper"

describe Time do

describe "#xs_datetime" do
let(:time) { Time.utc(2011, 01, 04, 13, 45, 55) }

it "should return an xs:dateTime formatted String" do
time.xs_datetime.should == "2011-01-04T13:45:55UTC"
end
end

end
10 changes: 1 addition & 9 deletions spec/savon/soap_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,9 @@
Savon::SOAP::Versions.should == (1..2)
end

it "should contain the xs:dateTime format" do
Savon::SOAP::DateTimeFormat.should be_a(String)
Savon::SOAP::DateTimeFormat.should_not be_empty

DateTime.new(2012, 03, 22, 16, 22, 33).strftime(Savon::SOAP::DateTimeFormat).
should == "2012-03-22T16:22:33+00:00"
end

it "should contain a Regexp matching the xs:dateTime format" do
Savon::SOAP::DateTimeRegexp.should be_a(Regexp)
(Savon::SOAP::DateTimeRegexp === "2012-03-22T16:22:33").should be_true
end

end
50 changes: 50 additions & 0 deletions spec/savon/wsse_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,56 @@
wsse.to_xml.should include(Savon::WSSE::PasswordDigestURI)
end
end

context "with #timestamp set to true" do
before { wsse.timestamp = true }

it "should contain a wsse:Timestamp node" do
wsse.to_xml.should include('<wsse:Timestamp wsu:Id="Timestamp-1" ' +
'xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">')
end

it "should contain a wsu:Created node defaulting to Time.now" do
created_at = Time.now
Timecop.freeze created_at do
wsse.to_xml.should include("<wsu:Created>#{created_at.xs_datetime}</wsu:Created>")
end
end

it "should contain a wsu:Expires node defaulting to Time.now + 60 seconds" do
created_at = Time.now
Timecop.freeze created_at do
wsse.to_xml.should include("<wsu:Expires>#{(created_at + 60).xs_datetime}</wsu:Expires>")
end
end
end

context "with #created_at" do
before { wsse.created_at = Time.now + 86400 }

it "should contain a wsu:Created node with the given time" do
wsse.to_xml.should include("<wsu:Created>#{wsse.created_at.xs_datetime}</wsu:Created>")
end

it "should contain a wsu:Expires node set to #created_at + 60 seconds" do
wsse.to_xml.should include("<wsu:Expires>#{(wsse.created_at + 60).xs_datetime}</wsu:Expires>")
end
end

context "with #expires_at" do
before { wsse.expires_at = Time.now + 86400 }

it "should contain a wsu:Created node defaulting to Time.now" do
created_at = Time.now
Timecop.freeze created_at do
wsse.to_xml.should include("<wsu:Created>#{created_at.xs_datetime}</wsu:Created>")
end
end

it "should contain a wsu:Expires node set to the given time" do
wsse.to_xml.should include("<wsu:Expires>#{wsse.expires_at.xs_datetime}</wsu:Expires>")
end
end
end

end

0 comments on commit 4cebc34

Please sign in to comment.