wash-out 0.10.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.travis.yml +35 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +102 -0
- data/Gemfile +16 -0
- data/Guardfile +12 -0
- data/LICENSE +22 -0
- data/README.md +246 -0
- data/Rakefile +13 -0
- data/app/helpers/wash_out_helper.rb +106 -0
- data/app/views/wash_out/document/error.builder +9 -0
- data/app/views/wash_out/document/response.builder +10 -0
- data/app/views/wash_out/document/wsdl.builder +68 -0
- data/app/views/wash_out/rpc/error.builder +10 -0
- data/app/views/wash_out/rpc/response.builder +11 -0
- data/app/views/wash_out/rpc/wsdl.builder +68 -0
- data/gemfiles/rails_3.1.3.gemfile +20 -0
- data/gemfiles/rails_3.2.12.gemfile +20 -0
- data/gemfiles/rails_4.0.0.gemfile +19 -0
- data/gemfiles/rails_4.1.0.gemfile +19 -0
- data/gemfiles/rails_4.2.0.gemfile +19 -0
- data/gemfiles/rails_5.0.0.beta2.gemfile +19 -0
- data/init.rb +1 -0
- data/lib/wash_out.rb +53 -0
- data/lib/wash_out/configurable.rb +41 -0
- data/lib/wash_out/dispatcher.rb +218 -0
- data/lib/wash_out/engine.rb +12 -0
- data/lib/wash_out/middleware.rb +41 -0
- data/lib/wash_out/model.rb +29 -0
- data/lib/wash_out/param.rb +200 -0
- data/lib/wash_out/router.rb +95 -0
- data/lib/wash_out/soap.rb +48 -0
- data/lib/wash_out/soap_config.rb +93 -0
- data/lib/wash_out/type.rb +29 -0
- data/lib/wash_out/version.rb +3 -0
- data/lib/wash_out/wsse.rb +101 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +51 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +23 -0
- data/spec/dummy/config/environments/test.rb +30 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +8 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/stylesheets/.gitkeep +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/lib/wash_out/dispatcher_spec.rb +99 -0
- data/spec/lib/wash_out/middleware_spec.rb +33 -0
- data/spec/lib/wash_out/param_spec.rb +94 -0
- data/spec/lib/wash_out/router_spec.rb +22 -0
- data/spec/lib/wash_out/type_spec.rb +41 -0
- data/spec/lib/wash_out_spec.rb +754 -0
- data/spec/spec_helper.rb +82 -0
- data/wash_out.gemspec +21 -0
- metadata +128 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'nori'
|
2
|
+
|
3
|
+
module WashOut
|
4
|
+
# This class is a Rack middleware used to route SOAP requests to a proper
|
5
|
+
# action of a given SOAP controller.
|
6
|
+
class Router
|
7
|
+
def initialize(controller_name)
|
8
|
+
@controller_name = "#{controller_name.to_s}_controller".camelize
|
9
|
+
end
|
10
|
+
|
11
|
+
def controller
|
12
|
+
@controller
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_soap_action(env)
|
16
|
+
return env['wash_out.soap_action'] if env['wash_out.soap_action']
|
17
|
+
|
18
|
+
soap_action = controller.soap_config.soap_action_routing ? env['HTTP_SOAPACTION'].to_s.gsub(/^"(.*)"$/, '\1')
|
19
|
+
: ''
|
20
|
+
|
21
|
+
if soap_action.blank?
|
22
|
+
parsed_soap_body = nori(controller.soap_config.snakecase_input).parse(soap_body env)
|
23
|
+
return nil if parsed_soap_body.blank?
|
24
|
+
|
25
|
+
soap_action = parsed_soap_body
|
26
|
+
.values_at(:envelope, :Envelope).compact.first
|
27
|
+
.values_at(:body, :Body).compact.first
|
28
|
+
.keys.first.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
# RUBY18 1.8 does not have force_encoding.
|
32
|
+
soap_action.force_encoding('UTF-8') if soap_action.respond_to? :force_encoding
|
33
|
+
|
34
|
+
if controller.soap_config.namespace
|
35
|
+
namespace = Regexp.escape controller.soap_config.namespace.to_s
|
36
|
+
soap_action.gsub!(/^(#{namespace}(\/|#)?)?([^"]*)$/, '\3')
|
37
|
+
end
|
38
|
+
|
39
|
+
env['wash_out.soap_action'] = soap_action
|
40
|
+
end
|
41
|
+
|
42
|
+
def nori(snakecase=false)
|
43
|
+
Nori.new(
|
44
|
+
:parser => controller.soap_config.parser,
|
45
|
+
:strip_namespaces => true,
|
46
|
+
:advanced_typecasting => true,
|
47
|
+
:convert_tags_to => (
|
48
|
+
snakecase ? lambda { |tag| tag.snakecase.to_sym }
|
49
|
+
: lambda { |tag| tag.to_sym }
|
50
|
+
)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def soap_body(env)
|
55
|
+
body = env['rack.input']
|
56
|
+
body.rewind if body.respond_to? :rewind
|
57
|
+
body.respond_to?(:string) ? body.string : body.read
|
58
|
+
ensure
|
59
|
+
body.rewind if body.respond_to? :rewind
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_soap_parameters(env)
|
63
|
+
return env['wash_out.soap_data'] if env['wash_out.soap_data']
|
64
|
+
|
65
|
+
env['wash_out.soap_data'] = nori(controller.soap_config.snakecase_input).parse(soap_body env)
|
66
|
+
references = WashOut::Dispatcher.deep_select(env['wash_out.soap_data']){|k,v| v.is_a?(Hash) && v.has_key?(:@id)}
|
67
|
+
|
68
|
+
unless references.blank?
|
69
|
+
replaces = {}; references.each{|r| replaces['#'+r[:@id]] = r}
|
70
|
+
env['wash_out.soap_data'] = WashOut::Dispatcher.deep_replace_href(env['wash_out.soap_data'], replaces)
|
71
|
+
end
|
72
|
+
|
73
|
+
env['wash_out.soap_data']
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(env)
|
77
|
+
@controller = @controller_name.constantize
|
78
|
+
|
79
|
+
soap_action = parse_soap_action(env)
|
80
|
+
return [200, {}, ['OK']] if soap_action.blank?
|
81
|
+
|
82
|
+
soap_parameters = parse_soap_parameters(env)
|
83
|
+
|
84
|
+
action_spec = controller.soap_actions[soap_action]
|
85
|
+
|
86
|
+
if action_spec
|
87
|
+
action = action_spec[:to]
|
88
|
+
else
|
89
|
+
action = '_invalid_action'
|
90
|
+
end
|
91
|
+
|
92
|
+
controller.action(action).call(env)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module WashOut
|
4
|
+
module SOAP
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr_accessor :soap_actions
|
9
|
+
|
10
|
+
# Define a SOAP action +action+. The function has two required +options+:
|
11
|
+
# :args and :return. Each is a type +definition+ of format described in
|
12
|
+
# WashOut::Param#parse_def.
|
13
|
+
#
|
14
|
+
# An optional option :to can be passed to allow for names of SOAP actions
|
15
|
+
# which are not valid Ruby function names.
|
16
|
+
def soap_action(action, options={})
|
17
|
+
if action.is_a?(Symbol)
|
18
|
+
if soap_config.camelize_wsdl.to_s == 'lower'
|
19
|
+
options[:to] ||= action.to_s
|
20
|
+
action = action.to_s.camelize(:lower)
|
21
|
+
elsif soap_config.camelize_wsdl
|
22
|
+
options[:to] ||= action.to_s
|
23
|
+
action = action.to_s.camelize
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
default_response_tag = soap_config.camelize_wsdl ? 'Response' : '_response'
|
29
|
+
default_response_tag = action+default_response_tag
|
30
|
+
|
31
|
+
self.soap_actions[action] = options.merge(
|
32
|
+
:in => WashOut::Param.parse_def(soap_config, options[:args]),
|
33
|
+
:request_tag => options[:as] || action,
|
34
|
+
:out => WashOut::Param.parse_def(soap_config, options[:return]),
|
35
|
+
:to => options[:to] || action,
|
36
|
+
:response_tag => options[:response_tag] || default_response_tag
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
included do
|
42
|
+
include WashOut::Configurable
|
43
|
+
include WashOut::Dispatcher
|
44
|
+
include WashOut::WsseParams
|
45
|
+
self.soap_actions = {}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module WashOut
|
2
|
+
require 'forwardable'
|
3
|
+
# Configuration options for {Client}, defaulting to values
|
4
|
+
# in {Default}
|
5
|
+
class SoapConfig
|
6
|
+
extend Forwardable
|
7
|
+
DEFAULT_CONFIG = {
|
8
|
+
parser: :rexml,
|
9
|
+
namespace: 'urn:WashOut',
|
10
|
+
wsdl_style: 'rpc',
|
11
|
+
snakecase_input: false,
|
12
|
+
camelize_wsdl: false,
|
13
|
+
catch_xml_errors: false,
|
14
|
+
wsse_username: nil,
|
15
|
+
wsse_password: nil,
|
16
|
+
wsse_auth_callback: nil,
|
17
|
+
soap_action_routing: true,
|
18
|
+
}
|
19
|
+
|
20
|
+
attr_reader :config
|
21
|
+
def_delegators :@config, :[], :[]=, :sort
|
22
|
+
|
23
|
+
|
24
|
+
# The keys allowed
|
25
|
+
def self.keys
|
26
|
+
@keys ||= config.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.config
|
30
|
+
DEFAULT_CONFIG
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.soap_accessor(*syms)
|
34
|
+
syms.each do |sym|
|
35
|
+
|
36
|
+
unless sym =~ /^[_A-Za-z]\w*$/
|
37
|
+
raise NameError.new("invalid class attribute name: #{sym}")
|
38
|
+
end
|
39
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
40
|
+
unless defined? @#{sym}
|
41
|
+
@#{sym} = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def #{sym}
|
45
|
+
@#{sym}
|
46
|
+
end
|
47
|
+
|
48
|
+
def #{sym}=(obj)
|
49
|
+
@#{sym} = obj
|
50
|
+
end
|
51
|
+
EOS
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
soap_accessor(*WashOut::SoapConfig.keys)
|
56
|
+
|
57
|
+
def initialize(options = {})
|
58
|
+
@config = {}
|
59
|
+
options.reverse_merge!(engine_config) if engine_config
|
60
|
+
options.reverse_merge!(DEFAULT_CONFIG)
|
61
|
+
configure options
|
62
|
+
end
|
63
|
+
|
64
|
+
def default?
|
65
|
+
DEFAULT_CONFIG.sort == config.sort
|
66
|
+
end
|
67
|
+
|
68
|
+
def configure(options = {})
|
69
|
+
@config.merge! validate_config!(options)
|
70
|
+
|
71
|
+
config.each do |key,value|
|
72
|
+
send("#{key}=", value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def engine_config
|
79
|
+
@engine_config ||= WashOut::Engine.config.wash_out
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_config!(options = {})
|
83
|
+
rejected_keys = options.keys.reject do |key|
|
84
|
+
WashOut::SoapConfig.keys.include?(key)
|
85
|
+
end
|
86
|
+
|
87
|
+
if rejected_keys.any?
|
88
|
+
raise "The following keys are not allows: #{rejected_keys}\n Did you intend for one of the following? #{WashOut::SoapConfig.keys}"
|
89
|
+
end
|
90
|
+
options
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module WashOut
|
2
|
+
class Type
|
3
|
+
|
4
|
+
def self.type_name(value)
|
5
|
+
@param_type_name = value.to_s
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.map(value)
|
9
|
+
raise RuntimeError, "Wrong definition: #{value.inspect}" unless value.is_a?(Hash)
|
10
|
+
@param_map = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.wash_out_param_map
|
14
|
+
@param_map
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.wash_out_param_name(soap_config = nil)
|
18
|
+
soap_config ||= WashOut::SoapConfig.new({})
|
19
|
+
@param_type_name ||= name.underscore.gsub '/', '.'
|
20
|
+
|
21
|
+
if soap_config.camelize_wsdl.to_s == 'lower'
|
22
|
+
@param_type_name = @param_type_name.camelize(:lower)
|
23
|
+
elsif soap_config.camelize_wsdl
|
24
|
+
@param_type_name = @param_type_name.camelize
|
25
|
+
end
|
26
|
+
@param_type_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module WashOut
|
2
|
+
|
3
|
+
module WsseParams
|
4
|
+
def wsse_username
|
5
|
+
if request.env['WSSE_TOKEN']
|
6
|
+
request.env['WSSE_TOKEN'].values_at(:username, :Username).compact.first
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Wsse
|
12
|
+
attr_reader :soap_config
|
13
|
+
def self.authenticate(soap_config, token)
|
14
|
+
wsse = self.new(soap_config, token)
|
15
|
+
|
16
|
+
unless wsse.eligible?
|
17
|
+
raise WashOut::Dispatcher::SOAPError, "Unauthorized"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(soap_config, token)
|
22
|
+
@soap_config = soap_config
|
23
|
+
if token.blank? && required?
|
24
|
+
raise WashOut::Dispatcher::SOAPError, "Missing required UsernameToken"
|
25
|
+
end
|
26
|
+
@username_token = token
|
27
|
+
end
|
28
|
+
|
29
|
+
def required?
|
30
|
+
!soap_config.wsse_username.blank? || auth_callback?
|
31
|
+
end
|
32
|
+
|
33
|
+
def auth_callback?
|
34
|
+
return !!soap_config.wsse_auth_callback && soap_config.wsse_auth_callback.respond_to?(:call) && soap_config.wsse_auth_callback.arity == 2
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform_auth_callback(user, password)
|
38
|
+
soap_config.wsse_auth_callback.call(user, password)
|
39
|
+
end
|
40
|
+
|
41
|
+
def expected_user
|
42
|
+
soap_config.wsse_username
|
43
|
+
end
|
44
|
+
|
45
|
+
def expected_password
|
46
|
+
soap_config.wsse_password
|
47
|
+
end
|
48
|
+
|
49
|
+
def matches_expected_digest?(password)
|
50
|
+
nonce = @username_token.values_at(:nonce, :Nonce).compact.first
|
51
|
+
timestamp = @username_token.values_at(:created, :Created).compact.first
|
52
|
+
return false if nonce.nil? || timestamp.nil?
|
53
|
+
timestamp = timestamp.to_datetime
|
54
|
+
|
55
|
+
# Token should not be accepted if timestamp is older than 5 minutes ago
|
56
|
+
# http://www.oasis-open.org/committees/download.php/16782/wss-v1.1-spec-os-UsernameTokenProfile.pdf
|
57
|
+
offset_in_minutes = ((DateTime.now - timestamp)* 24 * 60).to_i
|
58
|
+
return false if offset_in_minutes >= 5
|
59
|
+
|
60
|
+
# There are a few different implementations of the digest calculation
|
61
|
+
|
62
|
+
flavors = Array.new
|
63
|
+
|
64
|
+
# Ruby / Savon
|
65
|
+
token = nonce + timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") + expected_password
|
66
|
+
flavors << Base64.encode64(Digest::SHA1.hexdigest(token)).chomp!
|
67
|
+
|
68
|
+
# Java
|
69
|
+
token = Base64.decode64(nonce) + timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") + expected_password
|
70
|
+
flavors << Base64.encode64(Digest::SHA1.digest(token)).chomp!
|
71
|
+
|
72
|
+
flavors.each do |f|
|
73
|
+
return true if f == password
|
74
|
+
end
|
75
|
+
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
|
79
|
+
def eligible?
|
80
|
+
return true unless required?
|
81
|
+
|
82
|
+
user = @username_token.values_at(:username, :Username).compact.first
|
83
|
+
password = @username_token.values_at(:password, :Password).compact.first
|
84
|
+
|
85
|
+
if (expected_user == user && matches_expected_digest?(password))
|
86
|
+
return true
|
87
|
+
end
|
88
|
+
|
89
|
+
if auth_callback?
|
90
|
+
return perform_auth_callback(user, password)
|
91
|
+
end
|
92
|
+
|
93
|
+
if (expected_user == user && expected_password == password)
|
94
|
+
return true
|
95
|
+
end
|
96
|
+
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/spec/dummy/Rakefile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
2
|
+
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
3
|
+
|
4
|
+
require File.expand_path('../config/application', __FILE__)
|
5
|
+
require 'rake'
|
6
|
+
|
7
|
+
Dummy::Application.load_tasks
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require File.expand_path('../boot', __FILE__)
|
2
|
+
|
3
|
+
require "action_controller/railtie"
|
4
|
+
require "rails/test_unit/railtie"
|
5
|
+
|
6
|
+
if Rails::VERSION::MAJOR >= 4
|
7
|
+
# Ugly hack to make Rails 4 JRuby-compatible to escape Travis errors
|
8
|
+
Rails::Engine.class_eval do
|
9
|
+
def railties
|
10
|
+
@railties ||= self.class.const_get(:Railties).new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Bundler.require
|
16
|
+
require "wash_out"
|
17
|
+
|
18
|
+
module Dummy
|
19
|
+
class Application < Rails::Application
|
20
|
+
# Settings in config/environments/* take precedence over those specified here.
|
21
|
+
# Application configuration should go into files in config/initializers
|
22
|
+
# -- all .rb files in that directory are automatically loaded.
|
23
|
+
|
24
|
+
# Custom directories with classes and modules you want to be autoloadable.
|
25
|
+
# config.autoload_paths += %W(#{config.root}/extras)
|
26
|
+
|
27
|
+
# Only load the plugins named here, in the order given (default is alphabetical).
|
28
|
+
# :all can be used as a placeholder for all plugins not explicitly named.
|
29
|
+
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
|
30
|
+
|
31
|
+
# Activate observers that should always be running.
|
32
|
+
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
|
33
|
+
|
34
|
+
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
35
|
+
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
36
|
+
# config.time_zone = 'Central Time (US & Canada)'
|
37
|
+
|
38
|
+
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
39
|
+
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
40
|
+
# config.i18n.default_locale = :de
|
41
|
+
|
42
|
+
# JavaScript files you want as :defaults (application.js is always included).
|
43
|
+
# config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
|
44
|
+
|
45
|
+
# Configure the default encoding used in templates for Ruby 1.9.
|
46
|
+
config.encoding = "utf-8"
|
47
|
+
|
48
|
+
# Configure sensitive parameters which will be filtered from the log file.
|
49
|
+
config.filter_parameters += [:password]
|
50
|
+
end
|
51
|
+
end
|