Commit c422df78 authored by Romain Courteaud's avatar Romain Courteaud Committed by Vincent Pelletier

wsgi: Rework routing dict (again).

Remove special handling of first folder level.
Generalise CAU/CAS context decision.
Split functionalities further, making each method shorter.
Factorise subpath checks.
Factorise response generation when producing a body.

The resulting data structure, if more verbose than the original one, is
not harder to traverse and more extensible.
parent aa8ee0dc
...@@ -1198,6 +1198,17 @@ class CaucaseTest(unittest.TestCase): ...@@ -1198,6 +1198,17 @@ class CaucaseTest(unittest.TestCase):
""" """
raise ValueError('Some generic exception') raise ValueError('Some generic exception')
@staticmethod
def _placeholder(_):
"""
Placeholder methods, for when method lookup happens before noticing
issues in the query.
"""
raise AssertionError('code should fail before actually calling this')
getCertificateSigningRequest = _placeholder
getCertificate = _placeholder
application = wsgi.Application(DummyCAU(), None) application = wsgi.Application(DummyCAU(), None)
def request(environ): def request(environ):
""" """
...@@ -1226,12 +1237,15 @@ class CaucaseTest(unittest.TestCase): ...@@ -1226,12 +1237,15 @@ class CaucaseTest(unittest.TestCase):
})[0], 404) })[0], 404)
self.assertEqual(request({ self.assertEqual(request({
'PATH_INFO': '/cau', 'PATH_INFO': '/cau',
'REQUEST_METHOD': 'GET',
})[0], 404) })[0], 404)
self.assertEqual(request({ self.assertEqual(request({
'PATH_INFO': '/cau/__init__', 'PATH_INFO': '/cau/__init__',
'REQUEST_METHOD': 'GET',
})[0], 404) })[0], 404)
self.assertEqual(request({ self.assertEqual(request({
'PATH_INFO': '/cau/does_not_exist', 'PATH_INFO': '/cau/does_not_exist',
'REQUEST_METHOD': 'GET',
})[0], 404) })[0], 404)
self.assertEqual(request({ self.assertEqual(request({
......
...@@ -27,6 +27,10 @@ from . import exceptions ...@@ -27,6 +27,10 @@ from . import exceptions
__all__ = ('Application', ) __all__ = ('Application', )
SUBPATH_FORBIDDEN = object()
SUBPATH_REQUIRED = object()
SUBPATH_OPTIONAL = object()
def _getStatus(code): def _getStatus(code):
return '%i %s' % (code, httplib.responses[code]) return '%i %s' % (code, httplib.responses[code])
...@@ -119,33 +123,112 @@ class Application(object): ...@@ -119,33 +123,112 @@ class Application(object):
Will be hosted under /cas Will be hosted under /cas
""" """
self._cau = cau self._cau = cau
self._context_dict = { # Routing dict structure:
'cau': cau,
'cas': cas, # path entry dict:
} # "method": method dict
self._routing_dict = { # "context": any object
# "routing": routing dict
# routing dict:
# key: path entry (ie, everything but slashes)
# value: path entry dict
# method dict:
# key: HTTP method ("GET", "POST", ...)
# value: action dict
# action dict:
# "do": callable for the action
# If "subpath" forbidden:
# (context, environ) -> (status, header_list, iterator)
# Otherwise:
# (context, environ, subpath) -> (status, header_list, iterator)
# - context is the value of the nearest path entry dict's "context", None
# by default.
# - environ: wsgi environment
# - subpath: trailing path component list
# - status: HTTP status code & reason
# - header_list: HTTP reponse header list (see wsgi specs)
# - iterator: HTTP response body generator (see wsgi specs)
# "subpath": whether a subpath is expected, forbidden, or optional
# (default: forbidden)
caucase_routing_dict = {
'crl': { 'crl': {
'GET': { 'method': {
'method': self.getCRL, 'GET': {
'do': self.getCertificateRevocationList,
},
}, },
}, },
'csr': { 'csr': {
'GET': { 'method': {
'method': self.getCSR, 'GET': {
'do': self.getCSR,
'subpath': SUBPATH_OPTIONAL,
},
'PUT': {
'do': self.createCertificateSigningRequest,
},
'DELETE': {
'do': self.deletePendingCertificateRequest,
'subpath': SUBPATH_REQUIRED,
},
}, },
'PUT': { },
'method': self.putCSR, 'crt': {
'routing': {
'ca.crt.pem': {
'method': {
'GET': {
'do': self.getCACertificate,
},
},
},
'ca.crt.json': {
'method': {
'GET': {
'do': self.getCACertificateChain,
},
},
},
'revoke': {
'method': {
'PUT': {
'do': self.revokeCertificate,
},
},
},
'renew': {
'method': {
'PUT': {
'do': self.renewCertificate,
},
},
},
}, },
'DELETE': { 'method': {
'method': self.deleteCSR, 'GET': {
'do': self.getCertificate,
'subpath': SUBPATH_REQUIRED,
},
'PUT': {
'do': self.createCertificate,
'subpath': SUBPATH_REQUIRED,
},
}, },
}, },
'crt': { }
'GET': { self._root_dict = {
'method': self.getCRT, 'routing': {
'cas': {
'context': cas,
'routing': caucase_routing_dict,
}, },
'PUT': { 'cau': {
'method': self.putCRT, 'context': cau,
'routing': caucase_routing_dict,
}, },
}, },
} }
...@@ -156,27 +239,50 @@ class Application(object): ...@@ -156,27 +239,50 @@ class Application(object):
""" """
try: # Convert ApplicationError subclasses into error responses try: # Convert ApplicationError subclasses into error responses
try: # Convert exceptions into ApplicationError subclass exceptions try: # Convert exceptions into ApplicationError subclass exceptions
path_item_list = [x for x in environ['PATH_INFO'].split('/') if x] path_item_list = [
try: x
context_id, base_path = path_item_list[:2] for x in environ.get('PATH_INFO', '').split('/')
except ValueError: if x
raise NotFound ]
path_entry_dict = self._root_dict
context = None
while path_item_list:
context = path_entry_dict.get('context', context)
try:
path_entry_dict = path_entry_dict['routing'][path_item_list[0]]
except KeyError:
break
del path_item_list[0]
try: try:
context = self._context_dict[context_id] method_dict = path_entry_dict['method']
method_dict = self._routing_dict[base_path]
except KeyError: except KeyError:
raise NotFound raise NotFound
request_method = environ['REQUEST_METHOD'] request_method = environ['REQUEST_METHOD']
if request_method == 'OPTIONS': try:
status = STATUS_NO_CONTENT action_dict = method_dict[request_method]
header_list = [] except KeyError:
result = [] if request_method == 'OPTIONS':
else: status = STATUS_NO_CONTENT
try: header_list = []
entry = method_dict[request_method] result = []
except KeyError: else:
raise BadMethod raise BadMethod
status, header_list, result = entry['method'](context, environ, path_item_list[2:]) else:
subpath = action_dict.get('subpath', SUBPATH_FORBIDDEN)
if (
subpath is SUBPATH_FORBIDDEN and path_item_list or
subpath is SUBPATH_REQUIRED and not path_item_list
):
raise NotFound
if action_dict.get('context_is_routing'):
context = path_entry_dict.get('routing')
kw = {
'context': context,
'environ': environ,
}
if subpath != SUBPATH_FORBIDDEN:
kw['subpath'] = path_item_list
status, header_list, result = action_dict['do'](**kw)
except ApplicationError: except ApplicationError:
raise raise
except exceptions.NotFound: except exceptions.NotFound:
...@@ -200,6 +306,25 @@ class Application(object): ...@@ -200,6 +306,25 @@ class Application(object):
start_response(status, header_list) start_response(status, header_list)
return result return result
@staticmethod
def _returnFile(data, content_type, header_list=None):
if header_list is None:
header_list = []
header_list.append(('Content-Type', content_type))
header_list.append(('Content-Length', str(len(data))))
return (STATUS_OK, header_list, [data])
@staticmethod
def _getCSRID(subpath):
try:
crt_id, = subpath
except ValueError:
raise NotFound
try:
return int(crt_id)
except ValueError:
raise BadRequest('Invalid integer')
@staticmethod @staticmethod
def _read(environ): def _read(environ):
""" """
...@@ -253,53 +378,36 @@ class Application(object): ...@@ -253,53 +378,36 @@ class Application(object):
except ValueError: except ValueError:
raise BadRequest('Invalid json') raise BadRequest('Invalid json')
@staticmethod def getCertificateRevocationList(self, context, environ):
def getCRL(context, environ, subpath):
""" """
Handle GET /{context}/crl . Handle GET /{context}/crl .
""" """
if subpath: return self._returnFile(
raise NotFound context.getCertificateRevocationList(),
data = context.getCertificateRevocationList() 'application/pkix-crl',
return (
STATUS_OK,
[
('Content-Type', 'application/pkix-crl'),
('Content-Length', str(len(data))),
],
[data],
) )
def getCSR(self, context, environ, subpath): def getCSR(self, context, environ, subpath):
""" """
Handle GET /{context}/csr/{csr_id} and GET /{context}/csr. Handle GET /{context}/csr/{csr_id} and GET /{context}/csr.
""" """
header_list = []
if subpath: if subpath:
try: return self._returnFile(
csr_id, = subpath context.getCertificateSigningRequest(self._getCSRID(subpath)),
except ValueError: 'application/pkcs10',
raise NotFound )
try: header_list = []
csr_id = int(csr_id) self._authenticate(environ, header_list)
except ValueError: return self._returnFile(
raise BadRequest('Invalid integer') json.dumps(context.getCertificateRequestList()),
data = context.getCertificateSigningRequest(csr_id) 'application/json',
content_type = 'application/pkcs10' header_list,
else: )
self._authenticate(environ, header_list)
data = json.dumps(context.getCertificateRequestList())
content_type = 'application/json'
header_list.append(('Content-Type', content_type))
header_list.append(('Content-Length', str(len(data))))
return (STATUS_OK, header_list, [data])
def putCSR(self, context, environ, subpath): def createCertificateSigningRequest(self, context, environ):
""" """
Handle PUT /{context}/csr . Handle PUT /{context}/csr .
""" """
if subpath:
raise NotFound
try: try:
csr_id = context.appendCertificateSigningRequest(self._read(environ)) csr_id = context.appendCertificateSigningRequest(self._read(environ))
except exceptions.NotACertificateSigningRequest: except exceptions.NotACertificateSigningRequest:
...@@ -312,14 +420,13 @@ class Application(object): ...@@ -312,14 +420,13 @@ class Application(object):
[], [],
) )
def deleteCSR(self, context, environ, subpath): def deletePendingCertificateRequest(self, context, environ, subpath):
""" """
Handle DELETE /{context}/csr/{csr_id} . Handle DELETE /{context}/csr/{csr_id} .
""" """
try: # Note: single-use variable to verify subpath before allocating more
csr_id, = subpath # resources to this request
except ValueError: csr_id = self._getCSRID(subpath)
raise NotFound
header_list = [] header_list = []
self._authenticate(environ, header_list) self._authenticate(environ, header_list)
try: try:
...@@ -328,97 +435,91 @@ class Application(object): ...@@ -328,97 +435,91 @@ class Application(object):
raise NotFound raise NotFound
return (STATUS_NO_CONTENT, header_list, []) return (STATUS_NO_CONTENT, header_list, [])
def getCRT(self, context, environ, subpath): def getCACertificate(self, context, environ):
"""
Handle GET /{context}/crt/ca.crt.pem urls.
"""
return self._returnFile(
context.getCACertificate(),
'application/x-x509-ca-cert',
)
def getCACertificateChain(self, context, environ):
"""
Handle GET /{context}/crt/ca.crt.json urls.
"""
return self._returnFile(
json.dumps(context.getValidCACertificateChain()),
'application/json',
)
def getCertificate(self, context, environ, subpath):
""" """
Handle GET /{context}/crt/{crt_id} urls. Handle GET /{context}/crt/{crt_id} urls.
""" """
try: return self._returnFile(
crt_id, = subpath context.getCertificate(self._getCSRID(subpath)),
except ValueError: 'application/pkix-cert',
raise NotFound
if crt_id == 'ca.crt.pem':
data = context.getCACertificate()
content_type = 'application/x-x509-ca-cert'
elif crt_id == 'ca.crt.json':
data = json.dumps(context.getValidCACertificateChain())
content_type = 'application/json'
else:
try:
crt_id = int(crt_id)
except ValueError:
raise BadRequest('Invalid integer')
data = context.getCertificate(crt_id)
content_type = 'application/pkix-cert'
return (
STATUS_OK,
[
('Content-Type', content_type),
('Content-Length', str(len(data))),
],
[data],
) )
def putCRT(self, context, environ, subpath): def revokeCertificate(self, context, environ):
""" """
Handle PUT /{context}/crt/{crt_id} urls. Handle PUT /{context}/crt/revoke .
""" """
try: header_list = []
crt_id, = subpath data = self._readJSON(environ)
except ValueError: if data['digest'] is None:
raise NotFound self._authenticate(environ, header_list)
if crt_id == 'renew': payload = utils.nullUnwrap(data)
if 'revoke_crt_pem' not in payload:
context.revokeSerial(payload['revoke_serial'])
return (STATUS_NO_CONTENT, header_list, [])
else:
payload = utils.unwrap( payload = utils.unwrap(
self._readJSON(environ), data,
lambda x: x['crt_pem'], lambda x: x['revoke_crt_pem'],
context.digest_list, context.digest_list,
) )
data = context.renew( context.revoke(
crt_pem=payload['revoke_crt_pem'].encode('ascii'),
)
return (STATUS_NO_CONTENT, header_list, [])
def renewCertificate(self, context, environ):
"""
Handle PUT /{context}/crt/renew .
"""
payload = utils.unwrap(
self._readJSON(environ),
lambda x: x['crt_pem'],
context.digest_list,
)
return self._returnFile(
context.renew(
crt_pem=payload['crt_pem'].encode('ascii'), crt_pem=payload['crt_pem'].encode('ascii'),
csr_pem=payload['renew_csr_pem'].encode('ascii'), csr_pem=payload['renew_csr_pem'].encode('ascii'),
) ),
return ( 'application/pkix-cert',
STATUS_OK, )
[
('Content-Type', 'application/pkix-cert'), def createCertificate(self, context, environ, subpath):
('Content-Length', str(len(data))), """
], Handle PUT /{context}/crt/{crt_id} urls.
[data], """
) # Note: single-use variable to verify subpath before allocating more
elif crt_id == 'revoke': # resources to this request
header_list = [] crt_id = self._getCSRID(subpath)
data = self._readJSON(environ) body = self._read(environ)
if data['digest'] is None: if not body:
self._authenticate(environ, header_list) template_csr = None
payload = utils.nullUnwrap(data) elif environ.get('CONTENT_TYPE') == 'application/pkcs10':
if 'revoke_crt_pem' not in payload: template_csr = utils.load_certificate_request(body)
context.revokeSerial(payload['revoke_serial'])
return (STATUS_NO_CONTENT, header_list, [])
else:
payload = utils.unwrap(
data,
lambda x: x['revoke_crt_pem'],
context.digest_list,
)
context.revoke(
crt_pem=payload['revoke_crt_pem'].encode('ascii'),
)
return (STATUS_NO_CONTENT, header_list, [])
else: else:
try: raise BadRequest('Bad Content-Type')
crt_id = int(crt_id) header_list = []
except ValueError: self._authenticate(environ, header_list)
raise BadRequest('Invalid integer') context.createCertificate(
body = self._read(environ) csr_id=crt_id,
if not body: template_csr=template_csr,
template_csr = None )
elif environ.get('CONTENT_TYPE') == 'application/pkcs10': return (STATUS_NO_CONTENT, header_list, [])
template_csr = utils.load_certificate_request(body)
else:
raise BadRequest('Bad Content-Type')
header_list = []
self._authenticate(environ, header_list)
context.createCertificate(
csr_id=crt_id,
template_csr=template_csr,
)
return (STATUS_NO_CONTENT, header_list, [])
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment