escher 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/escher.gemspec +5 -1
- data/lib/escher/auth.rb +341 -0
- data/lib/escher/request/base.rb +29 -0
- data/lib/escher/request/factory.rb +24 -0
- data/lib/escher/request/hash_request.rb +67 -0
- data/lib/escher/request/legacy_request.rb +157 -0
- data/lib/escher/request/rack_request.rb +42 -0
- data/lib/escher/version.rb +2 -2
- data/lib/escher.rb +6 -2
- data/spec/escher/auth_spec.rb +408 -0
- data/spec/escher/request/factory_spec.rb +20 -0
- data/spec/escher/request/hash_request_spec.rb +121 -0
- data/spec/escher/request/rack_request_spec.rb +114 -0
- metadata +45 -21
- data/lib/escher/base.rb +0 -324
- data/lib/escher/request.rb +0 -83
- data/spec/escher_spec.rb +0 -358
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: a08450345302a7c0e0b1d789ef7a23850151f234
         | 
| 4 | 
            +
              data.tar.gz: ff3bfb71fba27c558bc3abd9e96331dc8bd1c354
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 2b68ae2a96d4ccfa0ad1fbbb2966056e316872e140ee984cd8d38eef474faee7c7b4f81b0e33ea654fce0aaa7f3785589fc53b8200b7ad24eb629b58d8ff5138
         | 
| 7 | 
            +
              data.tar.gz: 8ada366ab04fc7895aca5026dcb8a2fc08603cdd604e74979ecb1ad238436b64fc9a07d23fd1d53b23f4c7bdf7b37c3c672abe7205c647ebf10acbd496a84686
         | 
    
        data/escher.gemspec
    CHANGED
    
    | @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| | |
| 10 10 | 
             
              spec.email         = ["andras.barthazi@emarsys.com"]
         | 
| 11 11 | 
             
              spec.summary       = %q{Library for HTTP request signing (Ruby implementation)}
         | 
| 12 12 | 
             
              spec.description   = %q{Escher helps you creating secure HTTP requests (for APIs) by signing HTTP(s) requests.}
         | 
| 13 | 
            -
              spec.homepage      = " | 
| 13 | 
            +
              spec.homepage      = "http://escherauth.io/"
         | 
| 14 14 | 
             
              spec.license       = "MIT"
         | 
| 15 15 |  | 
| 16 16 | 
             
              spec.files         = `git ls-files -z`.split("\x0")
         | 
| @@ -18,9 +18,13 @@ Gem::Specification.new do |spec| | |
| 18 18 | 
             
              spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
         | 
| 19 19 | 
             
              spec.require_paths = ["lib"]
         | 
| 20 20 |  | 
| 21 | 
            +
              spec.required_ruby_version = '>= 1.9'
         | 
| 22 | 
            +
             | 
| 21 23 | 
             
              spec.add_development_dependency "bundler", "~> 1.6"
         | 
| 22 24 | 
             
              spec.add_development_dependency "rake", "~> 10"
         | 
| 23 25 | 
             
              spec.add_development_dependency "rspec", "~> 2"
         | 
| 24 26 |  | 
| 27 | 
            +
              spec.add_development_dependency "rack"
         | 
| 28 | 
            +
             | 
| 25 29 | 
             
              spec.add_runtime_dependency "addressable", "~> 2.3"
         | 
| 26 30 | 
             
            end
         | 
    
        data/lib/escher/auth.rb
    ADDED
    
    | @@ -0,0 +1,341 @@ | |
| 1 | 
            +
            module Escher
         | 
| 2 | 
            +
              class Auth
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                def initialize(credential_scope, options = {})
         | 
| 5 | 
            +
                  @credential_scope = credential_scope
         | 
| 6 | 
            +
                  @algo_prefix = options[:algo_prefix] || 'ESR'
         | 
| 7 | 
            +
                  @vendor_key = options[:vendor_key] || 'Escher'
         | 
| 8 | 
            +
                  @hash_algo = options[:hash_algo] || 'SHA256'
         | 
| 9 | 
            +
                  @current_time = options[:current_time] || Time.now
         | 
| 10 | 
            +
                  @auth_header_name = options[:auth_header_name] || 'X-Escher-Auth'
         | 
