right_support 1.0.11 → 1.1.0

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.
@@ -0,0 +1,130 @@
1
+ require 'json'
2
+
3
+ module RightSupport::Crypto
4
+ class SignedHash
5
+ DEFAULT_OPTIONS = {
6
+ :digest => Digest::SHA1,
7
+ :encoding => JSON
8
+ }
9
+
10
+ def initialize(hash={}, options={})
11
+ options = DEFAULT_OPTIONS.merge(options)
12
+ @hash = hash
13
+ @digest = options[:digest]
14
+ @encoding = options[:encoding]
15
+ @public_key = options[:public_key]
16
+ @private_key = options[:private_key]
17
+ duck_type_check
18
+ end
19
+
20
+ def sign(expires_at)
21
+ raise ArgumentError, "Cannot sign; missing private_key" unless @private_key
22
+ raise ArgumentError, "expires_at must be a Time in the future" unless time_check(expires_at)
23
+
24
+ metadata = {:expires_at => expires_at}
25
+ @private_key.private_encrypt( digest( encode( canonicalize( frame(@hash, metadata) ) ) ) )
26
+ end
27
+
28
+ def verify!(signature, expires_at)
29
+ raise ArgumentError, "Cannot verify; missing public_key" unless @public_key
30
+
31
+ metadata = {:expires_at => expires_at}
32
+ expected = digest( encode( canonicalize( frame(@hash, metadata) ) ) )
33
+ actual = @public_key.public_decrypt(signature)
34
+ raise SecurityError, "Signature mismatch: expected #{expected}, got #{actual}" unless actual == expected
35
+ raise SecurityError, "The signature has expired (or expires_at is not a Time)" unless time_check(expires_at)
36
+ end
37
+
38
+ def verify(signature, expires_at)
39
+ verify!(signature, expires_at)
40
+ true
41
+ rescue Exception => e
42
+ false
43
+ end
44
+
45
+ def method_missing(meth, *args)
46
+ @hash.__send__(meth, *args)
47
+ end
48
+
49
+ private
50
+
51
+ def duck_type_check
52
+ unless @digest.is_a?(Class) &&
53
+ @digest.instance_methods.include?(str_or_symb('update')) &&
54
+ @digest.instance_methods.include?(str_or_symb('digest'))
55
+ raise ArgumentError, "Digest class must respond to #update and #digest instance methods"
56
+ end
57
+ unless @encoding.respond_to?(str_or_symb('dump'))
58
+ raise ArgumentError, "Encoding class/module/object must respond to .dump method"
59
+ end
60
+ if @public_key && !@public_key.respond_to?(str_or_symb('public_decrypt'))
61
+ raise ArgumentError, "Public key must respond to :public_decrypt (e.g. an OpenSSL::PKey instance)"
62
+ end
63
+ if @private_key && !@private_key.respond_to?(str_or_symb('private_encrypt'))
64
+ raise ArgumentError, "Private key must respond to :private_encrypt (e.g. an OpenSSL::PKey instance)"
65
+ end
66
+ end
67
+
68
+ def str_or_symb(method)
69
+ RUBY_VERSION > '1.9' ? method.to_sym : method.to_s
70
+ end
71
+
72
+ def time_check(t)
73
+ t.is_a?(Time) && (t >= Time.now)
74
+ end
75
+
76
+ def frame(data, metadata) # :nodoc:
77
+ {:data => data, :metadata => metadata}
78
+ end
79
+
80
+ def digest(input) # :nodoc:
81
+ @digest.new.update(input).digest
82
+ end
83
+
84
+ def encode(input)
85
+ @encoding.dump(input)
86
+ end
87
+
88
+ def canonicalize(input) # :nodoc:
89
+ case input
90
+ when Hash
91
+ # Hash is the only complex case. We canonicalize a Hash as an Array of pairs, each of which
92
+ # consists of one key and one value. The ordering of the pairs is consistent with the
93
+ # ordering of the keys.
94
+ output = Array.new
95
+
96
+ # First, transform the original input hash into something that has canonicalized keys
97
+ # (which should make them sortable, too). Also canonicalize the values while we are
98
+ # at it...
99
+ sortable_input = {}
100
+ input.each { |k,v| sortable_input[canonicalize(k)] = canonicalize(v) }
101
+
102
+ # Sort the keys; guard this operation so we can raise an intelligent error if
103
+ # something is still not sortable even after canonicalization.
104
+ begin
105
+ ordered_keys = sortable_input.keys.sort
106
+ rescue Exception => e
107
+ msg = "SignedHash requires sortable hash keys; cannot sort #{sortable_input.keys.inspect} " +
108
+ "due to #{e.class.name}: #{e.message}"
109
+ e2 = ArgumentError.new(msg)
110
+ e2.set_backtrace(e.backtrace)
111
+ raise e2
112
+ end
113
+
114
+ ordered_keys.each do |key|
115
+ output << [ key, sortable_input[key] ]
116
+ end
117
+ when Array
118
+ output = input.collect { |x| canonicalize(x) }
119
+ when Time
120
+ output = input.to_i
121
+ when Symbol
122
+ output = input.to_s
123
+ else
124
+ output = input
125
+ end
126
+
127
+ output
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,32 @@
1
+ #
2
+ # Copyright (c) 2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightSupport
24
+ #
25
+ # A namespace for cryptographic functionality.
26
+ #
27
+ module Crypto
28
+
29
+ end
30
+ end
31
+
32
+ require 'right_support/crypto/signed_hash'
@@ -13,20 +13,33 @@ module RightSupport::Net
13
13
  #
