lib/atom/http.rb in atom-tools-0.9.1 vs lib/atom/http.rb in atom-tools-0.9.2
- old
+ new
@@ -1,7 +1,8 @@
require "net/http"
-require 'uri'
+require "net/https"
+require "uri"
require "sha1"
require "md5"
module URI # :nodoc: all
@@ -11,11 +12,11 @@
class String # :nodoc:
def to_uri; URI.parse(self); end
end
module Atom
- UA = "atom-tools 0.9.1"
+ UA = "atom-tools 0.9.2"
module DigestAuth
CNONCE = Digest::MD5.new("%x" % (Time.now.to_i + rand(65535))).hexdigest
@@nonce_count = -1
@@ -90,12 +91,16 @@
req["Authorization"] = header
end
end
- class Unauthorized < RuntimeError # :nodoc:
+ class HTTPException < RuntimeError # :nodoc:
end
+ class Unauthorized < Atom::HTTPException # :nodoc:
+ end
+ class WrongMimetype < Atom::HTTPException # :nodoc:
+ end
# An object which handles the details of HTTP - particularly
# authentication and caching (neither of which are fully implemented).
#
# This object can be used on its own, or passed to an Atom::Service,
@@ -106,14 +111,33 @@
include DigestAuth
# used by the default #when_auth
attr_accessor :user, :pass
- # XXX doc me
- # :basic, :wsse, nil
+ # the token used for Google's AuthSub authentication
+ attr_accessor :token
+
+ # when set to :basic, :wsse or :authsub, this will send an
+ # Authentication header with every request instead of waiting for a
+ # challenge from the server.
+ #
+ # be careful; always_auth :basic will send your username and
+ # password in plain text to every URL this object requests.
+ #
+ # :digest won't work, since Digest authentication requires an
+ # initial challenge to generate a response
+ #
+ # defaults to nil
attr_accessor :always_auth
+ # automatically handle redirects, even for POST/PUT/DELETE requests?
+ #
+ # defaults to false, which will transparently redirect GET requests
+ # but return a Net::HTTPRedirection object when the server
+ # indicates to redirect a POST/PUT/DELETE
+ attr_accessor :allow_all_redirects
+
def initialize # :nodoc:
@get_auth_details = lambda do |abs_url, realm|
if @user and @pass
[@user, @pass]
else
@@ -156,10 +180,33 @@
# it should return a value of the form [username, password]
def when_auth &block # :yields: abs_url, realm
@get_auth_details = block
end
+ # GET a URL and turn it into an Atom::Entry
+ def get_atom_entry(url)
+ res = get(url, "Accept" => "application/atom+xml")
+
+ # be picky for atom:entrys
+ res.validate_content_type( [ "application/atom+xml" ] )
+
+ # XXX handle other HTTP codes
+ if res.code != "200"
+ raise Atom::HTTPException, "expected Atom::Entry, didn't get it"
+ end
+
+ Atom::Entry.parse(res.body, url)
+ end
+
+ # PUT an Atom::Entry to a URL
+ def put_atom_entry(entry, url = entry.edit_url)
+ raise "Cowardly refusing to PUT a non-Atom::Entry (#{entry.class})" unless entry.is_a? Atom::Entry
+ headers = {"Content-Type" => "application/atom+xml" }
+
+ put(url, entry.to_s, headers)
+ end
+
private
# parses plain quoted-strings
def parse_quoted_wwwauth param_string
params = {}
@@ -175,26 +222,29 @@
user, pass = username_and_password_for_realm(url, params[:realm])
req.basic_auth user, pass
end
- # WSSE authentication <http://www.xml.com/pub/a/2003/12/17/dive.html>
+ # WSSE authentication
+ # <http://www.xml.com/pub/a/2003/12/17/dive.html>
def wsse_authenticate(req, url, params = {})
- # from <http://www.koders.com/ruby/fidFB0C7F9A0F36CB0F30B2280BDDC4F43FF1FA4589.aspx?s=ruby+cgi>.
- # (thanks midore!)
user, pass = username_and_password_for_realm(url, params["realm"])
nonce = Array.new(10){ rand(0x100000000) }.pack('I*')
- nonce_base64 = [nonce].pack("m").chomp
- now = Time.now.utc.iso8601
+ nonce_b64 = [nonce].pack("m").chomp
+
+ now = Time.now.iso8601
digest = [Digest::SHA1.digest(nonce + now + pass)].pack("m").chomp
- credentials = sprintf(%Q<UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s">,
- user, digest, nonce_base64, now)
- req['X-WSSE'] = credentials
+
+ req['X-WSSE'] = %Q<UsernameToken Username="#{user}", PasswordDigest="#{digest}", Nonce="#{nonce_b64}", Created="#{now}">
req["Authorization"] = 'WSSE profile="UsernameToken"'
end
+ def authsub_authenticate req, url
+ req["Authorization"] = %{AuthSub token="#{@token}"}
+ end
+
def username_and_password_for_realm(url, realm)
abs_url = (url + "/").to_s
user, pass = @get_auth_details.call(abs_url, realm)
unless user and pass
@@ -203,11 +253,11 @@
[ user, pass ]
end
# performs a generic HTTP request.
- def http_request(url_s, method, body = nil, init_headers = {}, www_authenticate = nil)
+ def http_request(url_s, method, body = nil, init_headers = {}, www_authenticate = nil, redirect_limit = 5)
req, url = new_request(url_s, method, init_headers)
# two reasons to authenticate;
if @always_auth
self.send("#{@always_auth}_authenticate", req, url)
@@ -215,27 +265,42 @@
# XXX multiple challenges, multiple headers
param_string = www_authenticate.sub!(/^(\w+) /, "")
auth_type = $~[1]
self.send("#{auth_type.downcase}_authenticate", req, url, param_string)
end
-
- res = Net::HTTP.start(url.host, url.port) { |h| h.request(req, body) }
- if res.kind_of? Net::HTTPUnauthorized
+ http_obj = Net::HTTP.new(url.host, url.port)
+ http_obj.use_ssl = true if url.scheme == "https"
+
+ res = http_obj.start do |h|
+ h.request(req, body)
+ end
+
+ case res
+ when Net::HTTPUnauthorized
if @always_auth or www_authenticate # XXX and not stale (Digest only)
# we've tried the credentials you gave us once and failed
- raise Unauthorized, "Your username and password were rejected"
+ raise Unauthorized, "Your authorization was rejected"
else
# once more, with authentication
res = http_request(url_s, method, body, init_headers, res["WWW-Authenticate"])
if res.kind_of? Net::HTTPUnauthorized
- raise Unauthorized, "Your username and password were rejected"
+ raise Unauthorized, "Your authorization was rejected"
end
end
+ when Net::HTTPRedirection
+ if res["Location"] and (allow_all_redirects or [Net::HTTP::Get, Net::HTTP::Head].member? method)
+ raise HTTPException, "Too many redirects" if redirect_limit.zero?
+
+ res = http_request res["Location"], method, body, init_headers, nil, (redirect_limit - 1)
+ end
end
+ # a bit of added convenience
+ res.extend Atom::HTTPResponse
+
res
end
def new_request(url_string, method, init_headers = {})
headers = { "User-Agent" => UA }.merge(init_headers)
@@ -244,8 +309,21 @@
rel = url.path
rel += "?" + url.query if url.query
[method.new(rel, headers), url]
+ end
+ end
+
+ module HTTPResponse
+ # this should probably support ranges (eg. text/*)
+ def validate_content_type( valid )
+ raise Atom::HTTPException, "HTTP response contains no Content-Type!" unless self.content_type
+
+ media_type = self.content_type.split(";").first
+
+ unless valid.member? media_type.downcase
+ raise Atom::WrongMimetype, "unexpected response Content-Type: #{media_type.inspect}. should be one of: #{valid.inspect}"
+ end
end
end
end