diff --git a/xcap/authentication.py b/xcap/authentication.py
index 24bb566..413f793 100644
--- a/xcap/authentication.py
+++ b/xcap/authentication.py
@@ -1,336 +1,366 @@
"""XCAP authentication module"""
# XXX this module should be either renamed or refactored as it does more then just auth.
-
-from xcap import tweaks; tweaks.tweak_BasicCredentialFactory()
-
+from hashlib import md5
from zope.interface import Interface, implements
from twisted.internet import defer
from twisted.python import failure
from twisted.cred import credentials, portal, checkers, error as credError
from twisted.web2 import http, server, stream, responsecode, http_headers
from twisted.web2.auth.wrapper import HTTPAuthResource, UnauthorizedResponse
from application.configuration.datatypes import NetworkRangeList
from application.configuration import ConfigSection, ConfigSetting
import struct
import socket
import urlparse
import xcap
from xcap.datatypes import XCAPRootURI
from xcap.appusage import getApplicationForURI, namespaces, public_get_applications
from xcap.errors import ResourceNotFound
from xcap.uri import XCAPUser, XCAPUri
+from twisted.web2.auth import basic, digest
# body of 404 error message to render when user requests xcap-root
# it's html, because XCAP root is often published on the web.
# NOTE: there're no plans to convert other error messages to html.
# Since a web-browser is not the primary tool for accessing XCAP server, text/plain
# is easier for clients to present to user/save to logs/etc.
WELCOME = ('
Not Found'
'Not Found
XCAP server does not serve anything '
'directly under XCAP Root URL. You have to be more specific.'
'
'
'OpenXCAP/%s'
'') % xcap.__version__
class AuthenticationConfig(ConfigSection):
__cfgfile__ = xcap.__cfgfile__
__section__ = 'Authentication'
default_realm = ConfigSetting(type=str, value=None)
trusted_peers = ConfigSetting(type=NetworkRangeList, value=NetworkRangeList('none'))
class ServerConfig(ConfigSection):
__cfgfile__ = xcap.__cfgfile__
__section__ = 'Server'
root = ConfigSetting(type=XCAPRootURI, value=None)
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).iteritems():
l.append("%s=%s" % (k, k in ("algorithm", "stale") and v or http_headers.quoteString(v)))
_generated.append("%s %s" % (scheme, ", ".join(l)))
except ValueError:
_generated.append("%s %s" % (scheme, challenge))
return _generated
http_headers.generator_response_headers["WWW-Authenticate"] = (generateWWWAuthenticate,)
http_headers.DefaultHTTPHandler.updateGenerators(http_headers.generator_response_headers)
del generateWWWAuthenticate
def parseNodeURI(node_uri, default_realm):
"""Parses the given Node URI, containing the XCAP root, document selector,
and node selector, and returns an XCAPUri instance if succesful."""
xcap_root = None
for uri in ServerConfig.root.uris:
if node_uri.startswith(uri):
xcap_root = uri
break
if xcap_root is None:
raise ResourceNotFound("XCAP root not found for URI: %s" % node_uri)
resource_selector = node_uri[len(xcap_root):]
if not resource_selector or resource_selector=='/':
raise ResourceNotFound(WELCOME, http_headers.MimeType("text", "html"))
r = XCAPUri(xcap_root, resource_selector, namespaces)
if r.user.domain is None:
r.user.domain = default_realm
return r
class ITrustedPeerCredentials(credentials.ICredentials):
def checkPeer(self, trusted_peers):
pass
class TrustedPeerCredentials(object):
implements(ITrustedPeerCredentials)
def __init__(self, peer):
self.peer = peer
def checkPeer(self, trusted_peers):
for range in trusted_peers:
if struct.unpack('!L', socket.inet_aton(self.peer))[0] & range[1] == range[0]:
return True
return False
class IPublicGetApplicationCredentials(credentials.ICredentials):
def checkApplication(self):
pass
class PublicGetApplicationCredentials(object):
implements(IPublicGetApplicationCredentials)
def checkApplication(self):
return True
## credentials checkers
class TrustedPeerChecker(object):
implements(checkers.ICredentialsChecker)
credentialInterfaces = (ITrustedPeerCredentials,)
def __init__(self, trusted_peers):
self.trusted_peers = trusted_peers
def requestAvatarId(self, credentials):
"""Return the avatar ID for the credentials which must have a 'peer' attribute,
or an UnauthorizedLogin in case of a failure."""
if credentials.checkPeer(self.trusted_peers):
return defer.succeed(credentials.peer)
return defer.fail(credError.UnauthorizedLogin())
class PublicGetApplicationChecker(object):
implements(checkers.ICredentialsChecker)
credentialInterfaces = (IPublicGetApplicationCredentials,)
def requestAvatarId(self, credentials):
"""We already know that the method is GET and the application is a 'public GET application',
we just need to say that the authentication succeeded."""
if credentials.checkApplication():
return defer.succeed(None)
return defer.fail(credError.UnauthorizedLogin())
## avatars
class IAuthUser(Interface):
pass
class ITrustedPeer(Interface):
pass
class IPublicGetApplication(Interface):
pass
class AuthUser(str):
"""Authenticated XCAP User avatar."""
implements(IAuthUser)
class TrustedPeer(str):
"""Trusted peer avatar."""
implements(ITrustedPeer)
class PublicGetApplication(str):
"""Public get application avatar."""
implements(IPublicGetApplication)
## realm
class XCAPAuthRealm(object):
"""XCAP authentication realm. Receives an avatar ID (a string identifying the user)
and a list of interfaces the avatar needs to support. It returns an avatar that
encapsulates data about that user."""
implements(portal.IRealm)
def requestAvatar(self, avatarId, mind, *interfaces):
if IAuthUser in interfaces:
return IAuthUser, AuthUser(avatarId)
elif ITrustedPeer in interfaces:
return ITrustedPeer, TrustedPeer(avatarId)
elif IPublicGetApplication in interfaces:
return IPublicGetApplication, PublicGetApplication(avatarId)
raise NotImplementedError("Only IAuthUser and ITrustedPeer interfaces are supported")
def get_cred(request, default_realm):
auth = request.headers.getHeader('authorization')
if auth:
typ, data = auth
if typ == 'basic':
return data.decode('base64').split(':', 1)[0], default_realm
elif typ == 'digest':
raise NotImplementedError
return None, default_realm
## authentication wrapper for XCAP resources
class XCAPAuthResource(HTTPAuthResource):
def allowedMethods(self):
return 'GET', 'PUT', 'DELETE'
def _updateRealm(self, realm):
"""Updates the realm of the attached credential factories."""
for factory in self.credentialFactories.values():
factory.realm = realm
def authenticate(self, request):
"""Authenticates an XCAP request."""
parsed_url = urlparse.urlparse(request.uri)
if request.port in (80, 443):
uri = request.scheme + "://" + request.host + parsed_url.path
else:
uri = request.scheme + "://" + request.host + ":" + str(request.port) + parsed_url.path
if parsed_url.query:
uri += "?%s" % parsed_url.query
xcap_uri = parseNodeURI(uri, AuthenticationConfig.default_realm)
request.xcap_uri = xcap_uri
if xcap_uri.doc_selector.context=='global':
return defer.succeed(self.wrappedResource)
## For each request the authentication realm must be
## dinamically deducted from the XCAP request URI
realm = xcap_uri.user.domain
if realm is None:
raise ResourceNotFound('Unknown domain (the domain part of "username@domain" is required because this server has no default domain)')
if not xcap_uri.user.username:
# for 'global' requests there's no username@domain in the URI,
# so we will use username and domain from Authorization header
xcap_uri.user.username, xcap_uri.user.domain = get_cred(request, AuthenticationConfig.default_realm)
self._updateRealm(realm)
# If we receive a GET to a 'public GET application' we will not authenticate it
if request.method == "GET" and public_get_applications.has_key(xcap_uri.application_id):
return self.portal.login(PublicGetApplicationCredentials(),
None,
IPublicGetApplication
).addCallbacks(self._loginSucceeded,
self._publicGetApplicationLoginFailed,
(request,), None,
(request,), None)
remote_addr = request.remoteAddr.host
if AuthenticationConfig.trusted_peers:
return self.portal.login(TrustedPeerCredentials(remote_addr),
None,
ITrustedPeer
).addCallbacks(self._loginSucceeded,
self._trustedPeerLoginFailed,
(request,), None,
(request,), None)
return HTTPAuthResource.authenticate(self, request)
def _trustedPeerLoginFailed(self, result, request):
"""If the peer is not trusted, fallback to HTTP basic/digest authentication."""
return HTTPAuthResource.authenticate(self, request)
def _publicGetApplicationLoginFailed(self, result, request):
return HTTPAuthResource.authenticate(self, request)
def _loginSucceeded(self, avatar, request):
"""Authorizes an XCAP request after it has been authenticated."""
interface, avatar_id = avatar ## the avatar is the authenticated XCAP User
xcap_uri = request.xcap_uri
application = getApplicationForURI(xcap_uri)
if not application:
raise ResourceNotFound
if interface is IAuthUser and application.is_authorized(XCAPUser.parse(avatar_id), xcap_uri):
return HTTPAuthResource._loginSucceeded(self, avatar, request)
elif interface is ITrustedPeer or interface is IPublicGetApplication:
return HTTPAuthResource._loginSucceeded(self, avatar, request)
else:
return failure.Failure(
http.HTTPError(
UnauthorizedResponse(
self.credentialFactories,
request.remoteAddr)))
def locateChild(self, request, seg):
"""
Authenticate the request then return the C{self.wrappedResource}
and the unmodified segments.
We're not using path location, we want to fall back to the renderHTTP() call.
"""
#return self.authenticate(request), seg
return self, server.StopTraversal
def renderHTTP(self, request):
"""
Authenticate the request then return the result of calling renderHTTP
on C{self.wrappedResource}
"""
if request.method not in self.allowedMethods():
response = http.Response(responsecode.NOT_ALLOWED)
response.headers.setHeader("allow", self.allowedMethods())
return response
def _renderResource(resource):
return resource.renderHTTP(request)
def _finished_reading(ignore, result):
data = ''.join(result)
request.attachment = data
d = self.authenticate(request)
d.addCallback(_renderResource)
return d
if request.method in ('PUT', 'DELETE'):
# we need to authenticate the request after all the attachment stream
# has been read
# QQQ DELETE doesn't have any attachments, does it? nor does GET.
# QQQ Reading attachment when there isn't one won't hurt, will it?
# QQQ So why don't we just do it all the time for all requests?
data = []
d = stream.readStream(request.stream, data.append)
d.addCallback(_finished_reading, data)
return d
else:
d = self.authenticate(request)
d.addCallback(_renderResource)
return d
+
+
+class BasicCredentials(credentials.UsernamePassword):
+ """Custom Basic Credentials, which support both plain and hashed checks."""
+
+ implements(credentials.IUsernamePassword, digest.IUsernameDigestHash)
+
+ def __init__(self, username, password, realm):
+ credentials.UsernamePassword.__init__(self, username, password)
+ self.realm = realm
+
+ @property
+ def hash(self):
+ return md5('{0.username}:{0.realm}:{0.password}'.format(self)).hexdigest()
+
+ def checkHash(self, digestHash):
+ return digestHash == self.hash
+
+
+class BasicCredentialFactory(basic.BasicCredentialFactory):
+ def decode(self, response, request):
+ credential = super(BasicCredentialFactory, self).decode(response, request)
+ return BasicCredentials(credential.username, credential.password, self.realm)
+
+
+class DigestCredentialFactory(digest.DigestCredentialFactory):
+ def generateOpaque(self, nonce, clientip):
+ return super(DigestCredentialFactory, self).generateOpaque(nonce=nonce, clientip=clientip or '')
+
+ def verifyOpaque(self, opaque, nonce, clientip):
+ return super(DigestCredentialFactory, self).verifyOpaque(opaque=opaque, nonce=nonce, clientip=clientip or '')
diff --git a/xcap/server.py b/xcap/server.py
index c62954d..2259d7f 100644
--- a/xcap/server.py
+++ b/xcap/server.py
@@ -1,179 +1,177 @@
"""HTTP handling for the XCAP server"""
from __future__ import absolute_import
import resource as _resource
import sys
from application.configuration.datatypes import IPAddress, NetworkRangeList
from application.configuration import ConfigSection, ConfigSetting
from application import log
from twisted.internet import reactor
from twisted.web2 import channel, resource, http, responsecode, server
from twisted.cred.portal import Portal
-from twisted.web2.auth import basic
-from xcap.tweaks import tweak_DigestCredentialFactory
import xcap
from xcap import authentication
from xcap.datatypes import XCAPRootURI
from xcap.appusage import getApplicationForURI, Backend
from xcap.resource import XCAPDocument, XCAPElement, XCAPAttribute, XCAPNamespaceBinding
from xcap.logutil import web_logger
from xcap.tls import Certificate, PrivateKey
from xcap.xpath import AttributeSelector, NamespaceSelector
server.VERSION = "OpenXCAP/%s" % xcap.__version__
class AuthenticationConfig(ConfigSection):
__cfgfile__ = xcap.__cfgfile__
__section__ = 'Authentication'
type = 'digest'
cleartext_passwords = True
default_realm = ConfigSetting(type=str, value=None)
trusted_peers = ConfigSetting(type=NetworkRangeList, value=NetworkRangeList('none'))
class ServerConfig(ConfigSection):
__cfgfile__ = xcap.__cfgfile__
__section__ = 'Server'
address = ConfigSetting(type=IPAddress, value='0.0.0.0')
root = ConfigSetting(type=XCAPRootURI, value=None)
backend = ConfigSetting(type=Backend, value=None)
class TLSConfig(ConfigSection):
__cfgfile__ = xcap.__cfgfile__
__section__ = 'TLS'
certificate = ConfigSetting(type=Certificate, value=None)
private_key = ConfigSetting(type=PrivateKey, value=None)
if ServerConfig.root is None:
log.critical('The XCAP root URI is not defined')
sys.exit(1)
if ServerConfig.backend is None:
log.critical('OpenXCAP needs a backend to be specified in order to run')
sys.exit(1)
# Increase the system limit for the maximum number of open file descriptors
try:
_resource.setrlimit(_resource.RLIMIT_NOFILE, (99999, 99999))
except ValueError:
log.warning('Could not raise open file descriptor limit')
class XCAPRoot(resource.Resource, resource.LeafResource):
addSlash = True
def allowedMethods(self):
# not used , but methods were already checked by XCAPAuthResource
return ('GET', 'PUT', 'DELETE')
def resourceForURI(self, xcap_uri):
application = getApplicationForURI(xcap_uri)
if not xcap_uri.node_selector:
return XCAPDocument(xcap_uri, application)
else:
terminal_selector = xcap_uri.node_selector.terminal_selector
if isinstance(terminal_selector, AttributeSelector):
return XCAPAttribute(xcap_uri, application)
elif isinstance(terminal_selector, NamespaceSelector):
return XCAPNamespaceBinding(xcap_uri, application)
else:
return XCAPElement(xcap_uri, application)
def renderHTTP(self, request):
application = getApplicationForURI(request.xcap_uri)
if not application:
return http.Response(responsecode.NOT_FOUND, stream="Application not supported")
resource = self.resourceForURI(request.xcap_uri)
return resource.renderHTTP(request)
class Request(server.Request):
def writeResponse(self, response):
web_logger.log_access(request=self, response=response)
return server.Request.writeResponse(self, response)
class HTTPChannel(channel.http.HTTPChannel):
inputTimeOut = 30
def __init__(self):
channel.http.HTTPChannel.__init__(self)
# if connection wasn't completed for 30 seconds, terminate it,
# this avoids having lingering TCP connections which don't complete
# the TLS handshake
self.setTimeout(30)
def timeoutConnection(self):
if self.transport:
log.info('Timing out client: {}'.format(self.transport.getPeer()))
channel.http.HTTPChannel.timeoutConnection(self)
class HTTPFactory(channel.HTTPFactory):
noisy = False
protocol = HTTPChannel
class XCAPSite(server.Site):
def __call__(self, *args, **kwargs):
return Request(site=self, *args, **kwargs)
class XCAPServer(object):
def __init__(self):
portal = Portal(authentication.XCAPAuthRealm())
if AuthenticationConfig.cleartext_passwords:
http_checker = ServerConfig.backend.PlainPasswordChecker()
else:
http_checker = ServerConfig.backend.HashPasswordChecker()
portal.registerChecker(http_checker)
trusted_peers = AuthenticationConfig.trusted_peers
portal.registerChecker(authentication.TrustedPeerChecker(trusted_peers))
portal.registerChecker(authentication.PublicGetApplicationChecker())
auth_type = AuthenticationConfig.type
if auth_type == 'basic':
- credential_factory = basic.BasicCredentialFactory(auth_type)
+ credential_factory = authentication.BasicCredentialFactory(auth_type)
elif auth_type == 'digest':
- credential_factory = tweak_DigestCredentialFactory('MD5', auth_type)
+ credential_factory = authentication.DigestCredentialFactory('MD5', auth_type)
else:
raise ValueError('Invalid authentication type: %r. Please check the configuration.' % auth_type)
root = authentication.XCAPAuthResource(XCAPRoot(),
(credential_factory,),
portal, (authentication.IAuthUser,))
self.site = XCAPSite(root)
def _start_https(self, reactor):
from gnutls.interfaces.twisted import X509Credentials
from gnutls.connection import TLSContext, TLSContextServerOptions
cert, pKey = TLSConfig.certificate, TLSConfig.private_key
if cert is None or pKey is None:
log.critical('The TLS certificate/key could not be loaded')
sys.exit(1)
credentials = X509Credentials(cert, pKey)
tls_context = TLSContext(credentials, server_options=TLSContextServerOptions(certificate_request=None))
reactor.listenTLS(ServerConfig.root.port, HTTPFactory(self.site), tls_context, interface=ServerConfig.address)
log.info('TLS started')
def start(self):
log.info('Listening on: %s:%d' % (ServerConfig.address, ServerConfig.root.port))
log.info('XCAP root: %s' % ServerConfig.root)
if ServerConfig.root.startswith('https'):
self._start_https(reactor)
else:
reactor.listenTCP(ServerConfig.root.port, HTTPFactory(self.site), interface=ServerConfig.address)
reactor.run(installSignalHandlers=ServerConfig.backend.installSignalHandlers)
diff --git a/xcap/tweaks.py b/xcap/tweaks.py
deleted file mode 100644
index 24b3c07..0000000
--- a/xcap/tweaks.py
+++ /dev/null
@@ -1,61 +0,0 @@
-
-from hashlib import md5
-from twisted.cred import credentials, error
-from twisted.web2.auth.digest import IUsernameDigestHash, DigestCredentialFactory
-
-from zope.interface import implements
-
-def makeHash(username, realm, password):
- s = '%s:%s:%s' % (username, realm, password)
- return md5(s).hexdigest()
-
-class BasicCredentials(credentials.UsernamePassword):
- """Custom Basic Credentials, which support both plain and hashed checks."""
-
- implements(credentials.IUsernamePassword, IUsernameDigestHash)
-
- def __init__(self, username, password, realm):
- self.username = username
- self.password = password
- self.realm = realm
-
- def checkHash(self, digestHash):
- return digestHash == makeHash(self.username, self.realm, self.password)
-
-
-def decode(self, response, request):
- try:
- creds = (response + '===').decode('base64')
- except Exception:
- raise error.LoginFailed('Invalid credentials')
-
- creds = creds.split(':', 1)
- if len(creds) == 2:
- creds = BasicCredentials(creds[0], creds[1], self.realm) # our change
- return creds
- else:
- raise error.LoginFailed('Invalid credentials')
-
-def tweak_BasicCredentialFactory():
- import new
- from twisted.web2.auth.basic import BasicCredentialFactory
- method = new.instancemethod(decode, None, BasicCredentialFactory)
- BasicCredentialFactory.decode = method
-
-class tweak_DigestCredentialFactory(DigestCredentialFactory):
-
- def generateOpaque(self, nonce, clientip):
- """
- Generate an opaque to be returned to the client. This is 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.
- now = str(int(self._getTime()))
- if clientip is None:
- clientip = ''
- key = "%s,%s,%s" % (nonce, clientip, now)
- digest = md5(key + self.privateKey).hexdigest()
- ekey = key.encode('base64')
- return "%s-%s" % (digest, ekey.replace('\n', ''))
-