| 11 | 
            +
                  @date_header_name = options[:date_header_name] || 'X-Escher-Date'
         | 
| 12 | 
            +
                  @clock_skew = options[:clock_skew] || 900
         | 
| 13 | 
            +
                  @algo = create_algo
         | 
| 14 | 
            +
                  @algo_id = @algo_prefix + '-HMAC-' + @hash_algo
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
             | 
| 19 | 
            +
                def sign!(req, client, headers_to_sign = [])
         | 
| 20 | 
            +
                  headers_to_sign |= [@date_header_name.downcase, 'host']
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  request = wrap_request req
         | 
| 23 | 
            +
                  raise EscherError, 'Missing header: Host' unless request.has_header? 'host'
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  request.set_header(@date_header_name, format_date_for_header) unless request.has_header? @date_header_name
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  signature = generate_signature(client[:api_secret], request.body, request.headers, request.method, headers_to_sign, request.path, request.query_values)
         | 
| 28 | 
            +
                  request.set_header(@auth_header_name, "#{@algo_id} Credential=#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}, SignedHeaders=#{prepare_headers_to_sign headers_to_sign}, Signature=#{signature}")
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  request.request
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
             | 
| 34 | 
            +
             | 
| 35 | 
            +
                def is_valid?(*args)
         | 
| 36 | 
            +
                  begin
         | 
| 37 | 
            +
                    authenticate(*args)
         | 
| 38 | 
            +
                    return true
         | 
| 39 | 
            +
                  rescue
         | 
| 40 | 
            +
                    return false
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
             | 
| 45 | 
            +
             | 
| 46 | 
            +
                def authenticate(req, key_db)
         | 
| 47 | 
            +
                  request = wrap_request req
         | 
| 48 | 
            +
                  method = request.method
         | 
| 49 | 
            +
                  body = request.body
         | 
| 50 | 
            +
                  headers = request.headers
         | 
| 51 | 
            +
                  path = request.path
         | 
| 52 | 
            +
                  query_parts = request.query_values
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  signature_from_query = get_signing_param('Signature', query_parts)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  (['Host'] + (signature_from_query ? [] : [@auth_header_name, @date_header_name])).each do |header|
         | 
| 57 | 
            +
                    raise EscherError, 'Missing header: ' + header unless request.header header
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  if method == 'GET' && signature_from_query
         | 
| 61 | 
            +
                    raw_date = get_signing_param('Date', query_parts)
         | 
| 62 | 
            +
                    algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_query(query_parts)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    body = 'UNSIGNED-PAYLOAD'
         | 
| 65 | 
            +
                    query_parts.delete [query_key_for('Signature'), signature]
         | 
| 66 | 
            +
                    query_parts = query_parts.map { |k, v| [uri_decode(k), uri_decode(v)] }
         | 
| 67 | 
            +
                  else
         | 
| 68 | 
            +
                    raw_date = request.header @date_header_name
         | 
| 69 | 
            +
                    auth_header = request.header @auth_header_name
         | 
| 70 | 
            +
                    algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_header(auth_header)
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  date = Time.parse(raw_date)
         | 
| 74 | 
            +
                  api_secret = key_db[api_key_id]
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  raise EscherError, 'Invalid API key' unless api_secret
         | 
| 77 | 
            +
                  raise EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed' unless %w(SHA256 SHA512).include?(algorithm)
         | 
| 78 | 
            +
                  raise EscherError, 'Invalid request date' unless short_date(date) == short_date
         | 
| 79 | 
            +
                  raise EscherError, 'The request date is not within the accepted time range' unless is_date_within_range?(date, expires)
         | 
| 80 | 
            +
                  raise EscherError, 'Invalid credentials' unless credential_scope == @credential_scope
         | 
| 81 | 
            +
                  raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
         | 
| 82 | 
            +
                  raise EscherError, 'Only the host header should be signed' if signature_from_query && signed_headers != ['host']
         | 
| 83 | 
            +
                  raise EscherError, 'Date header is not signed' if !signature_from_query && !signed_headers.include?(@date_header_name.downcase)
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  escher = reconfig(algorithm, credential_scope, date)
         | 
