wash_out_fork 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.travis.yml +36 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +102 -0
- data/Gemfile +17 -0
- data/Guardfile +12 -0
- data/LICENSE +22 -0
- data/README.md +242 -0
- data/Rakefile +25 -0
- data/app/helpers/wash_out_fork_helper.rb +110 -0
- data/app/views/wash_out_fork/document/error.builder +9 -0
- data/app/views/wash_out_fork/document/response.builder +8 -0
- data/app/views/wash_out_fork/document/wsdl.builder +68 -0
- data/app/views/wash_out_fork/rpc/error.builder +10 -0
- data/app/views/wash_out_fork/rpc/response.builder +9 -0
- data/app/views/wash_out_fork/rpc/wsdl.builder +68 -0
- data/gemfiles/rails_3.2.13.gemfile +21 -0
- data/gemfiles/rails_4.0.0.gemfile +20 -0
- data/gemfiles/rails_4.1.0.gemfile +20 -0
- data/gemfiles/rails_4.2.0.gemfile +20 -0
- data/gemfiles/rails_5.0.0.beta2.gemfile +19 -0
- data/gemfiles/rails_5.0.0.gemfile +19 -0
- data/init.rb +1 -0
- data/lib/wash_out_fork.rb +60 -0
- data/lib/wash_out_fork/configurable.rb +41 -0
- data/lib/wash_out_fork/dispatcher.rb +232 -0
- data/lib/wash_out_fork/engine.rb +12 -0
- data/lib/wash_out_fork/middleware.rb +41 -0
- data/lib/wash_out_fork/model.rb +29 -0
- data/lib/wash_out_fork/param.rb +200 -0
- data/lib/wash_out_fork/router.rb +131 -0
- data/lib/wash_out_fork/soap.rb +48 -0
- data/lib/wash_out_fork/soap_config.rb +93 -0
- data/lib/wash_out_fork/type.rb +29 -0
- data/lib/wash_out_fork/version.rb +3 -0
- data/lib/wash_out_fork/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/fixtures/nested_refs_to_arrays.xml +19 -0
- data/spec/fixtures/ref_to_one_array.xml +11 -0
- data/spec/fixtures/refs_to_arrays.xml +16 -0
- data/spec/lib/wash_out/dispatcher_spec.rb +206 -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 +50 -0
- data/spec/lib/wash_out/type_spec.rb +41 -0
- data/spec/lib/wash_out_spec.rb +797 -0
- data/spec/spec_helper.rb +88 -0
- data/wash_out_fork.gemspec +20 -0
- metadata +130 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'nori'
|
2
|
+
|
3
|
+
module WashOutFork
|
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 self.lookup_soap_routes(controller_name, routes)
|
8
|
+
results = []
|
9
|
+
|
10
|
+
routes.each do |x|
|
11
|
+
defaults = x.defaults
|
12
|
+
defaults = defaults[:defaults] if defaults.include?(:defaults) # Rails 5
|
13
|
+
if defaults[:controller] == controller_name && defaults[:action] == 'soap'
|
14
|
+
results << x
|
15
|
+
end
|
16
|
+
|
17
|
+
app = x.app
|
18
|
+
app = app.app if app.respond_to?(:app)
|
19
|
+
if app.is_a?(Class) && app.ancestors.include?(Rails::Engine)
|
20
|
+
results += lookup_soap_routes(controller_name, app.routes.routes)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
results
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.url(request, controller_name)
|
28
|
+
route = lookup_soap_routes(controller_name, Rails.application.routes.routes).first
|
29
|
+
|
30
|
+
path = if route.respond_to?(:optimized_path) # Rails 4
|
31
|
+
route.optimized_path
|
32
|
+
elsif route.path.respond_to?(:build_formatter) # Rails 5
|
33
|
+
route.path.build_formatter.evaluate(nil)
|
34
|
+
else
|
35
|
+
route.format({}) # Rails 3.2
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
path = path.join('') if path.is_a?(Array)
|
40
|
+
|
41
|
+
request.protocol + request.host_with_port + path
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(controller_name)
|
45
|
+
@controller_name = "#{controller_name.to_s}_controller".camelize
|
46
|
+
end
|
47
|
+
|
48
|
+
def controller
|
49
|
+
@controller
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_soap_action(env)
|
53
|
+
return env['wash_out_fork.soap_action'] if env['wash_out_fork.soap_action']
|
54
|
+
|
55
|
+
soap_action = controller.soap_config.soap_action_routing ? env['HTTP_SOAPACTION'].to_s.gsub(/^"(.*)"$/, '\1')
|
56
|
+
: ''
|
57
|
+
if soap_action.blank?
|
58
|
+
parsed_soap_body = nori(controller.soap_config.snakecase_input).parse(soap_body env)
|
59
|
+
return nil if parsed_soap_body.blank?
|
60
|
+
|
61
|
+
soap_action = parsed_soap_body.values_at(:envelope, :Envelope).try(:compact).try(:first)
|
62
|
+
soap_action = soap_action.values_at(:body, :Body).try(:compact).try(:first) if soap_action
|
63
|
+
soap_action = soap_action.keys.first.to_s if soap_action
|
64
|
+
end
|
65
|
+
|
66
|
+
# RUBY18 1.8 does not have force_encoding.
|
67
|
+
soap_action.force_encoding('UTF-8') if soap_action.respond_to? :force_encoding
|
68
|
+
|
69
|
+
if controller.soap_config.namespace
|
70
|
+
namespace = Regexp.escape controller.soap_config.namespace.to_s
|
71
|
+
soap_action.gsub!(/^(#{namespace}(\/|#)?)?([^"]*)$/, '\3')
|
72
|
+
end
|
73
|
+
|
74
|
+
env['wash_out_fork.soap_action'] = soap_action
|
75
|
+
end
|
76
|
+
|
77
|
+
def nori(snakecase=false)
|
78
|
+
Nori.new(
|
79
|
+
:parser => controller.soap_config.parser,
|
80
|
+
:strip_namespaces => true,
|
81
|
+
:advanced_typecasting => true,
|
82
|
+
:convert_tags_to => (
|
83
|
+
snakecase ? lambda { |tag| tag.snakecase.to_sym }
|
84
|
+
: lambda { |tag| tag.to_sym }
|
85
|
+
)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def soap_body(env)
|
90
|
+
body = env['rack.input']
|
91
|
+
body.rewind if body.respond_to? :rewind
|
92
|
+
body.respond_to?(:string) ? body.string : body.read
|
93
|
+
ensure
|
94
|
+
body.rewind if body.respond_to? :rewind
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_soap_parameters(env)
|
98
|
+
return env['wash_out_fork.soap_data'] if env['wash_out_fork.soap_data']
|
99
|
+
env['wash_out_fork.soap_data'] = nori(controller.soap_config.snakecase_input).parse(soap_body env)
|
100
|
+
references = WashOutFork::Dispatcher.deep_select(env['wash_out_fork.soap_data']){|v| v.is_a?(Hash) && v.has_key?(:@id)}
|
101
|
+
|
102
|
+
unless references.blank?
|
103
|
+
replaces = {}; references.each{|r| replaces['#'+r[:@id]] = r}
|
104
|
+
env['wash_out_fork.soap_data'] = WashOutFork::Dispatcher.deep_replace_href(env['wash_out_fork.soap_data'], replaces)
|
105
|
+
end
|
106
|
+
|
107
|
+
env['wash_out_fork.soap_data']
|
108
|
+
end
|
109
|
+
|
110
|
+
def call(env)
|
111
|
+
@controller = @controller_name.constantize
|
112
|
+
|
113
|
+
soap_action = parse_soap_action(env)
|
114
|
+
|
115
|
+
action = if soap_action.blank?
|
116
|
+
'_invalid_request'
|
117
|
+
else
|
118
|
+
soap_parameters = parse_soap_parameters(env)
|
119
|
+
action_spec = controller.soap_actions[soap_action]
|
120
|
+
|
121
|
+
if action_spec
|
122
|
+
action_spec[:to]
|
123
|
+
else
|
124
|
+
'_invalid_action'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
env["action_dispatch.request.content_type"] = Mime[:soap]
|
128
|
+
controller.action(action).call(env)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module WashOutFork
|
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
|
+
# WashOutFork::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 => WashOutFork::Param.parse_def(soap_config, options[:args]),
|
33
|
+
:request_tag => options[:as] || action,
|
34
|
+
:out => WashOutFork::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 WashOutFork::Configurable
|
43
|
+
include WashOutFork::Dispatcher
|
44
|
+
include WashOutFork::WsseParams
|
45
|
+
self.soap_actions = {}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module WashOutFork
|
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:WashOutFork',
|
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(*WashOutFork::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 ||= WashOutFork::Engine.config.wash_out_fork
|
80
|
+
end
|
81
|
+
|
82
|
+
def validate_config!(options = {})
|
83
|
+
rejected_keys = options.keys.reject do |key|
|
84
|
+
WashOutFork::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? #{WashOutFork::SoapConfig.keys}"
|
89
|
+
end
|
90
|
+
options
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module WashOutFork
|
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_fork_param_map
|
14
|
+
@param_map
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.wash_out_fork_param_name(soap_config = nil)
|
18
|
+
soap_config ||= WashOutFork::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 WashOutFork
|
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 WashOutFork::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 WashOutFork::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
|