Permalink
Browse files

Initial commit of Alexandria. Working token-based auth.

  • Loading branch information...
0 parents commit 37a7febe581c7be41520f7155b49a05c8fafa07f @wycats committed Mar 24, 2010
Showing with 269 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +8 −0 Gemfile
  3. +37 −0 Gemfile.lock
  4. +25 −0 alexandria.gemspec
  5. +75 −0 lib/alexandria.rb
  6. +88 −0 lib/alexandria/constants.rb
  7. +12 −0 lib/alexandria/resourceful_ext.rb
  8. +21 −0 spec/service_name_spec.rb
  9. +1 −0 spec/spec_helper.rb
@@ -0,0 +1,2 @@
+.bundle
+test.rb
@@ -0,0 +1,8 @@
+source :gemcutter
+
+gem "nokogiri"
+gem "resourceful"
+
+group :test do
+ gem "rspec", "2.0.0.beta.4"
+end
@@ -0,0 +1,37 @@
+---
+dependencies:
+ rspec:
+ group:
+ - :test
+ version: = 2.0.0.beta.4
+ resourceful:
+ group:
+ - :default
+ version: ">= 0"
+ nokogiri:
+ group:
+ - :default
+ version: ">= 0"
+specs:
+- addressable:
+ version: 2.1.1
+- httpauth:
+ version: "0.1"
+- nokogiri:
+ version: 1.4.1
+- options:
+ version: 2.1.1
+- resourceful:
+ version: 1.0.1
+- rspec-core:
+ version: 2.0.0.beta.4
+- rspec-expectations:
+ version: 2.0.0.beta.4
+- rspec-mocks:
+ version: 2.0.0.beta.4
+- rspec:
+ version: 2.0.0.beta.4
+hash: a43d56f8d314697489de4c828fd52b7f4bdfc00c
+sources:
+- Rubygems:
+ uri: http://gemcutter.org
@@ -0,0 +1,25 @@
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'alexandria'
+ s.version = '0.5'
+ s.summary = 'Tools for working with Google Data'
+ s.description = 'The core utilities for working with the Google Data API'
+ s.required_ruby_version = '>= 1.8.6'
+
+ s.author = 'Yehuda Katz'
+ s.email = 'wycats@gmail.com'
+ s.homepage = 'http://www.yehudakatz.com'
+ s.rubyforge_project = 'alexandria'
+
+ s.files = Dir['README.markdown', 'LICENSE', 'lib/**/{*,.[a-z]*}']
+ s.require_path = 'lib'
+
+ s.rdoc_options << '--exclude' << '.'
+ s.has_rdoc = false
+
+ require "bundler"
+
+ Bundler.definition.dependencies.select {|d| d.groups.include?(:default) }.each do |d|
+ s.add_dependency d.name, d.requirement.to_s
+ end
+end
@@ -0,0 +1,75 @@
+require "resourceful"
+require "alexandria/resourceful_ext"
+require "alexandria/constants"
+
+# Alexandria is a library for connecting to APIs that work
+# with the Google Data protocol.
+#
+# For each Google user, create an instance of Alexandria.
+# An instance can retrieve a token for a particular service
+# that you can use to authenticate the user in future requests
+# to the service.
+#
+# The Alexandria library itself provides the raw functionality
+# for accessing Google data. Other libraries, such as
+# alexandria-analytics, provide domain-specific wrappers
+# around the raw data access.
+class Alexandria
+
+ AUTH_URL = "https://www.google.com/accounts/ClientLogin"
+ CONTENT_TYPE = "application/x-www-form-urlencoded"
+
+ # Create a new instance of the Alexandria connector.
+ #
+ # @param [String] user the username of the Google account
+ # @param [String] password the password of the Google account
+ def initialize(user, password)
+ @user, @password = user, password
+ @tokens = {}
+ end
+
+ # Get a token for use with a particular service. Tokens
+ # for a service are remembered, so calling this method
+ # a second time with the same service will always return
+ # the same token.
+ #
+ # @param [String, Symbol] service the name of the service.
+ # If a String is provided, it must be a valid internal
+ # Google service name. If a Symbol is provided, it must
+ # be a valid service key. Alexandria defines the valid
+ # services it knows about. You may define additional
+ # ones using Alexandria.add_service.
+ #
+ # @return [String] The token
+ def token_for(service)
+ service = Alexandria::ServiceNames[service]
+
+ @tokens[service] ||= begin
+ data = body_parameters(
+ :accountType => "HOSTED_OR_GOOGLE",
+ :Email => @user,
+ :Passwd => @password,
+ :service => service,
+ :source => "alexandria"
+ )
+
+ http = Resourceful::HttpAccessor.new
+ resource = http.resource(AUTH_URL)
+ response = resource.post(data, :content_type => CONTENT_TYPE)
+ response.body.match(/^Auth=([^\n]*)/)[1]
+ end
+ rescue Resourceful::UnsuccessfulHttpRequestError => e
+ response = e.http_response.body
+ code = response.match(/^Error=([^\n]*)/)[1]
+ raise AuthenticationFailure.new(code)
+ end
+
+private
+
+ def body_parameters(hash)
+ uri = Addressable::URI.new
+ uri.query_values = hash
+ uri.query
+ end
+
+end
@@ -0,0 +1,88 @@
+class Alexandria
+ class AuthenticationFailure < StandardError
+ DOCS_URL = "http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html"
+
+ def initialize(code)
+ @code = code
+ @error = FailureCodes.const_get(code)
+ rescue NameError
+ raise NameError, "Google responded with #{@code}, but did not document that code. " \
+ "Please visit #{DOCS_URL} for possibly updated information"
+ end
+
+ def message
+ "Google responded to your authentication request with the " \
+ "code #{@code}, which is describes as `#{@error}`"
+ end
+ end
+
+ module FailureCodes
+ BadAuthentication = "The login request used a username or password that is not recognized. NOTE: This may also result from having specified an invalid service"
+ NotVerified = "The account email address has not been verified. The user will need to access their Google account directly to resolve the issue before logging in using a non-Google application."
+ TermsNotAgreed = "The user has not agreed to terms. The user will need to access their Google account directly to resolve the issue before logging in using a non-Google application."
+ CaptchaRequired = "A CAPTCHA is required. (A response with this error code will also contain an image URL and a CAPTCHA token.)"
+ Unknown = "The error is unknown or unspecified; the request contained invalid input or was malformed."
+ AccountDeleted = "The user account has been deleted."
+ AccountDisabled = "The user account has been disabled."
+ ServiceDisabled = "The user's access to the specified service has been disabled. (The user account may still be valid.)"
+ ServiceUnavailable = "The service is not available; try again later."
+ end
+
+ class InvalidServiceName < StandardError
+ def initialize(name, valid)
+ @name, @valid = name, valid
+ end
+
+ def message
+ valids = @valid.map {|v| "* #{v.inspect}" }.join("\n")
+
+ "You specified #{@name.inspect} as the service name.\n\nThis was an " \
+ "invalid service name.\n\nIf you wish to add a new service, " \
+ "use:\n> Alexandria.add_service(:name, 'str')\n\nThe list of valid " \
+ "names is:\n#{valids}"
+ end
+ end
+
+ module ServiceNames
+ NAMES = {}
+
+ def self.[](name)
+ if name.is_a?(Symbol) && !NAMES.key?(name)
+ raise InvalidServiceName.new(name, NAMES.keys)
+ end
+
+ value = name.is_a?(Symbol) ? NAMES[name] : name
+
+ unless NAMES.values.include?(value)
+ raise InvalidServiceName.new(value, NAMES.values)
+ end
+
+ value
+ end
+ end
+
+ def self.add_service(symbol, string)
+ ServiceNames::NAMES[symbol] = string
+ end
+
+ add_service :analytics, "analytics"
+ add_service :apps, "apps"
+ add_service :base, "gbase"
+ add_service :sites, "jotspot"
+ add_service :blogger, "blogger"
+ add_service :book_search, "print"
+ add_service :calendar, "cl"
+ add_service :code_search, "code_search"
+ add_service :contacts, "cp"
+ add_service :documents, "writely"
+ add_service :finance, "finance"
+ add_service :gmail, "mail"
+ add_service :health, "health"
+ add_service :maps, "local"
+ add_service :picasa, "lh2"
+ add_service :sidewiki, "annotateweb"
+ add_service :spreadsheets, "wise"
+ add_service :webmaster_tools, "sitemaps"
+ add_service :youtube, "youtube"
+
+end
@@ -0,0 +1,12 @@
+class Alexandria
+ module Net
+ class HTTP < ::Net::HTTP
+ def initialize(*)
+ super
+ self.verify_mode = OpenSSL::SSL::VERIFY_NONE if port == 443
+ end
+ end
+ end
+end
+
+Resourceful::NetHttpAdapter::Net = Alexandria::Net
@@ -0,0 +1,21 @@
+require "spec_helper"
+
+describe "Alexandria::ServiceNames#[]" do
+ it "returns a String for a valid service Symbol" do
+ Alexandria::ServiceNames[:calendar].should == "cl"
+ end
+
+ it "returns a String for a valid service String" do
+ Alexandria::ServiceNames["cl"].should == "cl"
+ end
+
+ it "raises an exception for an invalid service Symbol" do
+ lambda { Alexandria::ServiceNames[:omg] }.
+ should raise_error(Alexandria::InvalidServiceName, /:calendar/)
+ end
+
+ it "raises an exception for an invalid service String" do
+ lambda { Alexandria::ServiceNames["omg"] }.
+ should raise_error(Alexandria::InvalidServiceName, /"cl"/)
+ end
+end
@@ -0,0 +1 @@
+require "alexandria"

0 comments on commit 37a7feb

Please sign in to comment.