| 86 | 
            +
                  expected_signature = escher.generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
         | 
| 87 | 
            +
                  raise EscherError, 'The signatures do not match' unless signature == expected_signature
         | 
| 88 | 
            +
                  api_key_id
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
             | 
| 92 | 
            +
             | 
| 93 | 
            +
                def reconfig(algorithm, credential_scope, date)
         | 
| 94 | 
            +
                  self.class.new(
         | 
| 95 | 
            +
                    credential_scope,
         | 
| 96 | 
            +
                    algo_prefix: @algo_prefix,
         | 
| 97 | 
            +
                    vendor_key: @vendor_key,
         | 
| 98 | 
            +
                    hash_algo: algorithm,
         | 
| 99 | 
            +
                    auth_header_name: @auth_header_name,
         | 
| 100 | 
            +
                    date_header_name: @date_header_name,
         | 
| 101 | 
            +
                    current_time: date
         | 
| 102 | 
            +
                  )
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
             | 
| 106 | 
            +
             | 
| 107 | 
            +
                def generate_signed_url(url_to_sign, client, expires = 86400)
         | 
| 108 | 
            +
                  uri = Addressable::URI.parse(url_to_sign)
         | 
| 109 | 
            +
                  host = uri.host
         | 
| 110 | 
            +
                  path = uri.path
         | 
| 111 | 
            +
                  query_parts = (uri.query || '')
         | 
| 112 | 
            +
                  .split('&', -1)
         | 
| 113 | 
            +
                  .map { |pair| pair.split('=', -1) }
         | 
| 114 | 
            +
                  .map { |k, v| (k.include? ' ') ? [k.str(/\S+/), ''] : [k, v] }
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  headers = [['host', host]]
         | 
| 117 | 
            +
                  headers_to_sign = ['host']
         | 
| 118 | 
            +
                  body = 'UNSIGNED-PAYLOAD'
         | 
| 119 | 
            +
                  query_parts += [
         | 
| 120 | 
            +
                    ['Algorithm', @algo_id],
         | 
| 121 | 
            +
                    ['Credentials', "#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}"],
         | 
| 122 | 
            +
                    ['Date', long_date(@current_time)],
         | 
| 123 | 
            +
                    ['Expires', expires.to_s],
         | 
| 124 | 
            +
                    ['SignedHeaders', headers_to_sign.join(';')],
         | 
| 125 | 
            +
                  ].map { |k, v| query_pair(k, v) }
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  signature = generate_signature(client[:api_secret], body, headers, 'GET', headers_to_sign, path, query_parts)
         | 
| 128 | 
            +
                  query_parts_with_signature = (query_parts.map { |k, v| [uri_encode(k), uri_encode(v)] } << query_pair('Signature', signature))
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  uri.scheme + '://' + host + path + '?' + query_parts_with_signature.map { |k, v| k + '=' + v }.join('&')
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
             | 
| 134 | 
            +
             | 
| 135 | 
            +
                def query_pair(k, v)
         | 
| 136 | 
            +
                  [query_key_for(k), v]
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
             | 
| 140 | 
            +
             | 
| 141 | 
            +
                def query_key_for(key)
         | 
| 142 | 
            +
                  "X-#{@vendor_key}-#{key}"
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
             | 
| 146 | 
            +
             | 
| 147 | 
            +
                def get_signing_param(key, query_parts)
         | 
| 148 | 
            +
                  the_param = (query_parts.detect { |param| param[0] === query_key_for(key) })
         | 
| 149 | 
            +
                  the_param ? uri_decode(the_param[1]) : nil
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
             | 
| 153 | 
            +
             | 
| 154 | 
            +
                def get_auth_parts_from_header(auth_header)
         | 
| 155 | 
            +
                  m = /#{@algo_prefix}-HMAC-(?<algo>[A-Z0-9\,]+) Credential=(?<api_key_id>[A-Za-z0-9\-_]+)\/(?<short_date>[0-9]{8})\/(?<credentials>[A-Za-z0-9\-_\/]+), SignedHeaders=(?<signed_headers>[A-Za-z\-;]+), Signature=(?<signature>[0-9a-f]+)$/
         | 
