uri_signer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .rvmrc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rvmrc.sample ADDED
@@ -0,0 +1 @@
1
+ rvm use --install --create 1.9.3@km-uri_signer
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in uri_signer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Nate Klaiber
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # UriSigner
2
+
3
+ The KISSmetrics API provides an authentication realm of a digital
4
+ signature of the request. This gem helps to put the pieces together and
5
+ construct the URL with the signature.
6
+
7
+ This is used within the core KISSmetrics Ruby API Wrapper to help
8
+ abstract the building of the requests.
9
+
10
+ Using this without a wrapper will require you to manually provide your
11
+ *client_secret*.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'uri_signer'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install uri_signer
26
+
27
+ ## Usage
28
+
29
+ At any point, you can use the `reload_yard` command to view the
30
+ documentation of the provided code. The basic usage is:
31
+
32
+ ```ruby
33
+
34
+ http_method = "get"
35
+ uri = "https://api.example.com/core/people.json?page=5&per_page=25&order=name:desc&select=id,name"
36
+ secret = "my_secret"
37
+
38
+ signer = UriSigner::UriSigner.new(http_method, uri, secret)
39
+
40
+ signer.http_method
41
+ # => "GET"
42
+
43
+ signer.uri
44
+ # => "https://api.example.com/core/people.json?page=5&per_page=25&order=name:desc&select=id,name"
45
+
46
+ signer.signature
47
+ # => "1AaJvChjz%2BZYJKxWsUQWNK1a%2BeGjpCs6uwQKwPw1%2FV8%3D"
48
+
49
+ signer.uri_with_signature
50
+ # => "https://api.example.com/core/people.json?_signature=6G4xiABih7FGvjwB1JsYXoeETtBCOdshIu93X1hltzk%3D"
51
+
52
+ signer.valid?("1AaJvChjz%2BZYJKxWsUQWNK1a%2BeGjpCs6uwQKwPw1%2FV8%3D")
53
+ # => true
54
+
55
+ signer.valid?('1234')
56
+ # => false
57
+ ```
58
+
59
+ ## Contributing
60
+
61
+ 1. Fork it
62
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
63
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
64
+ 4. Push to the branch (`git push origin my-new-feature`)
65
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/uri_signer.rb ADDED
@@ -0,0 +1,26 @@
1
+ root = File.expand_path(File.dirname(__FILE__))
2
+
3
+ # Utilities
4
+ require 'rack/utils'
5
+ require 'active_support/core_ext/object/blank.rb'
6
+
7
+ # Version
8
+ require File.join(root, 'uri_signer', 'version')
9
+
10
+ # Errors
11
+ require File.join(root, 'uri_signer', 'errors')
12
+
13
+ # Core Extension Helpers
14
+ require File.join(root, 'uri_signer', 'helpers')
15
+ require File.join(root, 'uri_signer', 'helpers', 'string')
16
+ require File.join(root, 'uri_signer', 'helpers', 'hash')
17
+
18
+ # Parsers and Signers
19
+ require File.join(root, 'uri_signer', 'query_hash_parser')
20
+ require File.join(root, 'uri_signer', 'request_parser')
21
+ require File.join(root, 'uri_signer', 'request_signature')
22
+ require File.join(root, 'uri_signer', 'uri_signature')
23
+ require File.join(root, 'uri_signer', 'signer')
24
+
25
+ module UriSigner
26
+ end
@@ -0,0 +1,10 @@
1
+ module UriSigner
2
+ module Errors
3
+ autoload :MissingQueryHashError, File.join(File.dirname(__FILE__), 'errors/missing_query_hash_error')
4
+ autoload :MissingHttpMethodError, File.join(File.dirname(__FILE__), 'errors/missing_http_method_error')
5
+ autoload :MissingBaseUriError, File.join(File.dirname(__FILE__), 'errors/missing_base_uri_error')
6
+ autoload :MissingSignatureStringError, File.join(File.dirname(__FILE__), 'errors/missing_signature_string_error')
7
+ autoload :MissingSecretError, File.join(File.dirname(__FILE__), 'errors/missing_secret_error')
8
+ autoload :MissingUriError, File.join(File.dirname(__FILE__), 'errors/missing_uri_error')
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module UriSigner
2
+ module Errors
3
+ class MissingBaseUriError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module UriSigner
2
+ module Errors
3
+ class MissingHttpMethodError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module UriSigner
2
+ module Errors
3
+ class MissingQueryHashError < StandardError
4
+ end
5
+ end
6
+ end
7
+
@@ -0,0 +1,6 @@
1
+ module UriSigner
2
+ module Errors
3
+ class MissingSecretError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module UriSigner
2
+ module Errors
3
+ class MissingSignatureStringError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module UriSigner
2
+ module Errors
3
+ class MissingUriError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ module UriSigner
2
+ module Helpers
3
+ end
4
+ end
@@ -0,0 +1,12 @@
1
+ module UriSigner
2
+ module Helpers
3
+ module Hash
4
+ def stringify_keys
5
+ self.inject({}) do |options, (key, value)|
6
+ options[key.to_s] = value
7
+ options
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,59 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'rack/utils'
4
+ require 'base64'
5
+ require 'hmac'
6
+ require 'hmac-sha2'
7
+ require 'addressable/uri'
8
+
9
+ module UriSigner
10
+ module Helpers
11
+ module String
12
+ # Returns the string with newlines replaced with <br>
13
+ #
14
+ # @return [String] HTML version
15
+ def nl2br
16
+ self.gsub(/\n/, '<br>')
17
+ end
18
+
19
+ # This returns the string Base64 encoded with the newlines removed
20
+ #
21
+ # @return [String] encoded
22
+ def base64_encoded
23
+ Base64.encode64(self).chomp
24
+ end
25
+
26
+ # This delegates the call to Rack::Utils to escape a string
27
+ #
28
+ # @return [String] escaped
29
+ def escaped
30
+ return '' if self.nil?
31
+ unescaped = URI.unescape(self) # This will fix the percent encoding issue
32
+ Rack::Utils.escape(unescaped)
33
+ end
34
+
35
+ # This delegates the call to Rack::Utils to unescape a string
36
+ #
37
+ # @return [String] unescaped
38
+ def unescaped
39
+ Rack::Utils.unescape(self)
40
+ end
41
+
42
+ # Digitally sign a string with a secret and get the digest
43
+ #
44
+ # @return [String]
45
+ def hmac_signed_with(secret)
46
+ hmac = HMAC::SHA256.new(secret)
47
+ hmac << self
48
+ hmac.digest
49
+ end
50
+
51
+ # Take a URL string and convert it to a URI Parsed object
52
+ #
53
+ # @return [Addressable]
54
+ def to_parsed_uri
55
+ Addressable::URI.parse(CGI.unescapeHTML(self))
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,47 @@
1
+ module UriSigner
2
+ # This object takes in a hash, most likely from Rack::Utils.parse_query, and transforms it into
3
+ # a query string that's used for signing requests. It takes a hash, transforms the hash into a
4
+ # query string that has it's parts ordered accordingly.
5
+ #
6
+ # @example
7
+ # parser = UriSigner::QueryHashParser.new({"order"=>["name:desc", "id:desc"], "where"=>["name:nate", "id:123"]})
8
+ #
9
+ # parser.to_s
10
+ # # => "order=name:desc&order=id:desc&where=name:nate&where=id:123"
11
+ #
12
+ class QueryHashParser
13
+ # Creates a new QueryHashParser instance
14
+ #
15
+ # @param query_hash [Hash] A hash of key/values to turn into a query stringo
16
+ #
17
+ # @return [void]
18
+ def initialize(query_hash)
19
+ @query_hash = query_hash
20
+
21
+ raise UriSigner::Errors::MissingQueryHashError.new('Please provide a query string hash') unless query_hash?
22
+ end
23
+
24
+ # Returns the hash (key/values) as an ordered query string. This joins the keys and values, and then
25
+ # joins it all with the ampersand. This is not escaped
26
+ #
27
+ # @return [String]
28
+ def to_s
29
+ parts = @query_hash.sort.inject([]) do |arr, (key,value)|
30
+ if value.kind_of?(Array)
31
+ value.each do |nested|
32
+ arr << "%s=%s" % [key, nested]
33
+ end
34
+ else
35
+ arr << "%s=%s" % [key, value]
36
+ end
37
+ arr
38
+ end
39
+ parts.join('&')
40
+ end
41
+
42
+ private
43
+ def query_hash?
44
+ @query_hash.kind_of?(Hash) && !@query_hash.blank?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,143 @@
1
+ module UriSigner
2
+ # This object takes the raw request from the inbound API call. It takes the http method
3
+ # used to make the request, and the full #raw_uri of the request. This object extracts the
4
+ # pieces necessary to pass it to the signing class. The key components are #http_method, #base_uri,
5
+ # and #query_params. #query_params has the core params extracted (any param starting with
6
+ # an underscore)
7
+ #
8
+ # @example
9
+ # parser = UriSigner::RequestParser.new('get', 'https://api.example.com/core/people.json?_signature=1234&page=5&per_page=25')
10
+ #
11
+ # parser.http_method
12
+ # # => "GET"
13
+ #
14
+ # parser.https?
15
+ # # => true
16
+ #
17
+ # parser.http?
18
+ # # => false
19
+ #
20
+ # parser.raw_uri
21
+ # # => "https://api.example.com/core/people.json?_signature=1234&page=5&per_page=25
22
+ #
23
+ # parser.query_params
24
+ # # => {"page"=>"5", "per_page"=>"25"}
25
+ #
26
+ # parser.query_params?
27
+ # # => true
28
+ #
29
+ # parser.signature
30
+ # # => '1234'
31
+ #
32
+ # parser.signature?
33
+ # # => true
34
+ #
35
+ # parser.base_uri
36
+ # # => "https://api.example.com/core/people.json"
37
+ #
38
+ class RequestParser
39
+ # Create a new RequestParser instance
40
+ #
41
+ # @param http_method [String] The HTTP method used to make the request (GET, POST, PUT, or DELETE)
42
+ # @param raw_uri [String] The raw URI from the request
43
+ #
44
+ # @return [void]
45
+ def initialize(http_method, raw_uri)
46
+ @http_method = http_method
47
+ @raw_uri = raw_uri
48
+
49
+ raise UriSigner::Errors::MissingHttpMethodError.new("Please provide an HTTP method") unless http_method?
50
+ raise UriSigner::Errors::MissingUriError.new("Please provide a URI") unless raw_uri?
51
+
52
+ extract_core_params!
53
+ end
54
+
55
+ # Returns the uppercased HTTP Method
56
+ #
57
+ # @return [String]
58
+ def http_method
59
+ @http_method.upcase
60
+ end
61
+
62
+ # Returns true if the scheme/protocol used was HTTPS
63
+ #
64
+ # @return [Bool]
65
+ def https?
66
+ 'https' == self.parsed_uri.scheme.downcase
67
+ end
68
+
69
+ # Returns true if the scheme/protocol used was HTTP
70
+ #
71
+ # @return [Bool]
72
+ def http?
73
+ !self.https?
74
+ end
75
+
76
+ # Returns the raw_uri that was provided in the constructor
77
+ #
78
+ # @return [String]
79
+ def raw_uri
80
+ @raw_uri
81
+ end
82
+
83
+ # Returns an instance of Addressable::URI that has been parsed.
84
+ # This allows us to extract the core parts of the raw_uri
85
+ #
86
+ # @return [Addressable]
87
+ def parsed_uri
88
+ @parsed_uri ||= self.raw_uri.extend(UriSigner::Helpers::String).to_parsed_uri
89
+ end
90
+
91
+ # Returns the query params with the core params removed
92
+ #
93
+ # @return [Hash]
94
+ def query_params
95
+ @query_params ||= raw_query_params
96
+ end
97
+
98
+ # Returns true if query params were given
99
+ #
100
+ # @return [Bool]
101
+ def query_params?
102
+ !self.query_params.blank?
103
+ end
104
+
105
+ # Returns the base_uri of the request. This is the protocol, host with port, and the path.
106
+ #
107
+ # @return [String]
108
+ def base_uri
109
+ [self.parsed_uri.normalized_site, self.parsed_uri.normalized_path].join('')
110
+ end
111
+
112
+ # This returns the signature that was provided in the query params
113
+ #
114
+ # @return [String]
115
+ def signature
116
+ @_signature.extend(UriSigner::Helpers::String).escaped
117
+ end
118
+
119
+ # Returns true if a signature was provided in the raw_uri
120
+ #
121
+ # @return [Bool]
122
+ def signature?
123
+ !@_signature.blank?
124
+ end
125
+
126
+ private
127
+ def http_method?
128
+ !@http_method.blank?
129
+ end
130
+
131
+ def raw_uri?
132
+ !@raw_uri.blank?
133
+ end
134
+
135
+ def raw_query_params
136
+ @raw_query_params ||= Rack::Utils.parse_query(self.parsed_uri.query)
137
+ end
138
+
139
+ def extract_core_params!
140
+ @_signature = raw_query_params.delete('_signature')
141
+ end
142
+ end
143
+ end