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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +35 -0
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +102 -0
  7. data/Gemfile +16 -0
  8. data/Guardfile +12 -0
  9. data/LICENSE +22 -0
  10. data/README.md +246 -0
  11. data/Rakefile +13 -0
  12. data/app/helpers/wash_out_helper.rb +106 -0
  13. data/app/views/wash_out/document/error.builder +9 -0
  14. data/app/views/wash_out/document/response.builder +10 -0
  15. data/app/views/wash_out/document/wsdl.builder +68 -0
  16. data/app/views/wash_out/rpc/error.builder +10 -0
  17. data/app/views/wash_out/rpc/response.builder +11 -0
  18. data/app/views/wash_out/rpc/wsdl.builder +68 -0
  19. data/gemfiles/rails_3.1.3.gemfile +20 -0
  20. data/gemfiles/rails_3.2.12.gemfile +20 -0
  21. data/gemfiles/rails_4.0.0.gemfile +19 -0
  22. data/gemfiles/rails_4.1.0.gemfile +19 -0
  23. data/gemfiles/rails_4.2.0.gemfile +19 -0
  24. data/gemfiles/rails_5.0.0.beta2.gemfile +19 -0
  25. data/init.rb +1 -0
  26. data/lib/wash_out.rb +53 -0
  27. data/lib/wash_out/configurable.rb +41 -0
  28. data/lib/wash_out/dispatcher.rb +218 -0
  29. data/lib/wash_out/engine.rb +12 -0
  30. data/lib/wash_out/middleware.rb +41 -0
  31. data/lib/wash_out/model.rb +29 -0
  32. data/lib/wash_out/param.rb +200 -0
  33. data/lib/wash_out/router.rb +95 -0
  34. data/lib/wash_out/soap.rb +48 -0
  35. data/lib/wash_out/soap_config.rb +93 -0
  36. data/lib/wash_out/type.rb +29 -0
  37. data/lib/wash_out/version.rb +3 -0
  38. data/lib/wash_out/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/lib/wash_out/dispatcher_spec.rb +99 -0
  63. data/spec/lib/wash_out/middleware_spec.rb +33 -0
  64. data/spec/lib/wash_out/param_spec.rb +94 -0
  65. data/spec/lib/wash_out/router_spec.rb +22 -0
  66. data/spec/lib/wash_out/type_spec.rb +41 -0
  67. data/spec/lib/wash_out_spec.rb +754 -0
  68. data/spec/spec_helper.rb +82 -0
  69. data/wash_out.gemspec +21 -0
  70. 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,3 @@
1
+ module WashOut
2
+ VERSION = '0.10.1'.freeze
3
+ 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
@@ -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
@@ -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