Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import of plugin.

  • Loading branch information...
commit 782e529bf4a0594e1d67404c30a0ce3282a5e8b7 0 parents
Nathaniel Bibler authored
1  .gitignore
@@ -0,0 +1 @@
+rdoc
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Nathaniel E. Bibler
+
+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.
11 README
@@ -0,0 +1,11 @@
+== Devpay
+
+This Devpay plugin is designed to ease integration with
+{Amazon's DevPay}[http://www.amazon.com/DevPay-AWS-Service-Pricing/b/ref=sc_fe_l_3?ie=UTF8&node=342429011]
+services, specifically for web-hosted DevPay products.
+
+DevPay products for the desktop will very likely not find this plugin terribly
+useful. Sorry.
+
+
+Copyright (c) 2008 Nathaniel E. Bibler, released under the MIT license
22 Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the devpay plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the devpay plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Devpay'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
0  init.rb
No changes.
128 lib/devpay.rb
@@ -0,0 +1,128 @@
+require 'devpay/errors'
+require 'devpay/constants'
+require File.dirname(__FILE__) + '/../vendor/ls'
+
+##
+# Manages interactions with the Amazon DevPay system.
+#
+module Devpay
+
+ # Amazon Access Key ID
+ mattr_accessor :access_key_id
+
+ # Amazon Secret Access Key
+ mattr_accessor :secret_access_key
+
+ ##
+ # Returns a fully-qualified URL with the given offer code. The +offer_code+
+ # parameter may be either a +String+ (Amazon offer code) or an object which
+ # responds to an +offer_code+ method call.
+ #
+ # ===== What happens next?
+ #
+ # When the user successfully purchases the product, they will be redirected
+ # back to your site (to the url provided by you when you registered
+ # {your DevPay product}[http://aws.amazon.com/devpayactivity]) with
+ # query string parameters containing the Activation Key and purchased
+ # Product Code.
+ #
+ # ===== Exceptions
+ #
+ # Devpay::Errors::InvalidOfferCode:: If the given or retrieved offer code is not valid.
+ #
+ def self.purchase_url_for(offer_code)
+ offer_code = offer_code.offer_code if offer_code.respond_to?(:offer_code)
+ raise(Errors::InvalidOfferCode, "Invalid offer code given: #{offer_code.inspect}") unless valid_offer_code?(offer_code)
+ Constants::PURCHASE_URL + offer_code
+ end
+
+ ##
+ # Contacts the Amazon License Service to activate the given
+ # +activation_key+ for the given +product_token+ and returns a User Token
+ # for the customer.
+ #
+ # <b>The User Token should be permenantly stored and associated with
+ # your customer's records.</b> It is your responsibility to design your
+ # site so that it can recognize each customer an retrieve the user token
+ # associated with that customer.
+ #
+ # Amazon also suggests that you encrypt the token prior to storage.
+ #
+ # "If the user token is ever missing, the product must get a new one." -- Amazon
+ #
+ # ===== Parameters
+ #
+ # The +activation_key+ should have been received either directly from
+ # Amazon or have been provided to you by your customer. The +product_token+
+ # was provided to you when you registered your DevPay product.
+ #
+ # The +product_token+ parameter can either be a +String+ containing the
+ # token, or an object which responds to +product_token+.
+ #
+ # ===== Hosted DevPay products only
+ #
+ # This method will only activate 'hosted' DevPay products. This should not
+ # be used for 'desktop' DevPay products.
+ #
+ # ===== Exceptions
+ #
+ # Devpay::Errors::InvalidProductToken:: If the given or retrieved product token is not valid.
+ # Devpay::Errors::LicenseServiceError:: If an error occurs when contacting the Amazon License Service.
+ #
+ def self.activate!(activation_key, product_token, access_key_id = @@access_key_id, secret_access_key = @@secret_access_key)
+ product_token = product_token.product_token if product_token.respond_to?(:product_token)
+ raise(Errors::InvalidProductToken, "Invalid product token given: #{product_token.inspect}") unless valid_product_token?(product_token)
+ raise(Errors::InvalidActivationKey, "Invalid activation key given: #{activation_key.inspect}") unless valid_activation_key?(activation_key)
+
+ begin
+ license_service.activate_hosted_product(
+ activation_key,
+ product_token,
+ access_key_id,
+ secret_access_key
+ )
+ rescue RuntimeError => e
+ raise(Errors::LicenseServiceError, e.message, e.backtrace)
+ end
+ end
+
+
+ private
+
+
+ ##
+ # Returns +true+ if the given code is a valid Amazon offer code.
+ #
+ def self.valid_offer_code?(code)
+ code =~ Constants::OFFER_CODE_FORMAT
+ end
+
+ ##
+ # Returns +true+ if the given token is a valid Amazon product token.
+ #
+ def self.valid_product_token?(token)
+ token =~ Constants::PRODUCT_TOKEN_FORMAT
+ end
+
+ ##
+ # Returns +true+ if the given code is a valid Amazon product code.
+ #
+ def self.valid_product_code?(code)
+ code =~ Constants::PRODUCT_CODE_FORMAT
+ end
+
+ ##
+ # Returns +true+ if the given key is a valid Amazon activation key.
+ #
+ def self.valid_activation_key?(key)
+ key =~ Constants::ACTIVATION_KEY_FORMAT
+ end
+
+ ##
+ # Returns a new instance of an Amazon License Service object.
+ #
+ def self.license_service
+ DevPay::LicenseService.new
+ end
+
+end
31 lib/devpay/constants.rb
@@ -0,0 +1,31 @@
+module Devpay
+
+ ##
+ # Contains the majority of the constants utilized by the Devpay plugin.
+ #
+ module Constants
+
+ ##
+ # Character (byte) length of the Amazon product code
+ #
+ PRODUCT_CODE_LENGTH = 8
+
+ ##
+ # Character (byte) length of the Amazon offer code
+ #
+ OFFER_CODE_LENGTH = 8
+
+ PRODUCT_CODE_FORMAT = /\A[\w]{#{Constants::PRODUCT_CODE_LENGTH}}\Z/
+ OFFER_CODE_FORMAT = /\A[\w]{#{Constants::OFFER_CODE_LENGTH}}\Z/
+ PRODUCT_TOKEN_FORMAT = /\A\{ProductToken\}.+\Z/
+ USER_TOKEN_FORMAT = /\A\{UserToken\}.+\Z/
+ ACTIVATION_KEY_FORMAT = /\A[\w]+\Z/
+
+ ##
+ # The basic Amazon DevPay purchase url (without offeringCode)
+ #
+ PURCHASE_URL = 'https://aws-portal.amazon.com/gp/aws/user/subscription/index.html?offeringCode='
+
+ end
+
+end
47 lib/devpay/errors.rb
@@ -0,0 +1,47 @@
+module Devpay
+
+ ##
+ # This is a catch-all error for anything raised through the Devpay plugin.
+ # This will allow you to query for specific errors raised, or anything
+ # raised, at all.
+ #
+ # begin
+ # Devpay.erring.call
+ # rescue Devpay::SpecificError
+ # .. do something specifically useful ..
+ # rescue Devpay::Error
+ # .. do something generally useful that tripped something other than SpecificError ..
+ # end
+ #
+ class Error < Exception; end
+
+ module Errors #:nodoc:
+
+ ##
+ # Raised when a method using an offer code receives an invalid code.
+ #
+ class InvalidOfferCode < Devpay::Error; end
+
+ ##
+ # Raised when a method using the product code receives an invalid code.
+ #
+ class InvalidProductCode < Devpay::Error; end
+
+ ##
+ # Raised when a method using the product token receives an invalid token.
+ #
+ class InvalidProductToken < Devpay::Error; end
+
+ ##
+ # Raised when a method using an activation key receives and invalid key.
+ #
+ class InvalidActivationKey < Devpay::Error; end
+
+ ##
+ # Raised when an error occurs when dealing with the License Service
+ #
+ class LicenseServiceError < Devpay::Error; end
+
+ end
+
+end
60 test/activation_test.rb
@@ -0,0 +1,60 @@
+require 'test_helper'
+
+class ActivationTest < Test::Unit::TestCase
+
+ TEST_PRODUCT_TOKEN = "{ProductToken}b#q.079EUWQu;hsG2b0O3im<Ue=N9gum3UnLXNrrvd/ii0f5/y-MfnM:i7U+cWpOlxHxtWWa7KiAP$8U9+81ec3m89p4qvbY%h-IL_nJk36b8LHZly~TG3oZMhVMa'~HwAw3m$JO`bCP03f85sj4shHD2NANSZOyNWCQ5n>c#VCP[lF<Ce2az4Qh7m8-KI4d8pR.05]H7;OZYN{Jg{o=2ja51CS4EzlMEl77Zmh3EySvx4>G3CKbsRQ&gQ-T4gV4uk1!luzndC8N$2.w!M0UsqViczlPyfs6c5P8&Oacj6@Ibderfklpoiu="
+ TEST_ACTIVATION_KEY = "AC6PMWXPBRPDDB426RW4X76I2MXQ"
+ TEST_USER_TOKEN = "{UserToken}AAMHVXNlclRrbvclYiO3ipJOw3Bw2iIvWRGdDvrMV87ixFvPs0JYxLc3NHofLYf5azDvSQMhme/KbT4xknH0vhg7NMgJFq1OVe9C2jUMMoL8U2uwCj58QfQNlTHCXLUT5Pz4+cmd/9lKrdc8W3COBzg6SLbrjCev57WlIsmmLbD59UrrRfLzyfBlOHbbMyIW6wE/9dF54tmu2XKI7W6VMEpflQXZs4YCjkOmQM6AOQJXTBvq9QJqSL3dkbjsWzvay5XlRHSNgQkWfbm5NYEYBHtM3bO4iWNGlIO8bPKE2Jfu8BZ6Mpy7qSluOXgs8atZnk2PXbQA+MPvSZcsgcDn/2P+wNSBsk45vGEQ167gnPhrWPo3h5XrazaS8xkLldBRczBpPNDVoe2NEg=="
+ TEST_PID = "PPHUXKJYQBLH753XI5BBW5DMW54"
+
+ context "Product activation (activate!)" do
+
+ setup do
+ @product = Product.new
+ @ls = mock('License Service')
+ @product.stubs(:product_token).returns(TEST_PRODUCT_TOKEN)
+ Devpay.stubs(:license_service).returns(@ls)
+ end
+
+ context "in general" do
+
+ context "the license service" do
+
+ setup do
+ Devpay.expects(:license_service).returns(@ls)
+ end
+
+ should "be used" do
+ @ls.stubs(:activate_hosted_product).with(any_parameters).returns(TEST_USER_TOKEN)
+ Devpay.activate!(TEST_ACTIVATION_KEY, TEST_PRODUCT_TOKEN)
+ end
+
+ should "be passed the activation key" do
+ @ls.expects(:activate_hosted_product).with(TEST_ACTIVATION_KEY, anything, anything, anything).returns(TEST_USER_TOKEN)
+ Devpay.activate!(TEST_ACTIVATION_KEY, TEST_PRODUCT_TOKEN)
+ end
+
+ should "be passed the product token" do
+ @ls.expects(:activate_hosted_product).with(anything, TEST_PRODUCT_TOKEN, anything, anything).returns(TEST_USER_TOKEN)
+ Devpay.activate!(TEST_ACTIVATION_KEY, TEST_PRODUCT_TOKEN)
+ end
+
+ context "when a problem occurs" do
+
+ setup do
+ @ls.stubs(:activate_hosted_product).with(any_parameters).raises(RuntimeError, 'Test Exception')
+ end
+
+ should "raise a LicenseServiceError" do
+ assert_raise(Devpay::Errors::LicenseServiceError) { Devpay.activate!(TEST_ACTIVATION_KEY, TEST_PRODUCT_TOKEN) }
+ end
+
+ end
+
+ end
+
+ end
+
+ end
+
+end
64 test/purchase_url_test.rb
@@ -0,0 +1,64 @@
+require 'test_helper'
+
+class PurchaseUrlTest < Test::Unit::TestCase
+
+ context "The purchase url (purchase_url_for)" do
+
+ setup do
+ @product = Product.new
+ @product.stubs(:offer_code).returns("ABCD1234")
+ end
+
+ context "in general" do
+
+ should "use HTTPS" do
+ assert_match /\Ahttps:\/\//, Devpay.purchase_url_for(@product)
+ end
+
+ should "link to Amazon's aws-portal" do
+ assert_match /aws-portal\.amazon\.com/, Devpay.purchase_url_for(@product)
+ end
+
+ should "link to the proper location" do
+ assert_match /\/gp\/aws\/user\/subscription\/index\.html/, Devpay.purchase_url_for(@product)
+ end
+
+ should "contain an offeringCode parameter" do
+ assert_match /\?.*offeringCode=/, Devpay.purchase_url_for(@product)
+ end
+
+ end
+
+ context "when given an object" do
+
+ should "contain the object's offer code" do
+ @product.expects(:offer_code).returns("ABCD1234")
+ assert_match /\?.*offeringCode=ABCD1234/, Devpay.purchase_url_for(@product)
+ end
+
+ context "that doesn't respond to offer code" do
+ should "raise InvalidOfferCode" do
+ assert_raise(Devpay::Errors::InvalidOfferCode) { Devpay.purchase_url_for(Hash) }
+ end
+ end
+
+ end
+
+ context "when given a string" do
+
+ should "use the string as the offer code" do
+ assert_match /\?.*offeringCode=1234ABCD/, Devpay.purchase_url_for("1234ABCD")
+ end
+
+ context "that is not valid" do
+ should "raise InvalidOfferCode" do
+ assert_raise(Devpay::Errors::InvalidOfferCode) { Devpay.purchase_url_for("ab") }
+ end
+ end
+
+ end
+
+ end
+
+
+end
31 test/test_helper.rb
@@ -0,0 +1,31 @@
+$LOAD_PATH.unshift 'lib/'
+
+require 'rubygems'
+require 'multi_rails_init'
+require 'action_controller/test_process'
+require 'test/unit'
+require 'mocha'
+require 'shoulda'
+
+begin
+ require 'redgreen'
+rescue LoadError
+ nil
+end
+
+require 'devpay'
+
+RAILS_ROOT = '.' unless defined? RAILS_ROOT
+RAILS_ENV = 'test' unless defined? RAILS_ENV
+
+ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :dbfile => ':memory:')
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+
+ActiveRecord::Schema.define(:version => 1) do
+ create_table :products do |t|
+ t.string :offer_code
+ end
+end
+
+class Product < ActiveRecord::Base
+end
137 vendor/ls.rb
@@ -0,0 +1,137 @@
+###############################################################################
+# Copyright 2007 Amazon Technologies, Inc. Licensed under the Apache License,
+# Version 2.0 (the "License");
+# you may not use this file except in compliance with the License. You may
+# obtain a copy of the License at:
+#
+# http://aws.amazon.com/apache2.0
+#
+# This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+# CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+###############################################################################
+
+# This file contains the code to communicate with AmazonLS.
+
+require 'base64'
+require 'cgi'
+require 'net/https'
+require 'openssl'
+require 'rexml/document'
+require 'time'
+
+module DevPay
+ DEFAULT_HOST = 'ls.amazonaws.com'
+ DEFAULT_PORT = 443
+ DEFAULT_USE_SSL = true
+ DEFAULT_VERSION = '2008-04-28'
+
+ class LicenseService
+ def initialize(args={})
+
+ @use_ssl=DEFAULT_USE_SSL
+ @host=args[:host] || DEFAULT_HOST
+ @port=args[:port] || DEFAULT_PORT
+ @version=args[:version] || DEFAULT_VERSION
+ @use_ssl=args[:use_ssl] if args.has_key?(:use_ssl)
+
+
+
+
+ end
+ # Make an Activate Hosted Product call. This call requires
+ # the developer's AWS Access Key Id and the Secret Access Key
+ # to sign the request.
+ def activate_hosted_product(activation_key,
+ product_token,
+ access_key_id,
+ secret_access_key)
+ path_args = {
+ 'Action' => 'ActivateHostedProduct',
+ 'Version' => @version,
+ 'ActivationKey' => activation_key,
+ 'ProductToken' => product_token
+ }
+ res_doc = make_request(get_signed_path(path_args, access_key_id, secret_access_key))
+ extract_text(res_doc, "//UserToken")
+ end
+
+ # Make an Activate Desktop Product call. This is an unsigned
+ # call to AmazonLS.
+ def activate_desktop_product(activation_key, product_token)
+ path_args = {
+ 'Action' => 'ActivateDesktopProduct',
+ 'Version' => @version,
+ 'ActivationKey' => activation_key,
+ 'ProductToken' => product_token
+ }
+ res_doc = make_request(get_path(path_args))
+ user_token = extract_text(res_doc, "//UserToken")
+ access_key_id = extract_text(res_doc, "//AWSAccessKeyId")
+ secret_access_key = extract_text(res_doc, "//SecretAccessKey")
+ Credentials.new(user_token, access_key_id, secret_access_key)
+ end
+
+ private
+
+ # Extracts text from a document given the XPath expression.
+ def extract_text(doc, path)
+ node = REXML::XPath.first(doc, path)
+ return nil if !node
+ node.text
+ end
+
+ # Creates the query path
+ def get_path(args)
+ path = ''
+ args.each do |key, value|
+ path << "&" if path != ""
+ path << key << "=" << CGI::escape(value)
+ end
+ "/?" + path
+ end
+
+ # Creates a signed query path
+ def get_signed_path(args, access_key_id, secret_access_key)
+ args['AWSAccessKeyId'] = access_key_id
+ args['SignatureVersion'] = "1"
+ args['Timestamp'] = Time.now.iso8601
+ s_to_sign = ''
+ args.sort { |a, b| a[0].downcase <=> b[0].downcase }.each do |key, value|
+ s_to_sign += key + value
+ end
+ digest = OpenSSL::Digest::Digest.new('sha1')
+ args['Signature'] = Base64.encode64(OpenSSL::HMAC.digest(
+ digest, secret_access_key, s_to_sign)).strip
+ get_path(args)
+ end
+
+ # Makes a call to AmazonLS
+ def make_request(path)
+ http = Net::HTTP.new(@host, @port)
+ http.use_ssl = @use_ssl
+
+ http.start do
+ res = http.request(Net::HTTP::Get.new(path))
+ res_doc = REXML::Document.new(res.body)
+ if !res.is_a? Net::HTTPSuccess
+ code = extract_text(res_doc, "//Code")
+ message = extract_text(res_doc, "//Message")
+ raise RuntimeError, message, code
+ end
+ return res_doc
+ end
+ end
+
+ end
+
+ class Credentials
+ attr_accessor :user_token, :access_key_id, :secret_access_key
+ def initialize(user_token, access_key_id, secret_access_key)
+ @user_token = user_token
+ @access_key_id = access_key_id
+ @secret_access_key = secret_access_key
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.