| 156 | 
            +
                  .match auth_header
         | 
| 157 | 
            +
                  raise EscherError, 'Malformed authorization header' unless m && m['credentials']
         | 
| 158 | 
            +
                  return m['algo'], m['api_key_id'], m['short_date'], m['credentials'], m['signed_headers'].split(';'), m['signature'], 0
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
             | 
| 162 | 
            +
             | 
| 163 | 
            +
                def get_auth_parts_from_query(query_parts)
         | 
| 164 | 
            +
                  expires = get_signing_param('Expires', query_parts).to_i
         | 
| 165 | 
            +
                  api_key_id, short_date, credential_scope = get_signing_param('Credentials', query_parts).split('/', 3)
         | 
| 166 | 
            +
                  signed_headers = get_signing_param('SignedHeaders', query_parts).split ';'
         | 
| 167 | 
            +
                  algorithm = parse_algo(get_signing_param('Algorithm', query_parts))
         | 
| 168 | 
            +
                  signature = get_signing_param('Signature', query_parts)
         | 
| 169 | 
            +
                  return algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires
         | 
| 170 | 
            +
                end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
             | 
| 173 | 
            +
             | 
| 174 | 
            +
                def generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
         | 
| 175 | 
            +
                  canonicalized_request = canonicalize(method, path, query_parts, body, headers, signed_headers.uniq)
         | 
| 176 | 
            +
                  string_to_sign = get_string_to_sign(canonicalized_request)
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                  signing_key = Digest::HMAC.digest(short_date(@current_time), @algo_prefix + api_secret, @algo)
         | 
| 179 | 
            +
                  @credential_scope.split('/').each { |data|
         | 
| 180 | 
            +
                    signing_key = Digest::HMAC.digest(data, signing_key, @algo)
         | 
| 181 | 
            +
                  }
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  Digest::HMAC.hexdigest(string_to_sign, signing_key, @algo)
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
             | 
| 187 | 
            +
             | 
| 188 | 
            +
                def format_date_for_header
         | 
| 189 | 
            +
                  @date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
             | 
| 193 | 
            +
             | 
| 194 | 
            +
                def canonicalize(method, path, query_parts, body, headers, headers_to_sign)
         | 
| 195 | 
            +
                  [
         | 
| 196 | 
            +
                    method,
         | 
| 197 | 
            +
                    canonicalize_path(path),
         | 
| 198 | 
            +
                    canonicalize_query(query_parts),
         | 
| 199 | 
            +
                    canonicalize_headers(headers, headers_to_sign).join("\n"),
         | 
| 200 | 
            +
                    '',
         | 
| 201 | 
            +
                    prepare_headers_to_sign(headers_to_sign),
         | 
| 202 | 
            +
                    @algo.new.hexdigest(body)
         | 
| 203 | 
            +
                  ].join "\n"
         | 
| 204 | 
            +
                end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
             | 
| 207 | 
            +
             | 
| 208 | 
            +
                def prepare_headers_to_sign(headers_to_sign)
         | 
| 209 | 
            +
                  headers_to_sign.sort.uniq.join(';')
         | 
| 210 | 
            +
                end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
             | 
| 213 | 
            +
             | 
| 214 | 
            +
                def parse_uri(request_uri)
         | 
| 215 | 
            +
                  path, query = request_uri.split '?', 2
         | 
| 216 | 
            +
                  return path, (query || '')
         | 
| 217 | 
            +
                  .split('&', -1)
         | 
| 218 | 
            +
                  .map { |pair| pair.split('=', -1) }
         | 
| 219 | 
            +
                  .map { |k, v| (k.include? ' ') ? [k.str(/\S+/), ''] : [k, v] }
         | 
| 220 | 
            +
                end
         | 
| 221 | 
            +
             | 
| 222 | 
            +
             | 
| 223 | 
            +
             | 
| 224 | 
            +
                def get_string_to_sign(canonicalized_request)
         | 
| 225 | 
            +
                  [
         | 
| 226 | 
            +
                    @algo_id,
         | 
| 227 | 
            +
                    long_date(@current_time),
         | 
| 228 | 
            +
                    short_date(@current_time) + '/' + @credential_scope,
         | 
| 229 | 
            +
                    @algo.new.hexdigest(canonicalized_request)
         | 
| 230 | 
            +
                  ].join("\n")
         | 
