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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +36 -0
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +102 -0
  7. data/Gemfile +17 -0
  8. data/Guardfile +12 -0
  9. data/LICENSE +22 -0
  10. data/README.md +242 -0
  11. data/Rakefile +25 -0
  12. data/app/helpers/wash_out_fork_helper.rb +110 -0
  13. data/app/views/wash_out_fork/document/error.builder +9 -0
  14. data/app/views/wash_out_fork/document/response.builder +8 -0
  15. data/app/views/wash_out_fork/document/wsdl.builder +68 -0
  16. data/app/views/wash_out_fork/rpc/error.builder +10 -0
  17. data/app/views/wash_out_fork/rpc/response.builder +9 -0
  18. data/app/views/wash_out_fork/rpc/wsdl.builder +68 -0
  19. data/gemfiles/rails_3.2.13.gemfile +21 -0
  20. data/gemfiles/rails_4.0.0.gemfile +20 -0
  21. data/gemfiles/rails_4.1.0.gemfile +20 -0
  22. data/gemfiles/rails_4.2.0.gemfile +20 -0
  23. data/gemfiles/rails_5.0.0.beta2.gemfile +19 -0
  24. data/gemfiles/rails_5.0.0.gemfile +19 -0
  25. data/init.rb +1 -0
  26. data/lib/wash_out_fork.rb +60 -0
  27. data/lib/wash_out_fork/configurable.rb +41 -0
  28. data/lib/wash_out_fork/dispatcher.rb +232 -0
  29. data/lib/wash_out_fork/engine.rb +12 -0
  30. data/lib/wash_out_fork/middleware.rb +41 -0
  31. data/lib/wash_out_fork/model.rb +29 -0
  32. data/lib/wash_out_fork/param.rb +200 -0
  33. data/lib/wash_out_fork/router.rb +131 -0
  34. data/lib/wash_out_fork/soap.rb +48 -0
  35. data/lib/wash_out_fork/soap_config.rb +93 -0
  36. data/lib/wash_out_fork/type.rb +29 -0
  37. data/lib/wash_out_fork/version.rb +3 -0
  38. data/lib/wash_out_fork/wsse.rb +101 -0
  39. data/spec/dummy/Rakefile +7 -0
  40. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  41. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  42. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  43. data/spec/dummy/config.ru +4 -0
  44. data/spec/dummy/config/application.rb +51 -0
  45. data/spec/dummy/config/boot.rb +10 -0
  46. data/spec/dummy/config/environment.rb +5 -0
  47. data/spec/dummy/config/environments/development.rb +23 -0
  48. data/spec/dummy/config/environments/test.rb +30 -0
  49. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/spec/dummy/config/initializers/inflections.rb +10 -0
  51. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  52. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  53. data/spec/dummy/config/initializers/session_store.rb +8 -0
  54. data/spec/dummy/config/locales/en.yml +5 -0
  55. data/spec/dummy/config/routes.rb +58 -0
  56. data/spec/dummy/public/404.html +26 -0
  57. data/spec/dummy/public/422.html +26 -0
  58. data/spec/dummy/public/500.html +26 -0
  59. data/spec/dummy/public/favicon.ico +0 -0
  60. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  61. data/spec/dummy/script/rails +6 -0
  62. data/spec/fixtures/nested_refs_to_arrays.xml +19 -0
  63. data/spec/fixtures/ref_to_one_array.xml +11 -0
  64. data/spec/fixtures/refs_to_arrays.xml +16 -0
  65. data/spec/lib/wash_out/dispatcher_spec.rb +206 -0
  66. data/spec/lib/wash_out/middleware_spec.rb +33 -0
  67. data/spec/lib/wash_out/param_spec.rb +94 -0
  68. data/spec/lib/wash_out/router_spec.rb +50 -0
  69. data/spec/lib/wash_out/type_spec.rb +41 -0
  70. data/spec/lib/wash_out_spec.rb +797 -0
  71. data/spec/spec_helper.rb +88 -0
  72. data/wash_out_fork.gemspec +20 -0
  73. 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,3 @@
1
+ module WashOutFork
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag :all %>
6
+ <%= javascript_include_tag :defaults %>
7
+ <%= csrf_meta_tag %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Dummy::Application