Created
October 16, 2009 22:15
-
-
Save showyou/212110 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*** ./error.py.bak 2009-10-16 16:57:02.000000000 +0900 | |
--- ./error.py 2009-10-16 17:08:41.000000000 +0900 | |
*************** | |
*** 44,49 **** | |
--- 44,56 ---- | |
+ class Unauthorized(Error): | |
+ """A request that resulted in a 401 error.""" | |
+ def __init__(self, code, message, wwwAuthenticateHeaders = None): | |
+ Error.__init__(self, code, message) | |
+ self.wwwAuthenticateHeaders = wwwAuthenticateHeaders | |
+ | |
+ | |
class InfiniteRedirection(Error): | |
""" | |
HTTP redirection is occurring endlessly. | |
*** ./auth.py.bak 2009-10-17 06:57:38.000000000 +0900 | |
--- ./auth.py 2009-10-16 17:08:24.000000000 +0900 | |
*************** | |
*** 0 **** | |
--- 1,125 ---- | |
+ """Support functions for basic and digest HTTP authentication. See RFC 2617 | |
+ for more information.""" | |
+ | |
+ import sha | |
+ import md5 | |
+ import time | |
+ import urllib2 | |
+ import random | |
+ import base64 | |
+ | |
+ class AbstractAuthentication(object): | |
+ securityLevel = 0 | |
+ def __cmp__(self, right): | |
+ if right == None: return -1 | |
+ return cmp(self.securityLevel, right.securityLevel) | |
+ | |
+ def __init__(self, challenge): | |
+ self.realm = challenge.get("realm") | |
+ | |
+ | |
+ class UnsupportedAuthentication(AbstractAuthentication): | |
+ supported = 0 | |
+ securityLevel = 0 | |
+ | |
+ | |
+ class BasicAuthentication(AbstractAuthentication): | |
+ securityLevel = 1 | |
+ supported = 1 | |
+ | |
+ def __init__(self, challenge): | |
+ AbstractAuthentication.__init__(self, challenge) | |
+ | |
+ def getAuthorization(self, user, password, method, uri): | |
+ return "Basic " + base64.encodestring("%s:%s" % (user, password | |
)) | |
+ | |
+ | |
+ class DigestAuthentication(AbstractAuthentication): | |
+ securityLevel = 2 | |
+ | |
+ def __init__(self, challenge): | |
+ AbstractAuthentication.__init__(self, challenge) | |
+ self.domain = challenge.get("domain") | |
+ self.nonce = challenge.get("nonce") | |
+ self.opaque = challenge.get("opaque") | |
+ self.stale = challenge.get("stale", "false") | |
+ self.algorithmName = challenge.get("algorithm", "MD5") | |
+ self.algorithm = self.getAlgorithmObject(self.algorithmName) | |
+ self.qop = challenge.get("qop") | |
+ | |
+ self.nonce_count = 0 | |
+ self.supported = \ | |
+ (self.algorithm != None) and \ | |
+ (self.qop in "auth", None) | |
+ | |
+ def calculateCnonce(): | |
+ # The cnonce-value is an opaque quoted string value provided by the | |
+ # client and used by both client and server to avoid chosen plaintext | |
+ # attacks, to provide mutual authentication, and to provide some | |
+ # message integrity protection. (RFC 2617, sec. 3.2.2) | |
+ return md5.new("%s%s" % (time.ctime(), random.random())).hexdigest() | |
+ calculateCnonce = staticmethod(calculateCnonce) | |
+ | |
+ def getAlgorithmObject(algorithm): | |
+ if algorithm == "MD5": | |
+ return MD5Algorithm() | |
+ elif algorithm == "SHA": | |
+ return SHAAlgorithm() | |
+ return None | |
+ getAlgorithmObject = staticmethod(getAlgorithmObject) | |
+ | |
+ def getAuthorization(self, user, password, method, uri): | |
+ assert self.supported | |
+ | |
+ A1 = "%s:%s:%s" % (user, self.realm, password) | |
+ A2 = "%s:%s" % (method, uri) | |
+ if self.qop == "auth": | |
+ self.nonce_count += 1 | |
+ cnonce = self.calculateCnonce() | |
+ resp = self.algorithm.KD( | |
+ self.algorithm.H(A1), | |
+ "%s:%08x:%s:%s:%s" % ( | |
+ self.nonce, | |
+ self.nonce_count, | |
+ cnonce, | |
+ self.qop, | |
+ self.algorithm.H(A2))) | |
+ elif self.qop == None: | |
+ resp = self.algorithm.KD(self.algorithm.H(A1), | |
+ "%s:%s" % (self.nonce, self.algorithm.H(A2))) | |
+ else: | |
+ assert False # supported should have been set to False | |
+ | |
+ header = 'Digest username="%s" realm="%s" nonce="%s" uri="%s"' % \ | |
+ (user, self.realm, self.nonce, uri) | |
+ header += ' response="%s" algorithm="%s"' % (resp, self.algorithmName) | |
+ if self.opaque: | |
+ header += ' opaque="%s"' % (self.opaque) | |
+ if self.qop: | |
+ header += ' qop="%s", nc=%08x, cnonce="%s"' % \ | |
+ (self.qop, self.nonce_count, cnonce) | |
+ return header | |
+ | |
+ | |
+ class BaseAlgorithm(object): | |
+ def KD(self, s, d): | |
+ return self.H("%s:%s" % (s, d)) | |
+ | |
+ class MD5Algorithm(BaseAlgorithm): | |
+ def H(self, x): | |
+ return md5.new(x).hexdigest() | |
+ | |
+ class SHAAlgorithm(BaseAlgorithm): | |
+ def H(self, x): | |
+ return sha.new(x).hexdigest() | |
+ | |
+ | |
+ def createAuthObject(authHeader): | |
+ authType, challenge = authHeader.split(" ", 1) | |
+ challenge = urllib2.parse_keqv_list(urllib2.parse_http_list(challenge)) | |
+ if authType.lower() == "basic": | |
+ return BasicAuthentication(challenge) | |
+ elif authType.lower() == "digest": | |
+ return DigestAuthentication(challenge) | |
+ return UnsupportedAuthentication() | |
+ | |
*** ./client.py.bak 2009-10-16 15:35:13.000000000 +0900 | |
--- ./client.py 2009-10-16 18:24:08.000000000 +0900 | |
*************** | |
*** 9,15 **** | |
import os, types | |
from urlparse import urlunparse | |
! from twisted.web import http | |
from twisted.internet import defer, protocol, reactor | |
from twisted.python import failure | |
from twisted.python.util import InsensitiveDict | |
--- 9,15 ---- | |
import os, types | |
from urlparse import urlunparse | |
! from twisted.web import http, auth | |
from twisted.internet import defer, protocol, reactor | |
from twisted.python import failure | |
from twisted.python.util import InsensitiveDict | |
*************** | |
*** 49,54 **** | |
--- 49,60 ---- | |
self.sendHeader(key, value) | |
if key.lower() == 'cookie': | |
cookieData.append(value) | |
+ if self.factory.authMechanism != None: | |
+ user, passwd = self.factory.passwdMgr.find_user_password( | |
+ self.factory.authMechanism.realm, self.factory.host) | |
+ head = self.factory.authMechanism.getAuthorization( | |
+ user, passwd, method, self.factory.path) | |
+ self.sendHeader("Authorization", head) | |
for cookie, cookval in self.factory.cookies.items(): | |
cookieData.append('%s=%s' % (cookie, cookval)) | |
if cookieData: | |
*************** | |
*** 104,118 **** | |
return | |
self.factory.setURL(url) | |
! | |
! if self.factory.scheme == 'https': | |
! from twisted.internet import ssl | |
! contextFactory = ssl.ClientContextFactory() | |
! reactor.connectSSL(self.factory.host, self.factory.port, | |
! self.factory, contextFactory) | |
! else: | |
! reactor.connectTCP(self.factory.host, self.factory.port, | |
! self.factory) | |
else: | |
self.handleStatusDefault() | |
self.factory.noPage( | |
--- 110,116 ---- | |
return | |
self.factory.setURL(url) | |
! self.reconnect() | |
else: | |
self.handleStatusDefault() | |
self.factory.noPage( | |
*************** | |
*** 128,133 **** | |
--- 126,169 ---- | |
self.factory.method = 'GET' | |
self.handleStatus_301() | |
+ def handleStatus_401(self): | |
+ serverAuth = self.headers.get("www-authenticate") | |
+ if not serverAuth: | |
+ return self.handleStatusDefault() | |
+ # Only try to authenticate if we haven't already tried, and if we have | |
+ # a password manager that could do it. | |
+ if self.factory.passwdMgr and not self.factory.authMechanism: | |
+ authMechanisms = [auth.createAuthObject(x) for x in serverAuth] | |
+ # RFC 2617 requires us us to use the strongest authentication | |
+ # mechanism we support (section 4.6), so we need to look at all | |
+ # alternatives and sort them. | |
+ authMechanisms.sort() | |
+ authMechanisms.reverse() # highest first | |
+ for authMechanism in authMechanisms: | |
+ if authMechanism.supported: | |
+ self.factory.authMechanism = authMechanism | |
+ self.reconnect() | |
+ self.quietLoss = 1 | |
+ self.transport.loseConnection() | |
+ return | |
+ # no supported authentication mechanism, or authentication attempt | |
+ # failed. we pass the www-authenticate headers to the error object | |
+ # here so that maybe the client supports authentication we don't. | |
+ self.handleStatusDefault() | |
+ self.factory.noPage( | |
+ failure.Failure( | |
+ error.Unauthorized(self.status, self.message, serverAuth)) | |
) | |
+ | |
+ def reconnect(self): | |
+ if self.factory.scheme == 'https': | |
+ from twisted.internet import ssl | |
+ contextFactory = ssl.ClientContextFactory() | |
+ reactor.connectSSL(self.factory.host, self.factory.port, | |
+ self.factory, contextFactory) | |
+ else: | |
+ reactor.connectTCP(self.factory.host, self.factory.port, | |
+ self.factory) | |
+ | |
def connectionLost(self, reason): | |
if not self.quietLoss: | |
http.HTTPClient.connectionLost(self, reason) | |
*************** | |
*** 234,240 **** | |
def __init__(self, url, method='GET', postdata=None, headers=None, | |
agent="Twisted PageGetter", timeout=0, cookies=None, | |
! followRedirect=1, redirectLimit=20): | |
self.protocol.followRedirect = followRedirect | |
self.redirectLimit = redirectLimit | |
self._redirectCount = 0 | |
--- 270,286 ---- | |
def __init__(self, url, method='GET', postdata=None, headers=None, | |
agent="Twisted PageGetter", timeout=0, cookies=None, | |
! followRedirect=1, redirectLimit=20, passwdMgr=None): | |
! """ | |
! @param passwdMgr: An object such as an instance of | |
! urllib2.HTTPPasswordMgr. It must have a function | |
! find_user_password(realm, authuri) that returns a (username, | |
! passwd) sequence. | |
! @param headers: A dictionary of headers to be sent with this request. | |
! Note that Content-Length will be calculated from any postdata | |
! provided, and therefore any Content-Length header in this dict | |
! will be discarded. | |
! """ | |
self.protocol.followRedirect = followRedirect | |
self.redirectLimit = redirectLimit | |
self._redirectCount = 0 | |
*************** | |
*** 254,265 **** | |
self.headers.setdefault("connection", "close") | |
self.postdata = postdata | |
self.method = method | |
! | |
self.setURL(url) | |
self.waiting = 1 | |
self.deferred = defer.Deferred() | |
self.response_headers = None | |
def __repr__(self): | |
return "<%s: %s>" % (self.__class__.__name__, self.url) | |
--- 300,312 ---- | |
self.headers.setdefault("connection", "close") | |
self.postdata = postdata | |
self.method = method | |
! self.passwdMgr = passwdMgr | |
self.setURL(url) | |
self.waiting = 1 | |
self.deferred = defer.Deferred() | |
self.response_headers = None | |
+ self.authMechanism = None | |
def __repr__(self): | |
return "<%s: %s>" % (self.__class__.__name__, self.url) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment