Skip to content
Browse files

First commit

  • Loading branch information...
0 parents commit af89649a50a4431c42add61d3852c1e638c8dd46 @hakanensari hakanensari committed Jul 3, 2012
Showing with 433 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +7 −0 .travis.yml
  3. +4 −0 Gemfile
  4. +22 −0 LICENSE
  5. +25 −0 README.md
  6. +10 −0 Rakefile
  7. +22 −0 jeff.gemspec
  8. +19 −0 lib/jeff.rb
  9. +126 −0 lib/jeff/client.rb
  10. +42 −0 lib/jeff/query_builder.rb
  11. +18 −0 lib/jeff/signature.rb
  12. +11 −0 lib/jeff/user_agent.rb
  13. +3 −0 lib/jeff/version.rb
  14. +87 −0 spec/jeff/client_spec.rb
  15. +19 −0 spec/jeff/user_agent_spec.rb
  16. +9 −0 spec/jeff_spec.rb
  17. +7 −0 spec/spec_helper.rb
2 .gitignore
@@ -0,0 +1,2 @@
+Gemfile.lock
+pkg
7 .travis.yml
@@ -0,0 +1,7 @@
+rvm:
+ - 1.8.7
+ - 1.9.3
+ - jruby-18mode
+ - jruby-19mode
+ - rbx-18mode
+ - rbx-19mode
4 Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+gemspec
+
+gem 'jruby-openssl', :platform => :jruby
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Hakan Ensari
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 README.md
@@ -0,0 +1,25 @@
+# Jeff
+
+Jeff is a minimum-viable client for [Amazon Web Services (AWS) APIs][aws] that
+support [Signature Version 2][sign].
+
+## Usage
+
+```ruby
+client = Jeff.new 'http://some-aws-url.com/'
+client << params
+client << data
+
+client.request
+```
+
+Stream responses.
+
+```ruby
+client << ->(chunk, remaining, total) { puts chunk }
+
+client.request
+```
+
+[aws]: http://aws.amazon.com/
+[sign]: http://docs.amazonwebservices.com/general/latest/gr/signature-version-2.html
10 Rakefile
@@ -0,0 +1,10 @@
+#!/usr/bin/env rake
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
+
+desc 'Run all specs in spec directory'
+RSpec::Core::RakeTask.new(:spec) do |t|
+ t.pattern = 'spec/**/*_spec.rb'
+end
+
+task :default => [:spec]
22 jeff.gemspec
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path('../lib', __FILE__)
+require File.expand_path('../lib/jeff/version.rb', __FILE__)
+
+Gem::Specification.new do |gem|
+ gem.authors = ['Hakan Ensari']
+ gem.email = ['hakan.ensari@papercavalier.com']
+ gem.description = %q{A minimum-viable Amazon Web Services (AWS) client}
+ gem.summary = %q{An AWS client}
+ gem.homepage = 'https://github.com/hakanensari/jeff'
+
+ gem.files = `git ls-files`.split($\)
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
+ gem.name = 'jeff'
+ gem.require_paths = ['lib']
+ gem.version = Jeff::VERSION
+
+ gem.add_dependency 'excon', '~> 0.14'
+ gem.add_development_dependency 'rake', '~> 0.9'
+ gem.add_development_dependency 'rspec', '~> 2.10'
+end
19 lib/jeff.rb
@@ -0,0 +1,19 @@
+require 'base64'
+require 'forwardable'
+require 'time'
+
+require 'excon'
+
+require 'jeff/version'
+require 'jeff/query_builder'
+require 'jeff/user_agent'
+require 'jeff/client'
+require 'jeff/signature'
+
+module Jeff
+ class << self
+ extend Forwardable
+
+ def_delegator Client, :new
+ end
+end
126 lib/jeff/client.rb
@@ -0,0 +1,126 @@
+module Jeff
+ # A minimum-viable Amazon Web Services (AWS) client.
+ class Client
+ include UserAgent
+
+ # Internal: Returns the String request body.
+ attr :body
+
+ # Internal: Returns the Proc chunked request body.
+ attr :chunker
+
+ # Gets/Sets the String AWS access key id.
+ attr_accessor :key
+
+ # Gets/Sets the String AWS secret key.
+ attr_accessor :secret
+
+ # Creates a new client.
+ #
+ # endpoint - A String AWS endpoint.
+ #
+ # Examples
+ #
+ # client = Jeff.new 'http://ecs.amazonaws.com/onca/xml'
+ #
+ def initialize(endpoint)
+ @endpoint = URI endpoint
+ @connection = Excon.new endpoint.to_s, :headers => {
+ 'User-Agent' => USER_AGENT
+ }
+
+ reset_request_attributes
+ end
+
+ # Updates the request attributes.
+ #
+ # data - A Hash of parameters or a String request body or a Proc that will
+ # deliver chunks of data.
+ #
+ # Returns self.
+ #
+ # Examples
+ #
+ # @client << {
+ # 'AssociateTag' => 'tag',
+ # 'Service' => 'AWSECommerceService',
+ # 'Version' => '2011-08-01'
+ # }
+ #
+ def <<(data)
+ case data
+ when Hash
+ @params.update data
+ when String
+ @body ||= '' << data
+ when Proc
+ @chunker = data
+ end
+
+ self
+ end
+
+ # Configures the client.
+ #
+ # Yields self.
+ #
+ # Examples
+ #
+ # client.configure do |c|
+ # c.key = 'key'
+ # c.secret = 'secret'
+ # end
+ #
+ def configure
+ yield self
+ end
+
+ # Returns the Hash request parameters, including required defaults.
+ def params
+ {
+ 'AWSAccessKeyId' => @key,
+ 'SignatureVersion' => '2',
+ 'SignatureMethod' => 'HmacSHA256',
+ 'Timestamp' => Time.now.utc.iso8601
+ }.merge @params
+ end
+
+ # Makes an HTTP request.
+ #
+ # Returns an Excon::Response.
+ def request(opts = {}, &blk)
+ opts.update :body => @body,
+ :method => action,
+ :query => query,
+ :request_block => @chunker
+
+ begin
+ @connection.request opts, &blk
+ ensure
+ reset_request_attributes
+ end
+ end
+
+ # Returns the String URL.
+ def url
+ [@endpoint, query].join '?'
+ end
+
+ private
+
+ def action
+ @body || @chunker ? :post : :get
+ end
+
+ def query
+ @query_builder ||= QueryBuilder.new @endpoint, @secret
+ @query_builder.build action, params
+ end
+
+ def reset_request_attributes
+ @body = nil
+ @chunker = nil
+ @params = {}
+ end
+ end
+end
42 lib/jeff/query_builder.rb
@@ -0,0 +1,42 @@
+module Jeff
+ class QueryBuilder
+ UNRESERVED = /([^\w.~-]+)/
+
+ def initialize(endpoint, secret)
+ @endpoint = endpoint
+ @secret = secret
+ end
+
+ def build(mth, params)
+ @mth = mth.to_s.upcase
+ @query = stringify params
+
+ "#{@query}&Signature=#{escape signature}"
+ end
+
+ private
+
+ def escape(val)
+ val.to_s.gsub(UNRESERVED) do
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
+ end
+ end
+
+ def signature
+ Signature.new @secret, string_to_sign
+ end
+
+ def string_to_sign
+ [
+ @mth,
+ @endpoint.host,
+ @endpoint.path,
+ @query
+ ].join "\n"
+ end
+
+ def stringify(hsh)
+ hsh.map { |k, v| "#{k}=#{ escape v }" }.sort.join '&'
+ end
+ end
+end
18 lib/jeff/signature.rb
@@ -0,0 +1,18 @@
+module Jeff
+ class Signature
+ SHA256 = OpenSSL::Digest::SHA256.new
+
+ def initialize(secret, message)
+ @secret = secret
+ @message = message
+ end
+
+ def digest
+ OpenSSL::HMAC.digest SHA256, @secret, @message
+ end
+
+ def to_s
+ Base64.encode64(digest).chomp
+ end
+ end
+end
11 lib/jeff/user_agent.rb
@@ -0,0 +1,11 @@
+module Jeff
+ module UserAgent
+ USER_AGENT = begin
+ hostname = `hostname`.chomp
+ engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
+ language = [engine, RUBY_VERSION, "p#{RUBY_PATCHLEVEL}"].join ' '
+
+ "Jeff/#{VERSION} (Language=#{language}; Host=#{hostname})"
+ end
+ end
+end
3 lib/jeff/version.rb
@@ -0,0 +1,3 @@
+module Jeff
+ VERSION = '0.1.0'
+end
87 spec/jeff/client_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+module Jeff
+ describe Client do
+ before do
+ @endpoint = 'http://slowapi.com/delay/0'
+
+ @client = Client.new @endpoint
+ @client.configure do |config|
+ config.key = 'key'
+ config.secret = 'secret'
+ end
+ end
+
+ describe '#<<' do
+ it 'updates the request parameters' do
+ @client << { 'Foo' => 1 }
+ @client.params.should include 'Foo'
+ end
+
+ it 'updates the request body' do
+ @client << 'foo'
+ @client.body.should eql 'foo'
+ end
+
+ it 'updates the request chunked body' do
+ chunker = lambda {}
+ @client << chunker
+ @client.chunker.should eql chunker
+ end
+ end
+
+ describe '#params' do
+ subject { @client.params }
+
+ it 'includes a key' do
+ should include 'AWSAccessKeyId'
+ end
+
+ it 'includes a signature version' do
+ should include 'SignatureVersion'
+ end
+
+ it 'includes a signature method' do
+ should include 'SignatureMethod'
+ end
+
+ it 'includes a timestamp' do
+ should include 'Timestamp'
+ end
+ end
+
+ describe '#url' do
+ subject { @client.url }
+
+ it 'includes the endpoint' do
+ should include @endpoint
+ end
+
+ it 'sorts the parameters' do
+ @client << { 'Z' => 1, 'A' => 1 }
+ should match /\?A=[^&]+/
+ end
+
+ it 'is signed' do
+ should match /Signature=[^&]+$/
+ end
+ end
+
+ describe '#request' do
+ context 'given no body or chunker' do
+ it 'makes a GET request' do
+ Excon.stub({ :method => :get }, { :body => 'get' })
+ @client.request(:mock => true).body.should eql 'get'
+ end
+ end
+
+ context 'given a body' do
+ it 'makes a POST request' do
+ Excon.stub({ :method => :post }, { :body => 'post' })
+ @client << 'foo'
+ @client.request(:mock => true).body.should eql 'post'
+ end
+ end
+ end
+ end
+end
19 spec/jeff/user_agent_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+module Jeff
+ describe UserAgent do
+ subject { UserAgent::USER_AGENT }
+
+ it 'describes the library' do
+ should match /Jeff\/[\d\w.]+\s/
+ end
+
+ it 'describes the interpreter' do
+ should match /Language=(?:j?ruby|rbx)/
+ end
+
+ it 'describes the host' do
+ should match /Host=[\w\d]+/
+ end
+ end
+end
9 spec/jeff_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe Jeff do
+ describe '.new' do
+ it 'delegates to Client' do
+ Jeff.new('http://foo').should be_a Jeff::Client
+ end
+ end
+end
7 spec/spec_helper.rb
@@ -0,0 +1,7 @@
+require 'rspec'
+begin
+ require 'pry'
+rescue LoadError
+end
+
+require 'jeff'

0 comments on commit af89649

Please sign in to comment.
Something went wrong with that request. Please try again.