| 231 | 
            +
                end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
             | 
| 234 | 
            +
             | 
| 235 | 
            +
                def create_algo
         | 
| 236 | 
            +
                  case @hash_algo
         | 
| 237 | 
            +
                    when 'SHA256'
         | 
| 238 | 
            +
                      @algo = Digest::SHA2.new(256)
         | 
| 239 | 
            +
                    when 'SHA512'
         | 
| 240 | 
            +
                      @algo = Digest::SHA2.new(512)
         | 
| 241 | 
            +
                    else
         | 
| 242 | 
            +
                      raise EscherError, 'Unidentified hash algorithm'
         | 
| 243 | 
            +
                  end
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
             | 
| 246 | 
            +
             | 
| 247 | 
            +
             | 
| 248 | 
            +
                def long_date(date)
         | 
| 249 | 
            +
                  date.utc.strftime('%Y%m%dT%H%M%SZ')
         | 
| 250 | 
            +
                end
         | 
| 251 | 
            +
             | 
| 252 | 
            +
             | 
| 253 | 
            +
             | 
| 254 | 
            +
                def short_date(date)
         | 
| 255 | 
            +
                  date.utc.strftime('%Y%m%d')
         | 
| 256 | 
            +
                end
         | 
| 257 | 
            +
             | 
| 258 | 
            +
             | 
| 259 | 
            +
             | 
| 260 | 
            +
                def is_date_within_range?(request_date, expires)
         | 
| 261 | 
            +
                  (request_date - @clock_skew .. request_date + expires + @clock_skew).cover? @current_time
         | 
| 262 | 
            +
                end
         | 
| 263 | 
            +
             | 
| 264 | 
            +
             | 
| 265 | 
            +
             | 
| 266 | 
            +
                def parse_algo(algorithm)
         | 
| 267 | 
            +
                  m = /^#{@algo_prefix}-HMAC-(?<algo>[A-Z0-9\,]+)$/.match(algorithm)
         | 
| 268 | 
            +
                  m && m['algo']
         | 
| 269 | 
            +
                end
         | 
| 270 | 
            +
             | 
| 271 | 
            +
             | 
| 272 | 
            +
             | 
| 273 | 
            +
                def canonicalize_path(path)
         | 
| 274 | 
            +
                  while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do
         | 
| 275 | 
            +
                  end
         | 
| 276 | 
            +
                  path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
         | 
| 277 | 
            +
                end
         | 
| 278 | 
            +
             | 
| 279 | 
            +
             | 
| 280 | 
            +
             | 
| 281 | 
            +
                def canonicalize_headers(raw_headers, headers_to_sign)
         | 
| 282 | 
            +
                  headers = {}
         | 
| 283 | 
            +
                  raw_headers.each do |raw_header|
         | 
| 284 | 
            +
                    if raw_header[0].downcase != @auth_header_name.downcase
         | 
| 285 | 
            +
                      if headers[raw_header[0].downcase]
         | 
| 286 | 
            +
                        headers[raw_header[0].downcase] << raw_header[1]
         | 
| 287 | 
            +
                      else
         | 
| 288 | 
            +
                        headers[raw_header[0].downcase] = [raw_header[1]]
         | 
| 289 | 
            +
                      end
         | 
| 290 | 
            +
                    end
         | 
| 291 | 
            +
                  end
         | 
| 292 | 
            +
                  headers
         | 
| 293 | 
            +
                  .sort
         | 
| 294 | 
            +
                  .select { |h| headers_to_sign.include?(h[0]) }
         | 
| 295 | 
            +
                  .map { |k, v| k + ':' + v.map { |piece| normalize_white_spaces piece }.join(',') }
         | 
| 296 | 
            +
                end
         | 
| 297 | 
            +
             | 
| 298 | 
            +
             | 
| 299 | 
            +
             | 
| 300 | 
            +
                def normalize_white_spaces(value)
         | 
