diff --git a/xcap/web/__init__.py b/xcap/web/__init__.py deleted file mode 100644 index 31a9d18..0000000 --- a/xcap/web/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" - -Twisted Web2: a better Twisted Web Server. - -""" - -from xcap.web._version import version -__version__ = version.short() diff --git a/xcap/web/_version.py b/xcap/web/_version.py deleted file mode 100644 index e63ee75..0000000 --- a/xcap/web/_version.py +++ /dev/null @@ -1,3 +0,0 @@ -# This is an auto-generated file. Do not edit it. -from twisted.python import versions -version = versions.Version('xcap.web', 8, 1, 0) diff --git a/xcap/web/auth/__init__.py b/xcap/web/auth/__init__.py deleted file mode 100644 index 2a6344b..0000000 --- a/xcap/web/auth/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Client and server implementations of http authentication -""" diff --git a/xcap/web/auth/basic.py b/xcap/web/auth/basic.py deleted file mode 100644 index e5152f4..0000000 --- a/xcap/web/auth/basic.py +++ /dev/null @@ -1,32 +0,0 @@ - -from twisted.cred import credentials, error -from xcap.web.auth.interfaces import ICredentialFactory - -from zope.interface import implements - -class BasicCredentialFactory(object): - """ - Credential Factory for HTTP Basic Authentication - """ - - implements(ICredentialFactory) - - scheme = 'basic' - - def __init__(self, realm): - self.realm = realm - - def getChallenge(self, peer): - return {'realm': self.realm} - - def decode(self, response, request): - try: - creds = (response + '===').decode('base64') - except: - raise error.LoginFailed('Invalid credentials') - - creds = creds.split(':', 1) - if len(creds) == 2: - return credentials.UsernamePassword(*creds) - else: - raise error.LoginFailed('Invalid credentials') diff --git a/xcap/web/auth/digest.py b/xcap/web/auth/digest.py deleted file mode 100644 index a102bce..0000000 --- a/xcap/web/auth/digest.py +++ /dev/null @@ -1,349 +0,0 @@ -# Copyright (c) 2006-2008 Twisted Matrix Laboratories. - -""" -Implementation of RFC2617: HTTP Digest Authentication - -http://www.faqs.org/rfcs/rfc2617.html -""" -import sys -import time -import random - -from hashlib import md5, sha1 -from twisted.cred import credentials, error -from zope.interface import implements, Interface - -from xcap.web.auth.interfaces import ICredentialFactory - -# The digest math - -algorithms = { - 'md5': md5, - 'md5-sess': md5, - 'sha': sha1, -} - -# DigestCalcHA1 -def calcHA1( - pszAlg, - pszUserName, - pszRealm, - pszPassword, - pszNonce, - pszCNonce, - preHA1=None -): - """ - @param pszAlg: The name of the algorithm to use to calculate the digest. - Currently supported are md5 md5-sess and sha. - - @param pszUserName: The username - @param pszRealm: The realm - @param pszPassword: The password - @param pszNonce: The nonce - @param pszCNonce: The cnonce - - @param preHA1: If available this is a str containing a previously - calculated HA1 as a hex string. If this is given then the values for - pszUserName, pszRealm, and pszPassword are ignored. - """ - - if (preHA1 and (pszUserName or pszRealm or pszPassword)): - raise TypeError(("preHA1 is incompatible with the pszUserName, " - "pszRealm, and pszPassword arguments")) - - if preHA1 is None: - # We need to calculate the HA1 from the username:realm:password - m = algorithms[pszAlg]() - m.update(pszUserName) - m.update(":") - m.update(pszRealm) - m.update(":") - m.update(pszPassword) - HA1 = m.digest() - else: - # We were given a username:realm:password - HA1 = preHA1.decode('hex') - - if pszAlg == "md5-sess": - m = algorithms[pszAlg]() - m.update(HA1) - m.update(":") - m.update(pszNonce) - m.update(":") - m.update(pszCNonce) - HA1 = m.digest() - - return HA1.encode('hex') - -# DigestCalcResponse -def calcResponse( - HA1, - algo, - pszNonce, - pszNonceCount, - pszCNonce, - pszQop, - pszMethod, - pszDigestUri, - pszHEntity, -): - m = algorithms[algo]() - m.update(pszMethod) - m.update(":") - m.update(pszDigestUri) - if pszQop == "auth-int": - m.update(":") - m.update(pszHEntity) - HA2 = m.digest().encode('hex') - - m = algorithms[algo]() - m.update(HA1) - m.update(":") - m.update(pszNonce) - m.update(":") - if pszNonceCount and pszCNonce: # pszQop: - m.update(pszNonceCount) - m.update(":") - m.update(pszCNonce) - m.update(":") - m.update(pszQop) - m.update(":") - m.update(HA2) - respHash = m.digest().encode('hex') - return respHash - - -class IUsernameDigestHash(Interface): - """ - This credential is used when a CredentialChecker has access to the hash - of the username:realm:password as in an Apache .htdigest file. - """ - def checkHash(self, digestHash): - """ - @param digestHash: The hashed username:realm:password to check against. - - @return: a deferred which becomes, or a boolean indicating if the - hash matches. - """ - - -class DigestedCredentials: - """Yet Another Simple HTTP Digest authentication scheme""" - - implements(credentials.IUsernameHashedPassword, - IUsernameDigestHash) - - def __init__(self, username, method, realm, fields): - self.username = username - self.method = method - self.realm = realm - self.fields = fields - - def checkPassword(self, password): - response = self.fields.get('response') - uri = self.fields.get('uri') - nonce = self.fields.get('nonce') - cnonce = self.fields.get('cnonce') - nc = self.fields.get('nc') - algo = self.fields.get('algorithm', 'md5').lower() - qop = self.fields.get('qop', 'auth') - - expected = calcResponse( - calcHA1(algo, self.username, self.realm, password, nonce, cnonce), - algo, nonce, nc, cnonce, qop, self.method, uri, None - ) - - return expected == response - - def checkHash(self, digestHash): - response = self.fields.get('response') - uri = self.fields.get('uri') - nonce = self.fields.get('nonce') - cnonce = self.fields.get('cnonce') - nc = self.fields.get('nc') - algo = self.fields.get('algorithm', 'md5').lower() - qop = self.fields.get('qop', 'auth') - - expected = calcResponse( - calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash), - algo, nonce, nc, cnonce, qop, self.method, uri, None - ) - - return expected == response - - -class DigestCredentialFactory(object): - """ - Support for RFC2617 HTTP Digest Authentication - - @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an - opaque should be valid. - - @ivar privateKey: A random string used for generating the secure opaque. - """ - - implements(ICredentialFactory) - - CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes - - scheme = "digest" - - def __init__(self, algorithm, realm): - """ - @type algorithm: C{str} - @param algorithm: case insensitive string that specifies - the hash algorithm used, should be either, md5, md5-sess - or sha - - @type realm: C{str} - @param realm: case sensitive string that specifies the realm - portion of the challenge - """ - self.algorithm = algorithm - self.realm = realm - - c = tuple([random.randrange(sys.maxsize) for _ in range(3)]) - - self.privateKey = '%d%d%d' % c - - def generateNonce(self): - c = tuple([random.randrange(sys.maxsize) for _ in range(3)]) - c = '%d%d%d' % c - return c - - def _getTime(self): - """ - Parameterize the time based seed used in generateOpaque - so we can deterministically unittest it's behavior. - """ - return time.time() - - def generateOpaque(self, nonce, clientip): - """ - Generate an opaque to be returned to the client. - This should be a unique string that can be returned to us and verified. - """ - - # Now, what we do is encode the nonce, client ip and a timestamp - # in the opaque value with a suitable digest - key = "%s,%s,%s" % (nonce, clientip, str(int(self._getTime()))) - digest = md5(key + self.privateKey).hexdigest() - ekey = key.encode('base64') - return "%s-%s" % (digest, ekey.strip('\n')) - - def verifyOpaque(self, opaque, nonce, clientip): - """ - Given the opaque and nonce from the request, as well as the clientip - that made the request, verify that the opaque was generated by us. - And that it's not too old. - - @param opaque: The opaque value from the Digest response - @param nonce: The nonce value from the Digest response - @param clientip: The remote IP address of the client making the request - - @return: C{True} if the opaque was successfully verified. - - @raise error.LoginFailed: if C{opaque} could not be parsed or - contained the wrong values. - """ - - # First split the digest from the key - opaqueParts = opaque.split('-') - if len(opaqueParts) != 2: - raise error.LoginFailed('Invalid response, invalid opaque value') - - # Verify the key - key = opaqueParts[1].decode('base64') - keyParts = key.split(',') - - if len(keyParts) != 3: - raise error.LoginFailed('Invalid response, invalid opaque value') - - if keyParts[0] != nonce: - raise error.LoginFailed( - 'Invalid response, incompatible opaque/nonce values') - - if keyParts[1] != clientip: - raise error.LoginFailed( - 'Invalid response, incompatible opaque/client values') - - if (int(self._getTime()) - int(keyParts[2]) > - DigestCredentialFactory.CHALLENGE_LIFETIME_SECS): - - raise error.LoginFailed( - 'Invalid response, incompatible opaque/nonce too old') - - # Verify the digest - digest = md5(key + self.privateKey).hexdigest() - if digest != opaqueParts[0]: - raise error.LoginFailed('Invalid response, invalid opaque value') - - return True - - def getChallenge(self, peer): - """ - Generate the challenge for use in the WWW-Authenticate header - - @param peer: The L{IAddress} of the requesting client. - - @return: The C{dict} that can be used to generate a WWW-Authenticate - header. - """ - - c = self.generateNonce() - o = self.generateOpaque(c, peer.host) - - return {'nonce': c, - 'opaque': o, - 'qop': 'auth', - 'algorithm': self.algorithm, - 'realm': self.realm} - - def decode(self, response, request): - """ - Decode the given response and attempt to generate a - L{DigestedCredentials} from it. - - @type response: C{str} - @param response: A string of comma seperated key=value pairs - - @type request: L{xcap.web.server.Request} - @param request: the request being processed - - @return: L{DigestedCredentials} - - @raise: L{error.LoginFailed} if the response does not contain a - username, a nonce, an opaque, or if the opaque is invalid. - """ - def unq(s): - if s[0] == s[-1] == '"': - return s[1:-1] - return s - response = ' '.join(response.splitlines()) - parts = response.split(',') - - auth = {} - - for (k, v) in [p.split('=', 1) for p in parts]: - auth[k.strip()] = unq(v.strip()) - - username = auth.get('username') - if not username: - raise error.LoginFailed('Invalid response, no username given.') - - if 'opaque' not in auth: - raise error.LoginFailed('Invalid response, no opaque given.') - - if 'nonce' not in auth: - raise error.LoginFailed('Invalid response, no nonce given.') - - # Now verify the nonce/opaque values for this client - if self.verifyOpaque(auth.get('opaque'), - auth.get('nonce'), - request.remoteAddr.host): - - return DigestedCredentials(username, - request.method, - self.realm, - auth) diff --git a/xcap/web/auth/interfaces.py b/xcap/web/auth/interfaces.py deleted file mode 100644 index 6a2e89c..0000000 --- a/xcap/web/auth/interfaces.py +++ /dev/null @@ -1,59 +0,0 @@ -from zope.interface import Interface, Attribute - -class ICredentialFactory(Interface): - """ - A credential factory provides state between stages in HTTP - authentication. It is ultimately in charge of creating an - ICredential for the specified scheme, that will be used by - cred to complete authentication. - """ - scheme = Attribute(("string indicating the authentication scheme " - "this factory is associated with.")) - - def getChallenge(peer): - """ - Generate a challenge the client may respond to. - - @type peer: L{twisted.internet.interfaces.IAddress} - @param peer: The client's address - - @rtype: C{dict} - @return: dictionary of challenge arguments - """ - - def decode(response, request): - """ - Create a credentials object from the given response. - May raise twisted.cred.error.LoginFailed if the response is invalid. - - @type response: C{str} - @param response: scheme specific response string - - @type request: L{xcap.web.server.Request} - @param request: the request being processed - - @return: ICredentials - """ - - -class IAuthenticatedRequest(Interface): - """ - A request that has been authenticated with the use of Cred, - and holds a reference to the avatar returned by portal.login - """ - - avatarInterface = Attribute(("The credential interface implemented by " - "the avatar")) - - avatar = Attribute("The application specific avatar returned by " - "the application's realm") - - -class IHTTPUser(Interface): - """ - A generic interface that can implemented by an avatar to provide - access to the username used when authenticating. - """ - - username = Attribute(("A string representing the username portion of " - "the credentials used for authentication")) \ No newline at end of file diff --git a/xcap/web/auth/wrapper.py b/xcap/web/auth/wrapper.py deleted file mode 100644 index d8fbc6e..0000000 --- a/xcap/web/auth/wrapper.py +++ /dev/null @@ -1,200 +0,0 @@ - -""" -Wrapper Resources for rfc2617 HTTP Auth. -""" -from zope.interface import implements, directlyProvides -from twisted.cred import error, credentials -from twisted.python import failure -from xcap.web import responsecode -from xcap.web import http -from xcap.web import iweb -from xcap.web.auth.interfaces import IAuthenticatedRequest - -class UnauthorizedResponse(http.StatusResponse): - """A specialized response class for generating www-authenticate headers - from the given L{CredentialFactory} instances - """ - - def __init__(self, factories, remoteAddr=None): - """ - @param factories: A L{dict} of {'scheme': ICredentialFactory} - - @param remoteAddr: An L{IAddress} for the connecting client. - """ - - super(UnauthorizedResponse, self).__init__( - responsecode.UNAUTHORIZED, - "You are not authorized to access this resource.") - - authHeaders = [] - for factory in factories.values(): - authHeaders.append((factory.scheme, - factory.getChallenge(remoteAddr))) - - self.headers.setHeader('www-authenticate', authHeaders) - - -class HTTPAuthResource(object): - """I wrap a resource to prevent it being accessed unless the authentication - can be completed using the credential factory, portal, and interfaces - specified. - """ - - implements(iweb.IResource) - - def __init__(self, wrappedResource, credentialFactories, - portal, interfaces): - """ - @param wrappedResource: A L{xcap.web.iweb.IResource} to be returned - from locateChild and render upon successful - authentication. - - @param credentialFactories: A list of instances that implement - L{ICredentialFactory}. - @type credentialFactories: L{list} - - @param portal: Portal to handle logins for this resource. - @type portal: L{twisted.cred.portal.Portal} - - @param interfaces: the interfaces that are allowed to log in via the - given portal - @type interfaces: L{tuple} - """ - - self.wrappedResource = wrappedResource - - self.credentialFactories = dict([(factory.scheme, factory) - for factory in credentialFactories]) - self.portal = portal - self.interfaces = interfaces - - def _loginSucceeded(self, avatar, request): - """ - Callback for successful login. - - @param avatar: A tuple of the form (interface, avatar) as - returned by your realm. - - @param request: L{IRequest} that encapsulates this auth - attempt. - - @return: the IResource in C{self.wrappedResource} - """ - request.avatarInterface, request.avatar = avatar - - directlyProvides(request, IAuthenticatedRequest) - - def _addAuthenticateHeaders(request, response): - """ - A response filter that adds www-authenticate headers - to an outgoing response if it's code is UNAUTHORIZED (401) - and it does not already have them. - """ - if response.code == responsecode.UNAUTHORIZED: - if not response.headers.hasHeader('www-authenticate'): - newResp = UnauthorizedResponse(self.credentialFactories, - request.remoteAddr) - - response.headers.setHeader( - 'www-authenticate', - newResp.headers.getHeader('www-authenticate')) - - return response - - _addAuthenticateHeaders.handleErrors = True - - request.addResponseFilter(_addAuthenticateHeaders) - - return self.wrappedResource - - def _loginFailed(self, result, request): - """ - Errback for failed login. - - @param result: L{Failure} returned by portal.login - - @param request: L{IRequest} that encapsulates this auth - attempt. - - @return: A L{Failure} containing an L{HTTPError} containing the - L{UnauthorizedResponse} if C{result} is an L{UnauthorizedLogin} - or L{UnhandledCredentials} error - """ - result.trap(error.UnauthorizedLogin, error.UnhandledCredentials) - - return failure.Failure( - http.HTTPError( - UnauthorizedResponse( - self.credentialFactories, - request.remoteAddr))) - - def login(self, factory, response, request): - """ - @param factory: An L{ICredentialFactory} that understands the given - response. - - @param response: The client's authentication response as a string. - - @param request: The request that prompted this authentication attempt. - - @return: A L{Deferred} that fires with the wrappedResource on success - or a failure containing an L{UnauthorizedResponse} - """ - try: - creds = factory.decode(response, request) - except error.LoginFailed: - raise http.HTTPError(UnauthorizedResponse( - self.credentialFactories, - request.remoteAddr)) - - - return self.portal.login(creds, None, *self.interfaces - ).addCallbacks(self._loginSucceeded, - self._loginFailed, - (request,), None, - (request,), None) - - def authenticate(self, request): - """ - Attempt to authenticate the givin request - - @param request: An L{IRequest} to be authenticated. - """ - authHeader = request.headers.getHeader('authorization') - - if authHeader is None: - return self.portal.login(credentials.Anonymous(), - None, - *self.interfaces - ).addCallbacks(self._loginSucceeded, - self._loginFailed, - (request,), None, - (request,), None) - - elif authHeader[0] not in self.credentialFactories: - raise http.HTTPError(UnauthorizedResponse( - self.credentialFactories, - request.remoteAddr)) - else: - return self.login(self.credentialFactories[authHeader[0]], - authHeader[1], request) - - def locateChild(self, request, seg): - """ - Authenticate the request then return the C{self.wrappedResource} - and the unmodified segments. - """ - return self.authenticate(request), seg - - def renderHTTP(self, request): - """ - Authenticate the request then return the result of calling renderHTTP - on C{self.wrappedResource} - """ - def _renderResource(resource): - return resource.renderHTTP(request) - - d = self.authenticate(request) - d.addCallback(_renderResource) - - return d diff --git a/xcap/web/channel/__init__.py b/xcap/web/channel/__init__.py deleted file mode 100644 index 92766f3..0000000 --- a/xcap/web/channel/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# See LICENSE for details. - -""" -Various backend channel implementations for web. -""" -from xcap.web.channel.http import HTTPFactory - -__all__ = ['HTTPFactory'] diff --git a/xcap/web/channel/http.py b/xcap/web/channel/http.py deleted file mode 100644 index 36418cf..0000000 --- a/xcap/web/channel/http.py +++ /dev/null @@ -1,898 +0,0 @@ - -import socket -import warnings - -from io import StringIO -from twisted.internet import interfaces, protocol, reactor -from twisted.protocols import policies, basic -from twisted.python import log -from zope.interface import implements - -from xcap.web import responsecode -from xcap.web import http_headers -from xcap.web import http - -PERSIST_NO_PIPELINE, PERSIST_PIPELINE = (1,2) - -_cachedHostNames = {} -def _cachedGetHostByAddr(hostaddr): - hostname = _cachedHostNames.get(hostaddr) - if hostname is None: - try: - hostname = socket.gethostbyaddr(hostaddr)[0] - except socket.herror: - hostname = hostaddr - _cachedHostNames[hostaddr]=hostname - return hostname - -class StringTransport(object): - """ - I am a StringIO wrapper that conforms for the transport API. I support - the 'writeSequence' method. - """ - def __init__(self): - self.s = StringIO() - def writeSequence(self, seq): - self.s.write(''.join(seq)) - def __getattr__(self, attr): - return getattr(self.__dict__['s'], attr) - -class AbortedException(Exception): - pass - - -class HTTPParser(object): - """This class handles the parsing side of HTTP processing. With a suitable - subclass, it can parse either the client side or the server side of the - connection. - """ - - # Class config: - parseCloseAsEnd = False - - # Instance vars - chunkedIn = False - headerlen = 0 - length = None - inHeaders = None - partialHeader = '' - connHeaders = None - finishedReading = False - - channel = None - - # For subclassing... - # Needs attributes: - # version - - # Needs functions: - # createRequest() - # processRequest() - # _abortWithError() - # handleContentChunk(data) - # handleContentComplete() - - # Needs functions to exist on .channel - # channel.maxHeaderLength - # channel.requestReadFinished(self) - # channel.setReadPersistent(self, persistent) - # (from LineReceiver): - # channel.setRawMode() - # channel.setLineMode(extraneous) - # channel.pauseProducing() - # channel.resumeProducing() - # channel.stopProducing() - - - def __init__(self, channel): - self.inHeaders = http_headers.Headers() - self.channel = channel - - def lineReceived(self, line): - if self.chunkedIn: - # Parsing a chunked input - if self.chunkedIn == 1: - # First we get a line like "chunk-size [';' chunk-extension]" - # (where chunk extension is just random crap as far as we're concerned) - # RFC says to ignore any extensions you don't recognize -- that's all of them. - chunksize = line.split(';', 1)[0] - try: - self.length = int(chunksize, 16) - except: - self._abortWithError(responsecode.BAD_REQUEST, "Invalid chunk size, not a hex number: %s!" % chunksize) - if self.length < 0: - self._abortWithError(responsecode.BAD_REQUEST, "Invalid chunk size, negative.") - - if self.length == 0: - # We're done, parse the trailers line - self.chunkedIn = 3 - else: - # Read self.length bytes of raw data - self.channel.setRawMode() - elif self.chunkedIn == 2: - # After we got data bytes of the appropriate length, we end up here, - # waiting for the CRLF, then go back to get the next chunk size. - if line != '': - self._abortWithError(responsecode.BAD_REQUEST, "Excess %d bytes sent in chunk transfer mode" % len(line)) - self.chunkedIn = 1 - elif self.chunkedIn == 3: - # TODO: support Trailers (maybe! but maybe not!) - - # After getting the final "0" chunk we're here, and we *EAT MERCILESSLY* - # any trailer headers sent, and wait for the blank line to terminate the - # request. - if line == '': - self.allContentReceived() - # END of chunk handling - elif line == '': - # Empty line => End of headers - if self.partialHeader: - self.headerReceived(self.partialHeader) - self.partialHeader = '' - self.allHeadersReceived() # can set chunkedIn - self.createRequest() - if self.chunkedIn: - # stay in linemode waiting for chunk header - pass - elif self.length == 0: - # no content expected - self.allContentReceived() - else: - # await raw data as content - self.channel.setRawMode() - # Should I do self.pauseProducing() here? - self.processRequest() - else: - self.headerlen += len(line) - if self.headerlen > self.channel.maxHeaderLength: - self._abortWithError(responsecode.BAD_REQUEST, 'Headers too long.') - - if line[0] in ' \t': - # Append a header continuation - self.partialHeader += line - else: - if self.partialHeader: - self.headerReceived(self.partialHeader) - self.partialHeader = line - - def rawDataReceived(self, data): - """Handle incoming content.""" - datalen = len(data) - if datalen < self.length: - self.handleContentChunk(data) - self.length = self.length - datalen - else: - self.handleContentChunk(data[:self.length]) - extraneous = data[self.length:] - channel = self.channel # could go away from allContentReceived. - if not self.chunkedIn: - self.allContentReceived() - else: - # NOTE: in chunked mode, self.length is the size of the current chunk, - # so we still have more to read. - self.chunkedIn = 2 # Read next chunksize - - channel.setLineMode(extraneous) - - def headerReceived(self, line): - """Store this header away. Check for too much header data - (> channel.maxHeaderLength) and abort the connection if so. - """ - nameval = line.split(':', 1) - if len(nameval) != 2: - self._abortWithError(responsecode.BAD_REQUEST, "No ':' in header.") - - name, val = nameval - val = val.lstrip(' \t') - self.inHeaders.addRawHeader(name, val) - - - def allHeadersReceived(self): - # Split off connection-related headers - connHeaders = self.splitConnectionHeaders() - - # Set connection parameters from headers - self.setConnectionParams(connHeaders) - self.connHeaders = connHeaders - - def allContentReceived(self): - self.finishedReading = True - self.channel.requestReadFinished(self) - self.handleContentComplete() - - - def splitConnectionHeaders(self): - """ - Split off connection control headers from normal headers. - - The normal headers are then passed on to user-level code, while the - connection headers are stashed in .connHeaders and used for things like - request/response framing. - - This corresponds roughly with the HTTP RFC's description of 'hop-by-hop' - vs 'end-to-end' headers in RFC2616 S13.5.1, with the following - exceptions: - - * proxy-authenticate and proxy-authorization are not treated as - connection headers. - - * content-length is, as it is intimiately related with low-level HTTP - parsing, and is made available to user-level code via the stream - length, rather than a header value. (except for HEAD responses, in - which case it is NOT used by low-level HTTP parsing, and IS kept in - the normal headers. - """ - - def move(name): - h = inHeaders.getRawHeaders(name, None) - if h is not None: - inHeaders.removeHeader(name) - connHeaders.setRawHeaders(name, h) - - # NOTE: According to HTTP spec, we're supposed to eat the - # 'Proxy-Authenticate' and 'Proxy-Authorization' headers also, but that - # doesn't sound like a good idea to me, because it makes it impossible - # to have a non-authenticating transparent proxy in front of an - # authenticating proxy. An authenticating proxy can eat them itself. - # - # 'Proxy-Connection' is an undocumented HTTP 1.0 abomination. - connHeaderNames = ['content-length', 'connection', 'keep-alive', 'te', - 'trailers', 'transfer-encoding', 'upgrade', - 'proxy-connection'] - inHeaders = self.inHeaders - connHeaders = http_headers.Headers() - - move('connection') - if self.version < (1,1): - # Remove all headers mentioned in Connection, because a HTTP 1.0 - # proxy might have erroneously forwarded it from a 1.1 client. - for name in connHeaders.getHeader('connection', ()): - if inHeaders.hasHeader(name): - inHeaders.removeHeader(name) - else: - # Otherwise, just add the headers listed to the list of those to move - connHeaderNames.extend(connHeaders.getHeader('connection', ())) - - # If the request was HEAD, self.length has been set to 0 by - # HTTPClientRequest.submit; in this case, Content-Length should - # be treated as a response header, not a connection header. - - # Note: this assumes the invariant that .length will always be None - # coming into this function, unless this is a HEAD request. - if self.length is not None: - connHeaderNames.remove('content-length') - - for headername in connHeaderNames: - move(headername) - - return connHeaders - - def setConnectionParams(self, connHeaders): - # Figure out persistent connection stuff - if self.version >= (1,1): - if 'close' in connHeaders.getHeader('connection', ()): - readPersistent = False - else: - readPersistent = PERSIST_PIPELINE - elif 'keep-alive' in connHeaders.getHeader('connection', ()): - readPersistent = PERSIST_NO_PIPELINE - else: - readPersistent = False - - - # Okay, now implement section 4.4 Message Length to determine - # how to find the end of the incoming HTTP message. - transferEncoding = connHeaders.getHeader('transfer-encoding') - - if transferEncoding: - if transferEncoding[-1] == 'chunked': - # Chunked - self.chunkedIn = 1 - # Cut off the chunked encoding (cause it's special) - transferEncoding = transferEncoding[:-1] - elif not self.parseCloseAsEnd: - # Would close on end of connection, except this can't happen for - # client->server data. (Well..it could actually, since TCP has half-close - # but the HTTP spec says it can't, so we'll pretend it's right.) - self._abortWithError(responsecode.BAD_REQUEST, "Transfer-Encoding received without chunked in last position.") - - # TODO: support gzip/etc encodings. - # FOR NOW: report an error if the client uses any encodings. - # They shouldn't, because we didn't send a TE: header saying it's okay. - if transferEncoding: - self._abortWithError(responsecode.NOT_IMPLEMENTED, "Transfer-Encoding %s not supported." % transferEncoding) - else: - # No transfer-coding. - self.chunkedIn = 0 - if self.parseCloseAsEnd: - # If no Content-Length, then it's indeterminate length data - # (unless the responsecode was one of the special no body ones) - # Also note that for HEAD requests, connHeaders won't have - # content-length even if the response did. - if self.code in http.NO_BODY_CODES: - self.length = 0 - else: - self.length = connHeaders.getHeader('content-length', self.length) - - # If it's an indeterminate stream without transfer encoding, it must be - # the last request. - if self.length is None: - readPersistent = False - else: - # If no Content-Length either, assume no content. - self.length = connHeaders.getHeader('content-length', 0) - - # Set the calculated persistence - self.channel.setReadPersistent(readPersistent) - - def abortParse(self): - # If we're erroring out while still reading the request - if not self.finishedReading: - self.finishedReading = True - self.channel.setReadPersistent(False) - self.channel.requestReadFinished(self) - - # producer interface - def pauseProducing(self): - if not self.finishedReading: - self.channel.pauseProducing() - - def resumeProducing(self): - if not self.finishedReading: - self.channel.resumeProducing() - - def stopProducing(self): - if not self.finishedReading: - self.channel.stopProducing() - -class HTTPChannelRequest(HTTPParser): - """This class handles the state and parsing for one HTTP request. - It is responsible for all the low-level connection oriented behavior. - Thus, it takes care of keep-alive, de-chunking, etc., and passes - the non-connection headers on to the user-level Request object.""" - - command = path = version = None - queued = 0 - request = None - - out_version = "HTTP/1.1" - - def __init__(self, channel, queued=0): - HTTPParser.__init__(self, channel) - self.queued=queued - - # Buffer writes to a string until we're first in line - # to write a response - if queued: - self.transport = StringTransport() - else: - self.transport = self.channel.transport - - # set the version to a fallback for error generation - self.version = (1,0) - - - def gotInitialLine(self, initialLine): - parts = initialLine.split() - - # Parse the initial request line - if len(parts) != 3: - if len(parts) == 1: - parts.append('/') - if len(parts) == 2 and parts[1][0] == '/': - parts.append('HTTP/0.9') - else: - self._abortWithError(responsecode.BAD_REQUEST, 'Bad request line: %s' % initialLine) - - self.command, self.path, strversion = parts - try: - protovers = http.parseVersion(strversion) - if protovers[0] != 'http': - raise ValueError() - except ValueError: - self._abortWithError(responsecode.BAD_REQUEST, "Unknown protocol: %s" % strversion) - - self.version = protovers[1:3] - - # Ensure HTTP 0 or HTTP 1. - if self.version[0] > 1: - self._abortWithError(responsecode.HTTP_VERSION_NOT_SUPPORTED, 'Only HTTP 0.9 and HTTP 1.x are supported.') - - if self.version[0] == 0: - # simulate end of headers, as HTTP 0 doesn't have headers. - self.lineReceived('') - - def lineLengthExceeded(self, line, wasFirst=False): - code = wasFirst and responsecode.REQUEST_URI_TOO_LONG or responsecode.BAD_REQUEST - self._abortWithError(code, 'Header line too long.') - - def createRequest(self): - self.request = self.channel.requestFactory(self, self.command, self.path, self.version, self.length, self.inHeaders) - del self.inHeaders - - def processRequest(self): - self.request.process() - - def handleContentChunk(self, data): - self.request.handleContentChunk(data) - - def handleContentComplete(self): - self.request.handleContentComplete() - -############## HTTPChannelRequest *RESPONSE* methods ############# - producer = None - chunkedOut = False - finished = False - - ##### Request Callbacks ##### - def writeIntermediateResponse(self, code, headers=None): - if self.version >= (1,1): - self._writeHeaders(code, headers, False) - - def writeHeaders(self, code, headers): - self._writeHeaders(code, headers, True) - - def _writeHeaders(self, code, headers, addConnectionHeaders): - # HTTP 0.9 doesn't have headers. - if self.version[0] == 0: - return - - l = [] - code_message = responsecode.RESPONSES.get(code, "Unknown Status") - - l.append('%s %s %s\r\n' % (self.out_version, code, - code_message)) - if headers is not None: - for name, valuelist in headers.getAllRawHeaders(): - for value in valuelist: - l.append("%s: %s\r\n" % (name, value)) - - if addConnectionHeaders: - # if we don't have a content length, we send data in - # chunked mode, so that we can support persistent connections. - if (headers.getHeader('content-length') is None and - self.command != "HEAD" and code not in http.NO_BODY_CODES): - if self.version >= (1,1): - l.append("%s: %s\r\n" % ('Transfer-Encoding', 'chunked')) - self.chunkedOut = True - else: - # Cannot use persistent connections if we can't do chunking - self.channel.dropQueuedRequests() - - if self.channel.isLastRequest(self): - l.append("%s: %s\r\n" % ('Connection', 'close')) - elif self.version < (1,1): - l.append("%s: %s\r\n" % ('Connection', 'Keep-Alive')) - - l.append("\r\n") - self.transport.writeSequence(l) - - - def write(self, data): - if not data: - return - elif self.chunkedOut: - self.transport.writeSequence(("%X\r\n" % len(data), data, "\r\n")) - else: - self.transport.write(data) - - def finish(self): - """We are finished writing data.""" - if self.finished: - warnings.warn("Warning! request.finish called twice.", stacklevel=2) - return - - if self.chunkedOut: - # write last chunk and closing CRLF - self.transport.write("0\r\n\r\n") - - self.finished = True - if not self.queued: - self._cleanup() - - - def abortConnection(self, closeWrite=True): - """Abort the HTTP connection because of some kind of unrecoverable - error. If closeWrite=False, then only abort reading, but leave - the writing side alone. This is mostly for internal use by - the HTTP request parsing logic, so that it can call an error - page generator. - - Otherwise, completely shut down the connection. - """ - self.abortParse() - if closeWrite: - if self.producer: - self.producer.stopProducing() - self.unregisterProducer() - - self.finished = True - if self.queued: - self.transport.reset() - self.transport.truncate() - else: - self._cleanup() - - def getHostInfo(self): - t=self.channel.transport - secure = interfaces.ISSLTransport(t, None) is not None - host = t.getHost() - host.host = _cachedGetHostByAddr(host.host) - return host, secure - - def getRemoteHost(self): - return self.channel.transport.getPeer() - - ##### End Request Callbacks ##### - - def _abortWithError(self, errorcode, text=''): - """Handle low level protocol errors.""" - headers = http_headers.Headers() - headers.setHeader('content-length', len(text)+1) - - self.abortConnection(closeWrite=False) - self.writeHeaders(errorcode, headers) - self.write(text) - self.write("\n") - self.finish() - raise AbortedException - - def _cleanup(self): - """Called when have finished responding and are no longer queued.""" - if self.producer: - log.err(RuntimeError("Producer was not unregistered for %s" % self)) - self.unregisterProducer() - self.channel.requestWriteFinished(self) - del self.transport - - # methods for channel - end users should not use these - - def noLongerQueued(self): - """Notify the object that it is no longer queued. - - We start writing whatever data we have to the transport, etc. - - This method is not intended for users. - """ - if not self.queued: - raise RuntimeError("noLongerQueued() got called unnecessarily.") - - self.queued = 0 - - # set transport to real one and send any buffer data - data = self.transport.getvalue() - self.transport = self.channel.transport - if data: - self.transport.write(data) - - # if we have producer, register it with transport - if (self.producer is not None) and not self.finished: - self.transport.registerProducer(self.producer, True) - - # if we're finished, clean up - if self.finished: - self._cleanup() - - - # consumer interface - def registerProducer(self, producer, streaming): - """Register a producer. - """ - - if self.producer: - raise ValueError("registering producer %s before previous one (%s) was unregistered" % (producer, self.producer)) - - self.producer = producer - - if self.queued: - producer.pauseProducing() - else: - self.transport.registerProducer(producer, streaming) - - def unregisterProducer(self): - """Unregister the producer.""" - if not self.queued: - self.transport.unregisterProducer() - self.producer = None - - def connectionLost(self, reason): - """connection was lost""" - if self.queued and self.producer: - self.producer.stopProducing() - self.producer = None - if self.request: - self.request.connectionLost(reason) - -class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin, object): - """A receiver for HTTP requests. Handles splitting up the connection - for the multiple HTTPChannelRequests that may be in progress on this - channel. - - @ivar timeOut: number of seconds to wait before terminating an - idle connection. - - @ivar maxPipeline: number of outstanding in-progress requests - to allow before pausing the input. - - @ivar maxHeaderLength: number of bytes of header to accept from - the client. - - """ - - implements(interfaces.IHalfCloseableProtocol) - - ## Configuration parameters. Set in instances or subclasses. - - # How many simultaneous requests to handle. - maxPipeline = 4 - - # Timeout when between two requests - betweenRequestsTimeOut = 15 - # Timeout between lines or bytes while reading a request - inputTimeOut = 60 * 4 - - # maximum length of headers (10KiB) - maxHeaderLength = 10240 - - # Allow persistent connections? - allowPersistentConnections = True - - # ChannelRequest - chanRequestFactory = HTTPChannelRequest - requestFactory = http.Request - - - _first_line = 2 - readPersistent = PERSIST_PIPELINE - - _readLost = False - _writeLost = False - - _lingerTimer = None - chanRequest = None - - def _callLater(self, secs, fun): - reactor.callLater(secs, fun) - - def __init__(self): - # the request queue - self.requests = [] - - def connectionMade(self): - self.setTimeout(self.inputTimeOut) - self.factory.outstandingRequests+=1 - - def lineReceived(self, line): - if self._first_line: - self.setTimeout(self.inputTimeOut) - # if this connection is not persistent, drop any data which - # the client (illegally) sent after the last request. - if not self.readPersistent: - self.dataReceived = self.lineReceived = lambda *args: None - return - - # IE sends an extraneous empty line (\r\n) after a POST request; - # eat up such a line, but only ONCE - if not line and self._first_line == 1: - self._first_line = 2 - return - - self._first_line = 0 - - if not self.allowPersistentConnections: - # Don't allow a second request - self.readPersistent = False - - try: - self.chanRequest = self.chanRequestFactory(self, len(self.requests)) - self.requests.append(self.chanRequest) - self.chanRequest.gotInitialLine(line) - except AbortedException: - pass - else: - try: - self.chanRequest.lineReceived(line) - except AbortedException: - pass - - def lineLengthExceeded(self, line): - if self._first_line: - # Fabricate a request object to respond to the line length violation. - self.chanRequest = self.chanRequestFactory(self, - len(self.requests)) - self.requests.append(self.chanRequest) - self.chanRequest.gotInitialLine("GET fake HTTP/1.0") - try: - self.chanRequest.lineLengthExceeded(line, self._first_line) - except AbortedException: - pass - - def rawDataReceived(self, data): - self.setTimeout(self.inputTimeOut) - try: - self.chanRequest.rawDataReceived(data) - except AbortedException: - pass - - def requestReadFinished(self, request): - if(self.readPersistent is PERSIST_NO_PIPELINE or - len(self.requests) >= self.maxPipeline): - self.pauseProducing() - - # reset state variables - self._first_line = 1 - self.chanRequest = None - self.setLineMode() - - # Disable the idle timeout, in case this request takes a long - # time to finish generating output. - if len(self.requests) > 0: - self.setTimeout(None) - - def _startNextRequest(self): - # notify next request, if present, it can start writing - del self.requests[0] - - if self._writeLost: - self.transport.loseConnection() - elif self.requests: - self.requests[0].noLongerQueued() - - # resume reading if allowed to - if(not self._readLost and - self.readPersistent is not PERSIST_NO_PIPELINE and - len(self.requests) < self.maxPipeline): - self.resumeProducing() - elif self._readLost: - # No more incoming data, they already closed! - self.transport.loseConnection() - else: - # no requests in queue, resume reading - self.setTimeout(self.betweenRequestsTimeOut) - self.resumeProducing() - - def setReadPersistent(self, persistent): - if self.readPersistent: - # only allow it to be set if it's not currently False - self.readPersistent = persistent - - def dropQueuedRequests(self): - """Called when a response is written that forces a connection close.""" - self.readPersistent = False - # Tell all requests but first to abort. - for request in self.requests[1:]: - request.connectionLost(None) - del self.requests[1:] - - def isLastRequest(self, request): - # Is this channel handling the last possible request - return not self.readPersistent and self.requests[-1] == request - - def requestWriteFinished(self, request): - """Called by first request in queue when it is done.""" - if request != self.requests[0]: raise TypeError - - # Don't del because we haven't finished cleanup, so, - # don't want queue len to be 0 yet. - self.requests[0] = None - - if self.readPersistent or len(self.requests) > 1: - # Do this in the next reactor loop so as to - # not cause huge call stacks with fast - # incoming requests. - self._callLater(0, self._startNextRequest) - else: - self.lingeringClose() - - def timeoutConnection(self): - #log.msg("Timing out client: %s" % str(self.transport.getPeer())) - policies.TimeoutMixin.timeoutConnection(self) - - def lingeringClose(self): - """ - This is a bit complicated. This process is necessary to ensure proper - workingness when HTTP pipelining is in use. - - Here is what it wants to do: - - 1. Finish writing any buffered data, then close our write side. - While doing so, read and discard any incoming data. - - 2. When that happens (writeConnectionLost called), wait up to 20 - seconds for the remote end to close their write side (our read - side). - - 3. - - If they do (readConnectionLost called), close the socket, - and cancel the timeout. - - - If that doesn't happen, the timer fires, and makes the - socket close anyways. - """ - - # Close write half - self.transport.loseWriteConnection() - - # Throw out any incoming data - self.dataReceived = self.lineReceived = lambda *args: None - self.transport.resumeProducing() - - def writeConnectionLost(self): - # Okay, all data has been written - # In 20 seconds, actually close the socket - self._lingerTimer = reactor.callLater(20, self._lingerClose) - self._writeLost = True - - def _lingerClose(self): - self._lingerTimer = None - self.transport.loseConnection() - - def readConnectionLost(self): - """Read connection lost""" - # If in the lingering-close state, lose the socket. - if self._lingerTimer: - self._lingerTimer.cancel() - self._lingerTimer = None - self.transport.loseConnection() - return - - # If between requests, drop connection - # when all current requests have written their data. - self._readLost = True - if not self.requests: - # No requests in progress, lose now. - self.transport.loseConnection() - - # If currently in the process of reading a request, this is - # probably a client abort, so lose the connection. - if self.chanRequest: - self.transport.loseConnection() - - def connectionLost(self, reason): - self.factory.outstandingRequests-=1 - - self._writeLost = True - self.readConnectionLost() - self.setTimeout(None) - - # Tell all requests to abort. - for request in self.requests: - if request is not None: - request.connectionLost(reason) - -class OverloadedServerProtocol(protocol.Protocol): - def connectionMade(self): - self.transport.write("HTTP/1.0 503 Service Unavailable\r\n" - "Content-Type: text/html\r\n" - "Connection: close\r\n\r\n" - "503 Service Unavailable" - "