14
14
  # The balancer does not actually perform requests by itself, which makes this
15
15
  # class usable for various network protocols, and potentially even for non-
16
- # networking purposes. The block does all the work; the balancer merely selects
17
- # a random request endpoint to pass to the block.
16
+ # networking purposes. The block passed to #request does all the work; the
17
+ # balancer merely selects a suitable endpoint to pass to its block.
18
18
  #
19
- # PLEASE NOTE that the request balancer has a rather dumb notion of what is considered
20
- # a "fatal" error for purposes of being able to retry; by default, it will consider
21
- # any StandardError or any RestClient::Exception whose code is between 400-499. This
22
- # MAY NOT BE SUFFICIENT for some uses of the request balancer! Please use the :fatal
23
- # option if you need different behavior.
19
+ # PLEASE NOTE that it is VERY IMPORTANT that the balancer is able to properly
20
+ # distinguish between fatal and non-fatal (retryable) errors. Before you pass
21
+ # a :fatal option to the RequestBalancer constructor, carefully examine its
22
+ # default list of fatal exceptions and default logic for deciding whether a
23
+ # given exception is fatal! There are some subtleties.
24
24
  class RequestBalancer
25
25
  DEFAULT_RETRY_PROC = lambda do |ep, n|
26
26
  n < ep.size
27
27
  end
28
28
 
29
- DEFAULT_FATAL_EXCEPTIONS = [ScriptError, ArgumentError, IndexError, LocalJumpError, NameError]
29
+ # Built-in Ruby exceptions that should be considered fatal. Normally one would be
30
+ # inclined to simply say RuntimeError or StandardError, but because gem authors
31
+ # frequently make unwise choices of exception base class, including these top-level
32
+ # base classes could cause us to falsely think that retryable exceptions are fatal.
33
+ #
34
+ # A good example of this phenomenon is the rest-client gem, whose base exception
35
+ # class is derived from RuntimeError!!
36
+ DEFAULT_FATAL_EXCEPTIONS = [
37
+ NoMemoryError, SystemStackError, SignalException, SystemExit,
38
+ ScriptError,
39
+ #Subclasses of StandardError, which we can't mention directly
40
+ ArgumentError, IndexError, LocalJumpError, NameError, RangeError,
41
+ RegexpError, ThreadError, TypeError, ZeroDivisionError
42
+ ]
30
43
 
31
44
  DEFAULT_FATAL_PROC = lambda do |e|
32
45
  if DEFAULT_FATAL_EXCEPTIONS.any? { |c| e.is_a?(c) }
@@ -136,15 +149,13 @@ module RightSupport::Net
136
149
  complete = false
137
150
  n = 0
138
151
 
139
- retry_opt = @options[:retry] || DEFAULT_RETRY_PROC
140
-
141
152
  loop do
142
153
  if complete
143
154
  break
144
155
  else
145
- max_n = retry_opt
146
- max_n = max_n.call(@endpoints, n) if max_n.respond_to?(:call)
147
- break if (max_n.is_a?(Integer) && n >= max_n) || [nil, false].include?(max_n)
156
+ do_retry = @options[:retry] || DEFAULT_RETRY_PROC
157
+ do_retry = do_retry.call(@endpoints, n) if do_retry.respond_to?(:call)
158
+ break if (do_retry.is_a?(Integer) && n >= do_retry) || [nil, false].include?(do_retry)
148
159
  end
149
160
 
150
161
  endpoint, need_health_check = @policy.next
@@ -0,0 +1,99 @@
1
+ # Copyright (c) 2011 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'cgi'
23
+ require 'base64'
24
+ require 'zlib'
25
+
26
+ #
27
+ # A tool that encodes (binary or ASCII) strings into 7-bit ASCII
28
+ # using one or more encoding algorithms which are applied sequentially.
29
+ # The order of algorithms is reversed on decode, naturally!
30
+ #
31
+ # This class is designed to be used with network protocols implemented
32
+ # on top of HTTP, where binary data needs to be encapsulated in URL query
33
+ # strings, request bodies or other textual payloads. Sometimes multiple
34
+ # encodings are necessary in order to prevent unnecessary expansion of
35
+ # the encoded text.
36
+ #
37
+ module RightSupport::Net
38
+ class StringEncoder
39
+ ENCODINGS = [:base64, :url]
40
+ ENCODINGS.freeze
41
+
42
+ #
43
+ # Create a new instance.
44
+ #
45
+ # === Parameters
46
+ # *encodings:: list of Symbols representing an ordered sequence of encodings
47
+ #
48
+ def initialize(*args)
49
+ args = args.flatten
50
+ args.each do |enc|
51
+ raise ArgumentError, "Unknown encoding #{enc}" unless ENCODINGS.include?(enc)
52
+ end
53
+
54
+ @encodings = args
55
+ end
56
+
57
+ #
58
+ # Encode a binary or textual string.
59
+ #
60
+ # === Parameters
61
+ # value(String):: the value to be encoded
62
+ #
63
+ # === Return
64
+ # The encoded value, with all encodings applied.
65
+ def encode(value)
66
+ @encodings.each do |enc|
67
+ case enc
68
+ when :base64
69
+ value = Base64.encode64(value)
70
+ when :url
71
+ value = CGI.escape(value)
72
+ end
73
+ end
74
+
75
+ value
76
+ end
77
+
78
+ #
79
+ # Decode a binary or textual string.
80
+ #
81
+ # === Parameters
82
+ # value(String):: the value to be decoded
83
+ #
84
+ # === Return
85
+ # The decoded string value
86
+ def decode(value)
87
+ @encodings.reverse.each do |enc|
88
+ case enc
89
+ when :base64
90
+ value = Base64.decode64(value)
91
+ when :url
92
+ value = CGI.unescape(value)
93
+ end
94
+ end
95
+
96
+ value
97
+ end
98
+ end
99
+ end
data/lib/right_support.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # Namespaces for RightSupport
2
2
  require 'right_support/ruby'
3
+ require 'right_support/crypto'
3
4
  require 'right_support/db'
4
5
  require 'right_support/log'
5
6
  require 'right_support/net'
@@ -7,23 +7,17 @@ spec = Gem::Specification.new do |s|
7
7
  s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
8
8
 
9
9
  s.name = 'right_support'
10
- s.version = '1.0.11'
11
- s.date = '2011-10-21'
10
+ s.version = '1.1.0'
11
+ s.date = '2011-11-03'
12
12
 
13
- s.authors = ['Tony Spataro']
14
- s.email = 'tony@rightscale.com'
15
- s.homepage= 'https://github.com/xeger/right_support'
13
+ s.authors = ['Tony Spataro', 'Sergey Sergyenko', 'Ryan Williamson']
14
+ s.email = 'support@rightscale.com'
15
+ s.homepage= 'https://github.com/rightscale/right_support'
16
16
 
17
17
  s.summary = %q{Reusable foundation code.}
18
- s.description = %q{A toolkit of useful foundation code: logging, input validation, etc.}
18
+ s.description = %q{A toolkit of useful, reusable foundation code created by RightScale.}
19
19
 
20
- s.add_development_dependency('rake', [">= 0.8.7"])
21
- s.add_development_dependency('ruby-debug', [">= 0.10"])
22
- s.add_development_dependency('rspec', ["~> 1.3"])
23
- s.add_development_dependency('cucumber', ["~> 0.8"])
24
- s.add_development_dependency('flexmock', ["~> 0.8"])
25
- s.add_development_dependency('net-ssh', ["~> 2.0"])
26
- s.add_development_dependency('rest-client', ["~> 1.6"])
20
+ s.add_dependency('json', ['~> 1.4'])
27
21
 
28
22
  basedir = File.dirname(__FILE__)
29
23
  candidates = ['right_support.gemspec', 'LICENSE', 'README.rdoc'] + Dir['lib/**/*']
metadata CHANGED
@@ -1,131 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_support
3
3
  version: !ruby/object:Gem::Version
4
- hash: 1
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
+ - 1
8
9
  - 0