| 301 | 
            +
                  value.strip.split('"', -1).map.with_index { |piece, index|
         | 
| 302 | 
            +
                    is_inside_of_quotes = (index % 2 === 1)
         | 
| 303 | 
            +
                    is_inside_of_quotes ? piece : piece.gsub(/\s+/, ' ')
         | 
| 304 | 
            +
                  }.join '"'
         | 
| 305 | 
            +
                end
         | 
| 306 | 
            +
             | 
| 307 | 
            +
             | 
| 308 | 
            +
             | 
| 309 | 
            +
                def canonicalize_query(query_parts)
         | 
| 310 | 
            +
                  query_parts
         | 
| 311 | 
            +
                  .map { |k, v| uri_encode(k.gsub('+', ' ')) + '=' + uri_encode(v || '') }
         | 
| 312 | 
            +
                  .sort.join '&'
         | 
| 313 | 
            +
                end
         | 
| 314 | 
            +
             | 
| 315 | 
            +
             | 
| 316 | 
            +
             | 
| 317 | 
            +
                def uri_encode(component)
         | 
| 318 | 
            +
                  Addressable::URI.encode_component(component, Addressable::URI::CharacterClasses::UNRESERVED)
         | 
| 319 | 
            +
                end
         | 
| 320 | 
            +
             | 
| 321 | 
            +
             | 
| 322 | 
            +
             | 
| 323 | 
            +
                def uri_decode(component)
         | 
| 324 | 
            +
                  Addressable::URI.unencode_component(component)
         | 
| 325 | 
            +
                end
         | 
| 326 | 
            +
             | 
| 327 | 
            +
             | 
| 328 | 
            +
             | 
| 329 | 
            +
                private
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                def wrap_request(request)
         | 
| 332 | 
            +
                  Escher::Request::Factory.from_request request
         | 
| 333 | 
            +
                end
         | 
| 334 | 
            +
             | 
| 335 | 
            +
              end
         | 
| 336 | 
            +
             | 
| 337 | 
            +
             | 
| 338 | 
            +
              class EscherError < RuntimeError
         | 
| 339 | 
            +
              end
         | 
| 340 | 
            +
             | 
| 341 | 
            +
            end
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            module Escher
         | 
| 2 | 
            +
              module Request
         | 
| 3 | 
            +
                class Base
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  attr_reader :request
         | 
| 6 | 
            +
             | 
| 7 | 
            +
             | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(request)
         | 
| 10 | 
            +
                    @request = request
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
             | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def has_header?(name)
         | 
| 16 | 
            +
                    not header(name).nil?
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def header(name)
         | 
| 22 | 
            +
                    header = headers.find { |(header_name, _)| header_name.downcase == name.downcase }
         | 
| 23 | 
            +
                    return nil if header.nil?
         | 
| 24 | 
            +
                    header[1]
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            require_relative 'base'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'hash_request'
         | 
| 4 | 
            +
            require_relative 'legacy_request'
         | 
| 5 | 
            +
            require_relative 'rack_request'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Escher
         | 
| 8 | 
            +
              module Request
         | 
| 9 | 
            +
                class Factory
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.from_request(request)
         | 
| 12 | 
            +
                    case request
         | 
| 13 | 
            +
                      when Hash
         | 
| 14 | 
            +
                        HashRequest.new request
         | 
| 15 | 
            +
                      when lambda { |request| request.class.ancestors.map(&:to_s).include? "Rack::Request" }
         | 
| 16 | 
            +
                        RackRequest.new request
         | 
| 17 | 
            +
                      else
         | 
| 18 | 
            +
                        Escher::Request::LegacyRequest.new request
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            module Escher
         | 
| 2 | 
            +
              module Request
         | 
| 3 | 
            +
                class HashRequest < Base
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # Based on the example in RFC 3986, but scheme, user, password,
         | 
| 6 | 
            +
                  # host, port and fragment support removed, only path and query left
         | 
