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):
"""
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)
def request(environ):
"""
......@@ -1226,12 +1237,15 @@ class CaucaseTest(unittest.TestCase):
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/__init__',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
'PATH_INFO': '/cau/does_not_exist',
'REQUEST_METHOD': 'GET',
})[0], 404)
self.assertEqual(request({
......
......@@ -27,6 +27,10 @@ from . import exceptions
__all__ = ('Application', )
SUBPATH_FORBIDDEN = object()
SUBPATH_REQUIRED = object()
SUBPATH_OPTIONAL = object()
def _getStatus(code):
return '%i %s' % (code, httplib.responses[code])
......@@ -119,33 +123,112 @@ class Application(object):
Will be hosted under /cas
"""
self._cau = cau
self._context_dict = {
'cau': cau,
'cas': cas,
}
self._routing_dict = {
# Routing dict structure:
# path entry dict:
# "method": method 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': {
'GET': {
'method': self.getCRL,
'method': {
'GET': {
'do': self.getCertificateRevocationList,
},
},
},
'csr': {
'GET': {
'method': self.getCSR,
'method': {
'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': self.deleteCSR,
'method': {
'GET': {
'do': self.getCertificate,
'subpath': SUBPATH_REQUIRED,
},
'PUT': {
'do': self.createCertificate,
'subpath': SUBPATH_REQUIRED,
},
},
},
'crt': {
'GET': {
'method': self.getCRT,
}
self._root_dict = {
'routing': {
'cas': {
'context': cas,
'routing': caucase_routing_dict,
},
'PUT': {
'method': self.putCRT,
'cau': {
'context': cau,
'routing': caucase_routing_dict,
},
},
}
......@@ -156,27 +239,50 @@ class Application(object):
"""
try: # Convert ApplicationError subclasses into error responses
try: # Convert exceptions into ApplicationError subclass exceptions
path_item_list = [x for x in environ['PATH_INFO'].split('/') if x]
try:
context_id, base_path = path_item_list[:2]
except ValueError:
raise NotFound
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:
path_entry_dict = path_entry_dict['routing'][path_item_list[0]]
except KeyError:
break
del path_item_list[0]
try:
context = self._context_dict[context_id]
method_dict = self._routing_dict[base_path]
method_dict = path_entry_dict['method']
except KeyError:
raise NotFound
request_method = environ['REQUEST_METHOD']
if request_method == 'OPTIONS':
status = STATUS_NO_CONTENT
header_list = []
result = []
else:
try:
entry = method_dict[request_method]
except KeyError:
try:
action_dict = method_dict[request_method]
except KeyError:
if request_method == 'OPTIONS':
status = STATUS_NO_CONTENT
header_list = []
result = []
else:
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:
raise
except exceptions.NotFound:
......@@ -200,6 +306,25 @@ class Application(object):
start_response(status, header_list)
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
def _read(environ):
"""
......@@ -253,53 +378,36 @@ class Application(object):
except ValueError:
raise BadRequest('Invalid json')
@staticmethod
def getCRL(context, environ, subpath):
def getCertificateRevocationList(self, context, environ):
"""
Handle GET /{context}/crl .
"""
if subpath:
raise NotFound
data = context.getCertificateRevocationList()
return (
STATUS_OK,
[
('Content-Type', 'application/pkix-crl'),
('Content-Length', str(len(data))),
],
[data],
return self._returnFile(
context.getCertificateRevocationList(),
'application/pkix-crl',
)
def getCSR(self, context, environ, subpath):
"""
Handle GET /{context}/csr/{csr_id} and GET /{context}/csr.
"""
header_list = []
if subpath:
try:
csr_id, = subpath
except ValueError:
raise NotFound
try:
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)
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])
return self._returnFile(
context.getCertificateSigningRequest(self._getCSRID(subpath)),
'application/pkcs10',
)
header_list = []
self._authenticate(environ, header_list)
return self._returnFile(
json.dumps(context.getCertificateRequestList()),
'application/json',
header_list,
)
def putCSR(self, context, environ, subpath):
def createCertificateSigningRequest(self, context, environ):
"""
Handle PUT /{context}/csr .
"""
if subpath:
raise NotFound
try:
csr_id = context.appendCertificateSigningRequest(self._read(environ))
except exceptions.NotACertificateSigningRequest:
......@@ -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} .
"""
try:
csr_id, = subpath
except ValueError:
raise NotFound
# Note: single-use variable to verify subpath before allocating more
# resources to this request
csr_id = self._getCSRID(subpath)
header_list = []
self._authenticate(environ, header_list)
try:
......@@ -328,97 +435,91 @@ class Application(object):
raise NotFound
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.
"""
try:
crt_id, = subpath
except ValueError:
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],
return self._returnFile(
context.getCertificate(self._getCSRID(subpath)),
'application/pkix-cert',
)
def putCRT(self, context, environ, subpath):
def revokeCertificate(self, context, environ):
"""
Handle PUT /{context}/crt/{crt_id} urls.
Handle PUT /{context}/crt/revoke .
"""
try:
crt_id, = subpath
except ValueError:
raise NotFound
if crt_id == 'renew':
header_list = []
data = self._readJSON(environ)
if data['digest'] is None:
self._authenticate(environ, header_list)
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(
self._readJSON(environ),
lambda x: x['crt_pem'],
data,
lambda x: x['revoke_crt_pem'],
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'),
csr_pem=payload['renew_csr_pem'].encode('ascii'),
)
return (
STATUS_OK,
[
('Content-Type', 'application/pkix-cert'),
('Content-Length', str(len(data))),
],
[data],
)
elif crt_id == 'revoke':
header_list = []
data = self._readJSON(environ)
if data['digest'] is None:
self._authenticate(environ, header_list)
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(
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, [])
),
'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)
if not body:
template_csr = None
elif environ.get('CONTENT_TYPE') == 'application/pkcs10':
template_csr = utils.load_certificate_request(body)
else:
try:
crt_id = int(crt_id)
except ValueError:
raise BadRequest('Invalid integer')
body = self._read(environ)
if not body:
template_csr = None
elif environ.get('CONTENT_TYPE') == 'application/pkcs10':
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, [])
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