Service Unavailable

" - "The server is currently overloaded, " - "please try again later.") - self.transport.loseConnection() - -class HTTPFactory(protocol.ServerFactory): - """Factory for HTTP server.""" - - protocol = HTTPChannel - - protocolArgs = None - - outstandingRequests = 0 - - def __init__(self, requestFactory, maxRequests=600, **kwargs): - self.maxRequests=maxRequests - self.protocolArgs = kwargs - self.protocolArgs['requestFactory']=requestFactory - - def buildProtocol(self, addr): - if self.outstandingRequests >= self.maxRequests: - return OverloadedServerProtocol() - - p = protocol.ServerFactory.buildProtocol(self, addr) - - for arg,value in self.protocolArgs.items(): - setattr(p, arg, value) - return p - -__all__ = ['HTTPFactory', ] diff --git a/xcap/web/compat.py b/xcap/web/compat.py deleted file mode 100644 index e37543b..0000000 --- a/xcap/web/compat.py +++ /dev/null @@ -1,446 +0,0 @@ - -from urllib.parse import quote - -import UserDict, math, time -from io import StringIO - -from xcap.web import http_headers, iweb, stream, responsecode -from twisted.internet import defer, address -from twisted.python import components -from twisted.spread import pb - -from zope.interface import implements - -class HeaderAdapter(UserDict.DictMixin): - def __init__(self, headers): - self._headers = headers - - def __getitem__(self, name): - raw = self._headers.getRawHeaders(name) - if raw is None: - raise KeyError(name) - return ', '.join(raw) - - def __setitem__(self, name, value): - self._headers.setRawHeaders([value]) - - def __delitem__(self, name): - if not self._headers.hasHeader(name): - raise KeyError(name) - self._headers.removeHeader(name) - - def iteritems(self): - for k,v in self._headers.getAllRawHeaders(): - yield k, ', '.join(v) - - def keys(self): - return [k for k, _ in self.items()] - - def __iter__(self): - for k, _ in self.items(): - yield k - - def has_key(self, name): - return self._headers.hasHeader(name) - -def makeOldRequestAdapter(original): - # Cache the adapter. Replace this with a more better generalized - # mechanism when one becomes available. - if not hasattr(original, '_oldRequest'): - original._oldRequest = OldRequestAdapter(original) - return original._oldRequest - -def _addressToTuple(addr): - if isinstance(addr, address.IPv4Address): - return ('INET', addr.host, addr.port) - elif isinstance(addr, address.UNIXAddress): - return ('UNIX', addr.name) - else: - return tuple(addr) - -class OldRequestAdapter(pb.Copyable, components.Componentized, object): - """Adapt old requests to new request - """ - implements(iweb.IOldRequest) - - def _getFrom(where, name): - def _get(self): - return getattr(getattr(self, where), name) - return property(_get) - - def _getsetFrom(where, name): - def _get(self): - return getattr(getattr(self, where), name) - def _set(self, new): - setattr(getattr(self, where), name, new) - def _del(self): - delattr(getattr(self, where), name) - return property(_get, _set, _del) - - def _getsetHeaders(where): - def _get(self): - headers = getattr(self, where).headers - return HeaderAdapter(headers) - - def _set(self, newheaders): - headers = http_headers.Headers() - for n,v in list(newheaders.items()): - headers.setRawHeaders(n, (v,)) - newheaders = headers - getattr(self, where).headers = newheaders - - return property(_get, _set) - - - code = _getsetFrom('response', 'code') - code_message = "" - - method = _getsetFrom('request', 'method') - uri = _getsetFrom('request', 'uri') - def _getClientproto(self): - return "HTTP/%d.%d" % self.request.clientproto - clientproto = property(_getClientproto) - - received_headers = _getsetHeaders('request') - headers = _getsetHeaders('response') - path = _getsetFrom('request', 'path') - - # cookies = # Do I need this? - # received_cookies = # Do I need this? - content = StringIO() #### FIXME - args = _getsetFrom('request', 'args') - # stack = # WTF is stack? - prepath = _getsetFrom('request', 'prepath') - postpath = _getsetFrom('request', 'postpath') - - def _getClient(self): - return "WTF" - client = property(_getClient) - - def _getHost(self): - return address.IPv4Address("TCP", self.request.host, self.request.port) - host = property(_getHost) - - def __init__(self, request): - from xcap.web import http - components.Componentized.__init__(self) - self.request = request - self.response = http.Response(stream=stream.ProducerStream()) - # This deferred will be fired by the first call to write on OldRequestAdapter - # and will cause the headers to be output. - self.deferredResponse = defer.Deferred() - - def getStateToCopyFor(self, issuer): - # This is for distrib compatibility - x = {} - - x['prepath'] = self.prepath - x['postpath'] = self.postpath - x['method'] = self.method - x['uri'] = self.uri - - x['clientproto'] = self.clientproto - self.content.seek(0, 0) - x['content_data'] = self.content.read() - x['remote'] = pb.ViewPoint(issuer, self) - - x['host'] = _addressToTuple(self.request.chanRequest.channel.transport.getHost()) - x['client'] = _addressToTuple(self.request.chanRequest.channel.transport.getPeer()) - - return x - - def getTypeToCopy(self): - # lie to PB so the ResourcePublisher doesn't have to know xcap.web - # exists which is good because xcap.web doesn't exist. - return 'twisted.web.server.Request' - - def registerProducer(self, producer, streaming): - self.response.stream.registerProducer(producer, streaming) - - def unregisterProducer(self): - self.response.stream.unregisterProducer() - - def finish(self): - if self.deferredResponse is not None: - d = self.deferredResponse - self.deferredResponse = None - d.callback(self.response) - self.response.stream.finish() - - def write(self, data): - if self.deferredResponse is not None: - d = self.deferredResponse - self.deferredResponse = None - d.callback(self.response) - self.response.stream.write(data) - - def getHeader(self, name): - raw = self.request.headers.getRawHeaders(name) - if raw is None: - return None - return ', '.join(raw) - - def setHeader(self, name, value): - """Set an outgoing HTTP header. - """ - self.response.headers.setRawHeaders(name, [value]) - - def setResponseCode(self, code, message=None): - # message ignored - self.response.code = code - - def setLastModified(self, when): - # Never returns CACHED -- can it and still be compliant? - when = int(math.ceil(when)) - self.response.headers.setHeader('last-modified', when) - return None - - def setETag(self, etag): - self.response.headers.setRawHeaders('etag', [etag]) - return None - - def getAllHeaders(self): - return dict(iter(self.headers.items())) - - def getRequestHostname(self): - return self.request.host - - - def getCookie(self, key): - for cookie in self.request.headers.getHeader('cookie', ()): - if cookie.name == key: - return cookie.value - - return None - - def addCookie(self, k, v, expires=None, domain=None, path=None, max_age=None, comment=None, secure=None): - if expires is None and max_age is not None: - expires=max_age-time.time() - cookie = http_headers.Cookie(k,v, expires=expires, domain=domain, path=path, comment=comment, secure=secure) - self.response.headers.setHeader('set-cookie', self.request.headers.getHeader('set-cookie', ())+(cookie,)) - - def notifyFinish(self): - ### FIXME - return None -# return self.request.notifyFinish() - - def getHost(self): - return self.host - - def setHost(self, host, port, ssl=0): - self.request.host = host - self.request.port = port - self.request.scheme = ssl and 'https' or 'http' - - def isSecure(self): - return self.request.scheme == 'https' - - def getClientIP(self): - if isinstance(self.request.chanRequest.getRemoteHost(), address.IPv4Address): - return self.client.host - else: - return None - return self.request.chanRequest.getRemoteHost() - return "127.0.0.1" - - def getClient(self): - return "127.0.0.1" - -### FIXME: - def getUser(self): - return "" - - def getPassword(self): - return "" - -# Identical to original methods -- hopefully these don't have to change - def sibLink(self, name): - "Return the text that links to a sibling of the requested resource." - if self.postpath: - return (len(self.postpath)*"../") + name - else: - return name - - def childLink(self, name): - "Return the text that links to a child of the requested resource." - lpp = len(self.postpath) - if lpp > 1: - return ((lpp-1)*"../") + name - elif lpp == 1: - return name - else: # lpp == 0 - if len(self.prepath) and self.prepath[-1]: - return self.prepath[-1] + '/' + name - else: - return name - - def redirect(self, url): - """Utility function that does a redirect. - - The request should have finish() called after this. - """ - self.setResponseCode(responsecode.FOUND) - self.setHeader("location", url) - - def prePathURL(self): - port = self.getHost().port - if self.isSecure(): - default = 443 - else: - default = 80 - if port == default: - hostport = '' - else: - hostport = ':%d' % port - return quote('http%s://%s%s/%s' % ( - self.isSecure() and 's' or '', - self.getRequestHostname(), - hostport, - string.join(self.prepath, '/')), "/:") - -# def URLPath(self): -# from twisted.python import urlpath -# return urlpath.URLPath.fromRequest(self) - -# But nevow wants it to look like this... :( - def URLPath(self): - from nevow import url - return url.URL.fromContext(self) - - def rememberRootURL(self, url=None): - """ - Remember the currently-processed part of the URL for later - recalling. - """ - if url is None: - url = self.prePathURL() - # remove one segment - self.appRootURL = url[:url.rindex("/")] - else: - self.appRootURL = url - - def getRootURL(self): - """ - Get a previously-remembered URL. - """ - return self.appRootURL - - - session = None - - def getSession(self, sessionInterface = None): - # Session management - if not self.session: - # FIXME: make sitepath be something - cookiename = string.join(['TWISTED_SESSION'] + self.sitepath, "_") - sessionCookie = self.getCookie(cookiename) - if sessionCookie: - try: - self.session = self.site.getSession(sessionCookie) - except KeyError: - pass - # if it still hasn't been set, fix it up. - if not self.session: - self.session = self.site.makeSession() - self.addCookie(cookiename, self.session.uid, path='/') - self.session.touch() - if sessionInterface: - return self.session.getComponent(sessionInterface) - return self.session - - -class OldNevowResourceAdapter(object): - implements(iweb.IResource) - - def __init__(self, original): - # Can't use self.__original= because of __setattr__. - self.__dict__['_OldNevowResourceAdapter__original']=original - - def __getattr__(self, name): - return getattr(self.__original, name) - - def __setattr__(self, name, value): - setattr(self.__original, name, value) - - def __delattr__(self, name): - delattr(self.__original, name) - - def locateChild(self, ctx, segments): - from xcap.web.server import parsePOSTData - request = iweb.IRequest(ctx) - if request.method == "POST": - return parsePOSTData(request).addCallback( - lambda x: self.__original.locateChild(ctx, segments)) - return self.__original.locateChild(ctx, segments) - - def renderHTTP(self, ctx): - from xcap.web.server import parsePOSTData - request = iweb.IRequest(ctx) - if request.method == "POST": - return parsePOSTData(request).addCallback(self.__reallyRender, ctx) - return self.__reallyRender(None, ctx) - - def __reallyRender(self, ignored, ctx): - # This deferred will be called when our resource is _finished_ - # writing, and will make sure we write the rest of our data - # and finish the connection. - defer.maybeDeferred(self.__original.renderHTTP, ctx).addCallback(self.__finish, ctx) - - # Sometimes the __original.renderHTTP will write() before we - # even get this far, and we don't want to return - # oldRequest.deferred if it's already been set to None. - oldRequest = iweb.IOldRequest(ctx) - if oldRequest.deferredResponse is None: - return oldRequest.response - return oldRequest.deferredResponse - - def __finish(self, data, ctx): - oldRequest = iweb.IOldRequest(ctx) - oldRequest.write(data) - oldRequest.finish() - - -class OldResourceAdapter(object): - implements(iweb.IOldNevowResource) - - def __init__(self, original): - self.original = original - - def __repr__(self): - return "<%s @ 0x%x adapting %r>" % (self.__class__.__name__, id(self), self.original) - - def locateChild(self, req, segments): - from . import server - request = iweb.IOldRequest(req) - if self.original.isLeaf: - return self, server.StopTraversal - name = segments[0] - if name == '': - res = self - else: - request.prepath.append(request.postpath.pop(0)) - res = self.original.getChildWithDefault(name, request) - request.postpath.insert(0, request.prepath.pop()) - - if isinstance(res, defer.Deferred): - return res.addCallback(lambda res: (res, segments[1:])) - - return res, segments[1:] - - def _handle_NOT_DONE_YET(self, data, request): - from twisted.web.server import NOT_DONE_YET - if data == NOT_DONE_YET: - # Return a deferred that will never fire, so the finish - # callback doesn't happen. This is because, when returning - # NOT_DONE_YET, the page is responsible for calling finish. - return defer.Deferred() - else: - return data - - def renderHTTP(self, req): - request = iweb.IOldRequest(req) - result = defer.maybeDeferred(self.original.render, request).addCallback( - self._handle_NOT_DONE_YET, request) - return result - -__all__ = [] diff --git a/xcap/web/dirlist.py b/xcap/web/dirlist.py deleted file mode 100644 index 6fb855b..0000000 --- a/xcap/web/dirlist.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - -"""Directory listing.""" - -# system imports -import os -import urllib.request, urllib.parse, urllib.error -import stat -import time - -# twisted imports -from xcap.web import iweb, resource, http, http_headers - -def formatFileSize(size): - if size < 1024: - return '%i' % size - elif size < (1024**2): - return '%iK' % (size / 1024) - elif size < (1024**3): - return '%iM' % (size / (1024**2)) - else: - return '%iG' % (size / (1024**3)) - -class DirectoryLister(resource.Resource): - def __init__(self, pathname, dirs=None, - contentTypes={}, - contentEncodings={}, - defaultType='text/html'): - self.contentTypes = contentTypes - self.contentEncodings = contentEncodings - self.defaultType = defaultType - # dirs allows usage of the File to specify what gets listed - self.dirs = dirs - self.path = pathname - resource.Resource.__init__(self) - - def data_listing(self, request, data): - if self.dirs is None: - directory = os.listdir(self.path) - directory.sort() - else: - directory = self.dirs - - files = [] - - for path in directory: - url = urllib.parse.quote(path, '/') - fullpath = os.path.join(self.path, path) - try: - st = os.stat(fullpath) - except OSError: - continue - if stat.S_ISDIR(st.st_mode): - url = url + '/' - files.append({ - 'link': url, - 'linktext': path + "/", - 'size': '', - 'type': '-', - 'lastmod': time.strftime("%Y-%b-%d %H:%M", time.localtime(st.st_mtime)) - }) - else: - from xcap.web.static import getTypeAndEncoding - mimetype, encoding = getTypeAndEncoding( - path, - self.contentTypes, self.contentEncodings, self.defaultType) - - filesize = st.st_size - files.append({ - 'link': url, - 'linktext': path, - 'size': formatFileSize(filesize), - 'type': mimetype, - 'lastmod': time.strftime("%Y-%b-%d %H:%M", time.localtime(st.st_mtime)) - }) - - return files - - def __repr__(self): - return '' % self.path - - __str__ = __repr__ - - - def render(self, request): - title = "Directory listing for %s" % urllib.parse.unquote(request.path) - - s= """%s

