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': {
'method': {
'GET': { 'GET': {
'method': self.getCRL, 'do': self.getCertificateRevocationList,
},
}, },
}, },
'csr': { 'csr': {
'method': {
'GET': { 'GET': {
'method': self.getCSR, 'do': self.getCSR,
'subpath': SUBPATH_OPTIONAL,
}, },
'PUT': { 'PUT': {
'method': self.putCSR, 'do': self.createCertificateSigningRequest,
}, },
'DELETE': { 'DELETE': {
'method': self.deleteCSR, 'do': self.deletePendingCertificateRequest,
'subpath': SUBPATH_REQUIRED,
},
}, },
}, },
'crt': { '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,
},
},
},
},
'method': {
'GET': { 'GET': {
'method': self.getCRT, 'do': self.getCertificate,
'subpath': SUBPATH_REQUIRED,
}, },
'PUT': { 'PUT': {
'method': self.putCRT, 'do': self.createCertificate,
'subpath': SUBPATH_REQUIRED,
},
},
},
}
self._root_dict = {
'routing': {
'cas': {
'context': cas,
'routing': caucase_routing_dict,
},
'cau': {
'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 = [
x
for x in environ.get('PATH_INFO', '').split('/')
if x
]
path_entry_dict = self._root_dict
context = None
while path_item_list:
context = path_entry_dict.get('context', context)
try: try:
context_id, base_path = path_item_list[:2] path_entry_dict = path_entry_dict['routing'][path_item_list[0]]
except ValueError: except KeyError:
raise NotFound 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']
try:
action_dict = method_dict[request_method]
except KeyError:
if request_method == 'OPTIONS': if request_method == 'OPTIONS':
status = STATUS_NO_CONTENT status = STATUS_NO_CONTENT
header_list = [] header_list = []
result = [] result = []
else: else:
try:
entry = method_dict[request_method]
except KeyError:
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)
except ValueError:
raise BadRequest('Invalid integer')
data = context.getCertificateSigningRequest(csr_id)
content_type = 'application/pkcs10'
else:
self._authenticate(environ, header_list) self._authenticate(environ, header_list)
data = json.dumps(context.getCertificateRequestList()) return self._returnFile(
content_type = 'application/json' json.dumps(context.getCertificateRequestList()),
header_list.append(('Content-Type', content_type)) 'application/json',
header_list.append(('Content-Length', str(len(data)))) header_list,
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,63 +435,37 @@ class Application(object): ...@@ -328,63 +435,37 @@ 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/{crt_id} urls. Handle GET /{context}/crt/ca.crt.pem urls.
""" """
try: return self._returnFile(
crt_id, = subpath context.getCACertificate(),
except ValueError: 'application/x-x509-ca-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 getCACertificateChain(self, context, environ):
""" """
Handle PUT /{context}/crt/{crt_id} urls. Handle GET /{context}/crt/ca.crt.json urls.
""" """
try: return self._returnFile(
crt_id, = subpath json.dumps(context.getValidCACertificateChain()),
except ValueError: 'application/json',
raise NotFound
if crt_id == 'renew':
payload = utils.unwrap(
self._readJSON(environ),
lambda x: x['crt_pem'],
context.digest_list,
) )
data = context.renew(
crt_pem=payload['crt_pem'].encode('ascii'), def getCertificate(self, context, environ, subpath):
csr_pem=payload['renew_csr_pem'].encode('ascii'), """
) Handle GET /{context}/crt/{crt_id} urls.
return ( """
STATUS_OK, return self._returnFile(
[ context.getCertificate(self._getCSRID(subpath)),
('Content-Type', 'application/pkix-cert'), 'application/pkix-cert',
('Content-Length', str(len(data))),
],
[data],
) )
elif crt_id == 'revoke':
def revokeCertificate(self, context, environ):
"""
Handle PUT /{context}/crt/revoke .
"""
header_list = [] header_list = []
data = self._readJSON(environ) data = self._readJSON(environ)
if data['digest'] is None: if data['digest'] is None:
...@@ -403,11 +484,31 @@ class Application(object): ...@@ -403,11 +484,31 @@ class Application(object):
crt_pem=payload['revoke_crt_pem'].encode('ascii'), crt_pem=payload['revoke_crt_pem'].encode('ascii'),
) )
return (STATUS_NO_CONTENT, header_list, []) return (STATUS_NO_CONTENT, header_list, [])
else:
try: def renewCertificate(self, context, environ):
crt_id = int(crt_id) """
except ValueError: Handle PUT /{context}/crt/renew .
raise BadRequest('Invalid integer') """
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'),
csr_pem=payload['renew_csr_pem'].encode('ascii'),
),
'application/pkix-cert',
)
def createCertificate(self, context, environ, subpath):
"""
Handle PUT /{context}/crt/{crt_id} urls.
"""
# Note: single-use variable to verify subpath before allocating more
# resources to this request
crt_id = self._getCSRID(subpath)
body = self._read(environ) body = self._read(environ)
if not body: if not body:
template_csr = None template_csr = None
......
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