9
- - 11
10
- version: 1.0.11
10
+ version: 1.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Tony Spataro
14
+ - Sergey Sergyenko
15
+ - Ryan Williamson
14
16
  autorequire:
15
17
  bindir: bin
16
18
  cert_chain: []
17
19
 
18
- date: 2011-10-21 00:00:00 -07:00
20
+ date: 2011-11-03 00:00:00 -07:00
19
21
  default_executable:
20
22
  dependencies:
21
23
  - !ruby/object:Gem::Dependency
22
- name: rake
24
+ name: json
23
25
  prerelease: false
24
26
  requirement: &id001 !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 49
30
- segments:
31
- - 0
32
- - 8
33
- - 7
34
- version: 0.8.7
35
- type: :development
36
- version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
- name: ruby-debug
39
- prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- hash: 31
46
- segments:
47
- - 0
48
- - 10
49
- version: "0.10"
50
- type: :development
51
- version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
53
- name: rspec
54
- prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
56
- none: false
57
- requirements:
58
- - - ~>
59
- - !ruby/object:Gem::Version
60
- hash: 9
61
- segments:
62
- - 1
63
- - 3
64
- version: "1.3"
65
- type: :development
66
- version_requirements: *id003
67
- - !ruby/object:Gem::Dependency
68
- name: cucumber
69
- prerelease: false
70
- requirement: &id004 !ruby/object:Gem::Requirement
71
27
  none: false
72
28
  requirements:
73
29
  - - ~>
74
30
  - !ruby/object:Gem::Version
75
- hash: 27
76
- segments:
77
- - 0
78
- - 8
79
- version: "0.8"
80
- type: :development
81
- version_requirements: *id004
82
- - !ruby/object:Gem::Dependency
83
- name: flexmock
84
- prerelease: false
85
- requirement: &id005 !ruby/object:Gem::Requirement
86
- none: false
87
- requirements:
88
- - - ~>
89
- - !ruby/object:Gem::Version
90
- hash: 27
91
- segments:
92
- - 0
93
- - 8
94
- version: "0.8"
95
- type: :development
96
- version_requirements: *id005
97
- - !ruby/object:Gem::Dependency
98
- name: net-ssh
99
- prerelease: false
100
- requirement: &id006 !ruby/object:Gem::Requirement
101
- none: false
102
- requirements:
103
- - - ~>
104
- - !ruby/object:Gem::Version
105
- hash: 3
106
- segments:
107
- - 2
108
- - 0
109
- version: "2.0"
110
- type: :development
111
- version_requirements: *id006
112
- - !ruby/object:Gem::Dependency
113
- name: rest-client
114
- prerelease: false
115
- requirement: &id007 !ruby/object:Gem::Requirement
116
- none: false
117
- requirements:
118
- - - ~>
119
- - !ruby/object:Gem::Version
120
- hash: 3
31
+ hash: 7
121
32
  segments:
122
33
  - 1
123
- - 6
124
- version: "1.6"
125
- type: :development
126
- version_requirements: *id007
127
- description: "A toolkit of useful foundation code: logging, input validation, etc."
128
- email: tony@rightscale.com
34
+ - 4
35
+ version: "1.4"
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ description: A toolkit of useful, reusable foundation code created by RightScale.
39
+ email: support@rightscale.com
129
40
  executables: []
130
41
 
131
42
  extensions: []
@@ -136,6 +47,8 @@ files:
136
47
  - LICENSE
137
48
  - README.rdoc
138
49
  - lib/right_support.rb
50
+ - lib/right_support/crypto.rb
51
+ - lib/right_support/crypto/signed_hash.rb
139
52
  - lib/right_support/db.rb
140
53
  - lib/right_support/db/cassandra_model.rb
141
54
  - lib/right_support/log.rb
@@ -149,6 +62,7 @@ files:
149
62
  - lib/right_support/net/balancing/round_robin.rb
150
63
  - lib/right_support/net/http_client.rb
151
64
  - lib/right_support/net/request_balancer.rb
65
+ - lib/right_support/net/string_encoder.rb
152
66
  - lib/right_support/rack.rb
153
67
  - lib/right_support/rack/custom_logger.rb
154
68
  - lib/right_support/ruby.rb
@@ -158,7 +72,7 @@ files:
158
72
  - lib/right_support/validation/ssh.rb
159
73
  - right_support.gemspec
160
74
  has_rdoc: true
161
- homepage: https://github.com/xeger/right_support
75
+ homepage: https://github.com/rightscale/right_support
162
76
  licenses: []
163
77
 
164
78
  post_install_message: