lib/atom/http.rb in atom-tools-1.0.0 vs lib/atom/http.rb in atom-tools-2.0.0
- old
+ new
@@ -1,9 +1,11 @@
require "net/http"
require "net/https"
require "uri"
+require "atom/cache"
+
require "sha1"
require "digest/md5"
module URI # :nodoc: all
class Generic; def to_uri; self; end; end
@@ -12,11 +14,11 @@
class String # :nodoc:
def to_uri; URI.parse(self); end
end
module Atom
- UA = "atom-tools 1.0.0"
+ UA = "atom-tools 2.0.0"
module DigestAuth
CNONCE = Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
@@nonce_count = -1
@@ -64,20 +66,20 @@
if qop.nil?
response = kd(h(a1), params[:nonce] + ":" + h(a2))
else
@@nonce_count += 1
- nc = ('%08x' % @@nonce_count)
-
+ nc = ('%08x' % @@nonce_count)
+
# XXX auth-int
data = "#{params[:nonce]}:#{nc}:#{CNONCE}:#{"auth"}:#{h(a2)}"
response = kd(h(a1), data)
end
header = %Q<Digest username="#{user}", uri="#{req.path}", realm="#{params[:realm]}", response="#{response}", nonce="#{params[:nonce]}">
-
+
if params[:opaque]
header += %Q<, opaque="#{params[:opaque]}">
end
if params[:algorithm] != "MD5"
@@ -103,29 +105,29 @@
# 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,
# Atom::Collection or Atom::Feed, where it will be used for requests.
- #
+ #
# All its HTTP methods return a Net::HTTPResponse
class HTTP
include DigestAuth
# used by the default #when_auth
attr_accessor :user, :pass
# 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.
- #
+ # 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
+ # :digest won't work, since Digest authentication requires an
# initial challenge to generate a response
#
# defaults to nil
attr_accessor :always_auth
@@ -134,11 +136,22 @@
# 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:
+ # if set, 'cache' should be a directory for a disk cache, or an object
+ # with the same interface as Atom::FileCache
+ def initialize cache = nil
+ if cache.is_a? String
+ @cache = FileCache.new(cache)
+ elsif cache
+ @cache = cache
+ else
+ @cache = NilCache.new
+ end
+
+ # initialize default #when_auth
@get_auth_details = lambda do |abs_url, realm|
if @user and @pass
[@user, @pass]
else
nil
@@ -148,11 +161,11 @@
# GETs an url
def get url, headers = {}
http_request(url, Net::HTTP::Get, nil, headers)
end
-
+
# POSTs body to an url
def post url, body, headers = {}
http_request(url, Net::HTTP::Post, body, headers)
end
@@ -234,11 +247,11 @@
nonce = rand(16**32).to_s(16)
nonce_enc = [nonce].pack('m').chomp
now = Time.now.gmtime.iso8601
digest = [Digest::SHA1.digest(nonce + now + pass)].pack("m").chomp
-
+
req['X-WSSE'] = %Q<UsernameToken Username="#{user}", PasswordDigest="#{digest}", Nonce="#{nonce_enc}", Created="#{now}">
req["Authorization"] = 'WSSE profile="UsernameToken"'
end
def authsub_authenticate req, url
@@ -255,83 +268,150 @@
[ user, pass ]
end
# performs a generic HTTP request.
- 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)
-
+ def http_request(url_s, method, body = nil, headers = {}, www_authenticate = nil, redirect_limit = 5)
+ cachekey = url_s.to_s
+
+ cached_value = @cache[cachekey]
+ if cached_value
+ sock = Net::BufferedIO.new(StringIO.new(cached_value))
+ info = Net::HTTPResponse.read_new(sock)
+ info.reading_body(sock, true) {}
+
+ if method == Net::HTTP::Put and info.key? 'etag' and not headers['If-Match']
+ headers['If-Match'] = info['etag']
+ end
+ end
+
+ if cached_value and not [Net::HTTP::Get, Net::HTTP::Head].member? method
+ @cache.delete(cachekey)
+ elsif cached_value
+ entry_disposition = _entry_disposition(info, headers)
+
+ if entry_disposition == :FRESH
+ info.extend Atom::HTTPResponse
+
+ return info
+ elsif entry_disposition == :STALE
+ if info.key? 'etag' and not headers['If-None-Match']
+ headers['If-None-Match'] = info['etag']
+ end
+ if info.key? 'last-modified' and not headers['Last-Modified']
+ headers['If-Modified-Since'] = info['last-modified']
+ end
+ end
+ end
+
+ req, url = new_request(url_s, method, headers)
+
# two reasons to authenticate;
if @always_auth
self.send("#{@always_auth}_authenticate", req, url)
elsif www_authenticate
- # XXX multiple challenges, multiple headers
- param_string = www_authenticate.sub!(/^(\w+) /, "")
- auth_method = ($~[1].downcase + "_authenticate").to_sym
-
- if self.respond_to? auth_method, true # includes private methods
- self.send(auth_method, req, url, param_string)
- else
- raise "No support for #{$~[1]} authentication"
- end
+ dispatch_authorization www_authenticate, req, url
end
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
+ # a bit of added convenience
+ res.extend Atom::HTTPResponse
+
case res
when Net::HTTPUnauthorized
- if @always_auth or www_authenticate or not res["WWW-Authenticate"] # XXX and not stale (Digest only)
+ if @always_auth or www_authenticate or not res["WWW-Authenticate"] # XXX and not stale (Digest only)
# we've tried the credentials you gave us once
# and failed, or the server gave us no way to fix it
raise Unauthorized, "Your authorization was rejected"
else
# once more, with authentication
- res = http_request(url_s, method, body, init_headers, res["WWW-Authenticate"])
+ res = http_request(url_s, method, body, headers, res["WWW-Authenticate"])
if res.kind_of? Net::HTTPUnauthorized
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)
+ if res.code == "304" and method == Net::HTTP::Get
+ res.end2end_headers.each { |k| info[k] = res[k] }
+
+ res = info
+
+ res["Content-Length"] = res.body.length
+
+ res.extend Atom::HTTPResponse
+
+ _updateCache(headers, res, @cache, cachekey)
+ elsif 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)
+ res = http_request res["Location"], method, body, headers, nil, (redirect_limit - 1)
end
+ when Net::HTTPOK, Net::HTTPNonAuthoritativeInformation
+ unless res.key? 'Content-Location'
+ res['Content-Location'] = url_s
+ end
+ _updateCache(headers, res, @cache, cachekey)
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)
-
+
url = url_string.to_uri
-
+
rel = url.path
rel += "?" + url.query if url.query
[method.new(rel, headers), url]
end
+
+ def dispatch_authorization www_authenticate, req, url
+ param_string = www_authenticate.sub(/^(\w+) /, "")
+ auth_method = ($~[1].downcase + "_authenticate").to_sym
+
+ if self.respond_to? auth_method, true # includes private methods
+ self.send(auth_method, req, url, param_string)
+ else
+ # didn't support the first offered, find the next header
+ next_to_try = www_authenticate.sub(/.* ([\w]+ )/, '\1')
+ if next_to_try == www_authenticate
+ # this was the last WWW-Authenticate header
+ raise Atom::Unauthorized, "No support for offered authentication types"
+ else
+ dispatch_authorization next_to_try, req, url
+ end
+ end
+ end
end
module HTTPResponse
+ HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade']
+
# this should probably support ranges (eg. text/*)
def validate_content_type( valid )
raise Atom::HTTPException, "HTTP response contains no Content-Type!" if not self.content_type or self.content_type.empty?
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
+
+ def end2end_headers
+ hopbyhop = HOP_BY_HOP
+ if self['connection']
+ hopbyhop += self['connection'].split(',').map { |x| x.strip }
+ end
+ @header.keys.reject { |x| hopbyhop.member? x.downcase }
end
end
end