| 7 | 
            +
                  URI_REGEXP = /^([^?#]*)(\?(.*))?$/
         | 
| 8 | 
            +
             | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def initialize(request)
         | 
| 12 | 
            +
                    super request
         | 
| 13 | 
            +
                    @uri = parse_uri request[:uri]
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def headers
         | 
| 19 | 
            +
                    request[:headers].map { |(header_name, value)| [header_name.gsub('_', '-'), value] }
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
             | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def set_header(name, value)
         | 
| 25 | 
            +
                    request[:headers] ||= []
         | 
| 26 | 
            +
                    request[:headers] << [name, value] unless has_header? name
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def method
         | 
| 32 | 
            +
                    request[:method]
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
             | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def body
         | 
| 38 | 
            +
                    request[:body] or ''
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
             | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def path
         | 
| 44 | 
            +
                    @uri.path
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def query_values
         | 
| 50 | 
            +
                    @uri.query_values(Array) or []
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
             | 
| 54 | 
            +
             | 
| 55 | 
            +
                  private
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def parse_uri(uri)
         | 
| 58 | 
            +
                    uri.match URI_REGEXP do |match_data|
         | 
| 59 | 
            +
                      Addressable::URI.new({:path => match_data[1],
         | 
| 60 | 
            +
                                            :query => match_data[3]})
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
            end
         | 
| @@ -0,0 +1,157 @@ | |
| 1 | 
            +
            module Escher
         | 
| 2 | 
            +
              module Request
         | 
| 3 | 
            +
                class LegacyRequest
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  # based on the example in RFC 3986, but scheme, user, password,
         | 
| 6 | 
            +
                  # host, port and fraement support removed, only path and query left
         | 
| 7 | 
            +
                  URIREGEX = /^([^?#]*)(\?(.*))?$/
         | 
| 8 | 
            +
             | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def initialize(request)
         | 
| 12 | 
            +
                    @request = request
         | 
| 13 | 
            +
                    prepare_request_uri
         | 
| 14 | 
            +
                    prepare_request_headers
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def prepare_request_uri
         | 
| 20 | 
            +
                    case @request.class.to_s
         | 
| 21 | 
            +
                      when 'Hash'
         | 
| 22 | 
            +
                        uri = @request[:uri]
         | 
| 23 | 
            +
                      else
         | 
| 24 | 
            +
                        uri = @request.uri
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                    fragments = uri.scan(URIREGEX)[0]
         | 
| 27 | 
            +
                    @request_uri = Addressable::URI.new({
         | 
| 28 | 
            +
                                                          :path => fragments[0],
         | 
| 29 | 
            +
                                                          :query => fragments[2],
         | 
| 30 | 
            +
                                                        })
         | 
| 31 | 
            +
                    raise "Invalid request URI: #{@request_uri}" unless @request_uri
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
             | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def prepare_request_headers
         | 
| 37 | 
            +
                    @request_headers = []
         | 
| 38 | 
            +
                    case @request.class.to_s
         | 
| 39 | 
            +
                      when 'Hash'
         | 
| 40 | 
            +
                        @request_headers = @request[:headers]
         | 
| 41 | 
            +
                      when 'Sinatra::Request' # TODO: not working yet
         | 
| 42 | 
            +
                        @request.env.each { |key, value|
         | 
| 43 | 
            +
                          if key.downcase[0, 5] == "http_"
         | 
| 44 | 
            +
                            @request_headers += [[key[5..-1].gsub("_", "-"), value]]
         | 
| 45 | 
            +
                          end
         | 
| 46 | 
            +
                        }
         | 
| 47 | 
            +
                      when 'WEBrick::HTTPRequest'
         | 
| 48 | 
            +
                        @request.header.each { |key, values|
         | 
| 49 | 
            +
                          values.each { |value|
         | 
| 50 | 
            +
                            @request_headers += [[key, value]]
         | 
| 51 | 
            +
                          }
         | 
| 52 | 
            +
                        }
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
             | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def request
         | 
| 59 | 
            +
                    @request
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
             | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def headers
         | 
| 65 | 
            +
                    @request_headers
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
             | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def set_header(key, value)
         | 
| 71 | 
            +
                    found = false
         | 
| 72 | 
            +
                    @request_headers.each { |header|
         | 
| 73 | 
            +
                      if not found and header[0].downcase == key.downcase
         | 
| 74 | 
            +
                        header[1] = value
         | 
| 75 | 
            +
                        found = true
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                    }
         | 
| 78 | 
            +
                    unless found
         | 
| 79 | 
            +
                      @request_headers += [[key, value]]
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                    case @request.class.to_s
         | 
| 82 | 
            +
                      when 'Hash'
         | 
| 83 | 
            +
                        @request[:headers] = @request_headers
         | 
| 84 | 
            +
                      else
         | 
| 85 | 
            +
                        @request[key] = value
         | 
| 86 | 
            +
                    end
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
             | 
| 90 | 
            +
             | 
| 91 | 
            +
                  def has_header?(key)
         | 
| 92 | 
            +
                    @request_headers.each { |header|
         | 
| 93 | 
            +
                      if header[0].downcase == key.downcase
         | 
| 94 | 
            +
                        return true
         | 
| 95 | 
            +
                      end
         | 
| 96 | 
            +
                    }
         | 
| 97 | 
            +
                    return false
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
             | 
| 101 | 
            +
             | 
| 102 | 
            +
                  def method
         | 
| 103 | 
            +
                    case @request.class.to_s
         | 
| 104 | 
            +
                      when 'Hash'
         | 
| 105 | 
            +
                        @request[:method]
         | 
| 106 | 
            +
                      else
         | 
| 107 | 
            +
                        @request.request_method
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
             | 
| 112 | 
            +
             | 
| 113 | 
            +
                  # TODO: create a test for empty body (= nil)
         | 
| 114 | 
            +
                  def body
         | 
| 115 | 
            +
                    case @request.class.to_s
         | 
| 116 | 
            +
                      when 'Hash'
         | 
| 117 | 
            +
                        @request[:body] || ''
         | 
| 118 | 
            +
                      else
         | 
| 119 | 
            +
                        @request.body || ''
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
             | 
| 124 | 
            +
             | 
| 125 | 
            +
                  def host
         | 
| 126 | 
            +
                    @request_headers.each { |header|
         | 
| 127 | 
            +
                      if header[0].downcase == key.downcase
         | 
| 128 | 
            +
                        return header[1]
         | 
| 129 | 
            +
                      end
         | 
| 130 | 
            +
                    }
         | 
| 131 | 
            +
                    case @request.class.to_s
         | 
| 132 | 
            +
                      when 'Hash'
         | 
| 133 | 
            +
                        @request[:host]
         | 
| 134 | 
            +
                      else
         | 
| 135 | 
            +
                        begin
         | 
| 136 | 
            +
                          @request.host
         | 
| 137 | 
            +
                        rescue
         | 
| 138 | 
            +
                          ""
         | 
| 139 | 
            +
                        end
         | 
| 140 | 
            +
                    end
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
             | 
| 144 | 
            +
             | 
| 145 | 
            +
                  def path
         | 
| 146 | 
            +
                    @request_uri.path
         | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
             | 
| 150 | 
            +
             | 
| 151 | 
            +
                  def query_values
         | 
| 152 | 
            +
                    @request_uri.query_values(Array) || []
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
              end
         | 
| 157 | 
            +
            end
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            module Escher
         | 
| 2 | 
            +
              module Request
         | 
| 3 | 
            +
                class RackRequest < Base
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  def headers
         | 
| 6 | 
            +
                    request.env.
         | 
| 7 | 
            +
                      select { |header_name, _| header_name.start_with? "HTTP_" }.
         | 
| 8 | 
            +
                      map { |header_name, value| [header_name[5..-1].tr('_', '-'), value] }
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
             | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def method
         | 
| 14 | 
            +
                    request.request_method
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def body
         | 
| 20 | 
            +
                    request.body or ''
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
             | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def path
         | 
| 26 | 
            +
                    request.env['REQUEST_PATH']
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
             | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def query_values
         | 
| 32 | 
            +
                    Addressable::URI.new(:query => request.env['QUERY_STRING']).query_values(Array) or []
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
             | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def set_header(header_name, value)
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
    
        data/lib/escher/version.rb
    CHANGED
    
    | @@ -1,3 +1,3 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
              VERSION = '0. | 
| 1 | 
            +
            module Escher
         | 
| 2 | 
            +
              VERSION = '0.3.1'
         | 
| 3 3 | 
             
            end
         |