%s

""" % (title,title) - s+="" - s+="" - even = False - for row in self.data_listing(request, None): - s+='' % (even and 'even' or 'odd',) - s+='' % row - even = not even - - s+="
FilenameSizeLast ModifiedFile Type
%(linktext)s%(size)s%(lastmod)s%(type)s
" - response = http.Response(200, {}, s) - response.headers.setHeader("content-type", http_headers.MimeType('text', 'html')) - return response - -__all__ = ['DirectoryLister'] diff --git a/xcap/web/error.py b/xcap/web/error.py deleted file mode 100644 index 2a9b41b..0000000 --- a/xcap/web/error.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Default error output filter for xcap.web. -""" - -from xcap.web import stream, http_headers -from xcap.web.responsecode import * - -# 300 - Should include entity with choices -# 301 - -# 304 - Must include Date, ETag, Content-Location, Expires, Cache-Control, Vary. -# -# 401 - Must include WWW-Authenticate. -# 405 - Must include Allow. -# 406 - Should include entity describing allowable characteristics -# 407 - Must include Proxy-Authenticate -# 413 - May include Retry-After -# 416 - Should include Content-Range -# 503 - Should include Retry-After - -ERROR_MESSAGES = { - # 300 - # no MULTIPLE_CHOICES - MOVED_PERMANENTLY: 'The document has permanently moved here.', - FOUND: 'The document has temporarily moved here.', - SEE_OTHER: 'The results are available here.', - # no NOT_MODIFIED - USE_PROXY: "Access to this resource must be through the proxy %(location)s.", - # 306 unused - TEMPORARY_REDIRECT: 'The document has temporarily moved here.', - - # 400 - BAD_REQUEST: "Your browser sent an invalid request.", - UNAUTHORIZED: "You are not authorized to view the resource at %(uri)s. Perhaps you entered a wrong password, or perhaps your browser doesn't support authentication.", - PAYMENT_REQUIRED: "Payment Required (useful result code, this...).", - FORBIDDEN: "You don't have permission to access %(uri)s.", - NOT_FOUND: "The resource %(uri)s cannot be found.", - NOT_ALLOWED: "The requested method %(method)s is not supported by %(uri)s.", - NOT_ACCEPTABLE: "No representation of %(uri)s that is acceptable to your client could be found.", - PROXY_AUTH_REQUIRED: "You are not authorized to view the resource at %(uri)s. Perhaps you entered a wrong password, or perhaps your browser doesn't support authentication.", - REQUEST_TIMEOUT: "Server timed out waiting for your client to finish sending the HTTP request.", - CONFLICT: "Conflict (?)", - GONE: "The resource %(uri)s has been permanently removed.", - LENGTH_REQUIRED: "The resource %(uri)s requires a Content-Length header.", - PRECONDITION_FAILED: "A precondition evaluated to false.", - REQUEST_ENTITY_TOO_LARGE: "The provided request entity data is too longer than the maximum for the method %(method)s at %(uri)s.", - REQUEST_URI_TOO_LONG: "The request URL is longer than the maximum on this server.", - UNSUPPORTED_MEDIA_TYPE: "The provided request data has a format not understood by the resource at %(uri)s.", - REQUESTED_RANGE_NOT_SATISFIABLE: "None of the ranges given in the Range request header are satisfiable by the resource %(uri)s.", - EXPECTATION_FAILED: "The server does support one of the expectations given in the Expect header.", - - # 500 - INTERNAL_SERVER_ERROR: "An internal error occurred trying to process your request. Sorry.", - NOT_IMPLEMENTED: "Some functionality requested is not implemented on this server.", - BAD_GATEWAY: "An upstream server returned an invalid response.", - SERVICE_UNAVAILABLE: "This server cannot service your request becaues it is overloaded.", - GATEWAY_TIMEOUT: "An upstream server is not responding.", - HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported.", - INSUFFICIENT_STORAGE_SPACE: "There is insufficient storage space available to perform that request.", - NOT_EXTENDED: "This server does not support the a mandatory extension requested." -} - -# Is there a good place to keep this function? -def _escape(original): - if original is None: - return None - return original.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) - -def defaultErrorHandler(request, response): - if response.stream is not None: - # Already got an error message - return response - if response.code < 300: - # We only do error messages - return response - - message = ERROR_MESSAGES.get(response.code, None) - if message is None: - # No message specified for that code - return response - - message = message % { - 'uri':_escape(request.uri), - 'location':_escape(response.headers.getHeader('location')), - 'method':_escape(request.method) - } - - title = RESPONSES.get(response.code, "") - body = ("%d %s" - "

%s

