/
dispatcher.rb
265 lines (216 loc) · 8.43 KB
/
dispatcher.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
module WashOut
# The WashOut::Dispatcher module should be included in a controller acting
# as a SOAP endpoint. It includes actions for generating WSDL and handling
# SOAP requests.
module Dispatcher
# A SOAPError exception can be raised to return a correct SOAP error
# response.
class SOAPError < Exception
attr_accessor :code
def initialize(message, code=nil)
super(message)
@code = code
end
end
class ProgrammerError < Exception; end
def _authenticate_wsse
begin
xml_security = request.env['wash_out.soap_data'].values_at(:envelope, :Envelope).compact.first
xml_security = xml_security.values_at(:header, :Header).compact.first
xml_security = xml_security.values_at(:security, :Security).compact.first
username_token = xml_security.values_at(:username_token, :UsernameToken).compact.first
rescue
username_token = nil
end
WashOut::Wsse.authenticate soap_config, username_token
request.env['WSSE_TOKEN'] = username_token.with_indifferent_access unless username_token.blank?
end
def _map_soap_parameters
self.params = _load_params action_spec[:in],
_strip_empty_nodes(action_spec[:in], xml_data)
end
def _map_soap_headers
@_soap_headers = xml_header_data
end
def _strip_empty_nodes(params, hash)
hash.keys.each do |key|
param = params.detect { |a| a.raw_name.to_s == key.to_s }
next if !(param && hash[key].is_a?(Hash))
value = hash[key].delete_if do |k, _|
k.to_s[0] == '@' && !param.map.detect { |a| a.raw_name.to_s == k.to_s }
end
if value.length > 0
hash[key] = _strip_empty_nodes param.map, value
else
hash[key] = nil
end
end
hash
end
# Creates the final parameter hash based on the request spec and xml_data from the request
def _load_params(spec, xml_data)
params = HashWithIndifferentAccess.new
spec.each do |param|
key = param.raw_name.to_sym
if xml_data.has_key? key
params[param.raw_name] = param.load(xml_data, key)
end
end
params
end
# This action generates the WSDL for defined SOAP methods.
def _generate_wsdl
@map = self.class.soap_actions
@namespace = soap_config.namespace
@name = controller_path
@service_name = soap_config.service_name
render :template => "wash_out/#{soap_config.wsdl_style}/wsdl", :layout => false,
:content_type => 'text/xml'
end
# Render a SOAP response.
def _render_soap(result, options)
@namespace = soap_config.namespace
@operation = soap_action = request.env['wash_out.soap_action']
@action_spec = self.class.soap_actions[soap_action]
result = { 'value' => result } unless result.is_a? Hash
result = HashWithIndifferentAccess.new(result)
inject = lambda {|data, map|
result_spec = []
return result_spec if data.nil?
map.each_with_index do |param, i|
result_spec[i] = param.flat_copy
unless data.is_a?(Hash)
raise ProgrammerError,
"SOAP response used #{data.inspect} (which is #{data.class.name}), " +
"in the context where a Hash with key of '#{param.raw_name}' " +
"was expected."
end
value = data[param.raw_name]
unless value.nil?
if param.multiplied && !value.is_a?(Array)
raise ProgrammerError,
"SOAP response tried to use '#{value.inspect}' " +
"(which is of type #{value.class.name}), as the value for " +
"'#{param.raw_name}' (which expects an Array)."
end
# Inline complex structure {:foo => {bar: ...}}
if param.struct? && !param.multiplied
result_spec[i].map = inject.call(value, param.map)
# Inline array of complex structures {:foo => [{bar: ...}]}
elsif param.struct? && param.multiplied
result_spec[i].map = value.map{|e| inject.call(e, param.map)}
# Inline scalar {:foo => :string}
else
result_spec[i].value = value
end
end
end
return result_spec
}
header = options[:header]
if header.present?
header = { 'value' => header } unless header.is_a? Hash
header = HashWithIndifferentAccess.new(header)
end
render :template => "wash_out/#{soap_config.wsdl_style}/response",
:layout => false,
:locals => {
:header => header.present? ? inject.call(header, @action_spec[:header_out])
: nil,
:result => inject.call(result, @action_spec[:out])
},
:content_type => 'text/xml'
end
# This action is a fallback for all undefined SOAP actions.
def _invalid_action
render_soap_error("Cannot find SOAP action mapping for #{request.env['wash_out.soap_action']}")
end
def _invalid_request
render_soap_error("Invalid SOAP request")
end
def _catch_soap_errors
yield
rescue SOAPError => error
render_soap_error(error.message, error.code)
end
# Render a SOAP error response.
#
# Rails do not support sequental rescue_from handling, that is, rescuing an
# exception from a rescue_from handler. Hence this function is a public API.
def render_soap_error(message, code=nil)
render :template => "wash_out/#{soap_config.wsdl_style}/error", :status => 500,
:layout => false,
:locals => { :error_message => message, :error_code => (code || 'Server') },
:content_type => 'text/xml'
end
def soap_request
OpenStruct.new({
params: @_params,
headers: @_soap_headers
})
end
def self.included(controller)
entity = if defined?(Rails::VERSION::MAJOR) && (Rails::VERSION::MAJOR >= 4)
'action'
else
'filter'
end
controller.send :"around_#{entity}", :_catch_soap_errors
controller.send :helper, :wash_out
controller.send :"before_#{entity}", :_authenticate_wsse, :if => :soap_action?
controller.send :"before_#{entity}", :_map_soap_parameters, :if => :soap_action?
controller.send :"before_#{entity}", :_map_soap_headers, :if => :soap_action?
if defined?(Rails::VERSION::MAJOR) && (Rails::VERSION::MAJOR >= 5)
controller.send :"skip_before_#{entity}", :verify_authenticity_token, :raise => false
else
controller.send :"skip_before_#{entity}", :verify_authenticity_token
end
end
def self.deep_select(collection, result=[], &blk)
values = collection.respond_to?(:values) ? collection.values : collection
result += values.select(&blk)
values.each do |value|
if value.is_a?(Hash) || value.is_a?(Array)
result = deep_select(value, result, &blk)
end
end
result
end
def self.deep_replace_href(element, replace)
return element unless element.is_a?(Array) || element.is_a?(Hash)
if element.is_a?(Array) # Traverse arrays
return element.map{|x| deep_replace_href(x, replace)}
end
if element.has_key?(:@href) # Replace needle and traverse replacement
return deep_replace_href(replace[element[:@href]], replace)
end
element.each do |key, value| # Traverse hashes
element[key] = deep_replace_href(value, replace)
end
element
end
private
def soap_action?
soap_action.present?
end
def action_spec
self.class.soap_actions[soap_action]
end
def request_input_tag
action_spec[:request_tag]
end
def soap_action
request.env['wash_out.soap_action']
end
def xml_data
envelope = request.env['wash_out.soap_data'].values_at(:envelope, :Envelope).compact.first
xml_data = envelope.values_at(:body, :Body).compact.first || {}
return xml_data if soap_config.wsdl_style == "document"
xml_data = xml_data.values_at(soap_action.underscore.to_sym, soap_action.to_sym, request_input_tag.to_sym).compact.first || {}
end
def xml_header_data
envelope = request.env['wash_out.soap_data'].values_at(:envelope, :Envelope).compact.first
header_data = envelope.values_at(:header, :Header).compact.first || {}
end
end
end