diff --git a/config.ini.sample b/config.ini.sample index f60186e..2ce1d3b 100644 --- a/config.ini.sample +++ b/config.ini.sample @@ -1,125 +1,121 @@ ; ; Configuration file for OpenXCAP ; ; The values in the commented lines represent the defaults built in the ; server software ; [Server] ; IP address to listen for requests ; 0.0.0.0 means any address of this host ; address = 0.0.0.0 ; This is a comma separated list of XCAP root URIs. The first is the ; primary XCAP root URI, while the others (if specified) are aliases. ; The primary root URI is used when generating xcap-diff ; If the scheme is https, then the server will listen for requests in TLS mode. root = http://xcap.example.com/xcap-root ; The backend to be used for storage and authentication. Current supported ; values are Database and OpenSIPS. OpenSIPS backend inherits all the settings ; from the Database backend but performs extra actions related to the ; integration with OpenSIPS for which it read the settings from [OpenSIPS] ; section backend = OpenSIPS ; Validate XCAP documents against XML schemas ; document_validation = Yes ; Allow URIs in pres-rules and resource-lists to point to lists not served ; by this server allow_external_references = No ; List os applications that won't be enabled on the server ;disabled_applications = test-app, org.openxcap.dialog-rules [Logging] ; Start, stop and major server error messages are always logged to syslog. ; This section can be used to log more details about XCAP clients accessing ; the server. The values in the commented lines represent the defaults built ; in the server software ; Directory where to write access.log file that will contain requests and/or ; responses to OpenXCAP server in Apache style. If set to an empty string, ; access logs will be printed to stdout if the server runs in no-fork mode ; or to syslog if the server runs in the background ; directory=/var/log/openxcap -; The following parameters control what kind of information (like -; stacktrace, body or headers) is logged for which response codes. The -; values must be a comma-separated list of HTTP response codes or the -; keyword 'any' that matches all response codes. - -; log_stacktrace=500 -; log_response_headers=500 -; log_response_body=500 -; log_request_headers=500 -; log_request_body=500 +; The following parameters control the logging of requests/responses based +; on the response code. The values must be a comma-separated list of HTTP +; response codes or the keyword 'all' that matches all response codes. + +; log_request=500 +; log_response=500 [Authentication] ; The HTTP authentication type, this can be either 'basic' or 'digest'. The ; standard states 'digest' as the mandatory, however it can be changed to ; basic ; type = digest ; Specify if the passwords are stored as plain text - Yes ; or in a hashed format MD5('username:domain:password') - No ; cleartext_passwords = Yes ; The default authentication realm, if none indicated in the HTTP request ; URI default_realm = example.com ; A comma-separated list of hosts or networks to trust. ; The elements can be an IP address in CIDR format, a ; hostname or an IP address (in the latter 2 a mask of 32 ; is assumed), or the special keywords 'any' and 'none' ; (being equivalent to 0.0.0.0/0 and 0.0.0.0/32 ; respectively). ; trusted_peers = [TLS] ; Location of X509 certificate and private key that identify this server. ; The path is relative to /etc/openxcap, or it can be given as an absolute ; path. ; Server X509 certificate ; certificate = ; Server X509 private key ; private_key = [Database] ; The database connection URI for the datase with subscriber accounts authentication_db_uri = mysql://opensips:opensipsrw@localhost/opensips ; The database connection URI for the database that stores the XCAP documents storage_db_uri = mysql://opensips:opensipsrw@localhost/opensips ; Authentication and storage tables ; subscriber_table = subscriber ; xcap_table = xcap [OpenSIPS] ; Publish xcap-diff event (using a SIP PUBLISH) ; publish_xcapdiff = yes ; SIP proxy where the PUBLISH will be sent ; outbound_sip_proxy = sip.example.com diff --git a/openxcap b/openxcap index 2780df5..68e94bf 100644 --- a/openxcap +++ b/openxcap @@ -1,70 +1,80 @@ #!/usr/bin/env python # Copyright (C) 2007-2015 AG Projects. # """OpenXCAP""" MEMORY_DEBUG = False if __name__ == '__main__': import sys - from optparse import OptionParser - from application.process import process, ProcessError - from application import log import xcap + from application import log + from application.process import process, ProcessError + from optparse import OptionParser name = 'openxcap' + fullname = 'OpenXCAP' description = 'An open source XCAP Server' - version = xcap.__version__ - fullname = 'OpenXCAP %s' % version config_directory = '/etc/openxcap' runtime_directory = '/var/run/openxcap' default_pid = "%s/%s.pid" % (runtime_directory, name) - parser = OptionParser(version="%%prog %s" % version) + parser = OptionParser(version='%%prog %s' % xcap.__version__) parser.add_option("--no-fork", action="store_false", dest="fork", default=1, help="run the process in the foreground (for debugging)") parser.add_option("--pid", dest="pidfile", default=default_pid, help="pid file (%s)" % default_pid, metavar="File") (options, args) = parser.parse_args() + try: + from xcap.logutil import web_logger + except Exception as e: + log.critical('Failed to create %s: %s' % (fullname, e)) + if not isinstance(e, (RuntimeError, OSError, IOError)): + log.exception() + sys.exit(1) + + if web_logger.filename is None: # access log is reported along with the rest of the applications's logging + log.Formatter.prefix_format = '{record.levelname:<8s} [{record.name}] ' + else: + log.Formatter.prefix_format = '{record.levelname:<8s} ' + pidfile = options.pidfile process.system_config_directory = config_directory if not options.fork: process._runtime_directory = None else: try: process.runtime_directory = runtime_directory process.daemonize(pidfile) except ProcessError, e: - log.msg("Fatal error: %s" % e) + log.critical('Fatal error: %s' % e) sys.exit(1) - else: - from xcap.logutil import start_log - start_log() + log.use_syslog(name) - log.msg("Starting %s" % fullname) + log.info('Starting %s' % fullname) try: if not options.fork and MEMORY_DEBUG: from application.debug.memory import memory_dump from xcap.server import XCAPServer server = XCAPServer() server.start() except Exception, e: - log.fatal("failed to create %s: %s" % (fullname, e)) - if e.__class__ is not RuntimeError: - log.err() + log.critical("Failed to create %s: %s" % (fullname, e)) + if type(e) is not RuntimeError: + log.exception() sys.exit(1) if not options.fork and MEMORY_DEBUG: print "------------------" memory_dump() print "------------------" diff --git a/xcap/appusage/__init__.py b/xcap/appusage/__init__.py index 70832e8..cde1d8a 100644 --- a/xcap/appusage/__init__.py +++ b/xcap/appusage/__init__.py @@ -1,374 +1,374 @@ """XCAP application usage module""" import os import sys from cStringIO import StringIO from lxml import etree from application.configuration import ConfigSection, ConfigSetting from application.configuration.datatypes import StringList from application import log import xcap from xcap import errors from xcap import element from xcap.backend import StatusResponse class Backend(object): """Configuration datatype, used to select a backend module from the configuration file.""" def __new__(typ, value): value = value.lower() try: return __import__('xcap.backend.%s' % value, globals(), locals(), ['']) except (ImportError, AssertionError), e: - log.fatal("Cannot load '%s' backend module: %s" % (value, str(e))) + log.critical('Cannot load %r backend module: %s' % (value, e)) sys.exit(1) - except Exception, e: - log.err() + except Exception: + log.exception() sys.exit(1) class ServerConfig(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'Server' backend = ConfigSetting(type=Backend, value=None) disabled_applications = ConfigSetting(type=StringList, value=[]) document_validation = True if ServerConfig.backend is None: - log.fatal("OpenXCAP needs a backend to be specified in order to run") + log.critical('OpenXCAP needs a backend to be specified in order to run') sys.exit(1) class ApplicationUsage(object): """Base class defining an XCAP application""" id = None ## the Application Unique ID (AUID) default_ns = None ## the default XML namespace mime_type = None ## the MIME type schema_file = None ## filename of the schema for the application def __init__(self, storage): ## the XML schema that defines valid documents for this application if self.schema_file: xml_schema_doc = etree.parse(open(os.path.join(os.path.dirname(__file__), 'xml-schemas', self.schema_file), 'r')) self.xml_schema = etree.XMLSchema(xml_schema_doc) else: class EverythingIsValid(object): def __call__(self, *args, **kw): return True def validate(self, *args, **kw): return True self.xml_schema = EverythingIsValid() if storage is not None: self.storage = storage ## Validation def _check_UTF8_encoding(self, xml_doc): """Check if the document is UTF8 encoded. Raise an NotUTF8Error if it's not.""" if xml_doc.docinfo.encoding.lower() != 'utf-8': raise errors.NotUTF8Error(comment='document encoding is %s' % xml_doc.docinfo.encoding) def _check_schema_validation(self, xml_doc): """Check if the given XCAP document validates against the application's schema""" if not self.xml_schema(xml_doc): raise errors.SchemaValidationError(comment=self.xml_schema.error_log) def _check_additional_constraints(self, xml_doc): """Check additional validations constraints for this XCAP document. Should be overriden in subclasses if specified by the application usage, and raise a ConstraintFailureError if needed.""" def validate_document(self, xcap_doc): """Check if a document is valid for this application.""" try: xml_doc = etree.parse(StringIO(xcap_doc)) # XXX do not use TreeBuilder here except etree.XMLSyntaxError, ex: ex.http_error = errors.NotWellFormedError(comment=str(ex)) raise except Exception, ex: ex.http_error = errors.NotWellFormedError() raise self._check_UTF8_encoding(xml_doc) if ServerConfig.document_validation: self._check_schema_validation(xml_doc) self._check_additional_constraints(xml_doc) ## Authorization policy def is_authorized(self, xcap_user, xcap_uri): """Default authorization policy. Authorizes an XCAPUser for an XCAPUri. Return True if the user is authorized, False otherwise.""" return xcap_user and xcap_user == xcap_uri.user ## Document management def _not_implemented(self, context): raise errors.ResourceNotFound("Application %s does not implement %s context" % (self.id, context)) def get_document(self, uri, check_etag): context = uri.doc_selector.context if context == 'global': return self.get_document_global(uri, check_etag) elif context == 'users': return self.get_document_local(uri, check_etag) else: self._not_implemented(context) def get_document_global(self, uri, check_etag): self._not_implemented('global') def get_document_local(self, uri, check_etag): return self.storage.get_document(uri, check_etag) def put_document(self, uri, document, check_etag): self.validate_document(document) return self.storage.put_document(uri, document, check_etag) def delete_document(self, uri, check_etag): return self.storage.delete_document(uri, check_etag) ## Element management def _cb_put_element(self, response, uri, element_body, check_etag): """This is called when the document that relates to the element is retrieved.""" if response.code == 404: ### XXX let the storate raise raise errors.NoParentError ### catch error in errback and attach http_error fixed_element_selector = uri.node_selector.element_selector.fix_star(element_body) try: result = element.put(response.data, fixed_element_selector, element_body) except element.SelectorError, ex: ex.http_error = errors.NoParentError(comment=str(ex)) raise if result is None: raise errors.NoParentError new_document, created = result get_result = element.get(new_document, uri.node_selector.element_selector) if get_result != element_body.strip(): raise errors.CannotInsertError('PUT request failed GET(PUT(x))==x invariant') d = self.put_document(uri, new_document, check_etag) def set_201_code(response): try: if response.code==200: response.code = 201 except AttributeError: pass return response if created: d.addCallback(set_201_code) return d def put_element(self, uri, element_body, check_etag): try: element.check_xml_fragment(element_body) except element.sax.SAXParseException, ex: ex.http_error = errors.NotXMLFragmentError(comment=str(ex)) raise except Exception, ex: ex.http_error = errors.NotXMLFragmentError() raise d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_put_element, callbackArgs=(uri, element_body, check_etag)) def _cb_get_element(self, response, uri): """This is called when the document related to the element is retrieved.""" if response.code == 404: ## XXX why not let the storage raise? raise errors.ResourceNotFound("The requested document %s was not found on this server" % uri.doc_selector) result = element.get(response.data, uri.node_selector.element_selector) if not result: msg = "The requested element %s was not found in the document %s" % (uri.node_selector, uri.doc_selector) raise errors.ResourceNotFound(msg) return StatusResponse(200, response.etag, result) def get_element(self, uri, check_etag): d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_get_element, callbackArgs=(uri, )) def _cb_delete_element(self, response, uri, check_etag): if response.code == 404: raise errors.ResourceNotFound("The requested document %s was not found on this server" % uri.doc_selector) new_document = element.delete(response.data, uri.node_selector.element_selector) if not new_document: raise errors.ResourceNotFound get_result = element.find(new_document, uri.node_selector.element_selector) if get_result: raise errors.CannotDeleteError('DELETE request failed GET(DELETE(x))==404 invariant') return self.put_document(uri, new_document, check_etag) def delete_element(self, uri, check_etag): d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_delete_element, callbackArgs=(uri, check_etag)) ## Attribute management def _cb_get_attribute(self, response, uri): """This is called when the document that relates to the attribute is retrieved.""" if response.code == 404: raise errors.ResourceNotFound document = response.data xml_doc = etree.parse(StringIO(document)) application = getApplicationForURI(uri) ns_dict = uri.node_selector.get_ns_bindings(application.default_ns) try: xpath = uri.node_selector.replace_default_prefix() attribute = xml_doc.xpath(xpath, namespaces = ns_dict) except Exception, ex: ex.http_error = errors.ResourceNotFound() raise if not attribute: raise errors.ResourceNotFound elif len(attribute) != 1: raise errors.ResourceNotFound('XPATH expression is ambiguous') # TODO # The server MUST NOT add namespace bindings representing namespaces # used by the element or its children, but declared in ancestor elements return StatusResponse(200, response.etag, attribute[0]) def get_attribute(self, uri, check_etag): d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_get_attribute, callbackArgs=(uri, )) def _cb_delete_attribute(self, response, uri, check_etag): if response.code == 404: raise errors.ResourceNotFound document = response.data xml_doc = etree.parse(StringIO(document)) application = getApplicationForURI(uri) ns_dict = uri.node_selector.get_ns_bindings(application.default_ns) try: elem = xml_doc.xpath(uri.node_selector.replace_default_prefix(append_terminal=False),namespaces=ns_dict) except Exception, ex: ex.http_error = errors.ResourceNotFound() raise if not elem: raise errors.ResourceNotFound if len(elem) != 1: raise errors.ResourceNotFound('XPATH expression is ambiguous') elem = elem[0] attribute = uri.node_selector.terminal_selector.attribute if elem.get(attribute): ## check if the attribute exists XXX use KeyError instead del elem.attrib[attribute] else: raise errors.ResourceNotFound new_document = etree.tostring(xml_doc, encoding='UTF-8', xml_declaration=True) return self.put_document(uri, new_document, check_etag) def delete_attribute(self, uri, check_etag): d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_delete_attribute, callbackArgs=(uri, check_etag)) def _cb_put_attribute(self, response, uri, attribute, check_etag): """This is called when the document that relates to the element is retrieved.""" if response.code == 404: raise errors.NoParentError document = response.data xml_doc = etree.parse(StringIO(document)) application = getApplicationForURI(uri) ns_dict = uri.node_selector.get_ns_bindings(application.default_ns) try: elem = xml_doc.xpath(uri.node_selector.replace_default_prefix(append_terminal=False),namespaces=ns_dict) except Exception, ex: ex.http_error = errors.NoParentError() raise if not elem: raise errors.NoParentError if len(elem) != 1: raise errors.NoParentError('XPATH expression is ambiguous') elem = elem[0] attr_name = uri.node_selector.terminal_selector.attribute elem.set(attr_name, attribute) new_document = etree.tostring(xml_doc, encoding='UTF-8', xml_declaration=True) return self.put_document(uri, new_document, check_etag) def put_attribute(self, uri, attribute, check_etag): ## TODO verify if the attribute is valid d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_put_attribute, callbackArgs=(uri, attribute, check_etag)) ## Namespace Bindings def _cb_get_ns_bindings(self, response, uri): """This is called when the document that relates to the element is retrieved.""" if response.code == 404: raise errors.ResourceNotFound document = response.data xml_doc = etree.parse(StringIO(document)) application = getApplicationForURI(uri) ns_dict = uri.node_selector.get_ns_bindings(application.default_ns) try: elem = xml_doc.xpath(uri.node_selector.replace_default_prefix(append_terminal=False),namespaces=ns_dict) except Exception, ex: ex.http_error = errors.ResourceNotFound() raise if not elem: raise errors.ResourceNotFound elif len(elem)!=1: raise errors.ResourceNotFound('XPATH expression is ambiguous') elem = elem[0] namespaces = '' for prefix, ns in elem.nsmap.items(): namespaces += ' xmlns%s="%s"' % (prefix and ':%s' % prefix or '', ns) result = '<%s %s/>' % (elem.tag, namespaces) return StatusResponse(200, response.etag, result) def get_ns_bindings(self, uri, check_etag): d = self.get_document(uri, check_etag) return d.addCallbacks(self._cb_get_ns_bindings, callbackArgs=(uri, )) from xcap.appusage.capabilities import XCAPCapabilitiesApplication from xcap.appusage.dialogrules import DialogRulesApplication from xcap.appusage.directory import XCAPDirectoryApplication from xcap.appusage.pidf import PIDFManipulationApplication from xcap.appusage.prescontent import PresContentApplication from xcap.appusage.presrules import PresenceRulesApplication from xcap.appusage.purge import PurgeApplication from xcap.appusage.resourcelists import ResourceListsApplication from xcap.appusage.rlsservices import RLSServicesApplication from xcap.appusage.test import TestApplication from xcap.appusage.watchers import WatchersApplication storage = ServerConfig.backend.Storage() applications = { DialogRulesApplication.id: DialogRulesApplication(storage), PIDFManipulationApplication.id: PIDFManipulationApplication(storage), PresenceRulesApplication.id: PresenceRulesApplication(storage), PresenceRulesApplication.oma_id: PresenceRulesApplication(storage), PurgeApplication.id: PurgeApplication(storage), ResourceListsApplication.id: ResourceListsApplication(storage), RLSServicesApplication.id: RLSServicesApplication(storage), TestApplication.id: TestApplication(storage), WatchersApplication.id: WatchersApplication(storage), XCAPCapabilitiesApplication.id: XCAPCapabilitiesApplication(), XCAPDirectoryApplication.id: XCAPDirectoryApplication(storage) } # public GET applications (GET is not challenged for auth) public_get_applications = {PresContentApplication.id: PresContentApplication(storage)} applications.update(public_get_applications) for application in ServerConfig.disabled_applications: applications.pop(application, None) namespaces = dict((k, v.default_ns) for (k, v) in applications.items()) def getApplicationForURI(xcap_uri): return applications.get(xcap_uri.application_id, None) __all__ = ['applications', 'namespaces', 'public_get_applications', 'getApplicationForURI', 'ApplicationUsage', 'Backend'] diff --git a/xcap/backend/database.py b/xcap/backend/database.py index db2f3b7..8482278 100644 --- a/xcap/backend/database.py +++ b/xcap/backend/database.py @@ -1,407 +1,407 @@ """Implementation of a database backend.""" import sys from application import log from application.configuration import ConfigSection from application.python.types import Singleton from zope.interface import implements from twisted.cred import credentials, checkers, error as credError from twisted.internet import defer from _mysql_exceptions import IntegrityError import xcap from xcap.backend import IStorage, StatusResponse from xcap.dbutil import connectionForURI, repeat_on_error, make_random_etag class Config(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'Database' authentication_db_uri = '' storage_db_uri = '' subscriber_table = 'subscriber' user_col = 'username' domain_col = 'domain' password_col = 'password' ha1_col = 'ha1' xcap_table = 'xcap' if not Config.authentication_db_uri or not Config.storage_db_uri: - log.fatal("Authentication DB URI and Storage DB URI must be provided") + log.critical('Authentication DB URI and Storage DB URI must be provided') sys.exit(1) class DBBase(object): def __init__(self): self._db_connect() class PasswordChecker(DBBase): """A credentials checker against a database subscriber table.""" implements(checkers.ICredentialsChecker) credentialInterfaces = (credentials.IUsernamePassword, credentials.IUsernameHashedPassword) def _db_connect(self): self.conn = auth_db_connection(Config.authentication_db_uri) def _query_credentials(self, credentials): raise NotImplementedError def _got_query_results(self, rows, credentials): if not rows: raise credError.UnauthorizedLogin("Unauthorized login") else: return self._authenticate_credentials(rows[0][0], credentials) def _authenticate_credentials(self, password, credentials): raise NotImplementedError def _checkedPassword(self, matched, username, realm): if matched: username = username.split('@', 1)[0] ## this is the avatar ID return "%s@%s" % (username, realm) else: raise credError.UnauthorizedLogin("Unauthorized login") def requestAvatarId(self, credentials): """Return the avatar ID for the credentials which must have the username and realm attributes, or an UnauthorizedLogin in case of a failure.""" d = self._query_credentials(credentials) return d class PlainPasswordChecker(PasswordChecker): """A credentials checker against a database subscriber table, where the passwords are stored in plain text.""" implements(checkers.ICredentialsChecker) def _query_credentials(self, credentials): username, domain = credentials.username.split('@', 1)[0], credentials.realm query = """SELECT %(password_col)s FROM %(table)s WHERE %(user_col)s = %%(username)s AND %(domain_col)s = %%(domain)s""" % { "password_col": Config.password_col, "user_col": Config.user_col, "domain_col": Config.domain_col, "table": Config.subscriber_table } params = {"username": username, "domain": domain} return self.conn.runQuery(query, params).addCallback(self._got_query_results, credentials) def _authenticate_credentials(self, hash, credentials): return defer.maybeDeferred( credentials.checkPassword, hash).addCallback( self._checkedPassword, credentials.username, credentials.realm) class HashPasswordChecker(PasswordChecker): """A credentials checker against a database subscriber table, where the passwords are stored as MD5 hashes.""" implements(checkers.ICredentialsChecker) def _query_credentials(self, credentials): username, domain = credentials.username.split('@', 1)[0], credentials.realm query = """SELECT %(ha1_col)s FROM %(table)s WHERE %(user_col)s = %%(username)s AND %(domain_col)s = %%(domain)s""" % { "ha1_col": Config.ha1_col, "user_col": Config.user_col, "domain_col": Config.domain_col, "table": Config.subscriber_table} params = {"username": username, "domain": domain} return self.conn.runQuery(query, params).addCallback(self._got_query_results, credentials) def _authenticate_credentials(self, hash, credentials): return defer.maybeDeferred( credentials.checkHash, hash).addCallback( self._checkedPassword, credentials.username, credentials.realm) class Error(Exception): def __init__(self): if hasattr(self, 'msg'): return Exception.__init__(self, self.msg) else: return Exception.__init__(self) class RaceError(Error): """The errors of this type are raised for the requests that failed because of concurrent modification of the database by other clients. For example, before DELETE we do SELECT first, to check that a document of the right etag exists. The actual check is performed by a function in twisted that is passed as a callback. Then etag from the SELECT request is used in the DELETE request. This seems unnecessary convoluted and probably should be changed to 'DELETE .. WHERE etag=ETAG'. We still need to find out whether DELETE was actually performed. """ class UpdateFailed(RaceError): msg = 'UPDATE request failed' class DeleteFailed(RaceError): msg = 'DELETE request failed' class MultipleResultsError(Error): """This should never happen. If it did happen. that means either the table was corrupted or there's a logic error""" def __init__(self, params): Exception.__init__(self, 'database request has more than one result: ' + repr(params)) class Storage(DBBase): __metaclass__ = Singleton implements(IStorage) app_mapping = {"pres-rules" : 1<<1, "resource-lists" : 1<<2, "rls-services" : 1<<3, "pidf-manipulation" : 1<<4, "org.openmobilealliance.pres-rules" : 1<<5, "org.openmobilealliance.pres-content" : 1<<6, "org.openxcap.dialog-rules" : 1<<7, "test-app" : 0} def _db_connect(self): self.conn = storage_db_connection(Config.storage_db_uri) def _normalize_document_path(self, uri): if uri.application_id in ("pres-rules", "org.openmobilealliance.pres-rules"): # some clients e.g. counterpath's eyebeam save presence rules under # different filenames between versions and they expect to find the same # information, thus we are forcing all presence rules documents to be # saved under "index.xml" default filename uri.doc_selector.document_path = "index.xml" def _get_document(self, trans, uri, check_etag): username, domain = uri.user.username, uri.user.domain self._normalize_document_path(uri) doc_type = self.app_mapping[uri.application_id] query = """SELECT doc, etag FROM %(table)s WHERE username = %%(username)s AND domain = %%(domain)s AND doc_type= %%(doc_type)s AND doc_uri=%%(document_path)s""" % { "table": Config.xcap_table} params = {"username": username, "domain" : domain, "doc_type": doc_type, "document_path": uri.doc_selector.document_path} trans.execute(query, params) result = trans.fetchall() if len(result)>1: raise MultipleResultsError(params) elif result: doc, etag = result[0] if isinstance(doc, unicode): doc = doc.encode('utf-8') check_etag(etag) response = StatusResponse(200, etag, doc) else: response = StatusResponse(404) return response def _put_document(self, trans, uri, document, check_etag): username, domain = uri.user.username, uri.user.domain self._normalize_document_path(uri) doc_type = self.app_mapping[uri.application_id] document_path = uri.doc_selector.document_path query = """SELECT etag FROM %(table)s WHERE username = %%(username)s AND domain = %%(domain)s AND doc_type= %%(doc_type)s AND doc_uri=%%(document_path)s""" % { "table": Config.xcap_table} params = {"username": username, "domain" : domain, "doc_type": doc_type, "document_path": document_path} trans.execute(query, params) result = trans.fetchall() if len(result) > 1: raise MultipleResultsError(params) elif not result: check_etag(None, False) ## the document doesn't exist, create it etag = make_random_etag(uri) query = """INSERT INTO %(table)s (username, domain, doc_type, etag, doc, doc_uri) VALUES (%%(username)s, %%(domain)s, %%(doc_type)s, %%(etag)s, %%(document)s, %%(document_path)s)""" % { "table": Config.xcap_table } params = {"username": username, "domain" : domain, "doc_type": doc_type, "etag": etag, "document": document, "document_path": document_path} # may raise IntegrityError here, if the document was created in another connection # will be catched by repeat_on_error trans.execute(query, params) return StatusResponse(201, etag) else: old_etag = result[0][0] ## first check the etag of the existing resource check_etag(old_etag) ## the document exists, replace it etag = make_random_etag(uri) query = """UPDATE %(table)s SET doc = %%(document)s, etag = %%(etag)s WHERE username = %%(username)s AND domain = %%(domain)s AND doc_type = %%(doc_type)s AND etag = %%(old_etag)s AND doc_uri = %%(document_path)s""" % { "table": Config.xcap_table } params = {"document": document, "etag": etag, "username": username, "domain" : domain, "doc_type": doc_type, "old_etag": old_etag, "document_path": document_path} trans.execute(query, params) # the request may not update anything (e.g. if etag was changed by another connection # after we did SELECT); if so, we should retry updated = getattr(trans._connection, 'affected_rows', lambda : 1)() if not updated: raise UpdateFailed assert updated == 1, updated return StatusResponse(200, etag, old_etag=old_etag) def _delete_document(self, trans, uri, check_etag): username, domain = uri.user.username, uri.user.domain self._normalize_document_path(uri) doc_type = self.app_mapping[uri.application_id] document_path = uri.doc_selector.document_path query = """SELECT etag FROM %(table)s WHERE username = %%(username)s AND domain = %%(domain)s AND doc_type= %%(doc_type)s AND doc_uri = %%(document_path)s""" % { "table": Config.xcap_table} params = {"username": username, "domain" : domain, "doc_type": doc_type, "document_path": document_path} trans.execute(query, params) result = trans.fetchall() if len(result)>1: raise MultipleResultsError(params) elif result: etag = result[0][0] check_etag(etag) query = """DELETE FROM %(table)s WHERE username = %%(username)s AND domain = %%(domain)s AND doc_type= %%(doc_type)s AND doc_uri = %%(document_path)s AND etag = %%(etag)s""" % {"table" : Config.xcap_table} params = {"username": username, "domain" : domain, "doc_type": doc_type, "document_path": document_path, "etag": etag} trans.execute(query, params) deleted = getattr(trans._connection, 'affected_rows', lambda : 1)() if not deleted: # the document was replaced/removed after the SELECT but before the DELETE raise DeleteFailed assert deleted == 1, deleted return StatusResponse(200, old_etag=etag) else: return StatusResponse(404) def _delete_all_documents(self, trans, uri): username, domain = uri.user.username, uri.user.domain query = """DELETE FROM %(table)s WHERE username = %%(username)s AND domain = %%(domain)s """ % {"table" : Config.xcap_table} params = {"username": username, "domain" : domain} trans.execute(query, params) return StatusResponse(200) def get_document(self, uri, check_etag): return self.conn.runInteraction(self._get_document, uri, check_etag) def put_document(self, uri, document, check_etag): return repeat_on_error(10, (UpdateFailed, IntegrityError), self.conn.runInteraction, self._put_document, uri, document, check_etag) def delete_document(self, uri, check_etag): return repeat_on_error(10, DeleteFailed, self.conn.runInteraction, self._delete_document, uri, check_etag) def delete_documents(self, uri): return self.conn.runInteraction(self._delete_all_documents, uri) # Application-specific functions def _get_watchers(self, trans, uri): status_mapping = {1: "allow", 2: "confirm", 3: "deny"} presentity_uri = "sip:%s@%s" % (uri.user.username, uri.user.domain) query = """SELECT watcher_username, watcher_domain, status FROM watchers WHERE presentity_uri = %(puri)s""" params = {'puri': presentity_uri} trans.execute(query, params) result = trans.fetchall() watchers = [{"id": "%s@%s" % (w_user, w_domain), "status": status_mapping.get(subs_status, "unknown"), "online": "false"} for w_user, w_domain, subs_status in result] query = """SELECT watcher_username, watcher_domain FROM active_watchers WHERE presentity_uri = %(puri)s AND event = 'presence'""" trans.execute(query, params) result = trans.fetchall() active_watchers = set("%s@%s" % pair for pair in result) for watcher in watchers: if watcher["id"] in active_watchers: watcher["online"] = "true" return watchers def get_watchers(self, uri): return self.conn.runInteraction(self._get_watchers, uri) def _get_documents_list(self, trans, uri): query = """SELECT doc_type, doc_uri, etag FROM %(table)s WHERE username = %%(username)s AND domain = %%(domain)s""" % {'table': Config.xcap_table} params = {'username': uri.user.username, 'domain': uri.user.domain} trans.execute(query, params) result = trans.fetchall() docs = {} for r in result: app = [k for k, v in self.app_mapping.iteritems() if v == r[0]][0] if docs.has_key(app): docs[app].append((r[1], r[2])) else: docs[app] = [(r[1], r[2])] # Ex: {'pres-rules': [('index.html', '4564fd9c9a2a2e3e796310b00c9908aa')]} return docs def get_documents_list(self, uri): return self.conn.runInteraction(self._get_documents_list, uri) installSignalHandlers = True def auth_db_connection(uri): conn = connectionForURI(uri) return conn def storage_db_connection(uri): conn = connectionForURI(uri) def cb(res): if res[0:1][0:1] and res[0][0]: print '%s xcap documents in the database' % res[0][0] return res def eb(fail): fail.printTraceback() return fail # connect early, so database problem are detected early d = conn.runQuery('SELECT count(*) from %s' % Config.xcap_table) d.addCallback(cb) d.addErrback(eb) return conn diff --git a/xcap/backend/opensips.py b/xcap/backend/opensips.py index b340d9b..62dd40c 100644 --- a/xcap/backend/opensips.py +++ b/xcap/backend/opensips.py @@ -1,123 +1,123 @@ """Implementation of an OpenSIPS backend.""" import re from application import log from application.configuration import ConfigSection, ConfigSetting from application.configuration.datatypes import IPAddress from application.notification import IObserver, NotificationCenter from application.python import Null from application.python.types import Singleton from sipsimple.core import Engine, FromHeader, Header, Publication, RouteHeader, SIPURI from sipsimple.configuration.datatypes import SIPProxyAddress from sipsimple.threading import run_in_twisted_thread from zope.interface import implements import xcap from xcap.datatypes import XCAPRootURI from xcap.backend import database from xcap.xcapdiff import Notifier class ServerConfig(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'Server' address = ConfigSetting(type=IPAddress, value='0.0.0.0') root = ConfigSetting(type=XCAPRootURI, value=None) class Config(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'OpenSIPS' publish_xcapdiff = False outbound_sip_proxy = '' class PlainPasswordChecker(database.PlainPasswordChecker): pass class HashPasswordChecker(database.HashPasswordChecker): pass class SIPNotifier(object): __metaclass__ = Singleton implements(IObserver) def __init__(self): self.engine = Engine() self.engine.start( ip_address=None if ServerConfig.address == '0.0.0.0' else ServerConfig.address, user_agent="OpenXCAP %s" % xcap.__version__, ) self.sip_prefix_re = re.compile('^sips?:') try: self.outbound_proxy = SIPProxyAddress.from_description(Config.outbound_sip_proxy) except ValueError: log.warning('Invalid SIP proxy address specified: %s' % Config.outbound_sip_proxy) self.outbound_proxy = None def send_publish(self, uri, body): if self.outbound_proxy is None: return uri = self.sip_prefix_re.sub('', uri) publication = Publication(FromHeader(SIPURI(uri)), "xcap-diff", "application/xcap-diff+xml", duration=0, extra_headers=[Header('Thor-Scope', 'publish-xcap')]) NotificationCenter().add_observer(self, sender=publication) route_header = RouteHeader(SIPURI(host=self.outbound_proxy.host, port=self.outbound_proxy.port, parameters=dict(transport=self.outbound_proxy.transport))) publication.publish(body, route_header, timeout=5) @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPPublicationDidSucceed(self, notification): - log.msg("PUBLISH for xcap-diff event successfully sent to %s for %s" % (notification.data.route_header.uri, notification.sender.from_header.uri)) + log.info('PUBLISH for xcap-diff event successfully sent to %s for %s' % (notification.data.route_header.uri, notification.sender.from_header.uri)) def _NH_SIPPublicationDidEnd(self, notification): - log.msg("PUBLISH for xcap-diff event ended for %s" % notification.sender.from_header.uri) + log.info('PUBLISH for xcap-diff event ended for %s' % notification.sender.from_header.uri) notification.center.remove_observer(self, sender=notification.sender) def _NH_SIPPublicationDidFail(self, notification): - log.msg("PUBLISH for xcap-diff event failed to %s for %s" % (notification.data.route_header.uri, notification.sender.from_header.uri)) + log.info('PUBLISH for xcap-diff event failed to %s for %s' % (notification.data.route_header.uri, notification.sender.from_header.uri)) notification.center.remove_observer(self, sender=notification.sender) class NotifyingStorage(database.Storage): def __init__(self): super(NotifyingStorage, self).__init__() self._sip_notifier = SIPNotifier() self.notifier = Notifier(ServerConfig.root, self._sip_notifier.send_publish) def put_document(self, uri, document, check_etag): d = super(NotifyingStorage, self).put_document(uri, document, check_etag) d.addCallback(lambda result: self._on_put(result, uri)) return d def _on_put(self, result, uri): if result.succeed: self.notifier.on_change(uri, result.old_etag, result.etag) return result def delete_document(self, uri, check_etag): d = super(NotifyingStorage, self).delete_document(uri, check_etag) d.addCallback(lambda result: self._on_delete(result, uri)) return d def _on_delete(self, result, uri): if result.succeed: self.notifier.on_change(uri, result.old_etag, None) return result if Config.publish_xcapdiff: Storage = NotifyingStorage else: Storage = database.Storage installSignalHandlers = database.installSignalHandlers diff --git a/xcap/backend/sipthor.py b/xcap/backend/sipthor.py index 967095b..3290530 100644 --- a/xcap/backend/sipthor.py +++ b/xcap/backend/sipthor.py @@ -1,595 +1,592 @@ import re import signal import cjson from formencode import validators from application import log from application.notification import IObserver, NotificationCenter from application.python import Null from application.python.types import Singleton from application.system import host from application.process import process from application.configuration import ConfigSection, ConfigSetting from application.configuration.datatypes import IPAddress from sqlobject import sqlhub, connectionForURI, SQLObject, AND from sqlobject import StringCol, IntCol, DateTimeCol, SOBLOBCol, Col from sqlobject import MultipleJoin, ForeignKey from zope.interface import implements from twisted.internet import reactor from twisted.internet import defer from twisted.internet.defer import Deferred, maybeDeferred from twisted.cred.checkers import ICredentialsChecker from twisted.cred.credentials import IUsernamePassword, IUsernameHashedPassword from twisted.cred.error import UnauthorizedLogin from thor.link import ControlLink, Notification, Request from thor.eventservice import EventServiceClient, ThorEvent from thor.entities import ThorEntitiesRoleMap, GenericThorEntity as ThorEntity from gnutls.interfaces.twisted import TLSContext, X509Credentials from sipsimple.core import Engine, FromHeader, Header, Publication, RouteHeader, SIPURI from sipsimple.threading import run_in_twisted_thread import xcap from xcap.tls import Certificate, PrivateKey from xcap.backend import StatusResponse from xcap.datatypes import XCAPRootURI from xcap.dbutil import make_random_etag from xcap.xcapdiff import Notifier class ThorNodeConfig(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'ThorNetwork' domain = "sipthor.net" multiply = 1000 certificate = ConfigSetting(type=Certificate, value=None) private_key = ConfigSetting(type=PrivateKey, value=None) ca = ConfigSetting(type=Certificate, value=None) class ServerConfig(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'Server' address = ConfigSetting(type=IPAddress, value='0.0.0.0') root = ConfigSetting(type=XCAPRootURI, value=None) class JSONValidator(validators.Validator): def to_python(self, value, state): if value is None: return None try: return cjson.decode(value) except Exception: raise validators.Invalid("expected a decodable JSON object in the JSONCol '%s', got %s %r instead" % (self.name, type(value), value), value, state) def from_python(self, value, state): if value is None: return None try: return cjson.encode(value) except Exception: raise validators.Invalid("expected an encodable JSON object in the JSONCol '%s', got %s %r instead" % (self.name, type(value), value), value, state) class SOJSONCol(SOBLOBCol): def createValidators(self): return [JSONValidator()] + super(SOJSONCol, self).createValidators() class JSONCol(Col): baseClass = SOJSONCol class SipAccount(SQLObject): class sqlmeta: table = 'sip_accounts_meta' username = StringCol(length=64) domain = StringCol(length=64) firstName = StringCol(length=64) lastName = StringCol(length=64) email = StringCol(length=64) customerId = IntCol(default=0) resellerId = IntCol(default=0) ownerId = IntCol(default=0) changeDate = DateTimeCol(default=DateTimeCol.now) ## joins data = MultipleJoin('SipAccountData', joinColumn='account_id') def _set_profile(self, value): data = list(self.data) if not data: SipAccountData(account=self, profile=value) else: data[0].profile = value def _get_profile(self): return self.data[0].profile def set(self, **kwargs): kwargs = kwargs.copy() profile = kwargs.pop('profile', None) SQLObject.set(self, **kwargs) if profile is not None: self._set_profile(profile) class SipAccountData(SQLObject): class sqlmeta: table = 'sip_accounts_data' account = ForeignKey('SipAccount', cascade=True) profile = JSONCol() class ThorEntityAddress(str): def __new__(cls, ip, control_port=None, version='unknown'): instance = str.__new__(cls, ip) instance.ip = ip instance.version = version instance.control_port = control_port return instance class GetSIPWatchers(Request): def __new__(cls, account): command = "get sip_watchers for %s" % account instance = Request.__new__(cls, command) return instance class XCAPProvisioning(EventServiceClient): __metaclass__ = Singleton topics = ["Thor.Members"] def __init__(self): self._database = DatabaseConnection() self.node = ThorEntity(host.default_ip if ServerConfig.address == '0.0.0.0' else ServerConfig.address, ['xcap_server'], version=xcap.__version__) self.networks = {} self.presence_message = ThorEvent('Thor.Presence', self.node.id) self.shutdown_message = ThorEvent('Thor.Leave', self.node.id) credentials = X509Credentials(ThorNodeConfig.certificate, ThorNodeConfig.private_key, [ThorNodeConfig.ca]) credentials.verify_peer = True tls_context = TLSContext(credentials) self.control = ControlLink(tls_context) EventServiceClient.__init__(self, ThorNodeConfig.domain, tls_context) process.signals.add_handler(signal.SIGHUP, self._handle_SIGHUP) process.signals.add_handler(signal.SIGINT, self._handle_SIGINT) process.signals.add_handler(signal.SIGTERM, self._handle_SIGTERM) def _disconnect_all(self, result): self.control.disconnect_all() EventServiceClient._disconnect_all(self, result) def lookup(self, key): network = self.networks.get("sip_proxy", None) if network is None: return None try: node = network.lookup_node(key) except LookupError: node = None except Exception: - log.err() + log.exception() node = None return node def notify(self, operation, entity_type, entity): node = self.lookup(entity) if node is not None: if node.control_port is None: log.error("Could not send notify because node %s has no control port" % node.ip) return self.control.send_request(Notification("notify %s %s %s" % (operation, entity_type, entity)), (node.ip, node.control_port)) def get_watchers(self, key): node = self.lookup(key) if node is None: return defer.fail("no nodes found when searching for key %s" % str(key)) if node.control_port is None: return defer.fail("could not send notify because node %s has no control port" % node.ip) request = GetSIPWatchers(key) request.deferred = Deferred() self.control.send_request(request, (node.ip, node.control_port)) return request.deferred def handle_event(self, event): # print "Received event: %s" % event networks = self.networks role_map = ThorEntitiesRoleMap(event.message) ## mapping between role names and lists of nodes with that role thor_databases = role_map.get('thor_database', []) if thor_databases: thor_databases.sort(lambda x, y: cmp(x.priority, y.priority) or cmp(x.ip, y.ip)) dburi = thor_databases[0].dburi else: dburi = None self._database.update_dburi(dburi) all_roles = role_map.keys() + networks.keys() for role in all_roles: try: network = networks[role] ## avoid setdefault here because it always evaluates the 2nd argument except KeyError: from thor import network as thor_network if role in ["thor_manager", "thor_monitor", "provisioning_server", "media_relay", "thor_database"]: continue else: network = thor_network.new(ThorNodeConfig.multiply) networks[role] = network new_nodes = set([ThorEntityAddress(node.ip, getattr(node, 'control_port', None), getattr(node, 'version', 'unknown')) for node in role_map.get(role, [])]) old_nodes = set(network.nodes) - ## compute set differences added_nodes = new_nodes - old_nodes removed_nodes = old_nodes - new_nodes if removed_nodes: for node in removed_nodes: network.remove_node(node) self.control.discard_link((node.ip, node.control_port)) - plural = len(removed_nodes) != 1 and 's' or '' - log.msg("removed %s node%s: %s" % (role, plural, ', '.join(removed_nodes))) + log.info('Removed %s nodes: %s' % (role, ', '.join(removed_nodes))) if added_nodes: for node in added_nodes: network.add_node(node) - plural = len(added_nodes) != 1 and 's' or '' - log.msg("added %s node%s: %s" % (role, plural, ', '.join(added_nodes))) - #print "Thor %s nodes: %s" % (role, str(network.nodes)) + log.info('Added %s nodes: %s' % (role, ', '.join(added_nodes))) + # print('Thor %s nodes: %s' % (role, str(network.nodes))) class NotFound(Exception): pass class NoDatabase(Exception): pass class DatabaseConnection(object): __metaclass__ = Singleton def __init__(self): self.dburi = None # Methods to be called from the Twisted thread: def put(self, uri, document, check_etag, new_etag): defer = Deferred() operation = lambda profile: self._put_operation(uri, document, check_etag, new_etag, profile) reactor.callInThread(self.retrieve_profile, uri.user.username, uri.user.domain, operation, True, defer) return defer def delete(self, uri, check_etag): defer = Deferred() operation = lambda profile: self._delete_operation(uri, check_etag, profile) reactor.callInThread(self.retrieve_profile, uri.user.username, uri.user.domain, operation, True, defer) return defer def delete_all(self, uri): defer = Deferred() operation = lambda profile: self._delete_all_operation(uri, profile) reactor.callInThread(self.retrieve_profile, uri.user.username, uri.user.domain, operation, True, defer) return defer def get(self, uri): defer = Deferred() operation = lambda profile: self._get_operation(uri, profile) reactor.callInThread(self.retrieve_profile, uri.user.username, uri.user.domain, operation, False, defer) return defer def get_profile(self, username, domain): defer = Deferred() reactor.callInThread(self.retrieve_profile, username, domain, lambda profile: profile, False, defer) return defer def get_documents_list(self, uri): defer = Deferred() operation = lambda profile: self._get_documents_list_operation(uri, profile) reactor.callInThread(self.retrieve_profile, uri.user.username, uri.user.domain, operation, False, defer) return defer # Methods to be called in a separate thread: def _put_operation(self, uri, document, check_etag, new_etag, profile): xcap_docs = profile.setdefault("xcap", {}) try: etag = xcap_docs[uri.application_id][uri.doc_selector.document_path][1] except KeyError: found = False etag = None check_etag(None, False) else: found = True check_etag(etag) xcap_app = xcap_docs.setdefault(uri.application_id, {}) xcap_app[uri.doc_selector.document_path] = (document, new_etag) return found, etag, new_etag def _delete_operation(self, uri, check_etag, profile): xcap_docs = profile.setdefault("xcap", {}) try: etag = xcap_docs[uri.application_id][uri.doc_selector.document_path][1] except KeyError: raise NotFound() check_etag(etag) del(xcap_docs[uri.application_id][uri.doc_selector.document_path]) return (etag) def _delete_all_operation(self, uri, profile): xcap_docs = profile.setdefault("xcap", {}) xcap_docs.clear() return None def _get_operation(self, uri, profile): try: xcap_docs = profile["xcap"] doc, etag = xcap_docs[uri.application_id][uri.doc_selector.document_path] except KeyError: raise NotFound() return doc, etag def _get_documents_list_operation(self, uri, profile): try: xcap_docs = profile["xcap"] except KeyError: raise NotFound() return xcap_docs def retrieve_profile(self, username, domain, operation, update, defer): transaction = None try: if self.dburi is None: raise NoDatabase() transaction = sqlhub.processConnection.transaction() try: db_account = SipAccount.select(AND(SipAccount.q.username == username, SipAccount.q.domain == domain), connection = transaction, forUpdate = update)[0] except IndexError: raise NotFound() profile = db_account.profile result = operation(profile) # NB: may modify profile! if update: db_account.profile = profile transaction.commit(close=True) except Exception, e: if transaction: transaction.rollback() reactor.callFromThread(defer.errback, e) else: reactor.callFromThread(defer.callback, result) finally: if transaction: transaction.cache.clear() def update_dburi(self, dburi): if self.dburi != dburi: if self.dburi is not None: sqlhub.processConnection.close() if dburi is None: sqlhub.processConnection else: sqlhub.processConnection = connectionForURI(dburi) self.dburi = dburi class SipthorPasswordChecker(object): implements(ICredentialsChecker) credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword) def __init__(self): self._database = DatabaseConnection() def _query_credentials(self, credentials): username, domain = credentials.username.split('@', 1)[0], credentials.realm result = self._database.get_profile(username, domain) result.addCallback(self._got_query_results, credentials) result.addErrback(self._got_unsuccessfull) return result def _got_unsuccessfull(self, failure): failure.trap(NotFound) raise UnauthorizedLogin("Unauthorized login") def _got_query_results(self, profile, credentials): return self._authenticate_credentials(profile, credentials) def _authenticate_credentials(self, profile, credentials): raise NotImplementedError def _checkedPassword(self, matched, username, realm): if matched: username = username.split('@', 1)[0] ## this is the avatar ID return "%s@%s" % (username, realm) else: raise UnauthorizedLogin("Unauthorized login") def requestAvatarId(self, credentials): """Return the avatar ID for the credentials which must have the username and realm attributes, or an UnauthorizedLogin in case of a failure.""" d = self._query_credentials(credentials) return d class PlainPasswordChecker(SipthorPasswordChecker): """A credentials checker against a database subscriber table, where the passwords are stored in plain text.""" implements(ICredentialsChecker) def _authenticate_credentials(self, profile, credentials): return maybeDeferred( credentials.checkPassword, profile["password"]).addCallback( self._checkedPassword, credentials.username, credentials.realm) class HashPasswordChecker(SipthorPasswordChecker): """A credentials checker against a database subscriber table, where the passwords are stored as MD5 hashes.""" implements(ICredentialsChecker) def _authenticate_credentials(self, profile, credentials): return maybeDeferred( credentials.checkHash, profile["ha1"]).addCallback( self._checkedPassword, credentials.username, credentials.realm) class SIPNotifier(object): __metaclass__ = Singleton implements(IObserver) def __init__(self): self.provisioning = XCAPProvisioning() self.engine = Engine() self.engine.start( ip_address=None if ServerConfig.address == '0.0.0.0' else ServerConfig.address, user_agent="OpenXCAP %s" % xcap.__version__, ) def send_publish(self, uri, body): uri = re.sub("^(sip:|sips:)", "", uri) destination_node = self.provisioning.lookup(uri) if destination_node is not None: # TODO: add configuration settings for SIP transport. -Saul publication = Publication(FromHeader(SIPURI(uri)), "xcap-diff", "application/xcap-diff+xml", duration=0, extra_headers=[Header('Thor-Scope', 'publish-xcap')]) NotificationCenter().add_observer(self, sender=publication) route_header = RouteHeader(SIPURI(host=str(destination_node), port='5060', parameters=dict(transport='udp'))) publication.publish(body, route_header, timeout=5) @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPPublicationDidSucceed(self, notification): - log.msg("PUBLISH for xcap-diff event successfully sent to %s for %s" % (notification.data.route_header.uri, notification.sender.from_header.uri)) + log.info('PUBLISH for xcap-diff event successfully sent to %s for %s' % (notification.data.route_header.uri, notification.sender.from_header.uri)) def _NH_SIPPublicationDidEnd(self, notification): - log.msg("PUBLISH for xcap-diff event ended for %s" % notification.sender.from_header.uri) + log.info('PUBLISH for xcap-diff event ended for %s' % notification.sender.from_header.uri) NotificationCenter().remove_observer(self, sender=notification.sender) def _NH_SIPPublicationDidFail(self, notification): - log.msg("PUBLISH for xcap-diff event failed to %s for %s" % (notification.data.route_header.uri, notification.sender.from_header.uri)) + log.info('PUBLISH for xcap-diff event failed to %s for %s' % (notification.data.route_header.uri, notification.sender.from_header.uri)) NotificationCenter().remove_observer(self, sender=notification.sender) class Storage(object): __metaclass__ = Singleton def __init__(self): self._database = DatabaseConnection() self._provisioning = XCAPProvisioning() self._sip_notifier = SIPNotifier() self._notifier = Notifier(ServerConfig.root, self._sip_notifier.send_publish) def _normalize_document_path(self, uri): if uri.application_id in ("pres-rules", "org.openmobilealliance.pres-rules"): # some clients e.g. counterpath's eyebeam save presence rules under # different filenames between versions and they expect to find the same # information, thus we are forcing all presence rules documents to be # saved under "index.xml" default filename uri.doc_selector.document_path = "index.xml" def get_document(self, uri, check_etag): self._normalize_document_path(uri) result = self._database.get(uri) result.addCallback(self._got_document, check_etag) result.addErrback(self._eb_not_found) return result def _eb_not_found(self, failure): failure.trap(NotFound) return StatusResponse(404) def _got_document(self, (doc, etag), check_etag): check_etag(etag) return StatusResponse(200, etag, doc.encode('utf-8')) def put_document(self, uri, document, check_etag): document = document.decode('utf-8') self._normalize_document_path(uri) etag = make_random_etag(uri) result = self._database.put(uri, document, check_etag, etag) result.addCallback(self._cb_put, uri, "%s@%s" % (uri.user.username, uri.user.domain)) result.addErrback(self._eb_not_found) return result def _cb_put(self, result, uri, thor_key): if result[0]: code = 200 else: code = 201 self._provisioning.notify("update", "sip_account", thor_key) self._notifier.on_change(uri, result[1], result[2]) return StatusResponse(code, result[2]) def delete_documents(self, uri): result = self._database.delete_all(uri) result.addCallback(self._cb_delete_all, uri, "%s@%s" % (uri.user.username, uri.user.domain)) result.addErrback(self._eb_not_found) return result def _cb_delete_all(self, result, uri, thor_key): self._provisioning.notify("update", "sip_account", thor_key) return StatusResponse(200) def delete_document(self, uri, check_etag): self._normalize_document_path(uri) result = self._database.delete(uri, check_etag) result.addCallback(self._cb_delete, uri, "%s@%s" % (uri.user.username, uri.user.domain)) result.addErrback(self._eb_not_found) return result def _cb_delete(self, result, uri, thor_key): self._provisioning.notify("update", "sip_account", thor_key) self._notifier.on_change(uri, result[1], None) return StatusResponse(200) def get_watchers(self, uri): thor_key = "%s@%s" % (uri.user.username, uri.user.domain) result = self._provisioning.get_watchers(thor_key) result.addCallback(self._get_watchers_decode) return result def _get_watchers_decode(self, response): if response.code == 200: watchers = cjson.decode(response.data) for watcher in watchers: watcher["online"] = str(watcher["online"]).lower() return watchers else: print "error: %s" % response def get_documents_list(self, uri): result = self._database.get_documents_list(uri) result.addCallback(self._got_documents_list) result.addErrback(self._got_documents_list_error) return result def _got_documents_list(self, xcap_docs): docs = {} if xcap_docs: for k, v in xcap_docs.iteritems(): for k2, v2 in v.iteritems(): if docs.has_key(k): docs[k].append((k2, v2[1])) else: docs[k] = [(k2, v2[1])] return docs def _got_documents_list_error(self, failure): failure.trap(NotFound) return {} installSignalHandlers = False diff --git a/xcap/datatypes.py b/xcap/datatypes.py index c2b7539..e3f9d29 100644 --- a/xcap/datatypes.py +++ b/xcap/datatypes.py @@ -1,66 +1,66 @@ """Configuration data types""" import re import urlparse from application import log class XCAPRootURI(str): """An XCAP root URI and a number of optional aliases""" def __new__(cls, value): if value is None: return None elif not isinstance(value, basestring): raise TypeError("value must be a string, unicode or None") if value.strip() == '': return None valid_uris = [] for uri in re.split(r'\s*,\s*', value): scheme, host, path, params, query, fragment = urlparse.urlparse(uri) if host and scheme in ('http', 'https'): for u in valid_uris: if u == uri or uri.startswith(u) or u.startswith(uri): - log.warn("ignoring XCAP Root URI %r (similar to %r)" % (uri, u)) + log.warning('Ignoring XCAP Root URI %r (similar to %r)' % (uri, u)) break else: valid_uris.append(uri) else: - log.warn("Invalid XCAP Root URI: %r" % uri) + log.warning('Invalid XCAP Root URI: %r' % uri) if not valid_uris: return None instance = str.__new__(cls, valid_uris[0]) instance.uris = tuple(valid_uris) return instance def _get_port_from_uri(self, uri): scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri) if scheme and netloc: if len(netloc.split(":")) == 2: try: port = int(netloc.split(":")[1]) except ValueError: return None else: return port if port < 65536 else None if scheme.lower() == "http": return 80 if scheme.lower() == "https": return 443 return None @property def aliases(self): return self.uris[1:] @property def port(self): listen_port = self._get_port_from_uri(self) if listen_port: for uri in self.aliases: if self._get_port_from_uri(uri) != listen_port: raise ValueError("All XCAP root aliases must have the same port number") return listen_port else: raise ValueError("Invalid port specified") diff --git a/xcap/logutil.py b/xcap/logutil.py index e4b5745..314e594 100644 --- a/xcap/logutil.py +++ b/xcap/logutil.py @@ -1,274 +1,168 @@ import os import re -import logging -from StringIO import StringIO -from twisted.web2 import responsecode + from application import log from application.configuration import ConfigSection, ConfigSetting -from logging.handlers import RotatingFileHandler +from application.python.types import Singleton +from application.system import makedirs +from logging.handlers import WatchedFileHandler import xcap -class ResponseCodeList(set): - names = {} +class Code(int): + def __new__(cls, x): + instance = super(Code, cls).__new__(cls, x) + if not 100 <= instance <= 999: + raise ValueError('Invalid HTTP response code: {}'.format(x)) + return instance - for k, v in responsecode.__dict__.iteritems(): - if isinstance(v, int) and 100<=v<=999: - names[k.replace('_', '').lower()] = v - del k, v - def __new__(cls, value): - if value.lower() in ('*', 'any', 'all', 'yes'): - return AnyResponseCode - return set.__new__(cls, value) +class MatchAnyCode(object): + def __contains__(self, item): + return True - def __init__(self, value): - value = value.strip() - if value.lower() in ('none', '', 'no'): - return - for x in re.split(r'\s*,\s*', value): - x = x.lower() - n = self.names.get(x.replace(' ', '').replace('_', '')) - if n is None: - n = int(x) - if not 100<=n<=999: - raise ValueError, '%s cannot be an HTTP error code' % n - self.add(n) - else: - self.add(n) - -class AnyResponseCode(ResponseCodeList): - def __new__(cls, value): - return set.__new__(cls) + def __repr__(self): + return '{0.__class__.__name__}()'.format(self) + +class ResponseCodeList(object): def __init__(self, value): - pass + value = value.strip().lower() + if value in ('all', 'any', 'yes', '*'): + self._codes = MatchAnyCode() + elif value in ('none', 'no'): + self._codes = set() + else: + self._codes = {Code(code) for code in re.split(r'\s*,\s*', value)} - def __contains__(self, anything): - return True + def __contains__(self, item): + return item in self._codes def __repr__(self): - return "AnyResponseCode" - -AnyResponseCode = AnyResponseCode('') + if isinstance(self._codes, MatchAnyCode): + value = 'all' + elif not self._codes: + value = 'none' + else: + value = ','.join(sorted(self._codes)) + return '{0.__class__.__name__}({1!r})'.format(self, value) class Logging(ConfigSection): __cfgfile__ = xcap.__cfgfile__ __section__ = 'Logging' - # directory where access.log will be created - # if directory is empty, everything (access and error) will be - # printed to console - directory = '/var/log/openxcap' - - # each log message is followed by the headers of the request - log_request_headers = ConfigSetting(type=ResponseCodeList, value=[500]) - - log_request_body = ConfigSetting(type=ResponseCodeList, value=[500]) - - log_response_headers = ConfigSetting(type=ResponseCodeList, value=[500]) - - log_response_body = ConfigSetting(type=ResponseCodeList, value=[500]) - - log_stacktrace = ConfigSetting(type=ResponseCodeList, value=[500]) - - -def log_format_request_headers(code, r): - if matches(Logging.log_request_headers, code): - return format_headers(r, 'REQUEST headers:\n') - return '' - -def log_format_response_headers(code, r): - if matches(Logging.log_response_headers, code): - return format_headers(r, 'RESPONSE headers:\n') - return '' - -def log_format_request_body(code, request): - if matches(Logging.log_request_body, code): - return format_request_body(request) - return '' - -def log_format_response_body(code, response): - if matches(Logging.log_response_body, code): - return format_response_body(response) - return '' - -def log_format_stacktrace(code, reason): - if reason is not None and matches(Logging.log_stacktrace, code): - return format_stacktrace(reason) - return '' - - -def matches(cfg, code): - return cfg == '*' or code in cfg - -def format_response_body(response): - res = '' - content_type = None - if hasattr(response, 'headers'): - content_type = response.headers.getRawHeaders('content-type') - if hasattr(response, 'stream') and hasattr(response.stream, 'mem'): - res = str(response.stream.mem) - if res: - msg = '' - if content_type: - for x in content_type: - msg += 'Content-Type: %s\n' % x - msg += res - return 'RESPONSE: ' + msg.replace('\n', '\n\t') + '\n' - return res - -def format_headers(r, msg='REQUEST headers:\n'): - res = '' - if hasattr(r, 'headers'): - for (k, v) in r.headers.getAllRawHeaders(): - for x in v: - res += '\t%s: %s\n' % (k, x) - if res: - res = msg + res - return res - -def format_request_body(request): - res = '' - if hasattr(request, 'attachment'): - res = str(request.attachment) - if res: - return 'REQUEST: ' + res.replace('\n', '\n\t') + '\n' - return res - -def format_stacktrace(reason): - if hasattr(reason, 'getTracebackObject') and reason.getTracebackObject() is not None: - f = StringIO() - reason.printTraceback(file=f) - res = f.getvalue() - first, rest = res.split('\n', 1) - if rest[-1:]=='\n': - rest = rest[:-1] - if rest: - return first.replace('Traceback', 'TRACEBACK') + '\n\t' + rest.replace('\n', '\n\t') - return first - return '' - -def _repr(p): - if p is None: - return '-' - else: - return repr(p) - -def _str(p): - if p is None: - return '-' - else: - return str(p) - -def _etag(etag): - if etag is None: - return '-' - if hasattr(etag, 'generate'): - return etag.generate() - else: - return repr(etag) - -def get_ip(request): - if hasattr(request, 'remoteAddr'): - return str(getattr(request.remoteAddr, 'host', '-')) - else: - return '-' - -def get(obj, attr): - return _repr(getattr(obj, attr, None)) - -def format_access_record(request, response): - - def format_clientproto(proto): - try: - return "HTTP/%d.%d" % (proto[0], proto[1]) - except IndexError: - return "" - - ip = get_ip(request) - request_line = "'%s %s %s'" % (request.method, request.unparseURL(), format_clientproto(request.clientproto)) - code = get(response, 'code') - - if hasattr(request, 'stream'): - request_length = get(request.stream, 'length') - else: - request_length = '-' - - if hasattr(response, 'stream'): - response_length = get(response.stream, 'length') - else: - response_length = '-' - - if hasattr(request, 'headers'): - user_agent = _repr(request.headers.getHeader('user-agent')) - else: - user_agent = '-' - - if hasattr(response, 'headers'): - response_etag = _etag(response.headers.getHeader('etag')) - else: - response_etag = '-' - - params = [ip, request_line, code, request_length, response_length, user_agent, response_etag] - return ' '.join(params) - -def format_log_message(request, response, reason): - msg = '' - info = '' - try: - msg = format_access_record(request, response) - code = getattr(response, 'code', None) - info += log_format_request_headers(code, request) - info += log_format_request_body(code, request) - info += log_format_response_headers(code, response) - info += log_format_response_body(code, response) - info += log_format_stacktrace(code, reason) - except Exception: - log.error('Formatting log message failed') - log.err() - if info[-1:]=='\n': - info = info[:-1] - if info: - info = '\n' + info - return msg + info - -def log_access(request, response, reason=None): - if getattr(request, '_logged', False): - return - msg = format_log_message(request, response, reason) - request._logged = True - if msg and (response.stream is None or response.stream.length < 5000): - log.msg(AccessLog(msg)) - -def log_error(request, response, reason): - msg = format_log_message(request, response, reason) - request._logged = True - if msg: - log.error(msg) - -class AccessLog(str): pass - -class IsAccessLog(logging.Filter): - def filter(self, record): - return isinstance(record.msg, AccessLog) - -class IsNotAccessLog(logging.Filter): - def filter(self, record): - return not isinstance(record.msg, AccessLog) - -def start_log(): - log.start_syslog('openxcap') - if Logging.directory: - if not os.path.exists(Logging.directory): - os.mkdir(Logging.directory) - handler = RotatingFileHandler(os.path.join(Logging.directory, 'access.log'), 'a', 2*1024*1024, 5) - handler.addFilter(IsAccessLog()) - log.logger.addHandler(handler) - for handler in log.logger.handlers: - if isinstance(handler, log.SyslogHandler): - handler.addFilter(IsNotAccessLog()) + directory = '/var/log/openxcap' # directory where access.log will be created (if not specified, access logs will be logged as application log messages) + log_request = ConfigSetting(type=ResponseCodeList, value=ResponseCodeList('none')) + log_response = ConfigSetting(type=ResponseCodeList, value=ResponseCodeList('none')) + + +class _LoggedTransaction(object): + def __init__(self, request, response): + self._request = request + self._response = response + + def __str__(self): + return self.access_info + + @property + def access_info(self): + return '{request.remote_host} - {request.line!r} {response.code} {response.length} {request.user_agent!r} {response.etag!r}'.format(request=self._request, response=self._response) + + # + # Request related properties + # + + @property + def line(self): + return '{request.method} {request.uri} HTTP/{request.clientproto[0]}.{request.clientproto[1]}'.format(request=self._request) + + @property + def remote_host(self): + try: + return self._request.remoteAddr.host + except AttributeError: + try: + return self._request.chanRequest.getRemoteHost().host + except (AttributeError, TypeError): + return '-' + + @property + def user_agent(self): + return self._request.headers.getHeader('user-agent', '-') + + @property + def request_content(self): + headers = '\n'.join('{}: {}'.format(name, header) for name, headers in self._request.headers.getAllRawHeaders() for header in headers) + body = getattr(self._request, 'attachment', '') + content = '\n\n'.join(item for item in (headers, body) if item) + return '\nRequest:\n\n{}\n\n'.format(content) if content else '' + + # + # Response related properties + # + + @property + def code(self): + return self._response.code + + @property + def length(self): + return self._response.stream.length if self._response.stream else 0 + + @property + def etag(self): + etag = self._response.getHeader('etag') or '-' + if hasattr(etag, 'tag'): + etag = etag.tag + return etag + + @property + def response_content(self): + headers = '\n'.join('{}: {}'.format(name, header) for name, headers in self._response.headers.getAllRawHeaders() for header in headers) + body = self._response.stream.mem if self._response.stream else '' + content = '\n\n'.join(item for item in (headers, body) if item) + return '\nResponse:\n\n{}\n\n'.format(content) if content else '' + + +class WEBLogger(object): + __metaclass__ = Singleton + + def __init__(self): + self.logger = log.get_logger('weblog') + self.logger.setLevel(log.level.INFO) + if Logging.directory: + if not os.path.exists(Logging.directory): + try: + makedirs(Logging.directory) + except OSError as e: + raise RuntimeError('Cannot create logging directory {}: {}'.format(Logging.directory, e)) + self.filename = os.path.join(Logging.directory, 'access.log') + formatter = log.Formatter() + formatter.prefix_format = '' + handler = WatchedFileHandler(self.filename) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.logger.propagate = False + else: + self.filename = None + + def log_access(self, request, response): + web_transaction = _LoggedTransaction(request, response) + self.logger.info(web_transaction.access_info) + if response.code in Logging.log_request: + request_content = web_transaction.request_content + if request_content: + self.logger.info(request_content) + if response.code in Logging.log_response and web_transaction.length < 5000: + response_content = web_transaction.response_content + if response_content: + self.logger.info(response_content) + + +root_logger = log.get_logger() +root_logger.name = 'server' +web_logger = WEBLogger() diff --git a/xcap/server.py b/xcap/server.py index e0ef34c..a6abeed 100644 --- a/xcap/server.py +++ b/xcap/server.py @@ -1,240 +1,190 @@ """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, http_headers, server +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 -from twisted.python import failure 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 log_access, log_error +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.fatal("the XCAP root URI is not defined") + log.critical('The XCAP root URI is not defined') sys.exit(1) if ServerConfig.backend is None: - log.fatal("OpenXCAP needs a backend to be specified in order to run") + 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.warn("Could not raise open file descriptor limit") + 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) -def get_response_body(exc): - if hasattr(exc, 'stream') and hasattr(exc.stream, 'mem'): - return exc.stream.mem - else: - return str(exc) - class Request(server.Request): - - def __init__(self, *args, **kw): - server.Request.__init__(self, *args, **kw) - def writeResponse(self, response): - reason = getattr(self, '_reason', None) - log_access(self, response, reason) - try: - return server.Request.writeResponse(self, response) - finally: - if reason is not None: - del self._reason - - def _processingFailed(self, reason): - # save the reason, it will be used for the stacktrace - self._reason = reason - - exc = getattr(reason, 'value', None) - if exc: - # if the exception has 'http_error' and it is HTTPError, we use it to generate the response. - # this allows us to attach http_error to non-HTTPError errors (as opposed to - # re-raising HTTPError-derived exception) and enjoy the original stacktraces in the log - if not isinstance(exc, http.HTTPError) and hasattr(exc, 'http_error'): - http_error = exc.http_error - if isinstance(http_error, http.HTTPError): - return server.Request._processingFailed(self, failure.Failure(http_error)) - elif isinstance(http_error, int): - s = get_response_body(exc) - response = http.Response(http_error, - {'content-type': http_headers.MimeType('text','plain')}, - stream=s) - fail = failure.Failure(http.HTTPError(response)) - return server.Request._processingFailed(self, fail) - - return server.Request._processingFailed(self, reason) - - def renderHTTP_exception(self, req, reason): - response = http.Response( - responsecode.INTERNAL_SERVER_ERROR, - {'content-type': http_headers.MimeType('text','plain')}, - ("An error occurred while processing the request. " - "More information is available in the server log.")) - - log_error(req, response, reason) - return response + web_logger.log_access(request=self, response=response) + return server.Request.writeResponse(self, response) class HTTPChannelRequest(channel.http.HTTPChannelRequest): _base = channel.http.HTTPChannelRequest def gotInitialLine(self, line): self._initial_line = line return self._base.gotInitialLine(self, line) def createRequest(self): self._base.createRequest(self) self.request._initial_line = self._initial_line class HTTPChannel(channel.http.HTTPChannel): chanRequestFactory = HTTPChannelRequest 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.msg("Timing out client: %s" % str(self.transport.getPeer())) + 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) elif auth_type == 'digest': credential_factory = tweak_DigestCredentialFactory('MD5', auth_type) else: raise ValueError("Invalid authentication type: '%s'. 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.fatal("the TLS certificates or the private key could not be loaded") + 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.msg("TLS started") + log.info('TLS started') def start(self): - log.msg("Listening on: %s:%d" % (ServerConfig.address, ServerConfig.root.port)) - log.msg("XCAP root: %s" % ServerConfig.root) + 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/tls.py b/xcap/tls.py index fef1cc5..7538549 100644 --- a/xcap/tls.py +++ b/xcap/tls.py @@ -1,54 +1,54 @@ """TLS helper classes""" __all__ = ['Certificate', 'PrivateKey'] from gnutls.crypto import X509Certificate, X509PrivateKey from application import log from application.process import process class _FileError(Exception): pass def file_content(file): path = process.config_file(file) if path is None: raise _FileError("File '%s' does not exist" % file) try: f = open(path, 'rt') except Exception: raise _FileError("File '%s' could not be open" % file) try: return f.read() finally: f.close() class Certificate(object): """Configuration data type. Used to create a gnutls.crypto.X509Certificate object from a file given in the configuration file.""" def __new__(cls, value): if isinstance(value, str): try: return X509Certificate(file_content(value)) except Exception, e: - log.warn("Certificate file '%s' could not be loaded: %s" % (value, str(e))) + log.warning('Certificate file %r could not be loaded: %s' % (value, e)) return None else: raise TypeError('value should be a string') class PrivateKey(object): """Configuration data type. Used to create a gnutls.crypto.X509PrivateKey object from a file given in the configuration file.""" def __new__(cls, value): if isinstance(value, str): try: return X509PrivateKey(file_content(value)) except Exception, e: - log.warn("Private key file '%s' could not be loaded: %s" % (value, str(e))) + log.warning('Private key file %r could not be loaded: %s' % (value, e)) return None else: raise TypeError('value should be a string')