%s") % ( - response.code, title, title, message) - - response.headers.setHeader("content-type", http_headers.MimeType('text', 'html')) - response.stream = stream.MemoryStream(body) - - return response -defaultErrorHandler.handleErrors = True - - -__all__ = ['defaultErrorHandler',] diff --git a/xcap/web/fileupload.py b/xcap/web/fileupload.py deleted file mode 100644 index b654a39..0000000 --- a/xcap/web/fileupload.py +++ /dev/null @@ -1,374 +0,0 @@ - - -import re -from zope.interface import implements -import urllib.request, urllib.parse, urllib.error -import tempfile - -from twisted.internet import defer -from xcap.web.stream import IStream, FileStream, BufferedStream, readStream -from xcap.web.stream import generatorToStream, readAndDiscard -from xcap.web import http_headers -from io import StringIO - -################################### -##### Multipart MIME Reader ##### -################################### - -class MimeFormatError(Exception): - pass - -# parseContentDispositionFormData is absolutely horrible, but as -# browsers don't seem to believe in sensible quoting rules, it's -# really the only way to handle the header. (Quotes can be in the -# filename, unescaped) -cd_regexp = re.compile( - ' *form-data; *name="([^"]*)"(?:; *filename="(.*)")?$', - re.IGNORECASE) - -def parseContentDispositionFormData(value): - match = cd_regexp.match(value) - if not match: - # Error parsing. - raise ValueError("Unknown content-disposition format.") - name=match.group(1) - filename=match.group(2) - return name, filename - - -#@defer.deferredGenerator -def _readHeaders(stream): - """Read the MIME headers. Assumes we've just finished reading in the - boundary string.""" - - ctype = fieldname = filename = None - headers = [] - - # Now read headers - while 1: - line = stream.readline(size=1024) - if isinstance(line, defer.Deferred): - line = defer.waitForDeferred(line) - yield line - line = line.getResult() - #print "GOT", line - if not line.endswith('\r\n'): - if line == "": - raise MimeFormatError("Unexpected end of stream.") - else: - raise MimeFormatError("Header line too long") - - line = line[:-2] # strip \r\n - if line == "": - break # End of headers - - parts = line.split(':', 1) - if len(parts) != 2: - raise MimeFormatError("Header did not have a :") - name, value = parts - name = name.lower() - headers.append((name, value)) - - if name == "content-type": - ctype = http_headers.parseContentType(http_headers.tokenize((value,), foldCase=False)) - elif name == "content-disposition": - fieldname, filename = parseContentDispositionFormData(value) - - if ctype is None: - ctype == http_headers.MimeType('application', 'octet-stream') - if fieldname is None: - raise MimeFormatError('Content-disposition invalid or omitted.') - - # End of headers, return (field name, content-type, filename) - yield fieldname, filename, ctype - return -_readHeaders = defer.deferredGenerator(_readHeaders) - - -class _BoundaryWatchingStream(object): - def __init__(self, stream, boundary): - self.stream = stream - self.boundary = boundary - self.data = '' - self.deferred = defer.Deferred() - - length = None # unknown - def read(self): - if self.stream is None: - if self.deferred is not None: - deferred = self.deferred - self.deferred = None - deferred.callback(None) - return None - newdata = self.stream.read() - if isinstance(newdata, defer.Deferred): - return newdata.addCallbacks(self._gotRead, self._gotError) - return self._gotRead(newdata) - - def _gotRead(self, newdata): - if not newdata: - raise MimeFormatError("Unexpected EOF") - # BLECH, converting buffer back into string. - self.data += str(newdata) - data = self.data - boundary = self.boundary - off = data.find(boundary) - - if off == -1: - # No full boundary, check for the first character - off = data.rfind(boundary[0], max(0, len(data)-len(boundary))) - if off != -1: - # We could have a partial boundary, store it for next time - self.data = data[off:] - return data[:off] - else: - self.data = '' - return data - else: - self.stream.pushback(data[off+len(boundary):]) - self.stream = None - return data[:off] - - def _gotError(self, err): - # Propogate error back to MultipartMimeStream also - if self.deferred is not None: - deferred = self.deferred - self.deferred = None - deferred.errback(err) - return err - - def close(self): - # Assume error will be raised again and handled by MMS? - readAndDiscard(self).addErrback(lambda _: None) - -class MultipartMimeStream(object): - implements(IStream) - def __init__(self, stream, boundary): - self.stream = BufferedStream(stream) - self.boundary = "--"+boundary - self.first = True - - def read(self): - """ - Return a deferred which will fire with a tuple of: - (fieldname, filename, ctype, dataStream) - or None when all done. - - Format errors will be sent to the errback. - - Returns None when all done. - - IMPORTANT: you *must* exhaust dataStream returned by this call - before calling .read() again! - """ - if self.first: - self.first = False - d = self._readFirstBoundary() - else: - d = self._readBoundaryLine() - d.addCallback(self._doReadHeaders) - d.addCallback(self._gotHeaders) - return d - - def _readFirstBoundary(self): - #print "_readFirstBoundary" - line = self.stream.readline(size=1024) - if isinstance(line, defer.Deferred): - line = defer.waitForDeferred(line) - yield line - line = line.getResult() - if line != self.boundary + '\r\n': - raise MimeFormatError("Extra data before first boundary: %r looking for: %r" % (line, self.boundary + '\r\n')) - - self.boundary = "\r\n"+self.boundary - yield True - return - _readFirstBoundary = defer.deferredGenerator(_readFirstBoundary) - - def _readBoundaryLine(self): - #print "_readBoundaryLine" - line = self.stream.readline(size=1024) - if isinstance(line, defer.Deferred): - line = defer.waitForDeferred(line) - yield line - line = line.getResult() - - if line == "--\r\n": - # THE END! - yield False - return - elif line != "\r\n": - raise MimeFormatError("Unexpected data on same line as boundary: %r" % (line,)) - yield True - return - _readBoundaryLine = defer.deferredGenerator(_readBoundaryLine) - - def _doReadHeaders(self, morefields): - #print "_doReadHeaders", morefields - if not morefields: - return None - return _readHeaders(self.stream) - - def _gotHeaders(self, headers): - if headers is None: - return None - bws = _BoundaryWatchingStream(self.stream, self.boundary) - self.deferred = bws.deferred - ret=list(headers) - ret.append(bws) - return tuple(ret) - - -def readIntoFile(stream, outFile, maxlen): - """Read the stream into a file, but not if it's longer than maxlen. - Returns Deferred which will be triggered on finish. - """ - curlen = [0] - def done(_): - return _ - def write(data): - curlen[0] += len(data) - if curlen[0] > maxlen: - raise MimeFormatError("Maximum length of %d bytes exceeded." % - maxlen) - - outFile.write(data) - return readStream(stream, write).addBoth(done) - -#@defer.deferredGenerator -def parseMultipartFormData(stream, boundary, - maxMem=100*1024, maxFields=1024, maxSize=10*1024*1024): - # If the stream length is known to be too large upfront, abort immediately - - if stream.length is not None and stream.length > maxSize: - raise MimeFormatError("Maximum length of %d bytes exceeded." % - maxSize) - - mms = MultipartMimeStream(stream, boundary) - numFields = 0 - args = {} - files = {} - - while 1: - datas = mms.read() - if isinstance(datas, defer.Deferred): - datas = defer.waitForDeferred(datas) - yield datas - datas = datas.getResult() - if datas is None: - break - - numFields+=1 - if numFields == maxFields: - raise MimeFormatError("Maximum number of fields %d exceeded"%maxFields) - - # Parse data - fieldname, filename, ctype, stream = datas - if filename is None: - # Not a file - outfile = StringIO() - maxBuf = min(maxSize, maxMem) - else: - outfile = tempfile.NamedTemporaryFile() - maxBuf = maxSize - x = readIntoFile(stream, outfile, maxBuf) - if isinstance(x, defer.Deferred): - x = defer.waitForDeferred(x) - yield x - x = x.getResult() - if filename is None: - # Is a normal form field - outfile.seek(0) - data = outfile.read() - args.setdefault(fieldname, []).append(data) - maxMem -= len(data) - maxSize -= len(data) - else: - # Is a file upload - maxSize -= outfile.tell() - outfile.seek(0) - files.setdefault(fieldname, []).append((filename, ctype, outfile)) - - - yield args, files - return -parseMultipartFormData = defer.deferredGenerator(parseMultipartFormData) - -################################### -##### x-www-urlencoded reader ##### -################################### - - -def parse_urlencoded_stream(input, maxMem=100*1024, - keep_blank_values=False, strict_parsing=False): - lastdata = '' - still_going=1 - - while still_going: - try: - yield input.wait - data = next(input) - except StopIteration: - pairs = [lastdata] - still_going=0 - else: - maxMem -= len(data) - if maxMem < 0: - raise MimeFormatError("Maximum length of %d bytes exceeded." % - maxMem) - pairs = str(data).split('&') - pairs[0] = lastdata + pairs[0] - lastdata=pairs.pop() - - for name_value in pairs: - nv = name_value.split('=', 1) - if len(nv) != 2: - if strict_parsing: - raise MimeFormatError("bad query field: %s") % repr(name_value) - continue - if len(nv[1]) or keep_blank_values: - name = urllib.parse.unquote(nv[0].replace('+', ' ')) - value = urllib.parse.unquote(nv[1].replace('+', ' ')) - yield name, value -parse_urlencoded_stream = generatorToStream(parse_urlencoded_stream) - -def parse_urlencoded(stream, maxMem=100*1024, maxFields=1024, - keep_blank_values=False, strict_parsing=False): - d = {} - numFields = 0 - - s=parse_urlencoded_stream(stream, maxMem, keep_blank_values, strict_parsing) - - while 1: - datas = s.read() - if isinstance(datas, defer.Deferred): - datas = defer.waitForDeferred(datas) - yield datas - datas = datas.getResult() - if datas is None: - break - name, value = datas - - numFields += 1 - if numFields == maxFields: - raise MimeFormatError("Maximum number of fields %d exceeded"%maxFields) - - if name in d: - d[name].append(value) - else: - d[name] = [value] - yield d - return -parse_urlencoded = defer.deferredGenerator(parse_urlencoded) - - -if __name__ == '__main__': - d = parseMultipartFormData( - FileStream(open("upload.txt")), "----------0xKhTmLbOuNdArY") - from twisted.python import log - d.addErrback(log.err) - def pr(s): - print(s) - d.addCallback(pr) - -__all__ = ['parseMultipartFormData', 'parse_urlencoded', 'parse_urlencoded_stream', 'MultipartMimeStream', 'MimeFormatError'] diff --git a/xcap/web/filter/__init__.py b/xcap/web/filter/__init__.py deleted file mode 100644 index b13fbdd..0000000 --- a/xcap/web/filter/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Output filters. -""" diff --git a/xcap/web/filter/gzip.py b/xcap/web/filter/gzip.py deleted file mode 100644 index 9a93def..0000000 --- a/xcap/web/filter/gzip.py +++ /dev/null @@ -1,79 +0,0 @@ - -import struct -import zlib -from xcap.web import stream - -# TODO: ungzip (can any browsers actually generate gzipped -# upload data?) But it's necessary for client anyways. - -def gzipStream(input, compressLevel=6): - crc, size = zlib.crc32(''), 0 - # magic header, compression method, no flags - header = '\037\213\010\000' - # timestamp - header += struct.pack('= size: - end = size - 1 - - if start >= size: - raise UnsatisfiableRangeRequest - - return start,end - -def makeUnsatisfiable(request, oldresponse): - if request.headers.hasHeader('if-range'): - return oldresponse # Return resource instead of error - response = http.Response(responsecode.REQUESTED_RANGE_NOT_SATISFIABLE) - response.headers.setHeader("content-range", ('bytes', None, None, oldresponse.stream.length)) - return response - -def makeSegment(inputStream, lastOffset, start, end): - offset = start - lastOffset - length = end + 1 - start - - if offset != 0: - before, inputStream = inputStream.split(offset) - before.close() - return inputStream.split(length) - -def rangefilter(request, oldresponse): - if oldresponse.stream is None: - return oldresponse - size = oldresponse.stream.length - if size is None: - # Does not deal with indeterminate length outputs - return oldresponse - - oldresponse.headers.setHeader('accept-ranges',('bytes',)) - - rangespec = request.headers.getHeader('range') - - # If we've got a range header and the If-Range header check passes, and - # the range type is bytes, do a partial response. - if (rangespec is not None and http.checkIfRange(request, oldresponse) and - rangespec[0] == 'bytes'): - # If it's a single range, return a simple response - if len(rangespec[1]) == 1: - try: - start,end = canonicalizeRange(rangespec[1][0], size) - except UnsatisfiableRangeRequest: - return makeUnsatisfiable(request, oldresponse) - - response = http.Response(responsecode.PARTIAL_CONTENT, oldresponse.headers) - response.headers.setHeader('content-range',('bytes',start, end, size)) - - content, after = makeSegment(oldresponse.stream, 0, start, end) - after.close() - response.stream = content - return response - else: - # Return a multipart/byteranges response - lastOffset = -1 - offsetList = [] - for arange in rangespec[1]: - try: - start,end = canonicalizeRange(arange, size) - except UnsatisfiableRangeRequest: - continue - if start <= lastOffset: - # Stupid client asking for out-of-order or overlapping ranges, PUNT! - return oldresponse - offsetList.append((start,end)) - lastOffset = end - - if not offsetList: - return makeUnsatisfiable(request, oldresponse) - - content_type = oldresponse.headers.getRawHeaders('content-type') - boundary = "%x%x" % (int(time.time()*1000000), os.getpid()) - response = http.Response(responsecode.PARTIAL_CONTENT, oldresponse.headers) - - response.headers.setHeader('content-type', - http_headers.MimeType('multipart', 'byteranges', - [('boundary', boundary)])) - response.stream = out = stream.CompoundStream() - - - lastOffset = 0 - origStream = oldresponse.stream - - headerString = "\r\n--%s" % boundary - if len(content_type) == 1: - headerString+='\r\nContent-Type: %s' % content_type[0] - headerString+="\r\nContent-Range: %s\r\n\r\n" - - for start,end in offsetList: - out.addStream(headerString % - http_headers.generateContentRange(('bytes', start, end, size))) - - content, origStream = makeSegment(origStream, lastOffset, start, end) - lastOffset = end + 1 - out.addStream(content) - origStream.close() - out.addStream("\r\n--%s--\r\n" % boundary) - return response - else: - return oldresponse - - -__all__ = ['rangefilter'] diff --git a/xcap/web/http.py b/xcap/web/http.py deleted file mode 100644 index c2c5412..0000000 --- a/xcap/web/http.py +++ /dev/null @@ -1,473 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - -"""HyperText Transfer Protocol implementation. - -The second coming. - -Maintainer: U{James Y Knight } - -""" -# import traceback; log.msg(''.join(traceback.format_stack())) - -# system imports -import socket -import time -import cgi - -# twisted imports -from twisted.internet import interfaces, error -from twisted.python import log, components -from zope.interface import implements - -# sibling imports -from xcap.web import responsecode -from xcap.web import http_headers -from xcap.web import iweb -from xcap.web import stream -from xcap.web.stream import IByteStream - -defaultPortForScheme = {'http': 80, 'https':443, 'ftp':21} - -def splitHostPort(scheme, hostport): - """Split the host in "host:port" format into host and port fields. - If port was not specified, use the default for the given scheme, if - known. Returns a tuple of (hostname, portnumber).""" - - # Split hostport into host and port - hostport = hostport.split(':', 1) - try: - if len(hostport) == 2: - return hostport[0], int(hostport[1]) - except ValueError: - pass - return hostport[0], defaultPortForScheme.get(scheme, 0) - - -def parseVersion(strversion): - """Parse version strings of the form Protocol '/' Major '.' Minor. E.g. 'HTTP/1.1'. - Returns (protocol, major, minor). - Will raise ValueError on bad syntax.""" - - proto, strversion = strversion.split('/') - major, minor = strversion.split('.') - major, minor = int(major), int(minor) - if major < 0 or minor < 0: - raise ValueError("negative number") - return (proto.lower(), major, minor) - - -class HTTPError(Exception): - def __init__(self, codeOrResponse): - """An Exception for propagating HTTP Error Responses. - - @param codeOrResponse: The numeric HTTP code or a complete http.Response - object. - @type codeOrResponse: C{int} or L{http.Response} - """ - Exception.__init__(self) - self.response = iweb.IResponse(codeOrResponse) - - def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.response) - - -class Response(object): - """An object representing an HTTP Response to be sent to the client. - """ - implements(iweb.IResponse) - - code = responsecode.OK - headers = None - stream = None - - def __init__(self, code=None, headers=None, stream=None): - """ - @param code: The HTTP status code for this Response - @type code: C{int} - - @param headers: Headers to be sent to the client. - @type headers: C{dict}, L{xcap.web.http_headers.Headers}, or - C{None} - - @param stream: Content body to send to the HTTP client - @type stream: L{xcap.web.stream.IByteStream} - """ - - if code is not None: - self.code = int(code) - - if headers is not None: - if isinstance(headers, dict): - headers = http_headers.Headers(headers) - self.headers=headers - else: - self.headers = http_headers.Headers() - - if stream is not None: - self.stream = IByteStream(stream) - - def __repr__(self): - if self.stream is None: - streamlen = None - else: - streamlen = self.stream.length - - return "<%s.%s code=%d, streamlen=%s>" % (self.__module__, self.__class__.__name__, self.code, streamlen) - - -class StatusResponse (Response): - """ - A L{Response} object which simply contains a status code and a description of - what happened. - """ - def __init__(self, code, description, title=None): - """ - @param code: a response code in L{responsecode.RESPONSES}. - @param description: a string description. - @param title: the message title. If not specified or C{None}, defaults - to C{responsecode.RESPONSES[code]}. - """ - if title is None: - title = cgi.escape(responsecode.RESPONSES[code]) - - output = "".join(( - "", - "", - "%s" % (title,), - "", - "", - "

%s

" % (title,), - "

%s

" % (cgi.escape(description),), - "", - "", - )) - - if type(output) == str: - output = output.encode("utf-8") - mime_params = {"charset": "utf-8"} - else: - mime_params = {} - - super(StatusResponse, self).__init__(code=code, stream=output) - - self.headers.setHeader("content-type", http_headers.MimeType("text", "html", mime_params)) - - self.description = description - - def __repr__(self): - return "<%s %s %s>" % (self.__class__.__name__, self.code, self.description) - - -class RedirectResponse (StatusResponse): - """ - A L{Response} object that contains a redirect to another network location. - """ - def __init__(self, location): - """ - @param location: the URI to redirect to. - """ - super(RedirectResponse, self).__init__( - responsecode.MOVED_PERMANENTLY, - "Document moved to %s." % (location,) - ) - - self.headers.setHeader("location", location) - - -def NotModifiedResponse(oldResponse=None): - if oldResponse is not None: - headers=http_headers.Headers() - for header in ( - # Required from sec 10.3.5: - 'date', 'etag', 'content-location', 'expires', - 'cache-control', 'vary', - # Others: - 'server', 'proxy-authenticate', 'www-authenticate', 'warning'): - value = oldResponse.headers.getRawHeaders(header) - if value is not None: - headers.setRawHeaders(header, value) - else: - headers = None - return Response(code=responsecode.NOT_MODIFIED, headers=headers) - - -def checkPreconditions(request, response=None, entityExists=True, etag=None, lastModified=None): - """Check to see if this request passes the conditional checks specified - by the client. May raise an HTTPError with result codes L{NOT_MODIFIED} - or L{PRECONDITION_FAILED}, as appropriate. - - This function is called automatically as an output filter for GET and - HEAD requests. With GET/HEAD, it is not important for the precondition - check to occur before doing the action, as the method is non-destructive. - - However, if you are implementing other request methods, like PUT - for your resource, you will need to call this after determining - the etag and last-modified time of the existing resource but - before actually doing the requested action. In that case, - - This examines the appropriate request headers for conditionals, - (If-Modified-Since, If-Unmodified-Since, If-Match, If-None-Match, - or If-Range), compares with the etag and last and - and then sets the response code as necessary. - - @param response: This should be provided for GET/HEAD methods. If - it is specified, the etag and lastModified arguments will - be retrieved automatically from the response headers and - shouldn't be separately specified. Not providing the - response with a GET request may cause the emitted - "Not Modified" responses to be non-conformant. - - @param entityExists: Set to False if the entity in question doesn't - yet exist. Necessary for PUT support with 'If-None-Match: *'. - - @param etag: The etag of the resource to check against, or None. - - @param lastModified: The last modified date of the resource to check - against, or None. - - @raise: HTTPError: Raised when the preconditions fail, in order to - abort processing and emit an error page. - - """ - if response: - assert etag is None and lastModified is None - # if the code is some sort of error code, don't do anything - if not ((response.code >= 200 and response.code <= 299) - or response.code == responsecode.PRECONDITION_FAILED): - return False - etag = response.headers.getHeader("etag") - lastModified = response.headers.getHeader("last-modified") - - def matchETag(tags, allowWeak): - if entityExists and '*' in tags: - return True - if etag is None: - return False - return ((allowWeak or not etag.weak) and - ([etagmatch for etagmatch in tags if etag.match(etagmatch, strongCompare=not allowWeak)])) - - # First check if-match/if-unmodified-since - # If either one fails, we return PRECONDITION_FAILED - match = request.headers.getHeader("if-match") - if match: - if not matchETag(match, False): - raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "Requested resource does not have a matching ETag.")) - - unmod_since = request.headers.getHeader("if-unmodified-since") - if unmod_since: - if not lastModified or lastModified > unmod_since: - raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "Requested resource has changed.")) - - # Now check if-none-match/if-modified-since. - # This bit is tricky, because of the requirements when both IMS and INM - # are present. In that case, you can't return a failure code - # unless *both* checks think it failed. - # Also, if the INM check succeeds, ignore IMS, because INM is treated - # as more reliable. - - # I hope I got the logic right here...the RFC is quite poorly written - # in this area. Someone might want to verify the testcase against - # RFC wording. - - # If IMS header is later than current time, ignore it. - notModified = None - ims = request.headers.getHeader('if-modified-since') - if ims: - notModified = (ims < time.time() and lastModified and lastModified <= ims) - - inm = request.headers.getHeader("if-none-match") - if inm: - if request.method in ("HEAD", "GET"): - # If it's a range request, don't allow a weak ETag, as that - # would break. - canBeWeak = not request.headers.hasHeader('Range') - if notModified != False and matchETag(inm, canBeWeak): - raise HTTPError(NotModifiedResponse(response)) - else: - if notModified != False and matchETag(inm, False): - raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "Requested resource has a matching ETag.")) - else: - if notModified == True: - if request.method in ("HEAD", "GET"): - raise HTTPError(NotModifiedResponse(response)) - else: - # S14.25 doesn't actually say what to do for a failing IMS on - # non-GET methods. But Precondition Failed makes sense to me. - raise HTTPError(StatusResponse(responsecode.PRECONDITION_FAILED, "Requested resource has not changed.")) - -def checkIfRange(request, response): - """Checks for the If-Range header, and if it exists, checks if the - test passes. Returns true if the server should return partial data.""" - - ifrange = request.headers.getHeader("if-range") - - if ifrange is None: - return True - if isinstance(ifrange, http_headers.ETag): - return ifrange.match(response.headers.getHeader("etag"), strongCompare=True) - else: - return ifrange == response.headers.getHeader("last-modified") - - -class _NotifyingProducerStream(stream.ProducerStream): - doStartReading = None - - def __init__(self, length=None, doStartReading=None): - stream.ProducerStream.__init__(self, length=length) - self.doStartReading = doStartReading - - def read(self): - if self.doStartReading is not None: - doStartReading = self.doStartReading - self.doStartReading = None - doStartReading() - - return stream.ProducerStream.read(self) - - def write(self, data): - self.doStartReading = None - stream.ProducerStream.write(self, data) - - def finish(self): - self.doStartReading = None - stream.ProducerStream.finish(self) - - -# response codes that must have empty bodies -NO_BODY_CODES = (responsecode.NO_CONTENT, responsecode.NOT_MODIFIED) - -class Request(object): - """A HTTP request. - - Subclasses should override the process() method to determine how - the request will be processed. - - @ivar method: The HTTP method that was used. - @ivar uri: The full URI that was requested (includes arguments). - @ivar headers: All received headers - @ivar clientproto: client HTTP version - @ivar stream: incoming data stream. - """ - - implements(iweb.IRequest, interfaces.IConsumer) - - known_expects = ('100-continue',) - - def __init__(self, chanRequest, command, path, version, contentLength, headers): - """ - @param chanRequest: the channel request we're associated with. - """ - self.chanRequest = chanRequest - self.method = command - self.uri = path - self.clientproto = version - - self.headers = headers - - if '100-continue' in self.headers.getHeader('expect', ()): - doStartReading = self._sendContinue - else: - doStartReading = None - self.stream = _NotifyingProducerStream(contentLength, doStartReading) - self.stream.registerProducer(self.chanRequest, True) - - def checkExpect(self): - """Ensure there are no expectations that cannot be met. - Checks Expect header against self.known_expects.""" - expects = self.headers.getHeader('expect', ()) - for expect in expects: - if expect not in self.known_expects: - raise HTTPError(responsecode.EXPECTATION_FAILED) - - def process(self): - """Called by channel to let you process the request. - - Can be overridden by a subclass to do something useful.""" - pass - - def handleContentChunk(self, data): - """Callback from channel when a piece of data has been received. - Puts the data in .stream""" - self.stream.write(data) - - def handleContentComplete(self): - """Callback from channel when all data has been received. """ - self.stream.unregisterProducer() - self.stream.finish() - - def connectionLost(self, reason): - """connection was lost""" - pass - - def __repr__(self): - return '<%s %s %s>'% (self.method, self.uri, self.clientproto) - - def _sendContinue(self): - self.chanRequest.writeIntermediateResponse(responsecode.CONTINUE) - - def _finished(self, x): - """We are finished writing data.""" - self.chanRequest.finish() - - def _error(self, reason): - if reason.check(error.ConnectionLost): - log.msg("Request error: " + reason.getErrorMessage()) - else: - log.err(reason) - # Only bother with cleanup on errors other than lost connection. - self.chanRequest.abortConnection() - - def writeResponse(self, response): - """ - Write a response. - """ - if self.stream.doStartReading is not None: - # Expect: 100-continue was requested, but 100 response has not been - # sent, and there's a possibility that data is still waiting to be - # sent. - # - # Ideally this means the remote side will not send any data. - # However, because of compatibility requirements, it might timeout, - # and decide to do so anyways at the same time we're sending back - # this response. Thus, the read state is unknown after this. - # We must close the connection. - self.chanRequest.channel.setReadPersistent(False) - # Nothing more will be read - self.chanRequest.allContentReceived() - - if response.code != responsecode.NOT_MODIFIED: - # Not modified response is *special* and doesn't get a content-length. - if response.stream is None: - response.headers.setHeader('content-length', 0) - elif response.stream.length is not None: - response.headers.setHeader('content-length', response.stream.length) - self.chanRequest.writeHeaders(response.code, response.headers) - - # if this is a "HEAD" request, or a special response code, - # don't return any data. - if self.method == "HEAD" or response.code in NO_BODY_CODES: - if response.stream is not None: - response.stream.close() - self._finished(None) - return - - d = stream.StreamProducer(response.stream).beginProducing(self.chanRequest) - d.addCallback(self._finished).addErrback(self._error) - - -from xcap.web import compat -components.registerAdapter(compat.makeOldRequestAdapter, iweb.IRequest, iweb.IOldRequest) -components.registerAdapter(compat.OldNevowResourceAdapter, iweb.IOldNevowResource, iweb.IResource) -components.registerAdapter(Response, int, iweb.IResponse) - -try: - # If twisted.web is installed, add an adapter for it - from twisted.web import resource -except: - pass -else: - components.registerAdapter(compat.OldResourceAdapter, resource.IResource, iweb.IOldNevowResource) - -__all__ = ['HTTPError', 'NotModifiedResponse', 'Request', 'Response', 'checkIfRange', 'checkPreconditions', 'defaultPortForScheme', 'parseVersion', 'splitHostPort'] - diff --git a/xcap/web/http_headers.py b/xcap/web/http_headers.py deleted file mode 100644 index 17b12f7..0000000 --- a/xcap/web/http_headers.py +++ /dev/null @@ -1,1539 +0,0 @@ - - -import types, time -from calendar import timegm -import base64 -import re - -def dashCapitalize(s): - ''' Capitalize a string, making sure to treat - as a word seperator ''' - return '-'.join([ x.capitalize() for x in s.split('-')]) - -# datetime parsing and formatting -weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] -weekdayname_lower = [name.lower() for name in weekdayname] -monthname = [None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] -monthname_lower = [name and name.lower() for name in monthname] - -# HTTP Header parsing API - -header_case_mapping = {} - -def casemappingify(d): - global header_case_mapping - newd = dict([(key.lower(),key) for key in list(d.keys())]) - header_case_mapping.update(newd) - -def lowerify(d): - return dict([(key.lower(),value) for key,value in list(d.items())]) - - -class HeaderHandler(object): - """HeaderHandler manages header generating and parsing functions. - """ - HTTPParsers = {} - HTTPGenerators = {} - - def __init__(self, parsers=None, generators=None): - """ - @param parsers: A map of header names to parsing functions. - @type parsers: L{dict} - - @param generators: A map of header names to generating functions. - @type generators: L{dict} - """ - - if parsers: - self.HTTPParsers.update(parsers) - if generators: - self.HTTPGenerators.update(generators) - - def parse(self, name, header): - """ - Parse the given header based on its given name. - - @param name: The header name to parse. - @type name: C{str} - - @param header: A list of unparsed headers. - @type header: C{list} of C{str} - - @return: The return value is the parsed header representation, - it is dependent on the header. See the HTTP Headers document. - """ - parser = self.HTTPParsers.get(name, None) - if parser is None: - raise ValueError("No header parser for header '%s', either add one or use getHeaderRaw." % (name,)) - - try: - for p in parser: - # print "Parsing %s: %s(%s)" % (name, repr(p), repr(h)) - header = p(header) - # if isinstance(h, types.GeneratorType): - # h=list(h) - except ValueError as v: - # print v - header=None - - return header - - def generate(self, name, header): - """ - Generate the given header based on its given name. - - @param name: The header name to generate. - @type name: C{str} - - @param header: A parsed header, such as the output of - L{HeaderHandler}.parse. - - @return: C{list} of C{str} each representing a generated HTTP header. - """ - generator = self.HTTPGenerators.get(name, None) - - if generator is None: - # print self.generators - raise ValueError("No header generator for header '%s', either add one or use setHeaderRaw." % (name,)) - - for g in generator: - header = g(header) - - #self._raw_headers[name] = h - return header - - def updateParsers(self, parsers): - """Update en masse the parser maps. - - @param parsers: Map of header names to parser chains. - @type parsers: C{dict} - """ - casemappingify(parsers) - self.HTTPParsers.update(lowerify(parsers)) - - def addParser(self, name, value): - """Add an individual parser chain for the given header. - - @param name: Name of the header to add - @type name: C{str} - - @param value: The parser chain - @type value: C{str} - """ - self.updateParsers({name: value}) - - def updateGenerators(self, generators): - """Update en masse the generator maps. - - @param parsers: Map of header names to generator chains. - @type parsers: C{dict} - """ - casemappingify(generators) - self.HTTPGenerators.update(lowerify(generators)) - - def addGenerators(self, name, value): - """Add an individual generator chain for the given header. - - @param name: Name of the header to add - @type name: C{str} - - @param value: The generator chain - @type value: C{str} - """ - self.updateGenerators({name: value}) - - def update(self, parsers, generators): - """Conveniently update parsers and generators all at once. - """ - self.updateParsers(parsers) - self.updateGenerators(generators) - - -DefaultHTTPHandler = HeaderHandler() - - -## HTTP DateTime parser -def parseDateTime(dateString): - """Convert an HTTP date string (one of three formats) to seconds since epoch.""" - parts = dateString.split() - - if not parts[0][0:3].lower() in weekdayname_lower: - # Weekday is stupid. Might have been omitted. - try: - return parseDateTime("Sun, "+dateString) - except ValueError: - # Guess not. - pass - - partlen = len(parts) - if (partlen == 5 or partlen == 6) and parts[1].isdigit(): - # 1st date format: Sun, 06 Nov 1994 08:49:37 GMT - # (Note: "GMT" is literal, not a variable timezone) - # (also handles without "GMT") - # This is the normal format - day = parts[1] - month = parts[2] - year = parts[3] - time = parts[4] - elif (partlen == 3 or partlen == 4) and parts[1].find('-') != -1: - # 2nd date format: Sunday, 06-Nov-94 08:49:37 GMT - # (Note: "GMT" is literal, not a variable timezone) - # (also handles without without "GMT") - # Two digit year, yucko. - day, month, year = parts[1].split('-') - time = parts[2] - year=int(year) - if year < 69: - year = year + 2000 - elif year < 100: - year = year + 1900 - elif len(parts) == 5: - # 3rd date format: Sun Nov 6 08:49:37 1994 - # ANSI C asctime() format. - day = parts[2] - month = parts[1] - year = parts[4] - time = parts[3] - else: - raise ValueError("Unknown datetime format %r" % dateString) - - day = int(day) - month = int(monthname_lower.index(month.lower())) - year = int(year) - hour, min, sec = list(map(int, time.split(':'))) - return int(timegm((year, month, day, hour, min, sec))) - - -##### HTTP tokenizer -class Token(str): - __slots__=[] - tokens = {} - def __new__(self, char): - token = Token.tokens.get(char) - if token is None: - Token.tokens[char] = token = str.__new__(self, char) - return token - - def __repr__(self): - return "Token(%s)" % str.__repr__(self) - - -http_tokens = " \t\"()<>@,;:\\/[]?={}" -http_ctls = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f" - -def tokenize(header, foldCase=True): - """Tokenize a string according to normal HTTP header parsing rules. - - In particular: - - Whitespace is irrelevant and eaten next to special separator tokens. - Its existance (but not amount) is important between character strings. - - Quoted string support including embedded backslashes. - - Case is insignificant (and thus lowercased), except in quoted strings. - (unless foldCase=False) - - Multiple headers are concatenated with ',' - - NOTE: not all headers can be parsed with this function. - - Takes a raw header value (list of strings), and - Returns a generator of strings and Token class instances. - """ - tokens=http_tokens - ctls=http_ctls - - string = ",".join(header) - list = [] - start = 0 - cur = 0 - quoted = False - qpair = False - inSpaces = -1 - qstring = None - - for x in string: - if quoted: - if qpair: - qpair = False - qstring = qstring+string[start:cur-1]+x - start = cur+1 - elif x == '\\': - qpair = True - elif x == '"': - quoted = False - yield qstring+string[start:cur] - qstring=None - start = cur+1 - elif x in tokens: - if start != cur: - if foldCase: - yield string[start:cur].lower() - else: - yield string[start:cur] - - start = cur+1 - if x == '"': - quoted = True - qstring = "" - inSpaces = False - elif x in " \t": - if inSpaces is False: - inSpaces = True - else: - inSpaces = -1 - yield Token(x) - elif x in ctls: - raise ValueError("Invalid control character: %d in header" % ord(x)) - else: - if inSpaces is True: - yield Token(' ') - inSpaces = False - - inSpaces = False - cur = cur+1 - - if qpair: - raise ValueError("Missing character after '\\'") - if quoted: - raise ValueError("Missing end quote") - - if start != cur: - if foldCase: - yield string[start:cur].lower() - else: - yield string[start:cur] - -def split(seq, delim): - """The same as str.split but works on arbitrary sequences. - Too bad it's not builtin to python!""" - - cur = [] - for item in seq: - if item == delim: - yield cur - cur = [] - else: - cur.append(item) - yield cur - -# def find(seq, *args): -# """The same as seq.index but returns -1 if not found, instead -# Too bad it's not builtin to python!""" -# try: -# return seq.index(value, *args) -# except ValueError: -# return -1 - - -def filterTokens(seq): - """Filter out instances of Token, leaving only a list of strings. - - Used instead of a more specific parsing method (e.g. splitting on commas) - when only strings are expected, so as to be a little lenient. - - Apache does it this way and has some comments about broken clients which - forget commas (?), so I'm doing it the same way. It shouldn't - hurt anything, in any case. - """ - - l=[] - for x in seq: - if not isinstance(x, Token): - l.append(x) - return l - -##### parser utilities: -def checkSingleToken(tokens): - if len(tokens) != 1: - raise ValueError("Expected single token, not %s." % (tokens,)) - return tokens[0] - -def parseKeyValue(val): - if len(val) == 1: - return val[0],None - elif len(val) == 3 and val[1] == Token('='): - return val[0],val[2] - raise ValueError("Expected key or key=value, but got %s." % (val,)) - -def parseArgs(field): - args=split(field, Token(';')) - val = next(args) - args = [parseKeyValue(arg) for arg in args] - return val,args - -def listParser(fun): - """Return a function which applies 'fun' to every element in the - comma-separated list""" - def listParserHelper(tokens): - fields = split(tokens, Token(',')) - for field in fields: - if len(field) != 0: - yield fun(field) - - return listParserHelper - -def last(seq): - """Return seq[-1]""" - - return seq[-1] - -##### Generation utilities -def quoteString(s): - return '"%s"' % s.replace('\\', '\\\\').replace('"', '\\"') - -def listGenerator(fun): - """Return a function which applies 'fun' to every element in - the given list, then joins the result with generateList""" - def listGeneratorHelper(l): - return generateList([fun(e) for e in l]) - - return listGeneratorHelper - -def generateList(seq): - return ", ".join(seq) - -def singleHeader(item): - return [item] - -def generateKeyValues(kvs): - l = [] - # print kvs - for k,v in kvs: - if v is None: - l.append('%s' % k) - else: - l.append('%s=%s' % (k,v)) - return ";".join(l) - - -class MimeType(object): - def fromString(klass, mimeTypeString): - """Generate a MimeType object from the given string. - - @param mimeTypeString: The mimetype to parse - - @return: L{MimeType} - """ - return DefaultHTTPHandler.parse('content-type', [mimeTypeString]) - - fromString = classmethod(fromString) - - def __init__(self, mediaType, mediaSubtype, params={}, **kwargs): - """ - @type mediaType: C{str} - - @type mediaSubtype: C{str} - - @type params: C{dict} - """ - self.mediaType = mediaType - self.mediaSubtype = mediaSubtype - self.params = dict(params) - - if kwargs: - self.params.update(kwargs) - - def __eq__(self, other): - if not isinstance(other, MimeType): return NotImplemented - return (self.mediaType == other.mediaType and - self.mediaSubtype == other.mediaSubtype and - self.params == other.params) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "MimeType(%r, %r, %r)" % (self.mediaType, self.mediaSubtype, self.params) - - def __hash__(self): - return hash(self.mediaType)^hash(self.mediaSubtype)^hash(tuple(self.params.items())) - -##### Specific header parsers. -def parseAccept(field): - type,args = parseArgs(field) - - if len(type) != 3 or type[1] != Token('/'): - raise ValueError("MIME Type "+str(type)+" invalid.") - - # okay, this spec is screwy. A 'q' parameter is used as the separator - # between MIME parameters and (as yet undefined) additional HTTP - # parameters. - - num = 0 - for arg in args: - if arg[0] == 'q': - mimeparams=tuple(args[0:num]) - params=args[num:] - break - num = num + 1 - else: - mimeparams=tuple(args) - params=[] - - # Default values for parameters: - qval = 1.0 - - # Parse accept parameters: - for param in params: - if param[0] =='q': - qval = float(param[1]) - else: - # Warn? ignored parameter. - pass - - ret = MimeType(type[0],type[2],mimeparams),qval - return ret - -def parseAcceptQvalue(field): - type,args=parseArgs(field) - - type = checkSingleToken(type) - - qvalue = 1.0 # Default qvalue is 1 - for arg in args: - if arg[0] == 'q': - qvalue = float(arg[1]) - return type,qvalue - -def addDefaultCharset(charsets): - if charsets.get('*') is None and charsets.get('iso-8859-1') is None: - charsets['iso-8859-1'] = 1.0 - return charsets - -def addDefaultEncoding(encodings): - if encodings.get('*') is None and encodings.get('identity') is None: - # RFC doesn't specify a default value for identity, only that it - # "is acceptable" if not mentioned. Thus, give it a very low qvalue. - encodings['identity'] = .0001 - return encodings - - -def parseContentType(header): - # Case folding is disabled for this header, because of use of - # Content-Type: multipart/form-data; boundary=CaSeFuLsTuFf - # So, we need to explicitly .lower() the type/subtype and arg keys. - - type,args = parseArgs(header) - - if len(type) != 3 or type[1] != Token('/'): - raise ValueError("MIME Type "+str(type)+" invalid.") - - args = [(kv[0].lower(), kv[1]) for kv in args] - - return MimeType(type[0].lower(), type[2].lower(), tuple(args)) - -def parseContentMD5(header): - try: - return base64.decodestring(header) - except Exception as e: - raise ValueError(e) - -def parseContentRange(header): - """Parse a content-range header into (kind, start, end, realLength). - - realLength might be None if real length is not known ('*'). - start and end might be None if start,end unspecified (for response code 416) - """ - kind, other = header.strip().split() - if kind.lower() != "bytes": - raise ValueError("a range of type %r is not supported") - startend, realLength = other.split("/") - if startend.strip() == '*': - start,end=None,None - else: - start, end = list(map(int, startend.split("-"))) - if realLength == "*": - realLength = None - else: - realLength = int(realLength) - return (kind, start, end, realLength) - -def parseExpect(field): - type,args=parseArgs(field) - - type=parseKeyValue(type) - return (type[0], (lambda *args:args)(type[1], *args)) - -def parseExpires(header): - # """HTTP/1.1 clients and caches MUST treat other invalid date formats, - # especially including the value 0, as in the past (i.e., "already expired").""" - - try: - return parseDateTime(header) - except ValueError: - return 0 - -def parseIfModifiedSince(header): - # Ancient versions of netscape and *current* versions of MSIE send - # If-Modified-Since: Thu, 05 Aug 2004 12:57:27 GMT; length=123 - # which is blantantly RFC-violating and not documented anywhere - # except bug-trackers for web frameworks. - - # So, we'll just strip off everything after a ';'. - return parseDateTime(header.split(';', 1)[0]) - -def parseIfRange(headers): - try: - return ETag.parse(tokenize(headers)) - except ValueError: - return parseDateTime(last(headers)) - -def parseRange(range): - range = list(range) - if len(range) < 3 or range[1] != Token('='): - raise ValueError("Invalid range header format: %s" %(range,)) - - type=range[0] - if type != 'bytes': - raise ValueError("Unknown range unit: %s." % (type,)) - rangeset=split(range[2:], Token(',')) - ranges = [] - - for byterangespec in rangeset: - if len(byterangespec) != 1: - raise ValueError("Invalid range header format: %s" % (range,)) - start,end=byterangespec[0].split('-') - - if not start and not end: - raise ValueError("Invalid range header format: %s" % (range,)) - - if start: - start = int(start) - else: - start = None - - if end: - end = int(end) - else: - end = None - - if start and end and start > end: - raise ValueError("Invalid range header, start > end: %s" % (range,)) - ranges.append((start,end)) - return type,ranges - -def parseRetryAfter(header): - try: - # delta seconds - return time.time() + int(header) - except ValueError: - # or datetime - return parseDateTime(header) - -# WWW-Authenticate and Authorization - -def parseWWWAuthenticate(tokenized): - headers = [] - - tokenList = list(tokenized) - - while tokenList: - scheme = tokenList.pop(0) - challenge = {} - last = None - kvChallenge = False - - while tokenList: - token = tokenList.pop(0) - if token == Token('='): - kvChallenge = True - challenge[last] = tokenList.pop(0) - last = None - - elif token == Token(','): - if kvChallenge: - if len(tokenList) > 1 and tokenList[1] != Token('='): - break - - else: - break - - else: - last = token - - if last and scheme and not challenge and not kvChallenge: - challenge = last - last = None - - headers.append((scheme, challenge)) - - if last and last not in (Token('='), Token(',')): - if headers[-1] == (scheme, challenge): - scheme = last - challenge = {} - headers.append((scheme, challenge)) - - return headers - -def parseAuthorization(header): - scheme, rest = header.split(' ', 1) - # this header isn't tokenized because it may eat characters - # in the unquoted base64 encoded credentials - return scheme.lower(), rest - -#### Header generators -def generateAccept(accept): - mimeType,q = accept - - out="%s/%s"%(mimeType.mediaType, mimeType.mediaSubtype) - if mimeType.params: - out+=';'+generateKeyValues(iter(mimeType.params.items())) - - if q != 1.0: - out+=(';q=%.3f' % (q,)).rstrip('0').rstrip('.') - - return out - -def removeDefaultEncoding(seq): - for item in seq: - if item[0] != 'identity' or item[1] != .0001: - yield item - -def generateAcceptQvalue(keyvalue): - if keyvalue[1] == 1.0: - return "%s" % keyvalue[0:1] - else: - return ("%s;q=%.3f" % keyvalue).rstrip('0').rstrip('.') - -def parseCacheControl(kv): - k, v = parseKeyValue(kv) - if k == 'max-age' or k == 'min-fresh' or k == 's-maxage': - # Required integer argument - if v is None: - v = 0 - else: - v = int(v) - elif k == 'max-stale': - # Optional integer argument - if v is not None: - v = int(v) - elif k == 'private' or k == 'no-cache': - # Optional list argument - if v is not None: - v = [field.strip().lower() for field in v.split(',')] - return k, v - -def generateCacheControl(xxx_todo_changeme): - (k, v) = xxx_todo_changeme - if v is None: - return str(k) - else: - if k == 'no-cache' or k == 'private': - # quoted list of values - v = quoteString(generateList( - [header_case_mapping.get(name) or dashCapitalize(name) for name in v])) - return '%s=%s' % (k,v) - -def generateContentRange(tup): - """tup is (type, start, end, len) - len can be None. - """ - type, start, end, len = tup - if len == None: - len = '*' - else: - len = int(len) - if start == None and end == None: - startend = '*' - else: - startend = '%d-%d' % (start, end) - - return '%s %s/%s' % (type, startend, len) - -def generateDateTime(secSinceEpoch): - """Convert seconds since epoch to HTTP datetime string.""" - year, month, day, hh, mm, ss, wd, y, z = time.gmtime(secSinceEpoch) - s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( - weekdayname[wd], - day, monthname[month], year, - hh, mm, ss) - return s - -def generateExpect(item): - if item[1][0] is None: - out = '%s' % (item[0],) - else: - out = '%s=%s' % (item[0], item[1][0]) - if len(item[1]) > 1: - out += ';'+generateKeyValues(item[1][1:]) - return out - -def generateRange(range): - def noneOr(s): - if s is None: - return '' - return s - - type,ranges=range - - if type != 'bytes': - raise ValueError("Unknown range unit: "+type+".") - - return (type+'='+ - ','.join(['%s-%s' % (noneOr(startend[0]), noneOr(startend[1])) - for startend in ranges])) - -def generateRetryAfter(when): - # always generate delta seconds format - return str(int(when - time.time())) - -def generateContentType(mimeType): - out="%s/%s"%(mimeType.mediaType, mimeType.mediaSubtype) - if mimeType.params: - out+=';'+generateKeyValues(iter(mimeType.params.items())) - return out - -def generateIfRange(dateOrETag): - if isinstance(dateOrETag, ETag): - return dateOrETag.generate() - else: - return generateDateTime(dateOrETag) - -# WWW-Authenticate and Authorization - -def generateWWWAuthenticate(headers): - _generated = [] - for seq in headers: - scheme, challenge = seq[0], seq[1] - - # If we're going to parse out to something other than a dict - # we need to be able to generate from something other than a dict - - try: - l = [] - for k,v in dict(challenge).items(): - l.append("%s=%s" % (k, quoteString(v))) - - _generated.append("%s %s" % (scheme, ", ".join(l))) - except ValueError: - _generated.append("%s %s" % (scheme, challenge)) - - return _generated - -def generateAuthorization(seq): - return [' '.join(seq)] - - -#### -class ETag(object): - def __init__(self, tag, weak=False): - self.tag = str(tag) - self.weak = weak - - def match(self, other, strongCompare): - # Sec 13.3. - # The strong comparison function: in order to be considered equal, both - # validators MUST be identical in every way, and both MUST NOT be weak. - # - # The weak comparison function: in order to be considered equal, both - # validators MUST be identical in every way, but either or both of - # them MAY be tagged as "weak" without affecting the result. - - if not isinstance(other, ETag) or other.tag != self.tag: - return False - - if strongCompare and (other.weak or self.weak): - return False - return True - - def __eq__(self, other): - return isinstance(other, ETag) and other.tag == self.tag and other.weak == self.weak - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "Etag(%r, weak=%r)" % (self.tag, self.weak) - - def parse(tokens): - tokens=tuple(tokens) - if len(tokens) == 1 and not isinstance(tokens[0], Token): - return ETag(tokens[0]) - - if(len(tokens) == 3 and tokens[0] == "w" - and tokens[1] == Token('/')): - return ETag(tokens[2], weak=True) - - raise ValueError("Invalid ETag.") - - parse=staticmethod(parse) - - def generate(self): - if self.weak: - return 'W/'+quoteString(self.tag) - else: - return quoteString(self.tag) - -def parseStarOrETag(tokens): - tokens=tuple(tokens) - if tokens == ('*',): - return '*' - else: - return ETag.parse(tokens) - -def generateStarOrETag(etag): - if etag=='*': - return etag - else: - return etag.generate() - -#### Cookies. Blech! -class Cookie(object): - # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires', 'discard', 'secure', 'comment', 'commenturl', 'version'] - - def __init__(self, name, value, path=None, domain=None, ports=None, expires=None, discard=False, secure=False, comment=None, commenturl=None, version=0): - self.name=name - self.value=value - self.path=path - self.domain=domain - self.ports=ports - self.expires=expires - self.discard=discard - self.secure=secure - self.comment=comment - self.commenturl=commenturl - self.version=version - - def __repr__(self): - s="Cookie(%r=%r" % (self.name, self.value) - if self.path is not None: s+=", path=%r" % (self.path,) - if self.domain is not None: s+=", domain=%r" % (self.domain,) - if self.ports is not None: s+=", ports=%r" % (self.ports,) - if self.expires is not None: s+=", expires=%r" % (self.expires,) - if self.secure is not False: s+=", secure=%r" % (self.secure,) - if self.comment is not None: s+=", comment=%r" % (self.comment,) - if self.commenturl is not None: s+=", commenturl=%r" % (self.commenturl,) - if self.version != 0: s+=", version=%r" % (self.version,) - s+=")" - return s - - def __eq__(self, other): - return (isinstance(other, Cookie) and - other.path == self.path and - other.domain == self.domain and - other.ports == self.ports and - other.expires == self.expires and - other.secure == self.secure and - other.comment == self.comment and - other.commenturl == self.commenturl and - other.version == self.version) - - def __ne__(self, other): - return not self.__eq__(other) - - -def parseCookie(headers): - """Bleargh, the cookie spec sucks. - This surely needs interoperability testing. - There are two specs that are supported: - Version 0) http://wp.netscape.com/newsref/std/cookie_spec.html - Version 1) http://www.faqs.org/rfcs/rfc2965.html - """ - - cookies = [] - # There can't really be multiple cookie headers according to RFC, because - # if multiple headers are allowed, they must be joinable with ",". - # Neither new RFC2965 cookies nor old netscape cookies are. - - header = ';'.join(headers) - if header[0:8].lower() == "$version": - # RFC2965 cookie - h=tokenize([header], foldCase=False) - r_cookies = split(h, Token(',')) - for r_cookie in r_cookies: - last_cookie = None - rr_cookies = split(r_cookie, Token(';')) - for cookie in rr_cookies: - nameval = tuple(split(cookie, Token('='))) - if len(nameval) == 2: - (name,), (value,) = nameval - else: - (name,), = nameval - value = None - - name=name.lower() - if name == '$version': - continue - if name[0] == '$': - if last_cookie is not None: - if name == '$path': - last_cookie.path=value - elif name == '$domain': - last_cookie.domain=value - elif name == '$port': - if value is None: - last_cookie.ports = () - else: - last_cookie.ports=tuple([int(s) for s in value.split(',')]) - else: - last_cookie = Cookie(name, value, version=1) - cookies.append(last_cookie) - else: - # Oldstyle cookies don't do quoted strings or anything sensible. - # All characters are valid for names except ';' and '=', and all - # characters are valid for values except ';'. Spaces are stripped, - # however. - r_cookies = header.split(';') - for r_cookie in r_cookies: - name,value = r_cookie.split('=', 1) - name=name.strip(' \t') - value=value.strip(' \t') - - cookies.append(Cookie(name, value)) - - return cookies - -cookie_validname = "[^"+re.escape(http_tokens+http_ctls)+"]*$" -cookie_validname_re = re.compile(cookie_validname) -cookie_validvalue = cookie_validname+'|"([^"]|\\\\")*"$' -cookie_validvalue_re = re.compile(cookie_validvalue) - -def generateCookie(cookies): - # There's a fundamental problem with the two cookie specifications. - # They both use the "Cookie" header, and the RFC Cookie header only allows - # one version to be specified. Thus, when you have a collection of V0 and - # V1 cookies, you have to either send them all as V0 or send them all as - # V1. - - # I choose to send them all as V1. - - # You might think converting a V0 cookie to a V1 cookie would be lossless, - # but you'd be wrong. If you do the conversion, and a V0 parser tries to - # read the cookie, it will see a modified form of the cookie, in cases - # where quotes must be added to conform to proper V1 syntax. - # (as a real example: "Cookie: cartcontents=oid:94680,qty:1,auto:0,esp:y") - - # However, that is what we will do, anyways. It has a high probability of - # breaking applications that only handle oldstyle cookies, where some other - # application set a newstyle cookie that is applicable over for site - # (or host), AND where the oldstyle cookie uses a value which is invalid - # syntax in a newstyle cookie. - - # Also, the cookie name *cannot* be quoted in V1, so some cookies just - # cannot be converted at all. (e.g. "Cookie: phpAds_capAd[32]=2"). These - # are just dicarded during conversion. - - # As this is an unsolvable problem, I will pretend I can just say - # OH WELL, don't do that, or else upgrade your old applications to have - # newstyle cookie parsers. - - # I will note offhandedly that there are *many* sites which send V0 cookies - # that are not valid V1 cookie syntax. About 20% for my cookies file. - # However, they do not generally mix them with V1 cookies, so this isn't - # an issue, at least right now. I have not tested to see how many of those - # webapps support RFC2965 V1 cookies. I suspect not many. - - max_version = max([cookie.version for cookie in cookies]) - - if max_version == 0: - # no quoting or anything. - return ';'.join(["%s=%s" % (cookie.name, cookie.value) for cookie in cookies]) - else: - str_cookies = ['$Version="1"'] - for cookie in cookies: - if cookie.version == 0: - # Version 0 cookie: we make sure the name and value are valid - # V1 syntax. - - # If they are, we use them as is. This means in *most* cases, - # the cookie will look literally the same on output as it did - # on input. - # If it isn't a valid name, ignore the cookie. - # If it isn't a valid value, quote it and hope for the best on - # the other side. - - if cookie_validname_re.match(cookie.name) is None: - continue - - value=cookie.value - if cookie_validvalue_re.match(cookie.value) is None: - value = quoteString(value) - - str_cookies.append("%s=%s" % (cookie.name, value)) - else: - # V1 cookie, nice and easy - str_cookies.append("%s=%s" % (cookie.name, quoteString(cookie.value))) - - if cookie.path: - str_cookies.append("$Path=%s" % quoteString(cookie.path)) - if cookie.domain: - str_cookies.append("$Domain=%s" % quoteString(cookie.domain)) - if cookie.ports is not None: - if len(cookie.ports) == 0: - str_cookies.append("$Port") - else: - str_cookies.append("$Port=%s" % quoteString(",".join([str(x) for x in cookie.ports]))) - return ';'.join(str_cookies) - -def parseSetCookie(headers): - setCookies = [] - for header in headers: - try: - parts = header.split(';') - l = [] - - for part in parts: - namevalue = part.split('=',1) - if len(namevalue) == 1: - name=namevalue[0] - value=None - else: - name,value=namevalue - value=value.strip(' \t') - - name=name.strip(' \t') - - l.append((name, value)) - - setCookies.append(makeCookieFromList(l, True)) - except ValueError: - # If we can't parse one Set-Cookie, ignore it, - # but not the rest of Set-Cookies. - pass - return setCookies - -def parseSetCookie2(toks): - outCookies = [] - for cookie in [[parseKeyValue(x) for x in split(y, Token(';'))] - for y in split(toks, Token(','))]: - try: - outCookies.append(makeCookieFromList(cookie, False)) - except ValueError: - # Again, if we can't handle one cookie -- ignore it. - pass - return outCookies - -def makeCookieFromList(tup, netscapeFormat): - name, value = tup[0] - if name is None or value is None: - raise ValueError("Cookie has missing name or value") - if name.startswith("$"): - raise ValueError("Invalid cookie name: %r, starts with '$'." % name) - cookie = Cookie(name, value) - hadMaxAge = False - - for name,value in tup[1:]: - name = name.lower() - - if value is None: - if name in ("discard", "secure"): - # Boolean attrs - value = True - elif name != "port": - # Can be either boolean or explicit - continue - - if name in ("comment", "commenturl", "discard", "domain", "path", "secure"): - # simple cases - setattr(cookie, name, value) - elif name == "expires" and not hadMaxAge: - if netscapeFormat and value[0] == '"' and value[-1] == '"': - value = value[1:-1] - cookie.expires = parseDateTime(value) - elif name == "max-age": - hadMaxAge = True - cookie.expires = int(value) + time.time() - elif name == "port": - if value is None: - cookie.ports = () - else: - if netscapeFormat and value[0] == '"' and value[-1] == '"': - value = value[1:-1] - cookie.ports = tuple([int(s) for s in value.split(',')]) - elif name == "version": - cookie.version = int(value) - - return cookie - - -def generateSetCookie(cookies): - setCookies = [] - for cookie in cookies: - out = ["%s=%s" % (cookie.name, cookie.value)] - if cookie.expires: - out.append("expires=%s" % generateDateTime(cookie.expires)) - if cookie.path: - out.append("path=%s" % cookie.path) - if cookie.domain: - out.append("domain=%s" % cookie.domain) - if cookie.secure: - out.append("secure") - - setCookies.append('; '.join(out)) - return setCookies - -def generateSetCookie2(cookies): - setCookies = [] - for cookie in cookies: - out = ["%s=%s" % (cookie.name, quoteString(cookie.value))] - if cookie.comment: - out.append("Comment=%s" % quoteString(cookie.comment)) - if cookie.commenturl: - out.append("CommentURL=%s" % quoteString(cookie.commenturl)) - if cookie.discard: - out.append("Discard") - if cookie.domain: - out.append("Domain=%s" % quoteString(cookie.domain)) - if cookie.expires: - out.append("Max-Age=%s" % (cookie.expires - time.time())) - if cookie.path: - out.append("Path=%s" % quoteString(cookie.path)) - if cookie.ports is not None: - if len(cookie.ports) == 0: - out.append("Port") - else: - out.append("Port=%s" % quoteString(",".join([str(x) for x in cookie.ports]))) - if cookie.secure: - out.append("Secure") - out.append('Version="1"') - setCookies.append('; '.join(out)) - return setCookies - -def parseDepth(depth): - if depth not in ("0", "1", "infinity"): - raise ValueError("Invalid depth header value: %s" % (depth,)) - return depth - -def parseOverWrite(overwrite): - if overwrite == "F": - return False - elif overwrite == "T": - return True - raise ValueError("Invalid overwrite header value: %s" % (overwrite,)) - -def generateOverWrite(overwrite): - if overwrite: - return "T" - else: - return "F" - -##### Random stuff that looks useful. -# def sortMimeQuality(s): -# def sorter(item1, item2): -# if item1[0] == '*': -# if item2[0] == '*': -# return 0 - - -# def sortQuality(s): -# def sorter(item1, item2): -# if item1[1] < item2[1]: -# return -1 -# if item1[1] < item2[1]: -# return 1 -# if item1[0] == item2[0]: -# return 0 - - -# def getMimeQuality(mimeType, accepts): -# type,args = parseArgs(mimeType) -# type=type.split(Token('/')) -# if len(type) != 2: -# raise ValueError, "MIME Type "+s+" invalid." - -# for accept in accepts: -# accept,acceptQual=accept -# acceptType=accept[0:1] -# acceptArgs=accept[2] - -# if ((acceptType == type or acceptType == (type[0],'*') or acceptType==('*','*')) and -# (args == acceptArgs or len(acceptArgs) == 0)): -# return acceptQual - -# def getQuality(type, accepts): -# qual = accepts.get(type) -# if qual is not None: -# return qual - -# return accepts.get('*') - -# Headers object -class __RecalcNeeded(object): - def __repr__(self): - return "" - -_RecalcNeeded = __RecalcNeeded() - -class Headers(object): - """This class stores the HTTP headers as both a parsed representation and - the raw string representation. It converts between the two on demand.""" - - def __init__(self, headers=None, rawHeaders=None, handler=DefaultHTTPHandler): - self._raw_headers = {} - self._headers = {} - self.handler = handler - if headers is not None: - for key, value in headers.items(): - self.setHeader(key, value) - if rawHeaders is not None: - for key, value in rawHeaders.items(): - self.setRawHeaders(key, value) - - def _setRawHeaders(self, headers): - self._raw_headers = headers - self._headers = {} - - def _toParsed(self, name): - r = self._raw_headers.get(name, None) - h = self.handler.parse(name, r) - if h is not None: - self._headers[name] = h - return h - - def _toRaw(self, name): - h = self._headers.get(name, None) - r = self.handler.generate(name, h) - if r is not None: - self._raw_headers[name] = r - return r - - def hasHeader(self, name): - """Does a header with the given name exist?""" - name=name.lower() - return name in self._raw_headers - - def getRawHeaders(self, name, default=None): - """Returns a list of headers matching the given name as the raw string given.""" - - name=name.lower() - raw_header = self._raw_headers.get(name, default) - if raw_header is not _RecalcNeeded: - return raw_header - - return self._toRaw(name) - - def getHeader(self, name, default=None): - """Ret9urns the parsed representation of the given header. - The exact form of the return value depends on the header in question. - - If no parser for the header exists, raise ValueError. - - If the header doesn't exist, return default (or None if not specified) - """ - name=name.lower() - parsed = self._headers.get(name, default) - if parsed is not _RecalcNeeded: - return parsed - return self._toParsed(name) - - def setRawHeaders(self, name, value): - """Sets the raw representation of the given header. - Value should be a list of strings, each being one header of the - given name. - """ - name=name.lower() - self._raw_headers[name] = value - self._headers[name] = _RecalcNeeded - - def setHeader(self, name, value): - """Sets the parsed representation of the given header. - Value should be a list of objects whose exact form depends - on the header in question. - """ - name=name.lower() - self._raw_headers[name] = _RecalcNeeded - self._headers[name] = value - - def addRawHeader(self, name, value): - """ - Add a raw value to a header that may or may not already exist. - If it exists, add it as a separate header to output; do not - replace anything. - """ - name=name.lower() - raw_header = self._raw_headers.get(name) - if raw_header is None: - # No header yet - raw_header = [] - self._raw_headers[name] = raw_header - elif raw_header is _RecalcNeeded: - raw_header = self._toRaw(name) - - raw_header.append(value) - self._headers[name] = _RecalcNeeded - - def removeHeader(self, name): - """Removes the header named.""" - - name=name.lower() - if name in self._raw_headers: - del self._raw_headers[name] - del self._headers[name] - - def __repr__(self): - return ''% (self._raw_headers, self._headers) - - def canonicalNameCaps(self, name): - """Return the name with the canonical capitalization, if known, - otherwise, Caps-After-Dashes""" - return header_case_mapping.get(name) or dashCapitalize(name) - - def getAllRawHeaders(self): - """Return an iterator of key,value pairs of all headers - contained in this object, as strings. The keys are capitalized - in canonical capitalization.""" - for k,v in self._raw_headers.items(): - if v is _RecalcNeeded: - v = self._toRaw(k) - yield self.canonicalNameCaps(k), v - - def makeImmutable(self): - """Make this header set immutable. All mutating operations will - raise an exception.""" - self.setHeader = self.setRawHeaders = self.removeHeader = self._mutateRaise - - def _mutateRaise(self, *args): - raise AttributeError("This header object is immutable as the headers have already been sent.") - - -"""The following dicts are all mappings of header to list of operations - to perform. The first operation should generally be 'tokenize' if the - header can be parsed according to the normal tokenization rules. If - it cannot, generally the first thing you want to do is take only the - last instance of the header (in case it was sent multiple times, which - is strictly an error, but we're nice.). - """ - -iteritems = lambda x: iter(x.items()) - - -parser_general_headers = { - 'Cache-Control':(tokenize, listParser(parseCacheControl), dict), - 'Connection':(tokenize,filterTokens), - 'Date':(last,parseDateTime), -# 'Pragma':tokenize -# 'Trailer':tokenize - 'Transfer-Encoding':(tokenize,filterTokens), -# 'Upgrade':tokenize -# 'Via':tokenize,stripComment -# 'Warning':tokenize -} - -generator_general_headers = { - 'Cache-Control':(iteritems, listGenerator(generateCacheControl), singleHeader), - 'Connection':(generateList,singleHeader), - 'Date':(generateDateTime,singleHeader), -# 'Pragma': -# 'Trailer': - 'Transfer-Encoding':(generateList,singleHeader), -# 'Upgrade': -# 'Via': -# 'Warning': -} - -parser_request_headers = { - 'Accept': (tokenize, listParser(parseAccept), dict), - 'Accept-Charset': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultCharset), - 'Accept-Encoding':(tokenize, listParser(parseAcceptQvalue), dict, addDefaultEncoding), - 'Accept-Language':(tokenize, listParser(parseAcceptQvalue), dict), - 'Authorization': (last, parseAuthorization), - 'Cookie':(parseCookie,), - 'Expect':(tokenize, listParser(parseExpect), dict), - 'From':(last,), - 'Host':(last,), - 'If-Match':(tokenize, listParser(parseStarOrETag), list), - 'If-Modified-Since':(last, parseIfModifiedSince), - 'If-None-Match':(tokenize, listParser(parseStarOrETag), list), - 'If-Range':(parseIfRange,), - 'If-Unmodified-Since':(last,parseDateTime), - 'Max-Forwards':(last,int), -# 'Proxy-Authorization':str, # what is "credentials" - 'Range':(tokenize, parseRange), - 'Referer':(last,str), # TODO: URI object? - 'TE':(tokenize, listParser(parseAcceptQvalue), dict), - 'User-Agent':(last,str), -} - -generator_request_headers = { - 'Accept': (iteritems,listGenerator(generateAccept),singleHeader), - 'Accept-Charset': (iteritems, listGenerator(generateAcceptQvalue),singleHeader), - 'Accept-Encoding': (iteritems, removeDefaultEncoding, listGenerator(generateAcceptQvalue),singleHeader), - 'Accept-Language': (iteritems, listGenerator(generateAcceptQvalue),singleHeader), - 'Authorization': (generateAuthorization,), # what is "credentials" - 'Cookie':(generateCookie,singleHeader), - 'Expect':(iteritems, listGenerator(generateExpect), singleHeader), - 'From':(str,singleHeader), - 'Host':(str,singleHeader), - 'If-Match':(listGenerator(generateStarOrETag), singleHeader), - 'If-Modified-Since':(generateDateTime,singleHeader), - 'If-None-Match':(listGenerator(generateStarOrETag), singleHeader), - 'If-Range':(generateIfRange, singleHeader), - 'If-Unmodified-Since':(generateDateTime,singleHeader), - 'Max-Forwards':(str, singleHeader), -# 'Proxy-Authorization':str, # what is "credentials" - 'Range':(generateRange,singleHeader), - 'Referer':(str,singleHeader), - 'TE': (iteritems, listGenerator(generateAcceptQvalue),singleHeader), - 'User-Agent':(str,singleHeader), -} - -parser_response_headers = { - 'Accept-Ranges':(tokenize, filterTokens), - 'Age':(last,int), - 'ETag':(tokenize, ETag.parse), - 'Location':(last,), # TODO: URI object? -# 'Proxy-Authenticate' - 'Retry-After':(last, parseRetryAfter), - 'Server':(last,), - 'Set-Cookie':(parseSetCookie,), - 'Set-Cookie2':(tokenize, parseSetCookie2), - 'Vary':(tokenize, filterTokens), - 'WWW-Authenticate': (lambda h: tokenize(h, foldCase=False), - parseWWWAuthenticate,) -} - -generator_response_headers = { - 'Accept-Ranges':(generateList, singleHeader), - 'Age':(str, singleHeader), - 'ETag':(ETag.generate, singleHeader), - 'Location':(str, singleHeader), -# 'Proxy-Authenticate' - 'Retry-After':(generateRetryAfter, singleHeader), - 'Server':(str, singleHeader), - 'Set-Cookie':(generateSetCookie,), - 'Set-Cookie2':(generateSetCookie2,), - 'Vary':(generateList, singleHeader), - 'WWW-Authenticate':(generateWWWAuthenticate,) -} - -parser_entity_headers = { - 'Allow':(lambda str:tokenize(str, foldCase=False), filterTokens), - 'Content-Encoding':(tokenize, filterTokens), - 'Content-Language':(tokenize, filterTokens), - 'Content-Length':(last, int), - 'Content-Location':(last,), # TODO: URI object? - 'Content-MD5':(last, parseContentMD5), - 'Content-Range':(last, parseContentRange), - 'Content-Type':(lambda str:tokenize(str, foldCase=False), parseContentType), - 'Expires':(last, parseExpires), - 'Last-Modified':(last, parseDateTime), - } - -generator_entity_headers = { - 'Allow':(generateList, singleHeader), - 'Content-Encoding':(generateList, singleHeader), - 'Content-Language':(generateList, singleHeader), - 'Content-Length':(str, singleHeader), - 'Content-Location':(str, singleHeader), - 'Content-MD5':(base64.encodestring, lambda x: x.strip("\n"), singleHeader), - 'Content-Range':(generateContentRange, singleHeader), - 'Content-Type':(generateContentType, singleHeader), - 'Expires':(generateDateTime, singleHeader), - 'Last-Modified':(generateDateTime, singleHeader), - } - -DefaultHTTPHandler.updateParsers(parser_general_headers) -DefaultHTTPHandler.updateParsers(parser_request_headers) -DefaultHTTPHandler.updateParsers(parser_response_headers) -DefaultHTTPHandler.updateParsers(parser_entity_headers) - -DefaultHTTPHandler.updateGenerators(generator_general_headers) -DefaultHTTPHandler.updateGenerators(generator_request_headers) -DefaultHTTPHandler.updateGenerators(generator_response_headers) -DefaultHTTPHandler.updateGenerators(generator_entity_headers) - - -# casemappingify(DefaultHTTPParsers) -# casemappingify(DefaultHTTPGenerators) - -# lowerify(DefaultHTTPParsers) -# lowerify(DefaultHTTPGenerators) diff --git a/xcap/web/iweb.py b/xcap/web/iweb.py deleted file mode 100644 index b949db8..0000000 --- a/xcap/web/iweb.py +++ /dev/null @@ -1,378 +0,0 @@ - -""" - I contain the interfaces for several web related objects including IRequest - and IResource. I am based heavily on ideas from nevow.inevow -""" - -from zope.interface import Attribute, Interface, interface - -# server.py interfaces -class IResource(Interface): - """ - An HTTP resource. - - I serve 2 main purposes: one is to provide a standard representation for - what HTTP specification calls an 'entity', and the other is to provide an - mechanism for mapping URLs to content. - """ - - def locateChild(req, segments): - """Locate another object which can be adapted to IResource. - - @return: A 2-tuple of (resource, remaining-path-segments), - or a deferred which will fire the above. - - Causes the object publishing machinery to continue on - with specified resource and segments, calling the - appropriate method on the specified resource. - - If you return (self, L{server.StopTraversal}), this - instructs web to immediately stop the lookup stage, - and switch to the rendering stage, leaving the - remaining path alone for your render function to - handle. - """ - - def renderHTTP(req): - """Return an IResponse or a deferred which will fire an - IResponse. This response will be written to the web browser - which initiated the request. - """ - -# Is there a better way to do this than this funky extra class? -_default = object() -class SpecialAdaptInterfaceClass(interface.InterfaceClass): - # A special adapter for IResource to handle the extra step of adapting - # from IOldNevowResource-providing resources. - def __call__(self, other, alternate=_default): - result = super(SpecialAdaptInterfaceClass, self).__call__(other, alternate) - if result is not alternate: - return result - - result = IOldNevowResource(other, alternate) - if result is not alternate: - result = IResource(result) - return result - if alternate is not _default: - return alternate - raise TypeError('Could not adapt', other, self) -IResource.__class__ = SpecialAdaptInterfaceClass - -class IOldNevowResource(Interface): - # Shared interface with inevow.IResource - """ - I am a web resource. - """ - - def locateChild(ctx, segments): - """Locate another object which can be adapted to IResource - Return a tuple of resource, path segments - """ - - def renderHTTP(ctx): - """Return a string or a deferred which will fire a string. This string - will be written to the web browser which initiated this request. - - Unlike iweb.IResource, this expects the incoming data to have already been read - and parsed into request.args and request.content, and expects to return a - string instead of a response object. - """ - -class ICanHandleException(Interface): - # Shared interface with inevow.ICanHandleException - def renderHTTP_exception(request, failure): - """Render an exception to the given request object. - """ - - def renderInlineException(request, reason): - """Return stan representing the exception, to be printed in the page, - not replacing the page.""" - - -# http.py interfaces -class IResponse(Interface): - """I'm a response.""" - code = Attribute("The HTTP response code") - headers = Attribute("A http_headers.Headers instance of headers to send") - stream = Attribute("A stream.IByteStream of outgoing data, or else None.") - -class IRequest(Interface): - """I'm a request for a web resource - """ - - method = Attribute("The HTTP method from the request line, e.g. GET") - uri = Attribute("The raw URI from the request line. May or may not include host.") - clientproto = Attribute("Protocol from the request line, e.g. HTTP/1.1") - - headers = Attribute("A http_headers.Headers instance of incoming headers.") - stream = Attribute("A stream.IByteStream of incoming data.") - - def writeResponse(response): - """Write an IResponse object to the client""" - - chanRequest = Attribute("The ChannelRequest. I wonder if this is public really?") - -class IOldRequest(Interface): - # Shared interface with inevow.ICurrentSegments - """An old HTTP request. - - Subclasses should override the process() method to determine how - the request will be processed. - - @ivar method: The HTTP method that was used. - @ivar uri: The full URI that was requested (includes arguments). - @ivar path: The path only (arguments not included). - @ivar args: All of the arguments, including URL and POST arguments. - @type args: A mapping of strings (the argument names) to lists of values. - i.e., ?foo=bar&foo=baz&quux=spam results in - {'foo': ['bar', 'baz'], 'quux': ['spam']}. - @ivar received_headers: All received headers - """ - # Methods for received request - def getHeader(key): - """Get a header that was sent from the network. - """ - - def getCookie(key): - """Get a cookie that was sent from the network. - """ - - - def getAllHeaders(): - """Return dictionary of all headers the request received.""" - - def getRequestHostname(): - """Get the hostname that the user passed in to the request. - - This will either use the Host: header (if it is available) or the - host we are listening on if the header is unavailable. - """ - - def getHost(): - """Get my originally requesting transport's host. - - Don't rely on the 'transport' attribute, since Request objects may be - copied remotely. For information on this method's return value, see - twisted.internet.tcp.Port. - """ - - def getClientIP(): - pass - def getClient(): - pass - def getUser(): - pass - def getPassword(): - pass - def isSecure(): - pass - - def getSession(sessionInterface = None): - pass - - def URLPath(): - pass - - def prePathURL(): - pass - - def rememberRootURL(): - """ - Remember the currently-processed part of the URL for later - recalling. - """ - - def getRootURL(): - """ - Get a previously-remembered URL. - """ - - # Methods for outgoing request - def finish(): - """We are finished writing data.""" - - def write(data): - """ - Write some data as a result of an HTTP request. The first - time this is called, it writes out response data. - """ - - def addCookie(k, v, expires=None, domain=None, path=None, max_age=None, comment=None, secure=None): - """Set an outgoing HTTP cookie. - - In general, you should consider using sessions instead of cookies, see - twisted.web.server.Request.getSession and the - twisted.web.server.Session class for details. - """ - - def setResponseCode(code, message=None): - """Set the HTTP response code. - """ - - def setHeader(k, v): - """Set an outgoing HTTP header. - """ - - def redirect(url): - """Utility function that does a redirect. - - The request should have finish() called after this. - """ - - def setLastModified(when): - """Set the X{Last-Modified} time for the response to this request. - - If I am called more than once, I ignore attempts to set - Last-Modified earlier, only replacing the Last-Modified time - if it is to a later value. - - If I am a conditional request, I may modify my response code - to L{NOT_MODIFIED} if appropriate for the time given. - - @param when: The last time the resource being returned was - modified, in seconds since the epoch. - @type when: number - @return: If I am a X{If-Modified-Since} conditional request and - the time given is not newer than the condition, I return - L{http.CACHED} to indicate that you should write no - body. Otherwise, I return a false value. - """ - - def setETag(etag): - """Set an X{entity tag} for the outgoing response. - - That's \"entity tag\" as in the HTTP/1.1 X{ETag} header, \"used - for comparing two or more entities from the same requested - resource.\" - - If I am a conditional request, I may modify my response code - to L{NOT_MODIFIED} or L{PRECONDITION_FAILED}, if appropriate - for the tag given. - - @param etag: The entity tag for the resource being returned. - @type etag: string - @return: If I am a X{If-None-Match} conditional request and - the tag matches one in the request, I return - L{http.CACHED} to indicate that you should write - no body. Otherwise, I return a false value. - """ - - def setHost(host, port, ssl=0): - """Change the host and port the request thinks it's using. - - This method is useful for working with reverse HTTP proxies (e.g. - both Squid and Apache's mod_proxy can do this), when the address - the HTTP client is using is different than the one we're listening on. - - For example, Apache may be listening on https://www.example.com, and then - forwarding requests to http://localhost:8080, but we don't want HTML produced - by Twisted to say 'http://localhost:8080', they should say 'https://www.example.com', - so we do:: - - request.setHost('www.example.com', 443, ssl=1) - - This method is experimental. - """ - -class IChanRequestCallbacks(Interface): - """The bits that are required of a Request for interfacing with a - IChanRequest object""" - - def __init__(chanRequest, command, path, version, contentLength, inHeaders): - """Create a new Request object. - @param chanRequest: the IChanRequest object creating this request - @param command: the HTTP command e.g. GET - @param path: the HTTP path e.g. /foo/bar.html - @param version: the parsed HTTP version e.g. (1,1) - @param contentLength: how much data to expect, or None if unknown - @param inHeaders: the request headers""" - - def process(): - """Process the request. Called as soon as it's possibly reasonable to - return a response. handleContentComplete may or may not have been called already.""" - - def handleContentChunk(data): - """Called when a piece of incoming data has been received.""" - - def handleContentComplete(): - """Called when the incoming data stream is finished.""" - - def connectionLost(reason): - """Called if the connection was lost.""" - - -class IChanRequest(Interface): - def writeIntermediateResponse(code, headers=None): - """Write a non-terminating response. - - Intermediate responses cannot contain data. - If the channel does not support intermediate responses, do nothing. - - @ivar code: The response code. Should be in the 1xx range. - @type code: int - @ivar headers: the headers to send in the response - @type headers: C{twisted.web.http_headers.Headers} - """ - pass - - def writeHeaders(code, headers): - """Write a final response. - - @param code: The response code. Should not be in the 1xx range. - @type code: int - @param headers: the headers to send in the response. They will be augmented - with any connection-oriented headers as necessary for the protocol. - @type headers: C{twisted.web.http_headers.Headers} - """ - pass - - def write(data): - """Write some data. - - @param data: the data bytes - @type data: str - """ - pass - - def finish(): - """Finish the request, and clean up the connection if necessary. - """ - pass - - def abortConnection(): - """Forcibly abort the connection without cleanly closing. - Use if, for example, you can't write all the data you promised. - """ - pass - - def registerProducer(producer, streaming): - """Register a producer with the standard API.""" - pass - - def unregisterProducer(): - """Unregister a producer.""" - pass - - def getHostInfo(): - """Returns a tuple of (address, socket user connected to, - boolean, was it secure). Note that this should not necsessarily - always return the actual local socket information from - twisted. E.g. in a CGI, it should use the variables coming - from the invoking script. - """ - - def getRemoteHost(): - """Returns an address of the remote host. - - Like getHostInfo, this information may come from the real - socket, or may come from additional information, depending on - the transport. - """ - - persistent = Attribute("""Whether this request supports HTTP connection persistence. May be set to False. Should not be set to other values.""") - - -class ISite(Interface): - pass - -__all__ = ['ICanHandleException', 'IChanRequest', 'IChanRequestCallbacks', 'IOldNevowResource', 'IOldRequest', 'IRequest', 'IResource', 'IResponse', 'ISite'] diff --git a/xcap/web/resource.py b/xcap/web/resource.py deleted file mode 100644 index 961d808..0000000 --- a/xcap/web/resource.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright (c) 2001-2007 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -I hold the lowest-level L{Resource} class and related mix-in classes. -""" - -# System Imports -from zope.interface import implements - -from xcap.web import iweb, http, server, responsecode - -class RenderMixin(object): - """ - Mix-in class for L{iweb.IResource} which provides a dispatch mechanism for - handling HTTP methods. - """ - def allowedMethods(self): - """ - @return: A tuple of HTTP methods that are allowed to be invoked on this resource. - """ - if not hasattr(self, "_allowed_methods"): - self._allowed_methods = tuple([name[5:] for name in dir(self) if name.startswith('http_')]) - return self._allowed_methods - - def checkPreconditions(self, request): - """ - Checks all preconditions imposed by this resource upon a request made - against it. - @param request: the request to process. - @raise http.HTTPError: if any precondition fails. - @return: C{None} or a deferred whose callback value is C{request}. - """ - # - # http.checkPreconditions() gets called by the server after every - # GET or HEAD request. - # - # For other methods, we need to know to bail out before request - # processing, especially for methods that modify server state (eg. PUT). - # We also would like to do so even for methods that don't, if those - # methods might be expensive to process. We're assuming that GET and - # HEAD are not expensive. - # - if request.method not in ("GET", "HEAD"): - http.checkPreconditions(request) - - # Check per-method preconditions - method = getattr(self, "preconditions_" + request.method, None) - if method: - return method(request) - - def renderHTTP(self, request): - """ - See L{iweb.IResource.renderHTTP}. - - This implementation will dispatch the given C{request} to another method - of C{self} named C{http_}METHOD, where METHOD is the HTTP method used by - C{request} (eg. C{http_GET}, C{http_POST}, etc.). - - Generally, a subclass should implement those methods instead of - overriding this one. - - C{http_*} methods are expected provide the same interface and return the - same results as L{iweb.IResource}C{.renderHTTP} (and therefore this method). - - C{etag} and C{last-modified} are added to the response returned by the - C{http_*} header, if known. - - If an appropriate C{http_*} method is not found, a - L{responsecode.NOT_ALLOWED}-status response is returned, with an - appropriate C{allow} header. - - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - method = getattr(self, "http_" + request.method, None) - if not method: - response = http.Response(responsecode.NOT_ALLOWED) - response.headers.setHeader("allow", self.allowedMethods()) - return response - - d = self.checkPreconditions(request) - if d is None: - return method(request) - else: - return d.addCallback(lambda _: method(request)) - - def http_OPTIONS(self, request): - """ - Respond to a OPTIONS request. - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - response = http.Response(responsecode.OK) - response.headers.setHeader("allow", self.allowedMethods()) - return response - - def http_TRACE(self, request): - """ - Respond to a TRACE request. - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - return server.doTrace(request) - - def http_HEAD(self, request): - """ - Respond to a HEAD request. - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - return self.http_GET(request) - - def http_GET(self, request): - """ - Respond to a GET request. - - This implementation validates that the request body is empty and then - dispatches the given C{request} to L{render} and returns its result. - - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - if request.stream.length != 0: - return responsecode.REQUEST_ENTITY_TOO_LARGE - - return self.render(request) - - def render(self, request): - """ - Subclasses should implement this method to do page rendering. - See L{http_GET}. - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - raise NotImplementedError("Subclass must implement render method.") - -class Resource(RenderMixin): - """ - An L{iweb.IResource} implementation with some convenient mechanisms for - locating children. - """ - implements(iweb.IResource) - - addSlash = False - - def locateChild(self, request, segments): - """ - Locates a child resource of this resource. - @param request: the request to process. - @param segments: a sequence of URL path segments. - @return: a tuple of C{(child, segments)} containing the child - of this resource which matches one or more of the given C{segments} in - sequence, and a list of remaining segments. - """ - w = getattr(self, 'child_%s' % (segments[0], ), None) - - if w: - r = iweb.IResource(w, None) - if r: - return r, segments[1:] - return w(request), segments[1:] - - factory = getattr(self, 'childFactory', None) - if factory is not None: - r = factory(request, segments[0]) - if r: - return r, segments[1:] - - return None, [] - - def child_(self, request): - """ - This method locates a child with a trailing C{"/"} in the URL. - @param request: the request to process. - """ - if self.addSlash and len(request.postpath) == 1: - return self - return None - - def putChild(self, path, child): - """ - Register a static child. - - This implementation registers children by assigning them to attributes - with a C{child_} prefix. C{resource.putChild("foo", child)} is - therefore same as C{o.child_foo = child}. - - @param path: the name of the child to register. You almost certainly - don't want C{"/"} in C{path}. If you want to add a "directory" - resource (e.g. C{/foo/}) specify C{path} as C{""}. - @param child: an object adaptable to L{iweb.IResource}. - """ - setattr(self, 'child_%s' % (path, ), child) - - def http_GET(self, request): - if self.addSlash and request.prepath[-1] != '': - # If this is a directory-ish resource... - return http.RedirectResponse(request.unparseURL(path=request.path+'/')) - - return super(Resource, self).http_GET(request) - - -class PostableResource(Resource): - """ - A L{Resource} capable of handling the POST request method. - - @cvar maxMem: maximum memory used during the parsing of the data. - @type maxMem: C{int} - @cvar maxFields: maximum number of form fields allowed. - @type maxFields: C{int} - @cvar maxSize: maximum size of the whole post allowed. - @type maxSize: C{int} - """ - maxMem = 100 * 1024 - maxFields = 1024 - maxSize = 10 * 1024 * 1024 - - def http_POST(self, request): - """ - Respond to a POST request. - Reads and parses the incoming body data then calls L{render}. - - @param request: the request to process. - @return: an object adaptable to L{iweb.IResponse}. - """ - return server.parsePOSTData(request, - self.maxMem, self.maxFields, self.maxSize - ).addCallback(lambda res: self.render(request)) - - -class LeafResource(RenderMixin): - """ - A L{Resource} with no children. - """ - implements(iweb.IResource) - - def locateChild(self, request, segments): - return self, server.StopTraversal - -class RedirectResource(LeafResource): - """ - A L{LeafResource} which always performs a redirect. - """ - implements(iweb.IResource) - - def __init__(self, *args, **kwargs): - """ - Parameters are URL components and are the same as those for - L{urlparse.urlunparse}. URL components which are not specified will - default to the corresponding component of the URL of the request being - redirected. - """ - self._args = args - self._kwargs = kwargs - - def renderHTTP(self, request): - return http.RedirectResponse(request.unparseURL(*self._args, **self._kwargs)) - -class WrapperResource(object): - """ - An L{iweb.IResource} implementation which wraps a L{RenderMixin} instance - and provides a hook in which a subclass can implement logic that is called - before request processing on the contained L{Resource}. - """ - implements(iweb.IResource) - - def __init__(self, resource): - self.resource=resource - - def hook(self, request): - """ - Override this method in order to do something before passing control on - to the wrapped resource's C{renderHTTP} and C{locateChild} methods. - @return: None or a L{Deferred}. If a deferred object is - returned, it's value is ignored, but C{renderHTTP} and - C{locateChild} are chained onto the deferred as callbacks. - """ - raise NotImplementedError() - - def locateChild(self, request, segments): - x = self.hook(request) - if x is not None: - return x.addCallback(lambda data: (self.resource, segments)) - return self.resource, segments - - def renderHTTP(self, request): - x = self.hook(request) - if x is not None: - return x.addCallback(lambda data: self.resource) - return self.resource - - -__all__ = ['RenderMixin', 'Resource', 'PostableResource', 'LeafResource', 'WrapperResource'] diff --git a/xcap/web/responsecode.py b/xcap/web/responsecode.py deleted file mode 100644 index 942266d..0000000 --- a/xcap/web/responsecode.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. -# See LICENSE for details. - -CONTINUE = 100 -SWITCHING = 101 - -OK = 200 -CREATED = 201 -ACCEPTED = 202 -NON_AUTHORITATIVE_INFORMATION = 203 -NO_CONTENT = 204 -RESET_CONTENT = 205 -PARTIAL_CONTENT = 206 -MULTI_STATUS = 207 - -MULTIPLE_CHOICE = 300 -MOVED_PERMANENTLY = 301 -FOUND = 302 -SEE_OTHER = 303 -NOT_MODIFIED = 304 -USE_PROXY = 305 -TEMPORARY_REDIRECT = 307 - -BAD_REQUEST = 400 -UNAUTHORIZED = 401 -PAYMENT_REQUIRED = 402 -FORBIDDEN = 403 -NOT_FOUND = 404 -NOT_ALLOWED = 405 -NOT_ACCEPTABLE = 406 -PROXY_AUTH_REQUIRED = 407 -REQUEST_TIMEOUT = 408 -CONFLICT = 409 -GONE = 410 -LENGTH_REQUIRED = 411 -PRECONDITION_FAILED = 412 -REQUEST_ENTITY_TOO_LARGE = 413 -REQUEST_URI_TOO_LONG = 414 -UNSUPPORTED_MEDIA_TYPE = 415 -REQUESTED_RANGE_NOT_SATISFIABLE = 416 -EXPECTATION_FAILED = 417 -UNPROCESSABLE_ENTITY = 422 # RFC 2518 -LOCKED = 423 # RFC 2518 -FAILED_DEPENDENCY = 424 # RFC 2518 - -INTERNAL_SERVER_ERROR = 500 -NOT_IMPLEMENTED = 501 -BAD_GATEWAY = 502 -SERVICE_UNAVAILABLE = 503 -GATEWAY_TIMEOUT = 504 -HTTP_VERSION_NOT_SUPPORTED = 505 -INSUFFICIENT_STORAGE_SPACE = 507 -NOT_EXTENDED = 510 - -RESPONSES = { - # 100 - CONTINUE: "Continue", - SWITCHING: "Switching Protocols", - - # 200 - OK: "OK", - CREATED: "Created", - ACCEPTED: "Accepted", - NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information", - NO_CONTENT: "No Content", - RESET_CONTENT: "Reset Content.", - PARTIAL_CONTENT: "Partial Content", - MULTI_STATUS: "Multi-Status", - - # 300 - MULTIPLE_CHOICE: "Multiple Choices", - MOVED_PERMANENTLY: "Moved Permanently", - FOUND: "Found", - SEE_OTHER: "See Other", - NOT_MODIFIED: "Not Modified", - USE_PROXY: "Use Proxy", - # 306 unused - TEMPORARY_REDIRECT: "Temporary Redirect", - - # 400 - BAD_REQUEST: "Bad Request", - UNAUTHORIZED: "Unauthorized", - PAYMENT_REQUIRED: "Payment Required", - FORBIDDEN: "Forbidden", - NOT_FOUND: "Not Found", - NOT_ALLOWED: "Method Not Allowed", - NOT_ACCEPTABLE: "Not Acceptable", - PROXY_AUTH_REQUIRED: "Proxy Authentication Required", - REQUEST_TIMEOUT: "Request Time-out", - CONFLICT: "Conflict", - GONE: "Gone", - LENGTH_REQUIRED: "Length Required", - PRECONDITION_FAILED: "Precondition Failed", - REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large", - REQUEST_URI_TOO_LONG: "Request-URI Too Long", - UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type", - REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", - EXPECTATION_FAILED: "Expectation Failed", - UNPROCESSABLE_ENTITY: "Unprocessable Entity", - LOCKED: "Locked", - FAILED_DEPENDENCY: "Failed Dependency", - - # 500 - INTERNAL_SERVER_ERROR: "Internal Server Error", - NOT_IMPLEMENTED: "Not Implemented", - BAD_GATEWAY: "Bad Gateway", - SERVICE_UNAVAILABLE: "Service Unavailable", - GATEWAY_TIMEOUT: "Gateway Time-out", - HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", - INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", - NOT_EXTENDED: "Not Extended" - } - -# No __all__ necessary -- everything is exported diff --git a/xcap/web/server.py b/xcap/web/server.py deleted file mode 100644 index a5b8149..0000000 --- a/xcap/web/server.py +++ /dev/null @@ -1,575 +0,0 @@ -# Copyright (c) 2001-2008 Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -This is a web-server which integrates with the twisted.internet -infrastructure. -""" - -# System Imports -import cgi, time, urllib.parse -from urllib.parse import quote, unquote -from urllib.parse import urlsplit - -import weakref - -from zope.interface import implements -# Twisted Imports -from twisted.internet import defer -from twisted.python import log, failure - -# Sibling Imports -from xcap.web import http, iweb, fileupload, responsecode -from xcap.web import http_headers -from xcap.web.filter.range import rangefilter -from xcap.web import error - -from xcap.web import version as web_version -from twisted import __version__ as twisted_version - -VERSION = "Twisted/%s TwistedWeb/%s" % (twisted_version, web_version) -_errorMarker = object() - - -def defaultHeadersFilter(request, response): - if not response.headers.hasHeader('server'): - response.headers.setHeader('server', VERSION) - if not response.headers.hasHeader('date'): - response.headers.setHeader('date', time.time()) - return response -defaultHeadersFilter.handleErrors = True - -def preconditionfilter(request, response): - if request.method in ("GET", "HEAD"): - http.checkPreconditions(request, response) - return response - -def doTrace(request): - request = iweb.IRequest(request) - txt = "%s %s HTTP/%d.%d\r\n" % (request.method, request.uri, - request.clientproto[0], request.clientproto[1]) - - l=[] - for name, valuelist in request.headers.getAllRawHeaders(): - for value in valuelist: - l.append("%s: %s\r\n" % (name, value)) - txt += ''.join(l) - - return http.Response( - responsecode.OK, - {'content-type': http_headers.MimeType('message', 'http')}, - txt) - - -def parsePOSTData(request, maxMem=100*1024, maxFields=1024, - maxSize=10*1024*1024): - """ - Parse data of a POST request. - - @param request: the request to parse. - @type request: L{xcap.web.http.Request}. - @param maxMem: maximum memory used during the parsing of the data. - @type maxMem: C{int} - @param maxFields: maximum number of form fields allowed. - @type maxFields: C{int} - @param maxSize: maximum size of file upload allowed. - @type maxSize: C{int} - - @return: a deferred that will fire when the parsing is done. The deferred - itself doesn't hold a return value, the request is modified directly. - @rtype: C{defer.Deferred} - """ - if request.stream.length == 0: - return defer.succeed(None) - - parser = None - ctype = request.headers.getHeader('content-type') - - if ctype is None: - return defer.succeed(None) - - def updateArgs(data): - args = data - request.args.update(args) - - def updateArgsAndFiles(data): - args, files = data - request.args.update(args) - request.files.update(files) - - def error(f): - f.trap(fileupload.MimeFormatError) - raise http.HTTPError( - http.StatusResponse(responsecode.BAD_REQUEST, str(f.value))) - - if (ctype.mediaType == 'application' - and ctype.mediaSubtype == 'x-www-form-urlencoded'): - d = fileupload.parse_urlencoded(request.stream) - d.addCallbacks(updateArgs, error) - return d - elif (ctype.mediaType == 'multipart' - and ctype.mediaSubtype == 'form-data'): - boundary = ctype.params.get('boundary') - if boundary is None: - return defer.fail(http.HTTPError( - http.StatusResponse( - responsecode.BAD_REQUEST, - "Boundary not specified in Content-Type."))) - d = fileupload.parseMultipartFormData(request.stream, boundary, - maxMem, maxFields, maxSize) - d.addCallbacks(updateArgsAndFiles, error) - return d - else: - return defer.fail(http.HTTPError( - http.StatusResponse( - responsecode.BAD_REQUEST, - "Invalid content-type: %s/%s" % ( - ctype.mediaType, ctype.mediaSubtype)))) - - -class StopTraversal(object): - """ - Indicates to Request._handleSegment that it should stop handling - path segments. - """ - pass - - -class Request(http.Request): - """ - vars: - site - - remoteAddr - - scheme - host - port - path - params - querystring - - args - files - - prepath - postpath - - @ivar path: The path only (arguments not included). - @ivar args: All of the arguments, including URL and POST arguments. - @type args: A mapping of strings (the argument names) to lists of values. - i.e., ?foo=bar&foo=baz&quux=spam results in - {'foo': ['bar', 'baz'], 'quux': ['spam']}. - - """ - implements(iweb.IRequest) - - site = None - _initialprepath = None - responseFilters = [rangefilter, preconditionfilter, - error.defaultErrorHandler, defaultHeadersFilter] - - def __init__(self, *args, **kw): - if 'site' in kw: - self.site = kw['site'] - del kw['site'] - if 'prepathuri' in kw: - self._initialprepath = kw['prepathuri'] - del kw['prepathuri'] - - # Copy response filters from the class - self.responseFilters = self.responseFilters[:] - self.files = {} - self.resources = [] - http.Request.__init__(self, *args, **kw) - - def addResponseFilter(self, f, atEnd=False): - if atEnd: - self.responseFilters.append(f) - else: - self.responseFilters.insert(0, f) - - def unparseURL(self, scheme=None, host=None, port=None, - path=None, params=None, querystring=None, fragment=None): - """Turn the request path into a url string. For any pieces of - the url that are not specified, use the value from the - request. The arguments have the same meaning as the same named - attributes of Request.""" - - if scheme is None: scheme = self.scheme - if host is None: host = self.host - if port is None: port = self.port - if path is None: path = self.path - if params is None: params = self.params - if querystring is None: query = self.querystring - if fragment is None: fragment = '' - - if port == http.defaultPortForScheme.get(scheme, 0): - hostport = host - else: - hostport = host + ':' + str(port) - - return urllib.parse.urlunparse(( - scheme, hostport, path, - params, querystring, fragment)) - - def _parseURL(self): - if self.uri[0] == '/': - # Can't use urlparse for request_uri because urlparse - # wants to be given an absolute or relative URI, not just - # an abs_path, and thus gets '//foo' wrong. - self.scheme = self.host = self.path = self.params = self.querystring = '' - if '?' in self.uri: - self.path, self.querystring = self.uri.split('?', 1) - else: - self.path = self.uri - if ';' in self.path: - self.path, self.params = self.path.split(';', 1) - else: - # It is an absolute uri, use standard urlparse - (self.scheme, self.host, self.path, - self.params, self.querystring, fragment) = urllib.parse.urlparse(self.uri) - - if self.querystring: - self.args = cgi.parse_qs(self.querystring, True) - else: - self.args = {} - - path = list(map(unquote, self.path[1:].split('/'))) - if self._initialprepath: - # We were given an initial prepath -- this is for supporting - # CGI-ish applications where part of the path has already - # been processed - prepath = list(map(unquote, self._initialprepath[1:].split('/'))) - - if path[:len(prepath)] == prepath: - self.prepath = prepath - self.postpath = path[len(prepath):] - else: - self.prepath = [] - self.postpath = path - else: - self.prepath = [] - self.postpath = path - #print "_parseURL", self.uri, (self.uri, self.scheme, self.host, self.path, self.params, self.querystring) - - def _fixupURLParts(self): - hostaddr, secure = self.chanRequest.getHostInfo() - if not self.scheme: - self.scheme = ('http', 'https')[secure] - - if self.host: - self.host, self.port = http.splitHostPort(self.scheme, self.host) - else: - # If GET line wasn't an absolute URL - host = self.headers.getHeader('host') - if host: - self.host, self.port = http.splitHostPort(self.scheme, host) - else: - # When no hostname specified anywhere, either raise an - # error, or use the interface hostname, depending on - # protocol version - if self.clientproto >= (1,1): - raise http.HTTPError(responsecode.BAD_REQUEST) - self.host = hostaddr.host - self.port = hostaddr.port - - - def process(self): - "Process a request." - try: - self.checkExpect() - resp = self.preprocessRequest() - if resp is not None: - self._cbFinishRender(resp).addErrback(self._processingFailed) - return - self._parseURL() - self._fixupURLParts() - self.remoteAddr = self.chanRequest.getRemoteHost() - except: - failedDeferred = self._processingFailed(failure.Failure()) - return - - d = defer.Deferred() - d.addCallback(self._getChild, self.site.resource, self.postpath) - d.addCallback(lambda res, req: res.renderHTTP(req), self) - d.addCallback(self._cbFinishRender) - d.addErrback(self._processingFailed) - d.callback(None) - - def preprocessRequest(self): - """Do any request processing that doesn't follow the normal - resource lookup procedure. "OPTIONS *" is handled here, for - example. This would also be the place to do any CONNECT - processing.""" - - if self.method == "OPTIONS" and self.uri == "*": - response = http.Response(responsecode.OK) - response.headers.setHeader('allow', ('GET', 'HEAD', 'OPTIONS', 'TRACE')) - return response - # This is where CONNECT would go if we wanted it - return None - - def _getChild(self, _, res, path, updatepaths=True): - """Call res.locateChild, and pass the result on to _handleSegment.""" - - self.resources.append(res) - - if not path: - return res - - result = res.locateChild(self, path) - if isinstance(result, defer.Deferred): - return result.addCallback(self._handleSegment, res, path, updatepaths) - else: - return self._handleSegment(result, res, path, updatepaths) - - def _handleSegment(self, result, res, path, updatepaths): - """Handle the result of a locateChild call done in _getChild.""" - - newres, newpath = result - # If the child resource is None then display a error page - if newres is None: - raise http.HTTPError(responsecode.NOT_FOUND) - - # If we got a deferred then we need to call back later, once the - # child is actually available. - if isinstance(newres, defer.Deferred): - return newres.addCallback( - lambda actualRes: self._handleSegment( - (actualRes, newpath), res, path, updatepaths) - ) - - if path: - url = quote("/" + "/".join(path)) - else: - url = "/" - - if newpath is StopTraversal: - # We need to rethink how to do this. - #if newres is res: - self._rememberResource(res, url) - return res - #else: - # raise ValueError("locateChild must not return StopTraversal with a resource other than self.") - - newres = iweb.IResource(newres) - if newres is res: - assert not newpath is path, "URL traversal cycle detected when attempting to locateChild %r from resource %r." % (path, res) - assert len(newpath) < len(path), "Infinite loop impending..." - - if updatepaths: - # We found a Resource... update the request.prepath and postpath - for x in range(len(path) - len(newpath)): - self.prepath.append(self.postpath.pop(0)) - - child = self._getChild(None, newres, newpath, updatepaths=updatepaths) - self._rememberResource(child, url) - - return child - - _urlsByResource = weakref.WeakKeyDictionary() - - def _rememberResource(self, resource, url): - """ - Remember the URL of a visited resource. - """ - self._urlsByResource[resource] = url - return resource - - def urlForResource(self, resource): - """ - Looks up the URL of the given resource if this resource was found while - processing this request. Specifically, this includes the requested - resource, and resources looked up via L{locateResource}. - - Note that a resource may be found at multiple URIs; if the same resource - is visited at more than one location while processing this request, - this method will return one of those URLs, but which one is not defined, - nor whether the same URL is returned in subsequent calls. - - @param resource: the resource to find a URI for. This resource must - have been obtained from the request (ie. via its C{uri} attribute, or - through its C{locateResource} or C{locateChildResource} methods). - @return: a valid URL for C{resource} in this request. - @raise NoURLForResourceError: if C{resource} has no URL in this request - (because it was not obtained from the request). - """ - resource = self._urlsByResource.get(resource, None) - if resource is None: - raise NoURLForResourceError(resource) - return resource - - def locateResource(self, url): - """ - Looks up the resource with the given URL. - @param uri: The URL of the desired resource. - @return: a L{Deferred} resulting in the L{IResource} at the - given URL or C{None} if no such resource can be located. - @raise HTTPError: If C{url} is not a URL on the site that this - request is being applied to. The contained response will - have a status code of L{responsecode.BAD_GATEWAY}. - @raise HTTPError: If C{url} contains a query or fragment. - The contained response will have a status code of - L{responsecode.BAD_REQUEST}. - """ - if url is None: return None - - # - # Parse the URL - # - (scheme, host, path, query, fragment) = urlsplit(url) - - if query or fragment: - raise http.HTTPError(http.StatusResponse( - responsecode.BAD_REQUEST, - "URL may not contain a query or fragment: %s" % (url,) - )) - - # The caller shouldn't be asking a request on one server to lookup a - # resource on some other server. - if (scheme and scheme != self.scheme) or (host and host != self.headers.getHeader("host")): - raise http.HTTPError(http.StatusResponse( - responsecode.BAD_GATEWAY, - "URL is not on this site (%s://%s/): %s" % (scheme, self.headers.getHeader("host"), url) - )) - - segments = path.split("/") - assert segments[0] == "", "URL path didn't begin with '/': %s" % (path,) - segments = list(map(unquote, segments[1:])) - - def notFound(f): - f.trap(http.HTTPError) - if f.value.response.code != responsecode.NOT_FOUND: - return f - return None - - d = defer.maybeDeferred(self._getChild, None, self.site.resource, segments, updatepaths=False) - d.addCallback(self._rememberResource, path) - d.addErrback(notFound) - return d - - def locateChildResource(self, parent, childName): - """ - Looks up the child resource with the given name given the parent - resource. This is similar to locateResource(), but doesn't have to - start the lookup from the root resource, so it is potentially faster. - @param parent: the parent of the resource being looked up. This resource - must have been obtained from the request (ie. via its C{uri} attribute, - or through its C{locateResource} or C{locateChildResource} methods). - @param childName: the name of the child of C{parent} to looked up. - to C{parent}. - @return: a L{Deferred} resulting in the L{IResource} at the - given URL or C{None} if no such resource can be located. - @raise NoURLForResourceError: if C{resource} was not obtained from the - request. - """ - if parent is None or childName is None: - return None - - assert "/" not in childName, "Child name may not contain '/': %s" % (childName,) - - parentURL = self.urlForResource(parent) - if not parentURL.endswith("/"): - parentURL += "/" - url = parentURL + quote(childName) - - segment = childName - - def notFound(f): - f.trap(http.HTTPError) - if f.value.response.code != responsecode.NOT_FOUND: - return f - return None - - d = defer.maybeDeferred(self._getChild, None, parent, [segment], updatepaths=False) - d.addCallback(self._rememberResource, url) - d.addErrback(notFound) - return d - - def _processingFailed(self, reason): - if reason.check(http.HTTPError) is not None: - # If the exception was an HTTPError, leave it alone - d = defer.succeed(reason.value.response) - else: - # Otherwise, it was a random exception, so give a - # ICanHandleException implementer a chance to render the page. - def _processingFailed_inner(reason): - handler = iweb.ICanHandleException(self, self) - return handler.renderHTTP_exception(self, reason) - d = defer.maybeDeferred(_processingFailed_inner, reason) - - d.addCallback(self._cbFinishRender) - d.addErrback(self._processingReallyFailed, reason) - return d - - def _processingReallyFailed(self, reason, origReason): - log.msg("Exception rendering error page:", isErr=1) - log.err(reason) - log.msg("Original exception:", isErr=1) - log.err(origReason) - - body = ("Internal Server Error" - "

Internal Server Error

An error occurred rendering the requested page. Additionally, an error occured rendering the error page.") - - response = http.Response( - responsecode.INTERNAL_SERVER_ERROR, - {'content-type': http_headers.MimeType('text','html')}, - body) - self.writeResponse(response) - - def _cbFinishRender(self, result): - def filterit(response, f): - if (hasattr(f, 'handleErrors') or - (response.code >= 200 and response.code < 300)): - return f(self, response) - else: - return response - - response = iweb.IResponse(result, None) - if response: - d = defer.Deferred() - for f in self.responseFilters: - d.addCallback(filterit, f) - d.addCallback(self.writeResponse) - d.callback(response) - return d - - resource = iweb.IResource(result, None) - if resource: - self.resources.append(resource) - d = defer.maybeDeferred(resource.renderHTTP, self) - d.addCallback(self._cbFinishRender) - return d - - raise TypeError("html is not a resource or a response") - - def renderHTTP_exception(self, req, reason): - log.msg("Exception rendering:", isErr=1) - log.err(reason) - - body = ("Internal Server Error" - "

Internal Server Error

An error occurred rendering the requested page. More information is available in the server log.") - - return http.Response( - responsecode.INTERNAL_SERVER_ERROR, - {'content-type': http_headers.MimeType('text','html')}, - body) - -class Site(object): - def __init__(self, resource): - """Initialize. - """ - self.resource = iweb.IResource(resource) - - def __call__(self, *args, **kwargs): - return Request(site=self, *args, **kwargs) - - -class NoURLForResourceError(RuntimeError): - def __init__(self, resource): - RuntimeError.__init__(self, "Resource %r has no URL in this request." % (resource,)) - self.resource = resource - - -__all__ = ['Request', 'Site', 'StopTraversal', 'VERSION', 'defaultHeadersFilter', 'doTrace', 'parsePOSTData', 'preconditionfilter', 'NoURLForResourceError'] diff --git a/xcap/web/static.py b/xcap/web/static.py deleted file mode 100644 index 5c7e022..0000000 --- a/xcap/web/static.py +++ /dev/null @@ -1,597 +0,0 @@ -# Copyright (c) 2001-2008 Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" -I deal with static resources. -""" - -# System Imports -import os, time, stat -import tempfile - -# Sibling Imports -from xcap.web import http_headers, resource -from xcap.web import http, iweb, stream, responsecode, server, dirlist - -# Twisted Imports -from twisted.python import filepath -from twisted.internet.defer import maybeDeferred -from zope.interface import implements - -class MetaDataMixin(object): - """ - Mix-in class for L{iweb.IResource} which provides methods for accessing resource - metadata specified by HTTP. - """ - def etag(self): - """ - @return: The current etag for the resource if available, None otherwise. - """ - return None - - def lastModified(self): - """ - @return: The last modified time of the resource if available, None otherwise. - """ - return None - - def creationDate(self): - """ - @return: The creation date of the resource if available, None otherwise. - """ - return None - - def contentLength(self): - """ - @return: The size in bytes of the resource if available, None otherwise. - """ - return None - - def contentType(self): - """ - @return: The MIME type of the resource if available, None otherwise. - """ - return None - - def contentEncoding(self): - """ - @return: The encoding of the resource if available, None otherwise. - """ - return None - - def displayName(self): - """ - @return: The display name of the resource if available, None otherwise. - """ - return None - - def exists(self): - """ - @return: True if the resource exists on the server, False otherwise. - """ - return True - -class StaticRenderMixin(resource.RenderMixin, MetaDataMixin): - def checkPreconditions(self, request): - # This code replaces the code in resource.RenderMixin - if request.method not in ("GET", "HEAD"): - http.checkPreconditions( - request, - entityExists = self.exists(), - etag = self.etag(), - lastModified = self.lastModified(), - ) - - # Check per-method preconditions - method = getattr(self, "preconditions_" + request.method, None) - if method: - return method(request) - - def renderHTTP(self, request): - """ - See L{resource.RenderMixIn.renderHTTP}. - - This implementation automatically sets some headers on the response - based on data available from L{MetaDataMixin} methods. - """ - def setHeaders(response): - response = iweb.IResponse(response) - - # Don't provide additional resource information to error responses - if response.code < 400: - # Content-* headers refer to the response content, not - # (necessarily) to the resource content, so they depend on the - # request method, and therefore can't be set here. - for (header, value) in ( - ("etag", self.etag()), - ("last-modified", self.lastModified()), - ): - if value is not None: - response.headers.setHeader(header, value) - - return response - - def onError(f): - # If we get an HTTPError, run its response through setHeaders() as - # well. - f.trap(http.HTTPError) - return setHeaders(f.value.response) - - d = maybeDeferred(super(StaticRenderMixin, self).renderHTTP, request) - return d.addCallbacks(setHeaders, onError) - -class Data(resource.Resource): - """ - This is a static, in-memory resource. - """ - def __init__(self, data, type): - self.data = data - self.type = http_headers.MimeType.fromString(type) - self.created_time = time.time() - - def etag(self): - lastModified = self.lastModified() - return http_headers.ETag("%X-%X" % (lastModified, hash(self.data)), - weak=(time.time() - lastModified <= 1)) - - def lastModified(self): - return self.creationDate() - - def creationDate(self): - return self.created_time - - def contentLength(self): - return len(self.data) - - def contentType(self): - return self.type - - def render(self, req): - return http.Response( - responsecode.OK, - http_headers.Headers({'content-type': self.contentType()}), - stream=self.data) - - -class File(StaticRenderMixin): - """ - File is a resource that represents a plain non-interpreted file - (although it can look for an extension like .rpy or .cgi and hand the - file to a processor for interpretation if you wish). Its constructor - takes a file path. - - Alternatively, you can give a directory path to the constructor. In this - case the resource will represent that directory, and its children will - be files underneath that directory. This provides access to an entire - filesystem tree with a single Resource. - - If you map the URL 'http://server/FILE' to a resource created as - File('/tmp'), then http://server/FILE/ will return an HTML-formatted - listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will - return the contents of /tmp/foo/bar.html . - """ - implements(iweb.IResource) - - def _getContentTypes(self): - if not hasattr(File, "_sharedContentTypes"): - File._sharedContentTypes = loadMimeTypes() - return File._sharedContentTypes - - contentTypes = property(_getContentTypes) - - contentEncodings = { - ".gz" : "gzip", - ".bz2": "bzip2" - } - - processors = {} - - indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"] - - type = None - - def __init__(self, path, defaultType="text/plain", ignoredExts=(), processors=None, indexNames=None): - """Create a file with the given path. - """ - super(File, self).__init__() - - self.putChildren = {} - self.fp = filepath.FilePath(path) - # Remove the dots from the path to split - self.defaultType = defaultType - self.ignoredExts = list(ignoredExts) - if processors is not None: - self.processors = dict([ - (key.lower(), value) - for key, value in list(processors.items()) - ]) - - if indexNames is not None: - self.indexNames = indexNames - - def exists(self): - return self.fp.exists() - - def etag(self): - if not self.fp.exists(): return None - - st = self.fp.statinfo - - # - # Mark ETag as weak if it was modified more recently than we can - # measure and report, as it could be modified again in that span - # and we then wouldn't know to provide a new ETag. - # - weak = (time.time() - st.st_mtime <= 1) - - return http_headers.ETag( - "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime), - weak=weak - ) - - def lastModified(self): - if self.fp.exists(): - return self.fp.getmtime() - else: - return None - - def creationDate(self): - if self.fp.exists(): - return self.fp.getmtime() - else: - return None - - def contentLength(self): - if self.fp.exists(): - if self.fp.isfile(): - return self.fp.getsize() - else: - # Computing this would require rendering the resource; let's - # punt instead. - return None - else: - return None - - def _initTypeAndEncoding(self): - self._type, self._encoding = getTypeAndEncoding( - self.fp.basename(), - self.contentTypes, - self.contentEncodings, - self.defaultType - ) - - # Handle cases not covered by getTypeAndEncoding() - if self.fp.isdir(): self._type = "httpd/unix-directory" - - def contentType(self): - if not hasattr(self, "_type"): - self._initTypeAndEncoding() - return http_headers.MimeType.fromString(self._type) - - def contentEncoding(self): - if not hasattr(self, "_encoding"): - self._initTypeAndEncoding() - return self._encoding - - def displayName(self): - if self.fp.exists(): - return self.fp.basename() - else: - return None - - def ignoreExt(self, ext): - """Ignore the given extension. - - Serve file.ext if file is requested - """ - self.ignoredExts.append(ext) - - def directoryListing(self): - return dirlist.DirectoryLister(self.fp.path, - self.listChildren(), - self.contentTypes, - self.contentEncodings, - self.defaultType) - - def putChild(self, name, child): - """ - Register a child with the given name with this resource. - @param name: the name of the child (a URI path segment) - @param child: the child to register - """ - self.putChildren[name] = child - - def getChild(self, name): - """ - Look up a child resource. - @return: the child of this resource with the given name. - """ - if name == "": - return self - - child = self.putChildren.get(name, None) - if child: return child - - child_fp = self.fp.child(name) - if child_fp.exists(): - return self.createSimilarFile(child_fp.path) - else: - return None - - def listChildren(self): - """ - @return: a sequence of the names of all known children of this resource. - """ - children = list(self.putChildren.keys()) - if self.fp.isdir(): - children += [c for c in self.fp.listdir() if c not in children] - return children - - def locateChild(self, req, segments): - """ - See L{IResource}C{.locateChild}. - """ - # If getChild() finds a child resource, return it - child = self.getChild(segments[0]) - if child is not None: return (child, segments[1:]) - - # If we're not backed by a directory, we have no children. - # But check for existance first; we might be a collection resource - # that the request wants created. - self.fp.restat(False) - if self.fp.exists() and not self.fp.isdir(): return (None, ()) - - # OK, we need to return a child corresponding to the first segment - path = segments[0] - - if path: - fpath = self.fp.child(path) - else: - # Request is for a directory (collection) resource - return (self, server.StopTraversal) - - # Don't run processors on directories - if someone wants their own - # customized directory rendering, subclass File instead. - if fpath.isfile(): - processor = self.processors.get(fpath.splitext()[1].lower()) - if processor: - return ( - processor(fpath.path), - segments[1:]) - - elif not fpath.exists(): - sibling_fpath = fpath.siblingExtensionSearch(*self.ignoredExts) - if sibling_fpath is not None: - fpath = sibling_fpath - - return self.createSimilarFile(fpath.path), segments[1:] - - def renderHTTP(self, req): - self.fp.restat(False) - return super(File, self).renderHTTP(req) - - def render(self, req): - """You know what you doing.""" - if not self.fp.exists(): - return responsecode.NOT_FOUND - - if self.fp.isdir(): - if req.uri[-1] != "/": - # Redirect to include trailing '/' in URI - return http.RedirectResponse(req.unparseURL(path=req.path+'/')) - else: - ifp = self.fp.childSearchPreauth(*self.indexNames) - if ifp: - # Render from the index file - standin = self.createSimilarFile(ifp.path) - else: - # Render from a DirectoryLister - standin = dirlist.DirectoryLister( - self.fp.path, - self.listChildren(), - self.contentTypes, - self.contentEncodings, - self.defaultType - ) - return standin.render(req) - - try: - f = self.fp.open() - except IOError as e: - import errno - if e[0] == errno.EACCES: - return responsecode.FORBIDDEN - elif e[0] == errno.ENOENT: - return responsecode.NOT_FOUND - else: - raise - - response = http.Response() - response.stream = stream.FileStream(f, 0, self.fp.getsize()) - - for (header, value) in ( - ("content-type", self.contentType()), - ("content-encoding", self.contentEncoding()), - ): - if value is not None: - response.headers.setHeader(header, value) - - return response - - def createSimilarFile(self, path): - return self.__class__(path, self.defaultType, self.ignoredExts, - self.processors, self.indexNames[:]) - - -class FileSaver(resource.PostableResource): - allowedTypes = (http_headers.MimeType('text', 'plain'), - http_headers.MimeType('text', 'html'), - http_headers.MimeType('text', 'css')) - - def __init__(self, destination, expectedFields=[], allowedTypes=None, maxBytes=1000000, permissions=0o644): - self.destination = destination - self.allowedTypes = allowedTypes or self.allowedTypes - self.maxBytes = maxBytes - self.expectedFields = expectedFields - self.permissions = permissions - - def makeUniqueName(self, filename): - """Called when a unique filename is needed. - - filename is the name of the file as given by the client. - - Returns the fully qualified path of the file to create. The - file must not yet exist. - """ - - return tempfile.mktemp(suffix=os.path.splitext(filename)[1], dir=self.destination) - - def isSafeToWrite(self, filename, mimetype, filestream): - """Returns True if it's "safe" to write this file, - otherwise it raises an exception. - """ - - if filestream.length > self.maxBytes: - raise IOError("%s: File exceeds maximum length (%d > %d)" % (filename, - filestream.length, - self.maxBytes)) - - if mimetype not in self.allowedTypes: - raise IOError("%s: File type not allowed %s" % (filename, mimetype)) - - return True - - def writeFile(self, filename, mimetype, fileobject): - """Does the I/O dirty work after it calls isSafeToWrite to make - sure it's safe to write this file. - """ - filestream = stream.FileStream(fileobject) - - if self.isSafeToWrite(filename, mimetype, filestream): - outname = self.makeUniqueName(filename) - - flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0) - - fileobject = os.fdopen(os.open(outname, flags, self.permissions), 'wb', 0) - - stream.readIntoFile(filestream, fileobject) - - return outname - - def render(self, req): - content = [""] - - if req.files: - for fieldName in req.files: - if fieldName in self.expectedFields: - for finfo in req.files[fieldName]: - try: - outname = self.writeFile(*finfo) - content.append("Saved file %s
" % outname) - except IOError as err: - content.append(str(err) + "
") - else: - content.append("%s is not a valid field" % fieldName) - - else: - content.append("No files given") - - content.append("") - - return http.Response(responsecode.OK, {}, stream='\n'.join(content)) - - -# FIXME: hi there I am a broken class -# """I contain AsIsProcessor, which serves files 'As Is' -# Inspired by Apache's mod_asis -# """ -# -# class ASISProcessor: -# implements(iweb.IResource) -# -# def __init__(self, path): -# self.path = path -# -# def renderHTTP(self, request): -# request.startedWriting = 1 -# return File(self.path) -# -# def locateChild(self, request): -# return None, () - -## -# Utilities -## - -dangerousPathError = http.HTTPError(responsecode.NOT_FOUND) #"Invalid request URL." - -def isDangerous(path): - return path == '..' or '/' in path or os.sep in path - -def addSlash(request): - return "http%s://%s%s/" % ( - request.isSecure() and 's' or '', - request.getHeader("host"), - (request.uri.split('?')[0])) - -def loadMimeTypes(mimetype_locations=['/etc/mime.types']): - """ - Multiple file locations containing mime-types can be passed as a list. - The files will be sourced in that order, overriding mime-types from the - files sourced beforehand, but only if a new entry explicitly overrides - the current entry. - """ - import mimetypes - # Grab Python's built-in mimetypes dictionary. - contentTypes = mimetypes.types_map - # Update Python's semi-erroneous dictionary with a few of the - # usual suspects. - contentTypes.update( - { - '.conf': 'text/plain', - '.diff': 'text/plain', - '.exe': 'application/x-executable', - '.flac': 'audio/x-flac', - '.java': 'text/plain', - '.ogg': 'application/ogg', - '.oz': 'text/x-oz', - '.swf': 'application/x-shockwave-flash', - '.tgz': 'application/x-gtar', - '.wml': 'text/vnd.wap.wml', - '.xul': 'application/vnd.mozilla.xul+xml', - '.py': 'text/plain', - '.patch': 'text/plain', - } - ) - # Users can override these mime-types by loading them out configuration - # files (this defaults to ['/etc/mime.types']). - for location in mimetype_locations: - if os.path.exists(location): - contentTypes.update(mimetypes.read_mime_types(location)) - - return contentTypes - -def getTypeAndEncoding(filename, types, encodings, defaultType): - p, ext = os.path.splitext(filename) - ext = ext.lower() - if ext in encodings: - enc = encodings[ext] - ext = os.path.splitext(p)[1].lower() - else: - enc = None - type = types.get(ext, defaultType) - return type, enc - -## -# Test code -## - -if __name__ == '__builtin__': - # Running from twistd -y - from twisted.application import service, strports - from xcap.web import server - res = File('/') - application = service.Application("demo") - s = strports.service('8080', server.Site(res)) - s.setServiceParent(application) diff --git a/xcap/web/stream.py b/xcap/web/stream.py deleted file mode 100644 index 56ff1b1..0000000 --- a/xcap/web/stream.py +++ /dev/null @@ -1,1082 +0,0 @@ - -""" -The stream module provides a simple abstraction of streaming -data. While Twisted already has some provisions for handling this in -its Producer/Consumer model, the rather complex interactions between -producer and consumer makes it difficult to implement something like -the CompoundStream object. Thus, this API. - -The IStream interface is very simple. It consists of two methods: -read, and close. The read method should either return some data, None -if there is no data left to read, or a Deferred. Close frees up any -underlying resources and causes read to return None forevermore. - -IByteStream adds a bit more to the API: -1) read is required to return objects conforming to the buffer interface. -2) .length, which may either an integer number of bytes remaining, or -None if unknown -3) .split(position). Split takes a position, and splits the -stream in two pieces, returning the two new streams. Using the -original stream after calling split is not allowed. - -There are two builtin source stream classes: FileStream and -MemoryStream. The first produces data from a file object, the second -from a buffer in memory. Any number of these can be combined into one -stream with the CompoundStream object. Then, to interface with other -parts of Twisted, there are two transcievers: StreamProducer and -ProducerStream. The first takes a stream and turns it into an -IPushProducer, which will write to a consumer. The second is a -consumer which is a stream, so that other producers can write to it. -""" - - - -import copy, os, types, sys -from zope.interface import Interface, Attribute, implements -from twisted.internet.defer import Deferred -from twisted.internet import interfaces as ti_interfaces, defer, reactor, protocol, error as ti_error -from twisted.python import components, log -from twisted.python.failure import Failure - -# Python 2.4.2 (only) has a broken mmap that leaks a fd every time you call it. -if sys.version_info[0:3] != (2,4,2): - try: - import mmap - except ImportError: - mmap = None -else: - mmap = None - -############################## -#### Interfaces #### -############################## - -class IStream(Interface): - """A stream of arbitrary data.""" - - def read(): - """Read some data. - - Returns some object representing the data. - If there is no more data available, returns None. - Can also return a Deferred resulting in one of the above. - - Errors may be indicated by exception or by a Deferred of a Failure. - """ - - def close(): - """Prematurely close. Should also cause further reads to - return None.""" - -class IByteStream(IStream): - """A stream which is of bytes.""" - - length = Attribute("""How much data is in this stream. Can be None if unknown.""") - - def read(): - """Read some data. - - Returns an object conforming to the buffer interface, or - if there is no more data available, returns None. - Can also return a Deferred resulting in one of the above. - - Errors may be indicated by exception or by a Deferred of a Failure. - """ - def split(point): - """Split this stream into two, at byte position 'point'. - - Returns a tuple of (before, after). After calling split, no other - methods should be called on this stream. Doing so will have undefined - behavior. - - If you cannot implement split easily, you may implement it as:: - - return fallbackSplit(self, point) - """ - - def close(): - """Prematurely close this stream. Should also cause further reads to - return None. Additionally, .length should be set to 0. - """ - -class ISendfileableStream(Interface): - def read(sendfile=False): - """ - Read some data. - If sendfile == False, returns an object conforming to the buffer - interface, or else a Deferred. - - If sendfile == True, returns either the above, or a SendfileBuffer. - """ - -class SimpleStream(object): - """Superclass of simple streams with a single buffer and a offset and length - into that buffer.""" - implements(IByteStream) - - length = None - start = None - - def read(self): - return None - - def close(self): - self.length = 0 - - def split(self, point): - if self.length is not None: - if point > self.length: - raise ValueError("split point (%d) > length (%d)" % (point, self.length)) - b = copy.copy(self) - self.length = point - if b.length is not None: - b.length -= point - b.start += point - return (self, b) - -############################## -#### FileStream #### -############################## - -# maximum mmap size -MMAP_LIMIT = 4*1024*1024 -# minimum mmap size -MMAP_THRESHOLD = 8*1024 - -# maximum sendfile length -SENDFILE_LIMIT = 16777216 -# minimum sendfile size -SENDFILE_THRESHOLD = 256 - -def mmapwrapper(*args, **kwargs): - """ - Python's mmap call sucks and ommitted the "offset" argument for no - discernable reason. Replace this with a mmap module that has offset. - """ - - offset = kwargs.get('offset', None) - if offset in [None, 0]: - if 'offset' in kwargs: - del kwargs['offset'] - else: - raise mmap.error("mmap: Python sucks and does not support offset.") - return mmap.mmap(*args, **kwargs) - -class FileStream(SimpleStream): - implements(ISendfileableStream) - """A stream that reads data from a file. File must be a normal - file that supports seek, (e.g. not a pipe or device or socket).""" - # 65K, minus some slack - CHUNK_SIZE = 2 ** 2 ** 2 ** 2 - 32 - - f = None - def __init__(self, f, start=0, length=None, useMMap=bool(mmap)): - """ - Create the stream from file f. If you specify start and length, - use only that portion of the file. - """ - self.f = f - self.start = start - if length is None: - self.length = os.fstat(f.fileno()).st_size - else: - self.length = length - self.useMMap = useMMap - - def read(self, sendfile=False): - if self.f is None: - return None - - length = self.length - if length == 0: - self.f = None - return None - - if sendfile and length > SENDFILE_THRESHOLD: - # XXX: Yay using non-existent sendfile support! - # FIXME: if we return a SendfileBuffer, and then sendfile - # fails, then what? Or, what if file is too short? - readSize = min(length, SENDFILE_LIMIT) - res = SendfileBuffer(self.f, self.start, readSize) - self.length -= readSize - self.start += readSize - return res - - if self.useMMap and length > MMAP_THRESHOLD: - readSize = min(length, MMAP_LIMIT) - try: - res = mmapwrapper(self.f.fileno(), readSize, - access=mmap.ACCESS_READ, offset=self.start) - #madvise(res, MADV_SEQUENTIAL) - self.length -= readSize - self.start += readSize - return res - except mmap.error: - pass - - # Fall back to standard read. - readSize = min(length, self.CHUNK_SIZE) - - self.f.seek(self.start) - b = self.f.read(readSize) - bytesRead = len(b) - if not bytesRead: - raise RuntimeError("Ran out of data reading file %r, expected %d more bytes" % (self.f, length)) - else: - self.length -= bytesRead - self.start += bytesRead - return b - - def close(self): - self.f = None - SimpleStream.close(self) - -components.registerAdapter(FileStream, file, IByteStream) - -############################## -#### MemoryStream #### -############################## - -class MemoryStream(SimpleStream): - """A stream that reads data from a buffer object.""" - def __init__(self, mem, start=0, length=None): - """ - Create the stream from buffer object mem. If you specify start and length, - use only that portion of the buffer. - """ - self.mem = mem - self.start = start - if length is None: - self.length = len(mem) - start - else: - if len(mem) < length: - raise ValueError("len(mem) < start + length") - self.length = length - - def read(self): - if self.mem is None: - return None - if self.length == 0: - result = None - else: - result = buffer(self.mem, self.start, self.length) - self.mem = None - self.length = 0 - return result - - def close(self): - self.mem = None - SimpleStream.close(self) - -components.registerAdapter(MemoryStream, str, IByteStream) -components.registerAdapter(MemoryStream, memoryview, IByteStream) - -############################## -#### CompoundStream #### -############################## - -class CompoundStream(object): - """A stream which is composed of many other streams. - - Call addStream to add substreams. - """ - - implements(IByteStream, ISendfileableStream) - deferred = None - length = 0 - - def __init__(self, buckets=()): - self.buckets = [IByteStream(s) for s in buckets] - - def addStream(self, bucket): - """Add a stream to the output""" - bucket = IByteStream(bucket) - self.buckets.append(bucket) - if self.length is not None: - if bucket.length is None: - self.length = None - else: - self.length += bucket.length - - def read(self, sendfile=False): - if self.deferred is not None: - raise RuntimeError("Call to read while read is already outstanding") - - if not self.buckets: - return None - - if sendfile and ISendfileableStream.providedBy(self.buckets[0]): - try: - result = self.buckets[0].read(sendfile) - except: - return self._gotFailure(Failure()) - else: - try: - result = self.buckets[0].read() - except: - return self._gotFailure(Failure()) - - if isinstance(result, Deferred): - self.deferred = result - result.addCallbacks(self._gotRead, self._gotFailure, (sendfile,)) - return result - - return self._gotRead(result, sendfile) - - def _gotFailure(self, f): - self.deferred = None - del self.buckets[0] - self.close() - return f - - def _gotRead(self, result, sendfile): - self.deferred = None - if result is None: - del self.buckets[0] - # Next bucket - return self.read(sendfile) - - if self.length is not None: - self.length -= len(result) - return result - - def split(self, point): - num = 0 - origPoint = point - for bucket in self.buckets: - num+=1 - - if point == 0: - b = CompoundStream() - b.buckets = self.buckets[num:] - del self.buckets[num:] - return self,b - - if bucket.length is None: - # Indeterminate length bucket. - # give up and use fallback splitter. - return fallbackSplit(self, origPoint) - - if point < bucket.length: - before,after = bucket.split(point) - b = CompoundStream() - b.buckets = self.buckets[num:] - b.buckets[0] = after - - del self.buckets[num+1:] - self.buckets[num] = before - return self,b - - point -= bucket.length - - def close(self): - for bucket in self.buckets: - bucket.close() - self.buckets = [] - self.length = 0 - - -############################## -#### readStream #### -############################## - -class _StreamReader(object): - """Process a stream's data using callbacks for data and stream finish.""" - - def __init__(self, stream, gotDataCallback): - self.stream = stream - self.gotDataCallback = gotDataCallback - self.result = Deferred() - - def run(self): - # self.result may be del'd in _read() - result = self.result - self._read() - return result - - def _read(self): - try: - result = self.stream.read() - except: - self._gotError(Failure()) - return - if isinstance(result, Deferred): - result.addCallbacks(self._gotData, self._gotError) - else: - self._gotData(result) - - def _gotError(self, failure): - result = self.result - del self.result, self.gotDataCallback, self.stream - result.errback(failure) - - def _gotData(self, data): - if data is None: - result = self.result - del self.result, self.gotDataCallback, self.stream - result.callback(None) - return - try: - self.gotDataCallback(data) - except: - self._gotError(Failure()) - return - reactor.callLater(0, self._read) - -def readStream(stream, gotDataCallback): - """Pass a stream's data to a callback. - - Returns Deferred which will be triggered on finish. Errors in - reading the stream or in processing it will be returned via this - Deferred. - """ - return _StreamReader(stream, gotDataCallback).run() - - -def readAndDiscard(stream): - """Read all the data from the given stream, and throw it out. - - Returns Deferred which will be triggered on finish. - """ - return readStream(stream, lambda _: None) - -def readIntoFile(stream, outFile): - """Read a stream and write it into a file. - - Returns Deferred which will be triggered on finish. - """ - def done(_): - outFile.close() - return _ - return readStream(stream, outFile.write).addBoth(done) - -def connectStream(inputStream, factory): - """Connect a protocol constructed from a factory to stream. - - Returns an output stream from the protocol. - - The protocol's transport will have a finish() method it should - call when done writing. - """ - # XXX deal better with addresses - p = factory.buildProtocol(None) - out = ProducerStream() - out.disconnecting = False # XXX for LineReceiver suckage - p.makeConnection(out) - readStream(inputStream, lambda _: p.dataReceived(_)).addCallbacks( - lambda _: p.connectionLost(ti_error.ConnectionDone()), lambda _: p.connectionLost(_)) - return out - -############################## -#### fallbackSplit #### -############################## - -def fallbackSplit(stream, point): - after = PostTruncaterStream(stream, point) - before = TruncaterStream(stream, point, after) - return (before, after) - -class TruncaterStream(object): - def __init__(self, stream, point, postTruncater): - self.stream = stream - self.length = point - self.postTruncater = postTruncater - - def read(self): - if self.length == 0: - if self.postTruncater is not None: - postTruncater = self.postTruncater - self.postTruncater = None - postTruncater.sendInitialSegment(self.stream.read()) - self.stream = None - return None - - result = self.stream.read() - if isinstance(result, Deferred): - return result.addCallback(self._gotRead) - else: - return self._gotRead(result) - - def _gotRead(self, data): - if data is None: - raise ValueError("Ran out of data for a split of a indeterminate length source") - if self.length >= len(data): - self.length -= len(data) - return data - else: - before = buffer(data, 0, self.length) - after = buffer(data, self.length) - self.length = 0 - if self.postTruncater is not None: - postTruncater = self.postTruncater - self.postTruncater = None - postTruncater.sendInitialSegment(after) - self.stream = None - return before - - def split(self, point): - if point > self.length: - raise ValueError("split point (%d) > length (%d)" % (point, self.length)) - - post = PostTruncaterStream(self.stream, point) - trunc = TruncaterStream(post, self.length - point, self.postTruncater) - self.length = point - self.postTruncater = post - return self, trunc - - def close(self): - if self.postTruncater is not None: - self.postTruncater.notifyClosed(self) - else: - # Nothing cares about the rest of the stream - self.stream.close() - self.stream = None - self.length = 0 - - -class PostTruncaterStream(object): - deferred = None - sentInitialSegment = False - truncaterClosed = None - closed = False - - length = None - def __init__(self, stream, point): - self.stream = stream - self.deferred = Deferred() - if stream.length is not None: - self.length = stream.length - point - - def read(self): - if not self.sentInitialSegment: - self.sentInitialSegment = True - if self.truncaterClosed is not None: - readAndDiscard(self.truncaterClosed) - self.truncaterClosed = None - return self.deferred - - return self.stream.read() - - def split(self, point): - return fallbackSplit(self, point) - - def close(self): - self.closed = True - if self.truncaterClosed is not None: - # have first half close itself - self.truncaterClosed.postTruncater = None - self.truncaterClosed.close() - elif self.sentInitialSegment: - # first half already finished up - self.stream.close() - - self.deferred = None - - # Callbacks from TruncaterStream - def sendInitialSegment(self, data): - if self.closed: - # First half finished, we don't want data. - self.stream.close() - self.stream = None - if self.deferred is not None: - if isinstance(data, Deferred): - data.chainDeferred(self.deferred) - else: - self.deferred.callback(data) - - def notifyClosed(self, truncater): - if self.closed: - # we are closed, have first half really close - truncater.postTruncater = None - truncater.close() - elif self.sentInitialSegment: - # We are trying to read, read up first half - readAndDiscard(truncater) - else: - # Idle, store closed info. - self.truncaterClosed = truncater - -######################################## -#### ProducerStream/StreamProducer #### -######################################## - -class ProducerStream(object): - """Turns producers into a IByteStream. - Thus, implements IConsumer and IByteStream.""" - - implements(IByteStream, ti_interfaces.IConsumer) - length = None - closed = False - failed = False - producer = None - producerPaused = False - deferred = None - - bufferSize = 5 - - def __init__(self, length=None): - self.buffer = [] - self.length = length - - # IByteStream implementation - def read(self): - if self.buffer: - return self.buffer.pop(0) - elif self.closed: - self.length = 0 - if self.failed: - f = self.failure - del self.failure - return defer.fail(f) - return None - else: - deferred = self.deferred = Deferred() - if self.producer is not None and (not self.streamingProducer - or self.producerPaused): - self.producerPaused = False - self.producer.resumeProducing() - - return deferred - - def split(self, point): - return fallbackSplit(self, point) - - def close(self): - """Called by reader of stream when it is done reading.""" - self.buffer=[] - self.closed = True - if self.producer is not None: - self.producer.stopProducing() - self.producer = None - self.deferred = None - - # IConsumer implementation - def write(self, data): - if self.closed: - return - - if self.deferred: - deferred = self.deferred - self.deferred = None - deferred.callback(data) - else: - self.buffer.append(data) - if(self.producer is not None and self.streamingProducer - and len(self.buffer) > self.bufferSize): - self.producer.pauseProducing() - self.producerPaused = True - - def finish(self, failure=None): - """Called by producer when it is done. - - If the optional failure argument is passed a Failure instance, - the stream will return it as errback on next Deferred. - """ - self.closed = True - if not self.buffer: - self.length = 0 - if self.deferred is not None: - deferred = self.deferred - self.deferred = None - if failure is not None: - self.failed = True - deferred.errback(failure) - else: - deferred.callback(None) - else: - if failure is not None: - self.failed = True - self.failure = failure - - def registerProducer(self, producer, streaming): - if self.producer is not None: - raise RuntimeError("Cannot register producer %s, because producer %s was never unregistered." % (producer, self.producer)) - - if self.closed: - producer.stopProducing() - else: - self.producer = producer - self.streamingProducer = streaming - if not streaming: - producer.resumeProducing() - - def unregisterProducer(self): - self.producer = None - -class StreamProducer(object): - """A push producer which gets its data by reading a stream.""" - implements(ti_interfaces.IPushProducer) - - deferred = None - finishedCallback = None - paused = False - consumer = None - - def __init__(self, stream, enforceStr=True): - self.stream = stream - self.enforceStr = enforceStr - - def beginProducing(self, consumer): - if self.stream is None: - return defer.succeed(None) - - self.consumer = consumer - finishedCallback = self.finishedCallback = Deferred() - self.consumer.registerProducer(self, True) - self.resumeProducing() - return finishedCallback - - def resumeProducing(self): - self.paused = False - if self.deferred is not None: - return - - try: - data = self.stream.read() - except: - self.stopProducing(Failure()) - return - - if isinstance(data, Deferred): - self.deferred = data.addCallbacks(self._doWrite, self.stopProducing) - else: - self._doWrite(data) - - def _doWrite(self, data): - if self.consumer is None: - return - if data is None: - # The end. - if self.consumer is not None: - self.consumer.unregisterProducer() - if self.finishedCallback is not None: - self.finishedCallback.callback(None) - self.finishedCallback = self.deferred = self.consumer = self.stream = None - return - - self.deferred = None - if self.enforceStr: - # XXX: sucks that we have to do this. make transport.write(buffer) work! - data = str(buffer(data)) - self.consumer.write(data) - - if not self.paused: - self.resumeProducing() - - def pauseProducing(self): - self.paused = True - - def stopProducing(self, failure=ti_error.ConnectionLost()): - if self.consumer is not None: - self.consumer.unregisterProducer() - if self.finishedCallback is not None: - if failure is not None: - self.finishedCallback.errback(failure) - else: - self.finishedCallback.callback(None) - self.finishedCallback = None - self.paused = True - if self.stream is not None: - self.stream.close() - - self.finishedCallback = self.deferred = self.consumer = self.stream = None - -############################## -#### ProcessStreamer #### -############################## - -class _ProcessStreamerProtocol(protocol.ProcessProtocol): - - def __init__(self, inputStream, outStream, errStream): - self.inputStream = inputStream - self.outStream = outStream - self.errStream = errStream - self.resultDeferred = defer.Deferred() - - def connectionMade(self): - p = StreamProducer(self.inputStream) - # if the process stopped reading from the input stream, - # this is not an error condition, so it oughtn't result - # in a ConnectionLost() from the input stream: - p.stopProducing = lambda err=None: StreamProducer.stopProducing(p, err) - - d = p.beginProducing(self.transport) - d.addCallbacks(lambda _: self.transport.closeStdin(), - self._inputError) - - def _inputError(self, f): - log.msg("Error in input stream for %r" % self.transport) - log.err(f) - self.transport.closeStdin() - - def outReceived(self, data): - self.outStream.write(data) - - def errReceived(self, data): - self.errStream.write(data) - - def outConnectionLost(self): - self.outStream.finish() - - def errConnectionLost(self): - self.errStream.finish() - - def processEnded(self, reason): - self.resultDeferred.errback(reason) - del self.resultDeferred - - -class ProcessStreamer(object): - """Runs a process hooked up to streams. - - Requires an input stream, has attributes 'outStream' and 'errStream' - for stdout and stderr. - - outStream and errStream are public attributes providing streams - for stdout and stderr of the process. - """ - - def __init__(self, inputStream, program, args, env={}): - self.outStream = ProducerStream() - self.errStream = ProducerStream() - self._protocol = _ProcessStreamerProtocol(IByteStream(inputStream), self.outStream, self.errStream) - self._program = program - self._args = args - self._env = env - - def run(self): - """Run the process. - - Returns Deferred which will eventually have errback for non-clean (exit code > 0) - exit, with ProcessTerminated, or callback with None on exit code 0. - """ - # XXX what happens if spawn fails? - reactor.spawnProcess(self._protocol, self._program, self._args, env=self._env) - del self._env - return self._protocol.resultDeferred.addErrback(lambda _: _.trap(ti_error.ProcessDone)) - -############################## -#### generatorToStream #### -############################## - -class _StreamIterator(object): - done=False - - def __iter__(self): - return self - def __next__(self): - if self.done: - raise StopIteration - return self.value - wait=object() - -class _IteratorStream(object): - length = None - - def __init__(self, fun, stream, args, kwargs): - self._stream=stream - self._streamIterator = _StreamIterator() - self._gen = fun(self._streamIterator, *args, **kwargs) - - def read(self): - try: - val = next(self._gen) - except StopIteration: - return None - else: - if val is _StreamIterator.wait: - newdata = self._stream.read() - if isinstance(newdata, defer.Deferred): - return newdata.addCallback(self._gotRead) - else: - return self._gotRead(newdata) - return val - - def _gotRead(self, data): - if data is None: - self._streamIterator.done=True - else: - self._streamIterator.value=data - return self.read() - - def close(self): - self._stream.close() - del self._gen, self._stream, self._streamIterator - - def split(self): - return fallbackSplit(self) - -def generatorToStream(fun): - """Converts a generator function into a stream. - - The function should take an iterator as its first argument, - which will be converted *from* a stream by this wrapper, and - yield items which are turned *into* the results from the - stream's 'read' call. - - One important point: before every call to input.next(), you - *MUST* do a "yield input.wait" first. Yielding this magic value - takes care of ensuring that the input is not a deferred before - you see it. - - >>> from xcap.web import stream - >>> from string import maketrans - >>> alphabet = 'abcdefghijklmnopqrstuvwxyz' - >>> - >>> def encrypt(input, key): - ... code = alphabet[key:] + alphabet[:key] - ... translator = maketrans(alphabet+alphabet.upper(), code+code.upper()) - ... yield input.wait - ... for s in input: - ... yield str(s).translate(translator) - ... yield input.wait - ... - >>> encrypt = stream.generatorToStream(encrypt) - >>> - >>> plaintextStream = stream.MemoryStream('SampleSampleSample') - >>> encryptedStream = encrypt(plaintextStream, 13) - >>> encryptedStream.read() - 'FnzcyrFnzcyrFnzcyr' - >>> - >>> plaintextStream = stream.MemoryStream('SampleSampleSample') - >>> encryptedStream = encrypt(plaintextStream, 13) - >>> evenMoreEncryptedStream = encrypt(encryptedStream, 13) - >>> evenMoreEncryptedStream.read() - 'SampleSampleSample' - - """ - def generatorToStream_inner(stream, *args, **kwargs): - return _IteratorStream(fun, stream, args, kwargs) - return generatorToStream_inner - - -############################## -#### BufferedStream #### -############################## - -class BufferedStream(object): - """A stream which buffers its data to provide operations like - readline and readExactly.""" - - data = "" - def __init__(self, stream): - self.stream = stream - - def _readUntil(self, f): - """Internal helper function which repeatedly calls f each time - after more data has been received, until it returns non-None.""" - while True: - r = f() - if r is not None: - yield r; return - - newdata = self.stream.read() - if isinstance(newdata, defer.Deferred): - newdata = defer.waitForDeferred(newdata) - yield newdata; newdata = newdata.getResult() - - if newdata is None: - # End Of File - newdata = self.data - self.data = '' - yield newdata; return - self.data += str(newdata) - _readUntil = defer.deferredGenerator(_readUntil) - - def readExactly(self, size=None): - """Read exactly size bytes of data, or, if size is None, read - the entire stream into a string.""" - if size is not None and size < 0: - raise ValueError("readExactly: size cannot be negative: %s", size) - - def gotdata(): - data = self.data - if size is not None and len(data) >= size: - pre,post = data[:size], data[size:] - self.data = post - return pre - return self._readUntil(gotdata) - - - def readline(self, delimiter='\r\n', size=None): - """ - Read a line of data from the string, bounded by - delimiter. The delimiter is included in the return value. - - If size is specified, read and return at most that many bytes, - even if the delimiter has not yet been reached. If the size - limit falls within a delimiter, the rest of the delimiter, and - the next line will be returned together. - """ - if size is not None and size < 0: - raise ValueError("readline: size cannot be negative: %s" % (size, )) - - def gotdata(): - data = self.data - if size is not None: - splitpoint = data.find(delimiter, 0, size) - if splitpoint == -1: - if len(data) >= size: - splitpoint = size - else: - splitpoint += len(delimiter) - else: - splitpoint = data.find(delimiter) - if splitpoint != -1: - splitpoint += len(delimiter) - - if splitpoint != -1: - pre = data[:splitpoint] - self.data = data[splitpoint:] - return pre - return self._readUntil(gotdata) - - def pushback(self, pushed): - """Push data back into the buffer.""" - - self.data = pushed + self.data - - def read(self): - data = self.data - if data: - self.data = "" - return data - return self.stream.read() - - def _len(self): - l = self.stream.length - if l is None: - return None - return l + len(self.data) - - length = property(_len) - - def split(self, offset): - off = offset - len(self.data) - - pre, post = self.stream.split(max(0, off)) - pre = BufferedStream(pre) - post = BufferedStream(post) - if off < 0: - pre.data = self.data[:-off] - post.data = self.data[-off:] - else: - pre.data = self.data - - return pre, post - - -def substream(stream, start, end): - if start > end: - raise ValueError("start position must be less than end position %r" - % ((start, end),)) - stream = stream.split(start)[1] - return stream.split(end - start)[0] - - - -__all__ = ['IStream', 'IByteStream', 'FileStream', 'MemoryStream', 'CompoundStream', - 'readAndDiscard', 'fallbackSplit', 'ProducerStream', 'StreamProducer', - 'BufferedStream', 'readStream', 'ProcessStreamer', 'readIntoFile', - 'generatorToStream'] -