| 1 | # Copyright (C) 2007 AG Projects. |
|---|
| 2 | # |
|---|
| 3 | |
|---|
| 4 | """HTTP handling for the XCAP server""" |
|---|
| 5 | |
|---|
| 6 | import sys |
|---|
| 7 | |
|---|
| 8 | from application.configuration.datatypes import StringList |
|---|
| 9 | from application import log |
|---|
| 10 | |
|---|
| 11 | from twisted.web2 import channel, resource, http, responsecode, http_headers, server |
|---|
| 12 | from twisted.cred.portal import Portal |
|---|
| 13 | from twisted.web2.auth import digest, basic |
|---|
| 14 | from twisted.python import failure |
|---|
| 15 | |
|---|
| 16 | from xcap.config import ConfigFile, ConfigSection |
|---|
| 17 | from xcap import authentication |
|---|
| 18 | from xcap.appusage import getApplicationForURI, Backend |
|---|
| 19 | from xcap.resource import XCAPDocument, XCAPElement, XCAPAttribute, XCAPNamespaceBinding |
|---|
| 20 | from xcap.uri import AttributeSelector, NamespaceSelector |
|---|
| 21 | from xcap import __version__ as version |
|---|
| 22 | from xcap.logutil import log_access, log_error |
|---|
| 23 | |
|---|
| 24 | server.VERSION = "OpenXCAP/%s" % version |
|---|
| 25 | |
|---|
| 26 | class AuthenticationConfig(ConfigSection): |
|---|
| 27 | _datatypes = {'trusted_peers': StringList, |
|---|
| 28 | 'default_realm': str} |
|---|
| 29 | type = 'basic' |
|---|
| 30 | cleartext_passwords = True |
|---|
| 31 | default_realm = None |
|---|
| 32 | trusted_peers = [] |
|---|
| 33 | |
|---|
| 34 | class ServerConfig(ConfigSection): |
|---|
| 35 | _datatypes = {'backend': Backend} |
|---|
| 36 | port = 8000 |
|---|
| 37 | address = '0.0.0.0' |
|---|
| 38 | root = 'http://127.0.0.1/' |
|---|
| 39 | backend = Backend('Database') |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | ## We use this to overwrite some of the settings above on a local basis if needed |
|---|
| 43 | configuration = ConfigFile() |
|---|
| 44 | configuration.read_settings('Authentication', AuthenticationConfig) |
|---|
| 45 | configuration.read_settings('Server', ServerConfig) |
|---|
| 46 | |
|---|
| 47 | |
|---|
| 48 | class XCAPRoot(resource.Resource, resource.LeafResource): |
|---|
| 49 | addSlash = True |
|---|
| 50 | |
|---|
| 51 | def allowedMethods(self): |
|---|
| 52 | # not used , but methods were already checked by XCAPAuthResource |
|---|
| 53 | return ('GET', 'PUT', 'DELETE') |
|---|
| 54 | |
|---|
| 55 | def resourceForURI(self, xcap_uri): |
|---|
| 56 | application = getApplicationForURI(xcap_uri) |
|---|
| 57 | if not xcap_uri.node_selector: |
|---|
| 58 | return XCAPDocument(xcap_uri, application) |
|---|
| 59 | else: |
|---|
| 60 | terminal_selector = xcap_uri.node_selector.terminal_selector |
|---|
| 61 | if isinstance(terminal_selector, AttributeSelector): |
|---|
| 62 | return XCAPAttribute(xcap_uri, application) |
|---|
| 63 | elif isinstance(terminal_selector, NamespaceSelector): |
|---|
| 64 | return XCAPNamespaceBinding(xcap_uri, application) |
|---|
| 65 | else: |
|---|
| 66 | return XCAPElement(xcap_uri, application) |
|---|
| 67 | |
|---|
| 68 | def renderHTTP(self, request): |
|---|
| 69 | application = getApplicationForURI(request.xcap_uri) |
|---|
| 70 | if not application: |
|---|
| 71 | return http.Response(responsecode.NOT_FOUND, stream="Application not supported") |
|---|
| 72 | resource = self.resourceForURI(request.xcap_uri) |
|---|
| 73 | return resource.renderHTTP(request) |
|---|
| 74 | |
|---|
| 75 | |
|---|
| 76 | def get_response_body(exc): |
|---|
| 77 | if hasattr(exc, 'stream') and hasattr(exc.stream, 'mem'): |
|---|
| 78 | return exc.stream.mem |
|---|
| 79 | else: |
|---|
| 80 | return str(exc) |
|---|
| 81 | |
|---|
| 82 | class Request(server.Request): |
|---|
| 83 | |
|---|
| 84 | def __init__(self, *args, **kw): |
|---|
| 85 | server.Request.__init__(self, *args, **kw) |
|---|
| 86 | |
|---|
| 87 | def writeResponse(self, response): |
|---|
| 88 | reason = getattr(self, '_reason', None) |
|---|
| 89 | log_access(self, response, reason) |
|---|
| 90 | try: |
|---|
| 91 | return server.Request.writeResponse(self, response) |
|---|
| 92 | finally: |
|---|
| 93 | if reason is not None: |
|---|
| 94 | del self._reason |
|---|
| 95 | |
|---|
| 96 | def _processingFailed(self, reason): |
|---|
| 97 | # save the reason, it will be used for the stacktrace |
|---|
| 98 | self._reason = reason |
|---|
| 99 | |
|---|
| 100 | exc = getattr(reason, 'value', None) |
|---|
| 101 | if exc: |
|---|
| 102 | # if the exception has 'http_error' and it is HTTPError, we use it to generate the response. |
|---|
| 103 | # this allows us to attach http_error to non-HTTPError errors (as opposed to |
|---|
| 104 | # re-raising HTTPError-derived exception) and enjoy the original stacktraces in the log |
|---|
| 105 | if not isinstance(exc, http.HTTPError) and hasattr(exc, 'http_error'): |
|---|
| 106 | http_error = exc.http_error |
|---|
| 107 | if isinstance(http_error, http.HTTPError): |
|---|
| 108 | return server.Request._processingFailed(self, failure.Failure(http_error)) |
|---|
| 109 | elif isinstance(http_error, int): |
|---|
| 110 | s = get_response_body(exc) |
|---|
| 111 | response = http.Response(http_error, |
|---|
| 112 | {'content-type': http_headers.MimeType('text','plain')}, |
|---|
| 113 | stream=s) |
|---|
| 114 | fail = failure.Failure(http.HTTPError(response)) |
|---|
| 115 | return server.Request._processingFailed(self, fail) |
|---|
| 116 | |
|---|
| 117 | return server.Request._processingFailed(self, reason) |
|---|
| 118 | |
|---|
| 119 | def renderHTTP_exception(self, req, reason): |
|---|
| 120 | response = http.Response( |
|---|
| 121 | responsecode.INTERNAL_SERVER_ERROR, |
|---|
| 122 | {'content-type': http_headers.MimeType('text','plain')}, |
|---|
| 123 | ("An error occurred while processing the request. " |
|---|
| 124 | "More information is available in the server log.")) |
|---|
| 125 | |
|---|
| 126 | log_error(req, response, reason) |
|---|
| 127 | return response |
|---|
| 128 | |
|---|
| 129 | |
|---|
| 130 | class HTTPChannelRequest(channel.http.HTTPChannelRequest): |
|---|
| 131 | _base = channel.http.HTTPChannelRequest |
|---|
| 132 | |
|---|
| 133 | def gotInitialLine(self, line): |
|---|
| 134 | self._initial_line = line |
|---|
| 135 | return self._base.gotInitialLine(self, line) |
|---|
| 136 | |
|---|
| 137 | def createRequest(self): |
|---|
| 138 | self._base.createRequest(self) |
|---|
| 139 | self.request._initial_line = self._initial_line |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | class HTTPChannel(channel.http.HTTPChannel): |
|---|
| 143 | chanRequestFactory = HTTPChannelRequest |
|---|
| 144 | |
|---|
| 145 | |
|---|
| 146 | class HTTPFactory(channel.HTTPFactory): |
|---|
| 147 | noisy = False |
|---|
| 148 | protocol = HTTPChannel |
|---|
| 149 | |
|---|
| 150 | |
|---|
| 151 | class XCAPSite(server.Site): |
|---|
| 152 | |
|---|
| 153 | def __call__(self, *args, **kwargs): |
|---|
| 154 | return Request(site=self, *args, **kwargs) |
|---|
| 155 | |
|---|
| 156 | |
|---|
| 157 | class XCAPServer: |
|---|
| 158 | |
|---|
| 159 | def __init__(self): |
|---|
| 160 | portal = Portal(authentication.XCAPAuthRealm()) |
|---|
| 161 | if AuthenticationConfig.cleartext_passwords: |
|---|
| 162 | http_checker = ServerConfig.backend.PlainPasswordChecker() |
|---|
| 163 | else: |
|---|
| 164 | http_checker = ServerConfig.backend.HashPasswordChecker() |
|---|
| 165 | portal.registerChecker(http_checker) |
|---|
| 166 | trusted_peers = AuthenticationConfig.trusted_peers |
|---|
| 167 | if trusted_peers: |
|---|
| 168 | log.info("Trusted peers: %s" % ", ".join(trusted_peers)) |
|---|
| 169 | portal.registerChecker(authentication.TrustedPeerChecker(trusted_peers)) |
|---|
| 170 | |
|---|
| 171 | auth_type = AuthenticationConfig.type |
|---|
| 172 | if auth_type == 'basic': |
|---|
| 173 | credential_factory = basic.BasicCredentialFactory(auth_type) |
|---|
| 174 | elif auth_type == 'digest': |
|---|
| 175 | credential_factory = digest.DigestCredentialFactory('MD5', auth_type) |
|---|
| 176 | else: |
|---|
| 177 | raise ValueError("Invalid authentication type: '%s'. Please check the configuration." % auth_type) |
|---|
| 178 | |
|---|
| 179 | root = authentication.XCAPAuthResource(XCAPRoot(), |
|---|
| 180 | (credential_factory,), |
|---|
| 181 | portal, (authentication.IAuthUser,)) |
|---|
| 182 | self.site = XCAPSite(root) |
|---|
| 183 | |
|---|
| 184 | def _start_https(self, reactor): |
|---|
| 185 | from xcap.tls import Certificate, PrivateKey |
|---|
| 186 | class TLSConfig(ConfigSection): |
|---|
| 187 | _datatypes = {'certificate': Certificate, 'private_key': PrivateKey} |
|---|
| 188 | certificate = None |
|---|
| 189 | private_key = None |
|---|
| 190 | |
|---|
| 191 | configuration.read_settings('TLS', TLSConfig) |
|---|
| 192 | |
|---|
| 193 | from gnutls.interfaces.twisted import X509Credentials |
|---|
| 194 | cert, pKey = TLSConfig.certificate, TLSConfig.private_key |
|---|
| 195 | if cert is None or pKey is None: |
|---|
| 196 | log.fatal("the TLS certificates or the private key could not be loaded") |
|---|
| 197 | sys.exit(1) |
|---|
| 198 | credentials = X509Credentials(cert, pKey) |
|---|
| 199 | reactor.listenTLS(ServerConfig.port, HTTPFactory(self.site), credentials, interface=ServerConfig.address) |
|---|
| 200 | log.msg("TLS started") |
|---|
| 201 | |
|---|
| 202 | def start(self): |
|---|
| 203 | if 'twisted.internet.reactor' not in sys.modules: |
|---|
| 204 | from twisted.internet import pollreactor; pollreactor.install() |
|---|
| 205 | from twisted.internet import reactor |
|---|
| 206 | |
|---|
| 207 | if ServerConfig.root.startswith('https'): |
|---|
| 208 | self._start_https(reactor) |
|---|
| 209 | else: |
|---|
| 210 | reactor.listenTCP(ServerConfig.port, HTTPFactory(self.site), interface=ServerConfig.address) |
|---|
| 211 | self.run(reactor) |
|---|
| 212 | |
|---|
| 213 | def run(self, reactor): |
|---|
| 214 | reactor.run(installSignalHandlers=ServerConfig.backend.installSignalHandlers) |
|---|