From 99347717ed2c7e92b3dc3bd33c12a3a05e8e349d Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 15 Dec 2010 00:25:04 +0000 Subject: Lockout middleware for ec2 api --- nova/api/ec2/__init__.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a6ee16c33..19eb666cd 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -22,12 +22,13 @@ Starting point for routing EC2 requests. import logging import routes +import time import webob import webob.dec import webob.exc -from nova import exception from nova import context +from nova import exception from nova import flags from nova import wsgi from nova.api.ec2 import apirequest @@ -37,6 +38,16 @@ from nova.auth import manager FLAGS = flags.FLAGS +flags.DEFINE_boolean('use_lockout', False, + 'Whether or not to use lockout middleware.') +flags.DEFINE_integer('lockout_attempts', 5, + 'Number of failed auths before lockout.') +flags.DEFINE_integer('lockout_minutes', 15, + 'Number of minutes to lockout if triggered.') +flags.DEFINE_list('lockout_memcached_servers', None, + 'Memcached servers or None for in process cache.') + + _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -47,6 +58,63 @@ class API(wsgi.Middleware): def __init__(self): self.application = Authenticate(Router(Authorizer(Executor()))) + if FLAGS.use_lockout: + self.application = Lockout(self.application) + + +class Lockout(wsgi.Middleware): + """Only allow x failed auths in a y minute period. + + x = lockout_attempts flag + y = lockout_timeout flag + + Uses memcached if lockout_memcached_servers flag is set, otherwise it + uses a very simple in-proccess cache. Due to the simplicity of + the implementation, the timeout window is reset with every failed + request, so it actually blocks if there are x failed logins with no + more than y minutes between any two failures. + + There is a possible race condition where simultaneous requests could + sneak in before the lockout hits, but this is extremely rare and would + only result in a couple of extra failed attempts.""" + + def __init__(self, application, time_fn=time.time): + """The middleware can use a custom time function for testing.""" + self.time_fn = time_fn + if FLAGS.lockout_memcached_servers: + import memcache + else: + from nova import fakememcache as memcache + self.mc = memcache.Client(FLAGS.lockout_memcached_servers, debug=0) + super(Lockout, self).__init__(application) + + @webob.dec.wsgify + def __call__(self, req): + access_key = req.params['AWSAccessKeyId'] + failures_key = "%s-failures" % access_key + last_key = "%s-last" % access_key + now = self.time_fn() + timeout = now - FLAGS.lockout_minutes * 60 + # NOTE(vish): To use incr, failures has to be a string. + failures = int(self.mc.get(failures_key) or 0) + last = self.mc.get(last_key) + if (failures and failures >= FLAGS.lockout_attempts + and last > timeout): + self.mc.set(last_key, now) + detail = "Too many failed authentications." + raise webob.exc.HTTPForbidden(detail=detail) + res = req.get_response(self.application) + if res.status_int == 403: + if last > timeout: + failures = int(self.mc.incr(failures_key)) + if failures >= FLAGS.lockout_attempts: + _log.warn('Access key %s has had %d failed authentications' + ' and will be locked out for %d minutes.' % + (access_key, failures, FLAGS.lockout_minutes)) + else: + self.mc.set(failures_key, '1') + self.mc.set(last_key, now) + return res class Authenticate(wsgi.Middleware): -- cgit From b0279030127b7fe8df21db12a8727ea623ca46e2 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Dec 2010 09:38:38 -0800 Subject: clean up code to use timeout instead of two keys --- nova/api/ec2/__init__.py | 58 ++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 31 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 19eb666cd..381b0e871 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -22,7 +22,6 @@ Starting point for routing EC2 requests. import logging import routes -import time import webob import webob.dec import webob.exc @@ -44,6 +43,8 @@ flags.DEFINE_integer('lockout_attempts', 5, 'Number of failed auths before lockout.') flags.DEFINE_integer('lockout_minutes', 15, 'Number of minutes to lockout if triggered.') +flags.DEFINE_integer('lockout_window', 15, + 'Number of minutes for lockout window.') flags.DEFINE_list('lockout_memcached_servers', None, 'Memcached servers or None for in process cache.') @@ -53,7 +54,6 @@ _log.setLevel(logging.DEBUG) class API(wsgi.Middleware): - """Routing for all EC2 API requests.""" def __init__(self): @@ -63,57 +63,53 @@ class API(wsgi.Middleware): class Lockout(wsgi.Middleware): - """Only allow x failed auths in a y minute period. + """Lockout for x minutes on y failed auths in a z minute period. - x = lockout_attempts flag - y = lockout_timeout flag + x = lockout_timeout flag + y = lockout_window flag + z = lockout_attempts flag Uses memcached if lockout_memcached_servers flag is set, otherwise it uses a very simple in-proccess cache. Due to the simplicity of - the implementation, the timeout window is reset with every failed - request, so it actually blocks if there are x failed logins with no - more than y minutes between any two failures. + the implementation, the timeout window is started with the first + failed request, so it will block if there are x failed logins within + that period. There is a possible race condition where simultaneous requests could sneak in before the lockout hits, but this is extremely rare and would only result in a couple of extra failed attempts.""" - def __init__(self, application, time_fn=time.time): - """The middleware can use a custom time function for testing.""" - self.time_fn = time_fn + def __init__(self, application, time_fn=None): + """middleware can pass a custom time function to fake for testing.""" if FLAGS.lockout_memcached_servers: import memcache + self.mc = memcache.Client(FLAGS.lockout_memcached_servers, + debug=0) else: - from nova import fakememcache as memcache - self.mc = memcache.Client(FLAGS.lockout_memcached_servers, debug=0) + from nova import fakememcache + self.mc = fakememcache.Client(time_fn=time_fn) super(Lockout, self).__init__(application) @webob.dec.wsgify def __call__(self, req): access_key = req.params['AWSAccessKeyId'] - failures_key = "%s-failures" % access_key - last_key = "%s-last" % access_key - now = self.time_fn() - timeout = now - FLAGS.lockout_minutes * 60 - # NOTE(vish): To use incr, failures has to be a string. + failures_key = "authfailures-%s" % access_key failures = int(self.mc.get(failures_key) or 0) - last = self.mc.get(last_key) - if (failures and failures >= FLAGS.lockout_attempts - and last > timeout): - self.mc.set(last_key, now) + if failures >= FLAGS.lockout_attempts: detail = "Too many failed authentications." raise webob.exc.HTTPForbidden(detail=detail) res = req.get_response(self.application) if res.status_int == 403: - if last > timeout: - failures = int(self.mc.incr(failures_key)) - if failures >= FLAGS.lockout_attempts: - _log.warn('Access key %s has had %d failed authentications' - ' and will be locked out for %d minutes.' % - (access_key, failures, FLAGS.lockout_minutes)) - else: - self.mc.set(failures_key, '1') - self.mc.set(last_key, now) + failures = self.mc.incr(failures_key) + if failures is None: + # NOTE(vish): To use incr, failures has to be a string. + self.mc.set(failures_key, '1', time=FLAGS.lockout_window * 60) + elif failures >= FLAGS.lockout_attempts: + _log.warn('Access key %s has had %d failed authentications' + ' and will be locked out for %d minutes.' % + (access_key, failures, FLAGS.lockout_minutes)) + self.mc.set(failures_key, str(failures), + time=FLAGS.lockout_minutes * 60) return res -- cgit From dd4ee43cc2042299ed7a56b4690999fa1df120a1 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Dec 2010 11:23:33 -0800 Subject: clean up tests and add overriden time method to utils --- nova/api/ec2/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 381b0e871..5ae15f2ae 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -79,15 +79,14 @@ class Lockout(wsgi.Middleware): sneak in before the lockout hits, but this is extremely rare and would only result in a couple of extra failed attempts.""" - def __init__(self, application, time_fn=None): - """middleware can pass a custom time function to fake for testing.""" + def __init__(self, application): + """middleware can use fake for testing.""" if FLAGS.lockout_memcached_servers: import memcache - self.mc = memcache.Client(FLAGS.lockout_memcached_servers, - debug=0) else: - from nova import fakememcache - self.mc = fakememcache.Client(time_fn=time_fn) + from nova import fakememcache as memcache + self.mc = memcache.Client(FLAGS.lockout_memcached_servers, + debug=0) super(Lockout, self).__init__(application) @webob.dec.wsgify -- cgit From e1da5d66b2e33a043e7e9ee357d9769276d6e302 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Dec 2010 13:14:28 -0800 Subject: memcached requires strings not unicode --- nova/api/ec2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 5ae15f2ae..def0ee207 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -91,7 +91,7 @@ class Lockout(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): - access_key = req.params['AWSAccessKeyId'] + access_key = str(req.params['AWSAccessKeyId']) failures_key = "authfailures-%s" % access_key failures = int(self.mc.get(failures_key) or 0) if failures >= FLAGS.lockout_attempts: -- cgit From e01e6d7976adfd99addf31f4f914c7625a394fda Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 16 Dec 2010 12:09:38 -0600 Subject: Moved implementation specific stuff from the middleware into their respective modules --- nova/api/openstack/__init__.py | 83 ++++------------------------- nova/api/openstack/auth.py | 20 ++++--- nova/api/openstack/common.py | 17 ++++++ nova/api/openstack/flavors.py | 3 +- nova/api/openstack/images.py | 6 ++- nova/api/openstack/ratelimiting/__init__.py | 60 +++++++++++++++++++++ nova/api/openstack/servers.py | 3 +- 7 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 nova/api/openstack/common.py (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index b9ecbd9b8..e941694d9 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -49,6 +49,10 @@ flags.DEFINE_string('nova_api_auth', 'nova.api.openstack.auth.BasicApiAuthManager', 'The auth mechanism to use for the OpenStack API implemenation') +flags.DEFINE_string('os_api_ratelimiting', + 'nova.api.openstack.ratelimiting.BasicRateLimiting', + 'Default ratelimiting implementation for the Openstack API') + flags.DEFINE_bool('allow_admin_api', False, 'When True, this API service will accept admin operations.') @@ -81,10 +85,10 @@ class AuthMiddleware(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): - if 'X-Auth-Token' not in req.headers: + if not self.auth_driver.has_authentication(req) return self.auth_driver.authenticate(req) - user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + user = self.auth_driver.get_user_by_authentication(req) if not user: return faults.Fault(webob.exc.HTTPUnauthorized()) @@ -104,62 +108,12 @@ class RateLimitingMiddleware(wsgi.Middleware): at the given host+port to keep rate counters. """ super(RateLimitingMiddleware, self).__init__(application) - if not service_host: - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ - 'DELETE': (100, ratelimiting.PER_MINUTE), - 'PUT': (10, ratelimiting.PER_MINUTE), - 'POST': (10, ratelimiting.PER_MINUTE), - 'POST servers': (50, ratelimiting.PER_DAY), - 'GET changes-since': (3, ratelimiting.PER_MINUTE), - }) - else: - self.limiter = ratelimiting.WSGIAppProxy(service_host) + self._limiting_driver = + utils.import_class(FLAGS.os_api_ratelimiting)(service_host) @webob.dec.wsgify def __call__(self, req): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - action_name = self.get_action_name(req) - if not action_name: - # Not rate limited - return self.application - delay = self.get_delay(action_name, - req.environ['nova.context'].user_id) - if delay: - # TODO(gundlach): Get the retry-after format correct. - exc = webob.exc.HTTPRequestEntityTooLarge( - explanation='Too many requests.', - headers={'Retry-After': time.time() + delay}) - raise faults.Fault(exc) - return self.application - - def get_delay(self, action_name, username): - """Return the delay for the given action and username, or None if - the action would not be rate limited. - """ - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - # Attempt the "POST" first, lest we are rate limited by "POST" but - # use up a precious "POST servers" call. - delay = self.limiter.perform("POST", username=username) - if delay: - return delay - return self.limiter.perform(action_name, username=username) - - def get_action_name(self, req): - """Return the action name for this request.""" - if req.method == 'GET' and 'changes-since' in req.GET: - return 'GET changes-since' - if req.method == 'POST' and req.path_info.startswith('/servers'): - return 'POST servers' - if req.method in ['PUT', 'POST', 'DELETE']: - return req.method - return None + return self._limiting_driver.limited_request(req) class APIRouter(wsgi.Router): @@ -191,22 +145,3 @@ class APIRouter(wsgi.Router): # TODO: Place routes for admin operations here. super(APIRouter, self).__init__(mapper) - - -def limited(items, req): - """Return a slice of items according to requested offset and limit. - - items - a sliceable - req - wobob.Request possibly containing offset and limit GET variables. - offset is where to start in the list, and limit is the maximum number - of items to return. - - If limit is not specified, 0, or > 1000, defaults to 1000. - """ - offset = int(req.GET.get('offset', 0)) - limit = int(req.GET.get('limit', 0)) - if not limit: - limit = 1000 - limit = min(1000, limit) - range_end = offset + limit - return items[offset:range_end] diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index fcda97ab1..da8ebcfcd 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -7,6 +7,7 @@ import webob.exc import webob.dec from nova import auth +from nova import context from nova import db from nova import flags from nova import manager @@ -16,10 +17,6 @@ from nova.api.openstack import faults FLAGS = flags.FLAGS -class Context(object): - pass - - class BasicApiAuthManager(object): """ Implements a somewhat rudimentary version of OpenStack Auth""" @@ -28,9 +25,14 @@ class BasicApiAuthManager(object): db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) self.auth = auth.manager.AuthManager() - self.context = Context() super(BasicApiAuthManager, self).__init__() + def has_authentication(self, req): + return 'X-Auth-Token' in req.headers: + + def get_user_by_authentication(self, req): + return self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + def authenticate(self, req): # Unless the request is explicitly made against // don't # honor it @@ -68,11 +70,12 @@ class BasicApiAuthManager(object): This method will also remove the token if the timestamp is older than 2 days ago. """ - token = self.db.auth_get_token(self.context, token_hash) + ctxt = context.get_admin_context() + token = self.db.auth_get_token(ctxt, token_hash) if token: delta = datetime.datetime.now() - token.created_at if delta.days >= 2: - self.db.auth_destroy_token(self.context, token) + self.db.auth_destroy_token(ctxt, token) else: return self.auth.get_user(token.user_id) return None @@ -84,6 +87,7 @@ class BasicApiAuthManager(object): key - string API key req - webob.Request object """ + ctxt = context.get_admin_context() user = self.auth.get_user_from_access_key(key) if user and user.name == username: token_hash = hashlib.sha1('%s%s%f' % (username, key, @@ -95,6 +99,6 @@ class BasicApiAuthManager(object): token_dict['server_management_url'] = req.url token_dict['storage_url'] = '' token_dict['user_id'] = user.id - token = self.db.auth_create_token(self.context, token_dict) + token = self.db.auth_create_token(ctxt, token_dict) return token, user return None, None diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py new file mode 100644 index 000000000..29e9a8623 --- /dev/null +++ b/nova/api/openstack/common.py @@ -0,0 +1,17 @@ +def limited(items, req): + """Return a slice of items according to requested offset and limit. + + items - a sliceable + req - wobob.Request possibly containing offset and limit GET variables. + offset is where to start in the list, and limit is the maximum number + of items to return. + + If limit is not specified, 0, or > 1000, defaults to 1000. + """ + offset = int(req.GET.get('offset', 0)) + limit = int(req.GET.get('limit', 0)) + if not limit: + limit = 1000 + limit = min(1000, limit) + range_end = offset + limit + return items[offset:range_end] diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f23f74fd1..f620d4107 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -18,6 +18,7 @@ from webob import exc from nova.api.openstack import faults +from nova.api.openstack import common from nova.compute import instance_types from nova import wsgi import nova.api.openstack @@ -39,7 +40,7 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all flavors in detail.""" items = [self.show(req, id)['flavor'] for id in self._all_ids()] - items = nova.api.openstack.limited(items, req) + items = common.limited(items, req) return dict(flavors=items) def show(self, req, id): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 4a0a8e6f1..fe8d9d75f 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -22,6 +22,8 @@ from nova import utils from nova import wsgi import nova.api.openstack import nova.image.service + +from nova.api.openstack import common from nova.api.openstack import faults @@ -48,11 +50,11 @@ class Controller(wsgi.Controller): """Return all public images in detail.""" try: images = self._service.detail(req.environ['nova.context']) - images = nova.api.openstack.limited(images, req) + images = common.limited(images, req) except NotImplementedError: # Emulate detail() using repeated calls to show() images = self._service.index(ctxt) - images = nova.api.openstack.limited(images, req) + images = common.limited(images, req) images = [self._service.show(ctxt, i['id']) for i in images] return dict(images=images) diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index 918caf055..d1da9afa7 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -14,6 +14,66 @@ PER_HOUR = 60 * 60 PER_DAY = 60 * 60 * 24 +class BasicRateLimiting(object): + """ Implements Rate limits as per the Rackspace CloudServers API spec. """ + + def __init__(self, service_host): + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + else: + self.limiter = ratelimiting.WSGIAppProxy(service_host) + + def limited_request(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + action_name = self.get_action_name(req) + if not action_name: + # Not rate limited + return self.application + delay = self.get_delay(action_name, + req.environ['nova.context'].user_id) + if delay: + # TODO(gundlach): Get the retry-after format correct. + exc = webob.exc.HTTPRequestEntityTooLarge( + explanation='Too many requests.', + headers={'Retry-After': time.time() + delay}) + raise faults.Fault(exc) + return self.application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.startswith('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + class Limiter(object): """Class providing rate limiting of arbitrary actions.""" diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 7704f48f1..9e6047805 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -19,6 +19,7 @@ from webob import exc from nova import exception from nova import wsgi +from nova.api.openstack import common from nova.api.openstack import faults from nova.auth import manager as auth_manager from nova.compute import api as compute_api @@ -91,7 +92,7 @@ class Controller(wsgi.Controller): """ instance_list = self.compute_api.get_instances( req.environ['nova.context']) - limited_list = nova.api.openstack.limited(instance_list, req) + limited_list = common.limited(instance_list, req) res = [entity_maker(inst)['server'] for inst in limited_list] return _entity_list(res) -- cgit From 6383f7f9f63e348a12adeff66a266ef796d98ded Mon Sep 17 00:00:00 2001 From: Cerberus Date: Fri, 17 Dec 2010 11:54:59 -0600 Subject: Some typo fixes --- nova/api/openstack/__init__.py | 4 ++-- nova/api/openstack/auth.py | 4 ++-- nova/api/openstack/ratelimiting/__init__.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index e941694d9..e78080012 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -85,7 +85,7 @@ class AuthMiddleware(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): - if not self.auth_driver.has_authentication(req) + if not self.auth_driver.has_authentication(req): return self.auth_driver.authenticate(req) user = self.auth_driver.get_user_by_authentication(req) @@ -108,7 +108,7 @@ class RateLimitingMiddleware(wsgi.Middleware): at the given host+port to keep rate counters. """ super(RateLimitingMiddleware, self).__init__(application) - self._limiting_driver = + self._limiting_driver = \ utils.import_class(FLAGS.os_api_ratelimiting)(service_host) @webob.dec.wsgify diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index da8ebcfcd..26cb50dca 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -28,10 +28,10 @@ class BasicApiAuthManager(object): super(BasicApiAuthManager, self).__init__() def has_authentication(self, req): - return 'X-Auth-Token' in req.headers: + return 'X-Auth-Token' in req.headers def get_user_by_authentication(self, req): - return self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + return self.authorize_token(req.headers["X-Auth-Token"]) def authenticate(self, req): # Unless the request is explicitly made against // don't diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index d1da9afa7..2dc5ec32e 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -21,7 +21,7 @@ class BasicRateLimiting(object): if not service_host: #TODO(gundlach): These limits were based on limitations of Cloud #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ + self.limiter = Limiter(limits={ 'DELETE': (100, ratelimiting.PER_MINUTE), 'PUT': (10, ratelimiting.PER_MINUTE), 'POST': (10, ratelimiting.PER_MINUTE), @@ -29,7 +29,7 @@ class BasicRateLimiting(object): 'GET changes-since': (3, ratelimiting.PER_MINUTE), }) else: - self.limiter = ratelimiting.WSGIAppProxy(service_host) + self.limiter = WSGIAppProxy(service_host) def limited_request(self, req): """Rate limit the request. -- cgit From ca1017988f98a246aa82c16f471791c7ea3eceec Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 19 Dec 2010 20:05:41 +0000 Subject: Support proxying api by using X-Forwarded-For --- nova/api/ec2/__init__.py | 8 +++++++- nova/api/ec2/metadatarequesthandler.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index a6ee16c33..c455144a9 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -37,6 +37,9 @@ from nova.auth import manager FLAGS = flags.FLAGS +flags.DEFINE_boolean('use_forwarded_for', False, + 'Treat X-Forwarded-For as the canonical remote address. ' + 'Only enable this if you have a sanitizing proxy.') _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -81,9 +84,12 @@ class Authenticate(wsgi.Middleware): raise webob.exc.HTTPForbidden() # Authenticated! + remote_address = req.remote_addr + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) ctxt = context.RequestContext(user=user, project=project, - remote_address=req.remote_addr) + remote_address=remote_address) req.environ['ec2.context'] = ctxt return self.application diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py index 2f4f414cc..66439a0df 100644 --- a/nova/api/ec2/metadatarequesthandler.py +++ b/nova/api/ec2/metadatarequesthandler.py @@ -23,9 +23,13 @@ import logging import webob.dec import webob.exc +from nova import flags from nova.api.ec2 import cloud +FLAGS = flags.FLAGS + + class MetadataRequestHandler(object): """Serve metadata from the EC2 API.""" @@ -63,10 +67,13 @@ class MetadataRequestHandler(object): @webob.dec.wsgify def __call__(self, req): cc = cloud.CloudController() - meta_data = cc.get_metadata(req.remote_addr) + remote_address = req.remote_addr + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + meta_data = cc.get_metadata(remote_address) if meta_data is None: logging.error('Failed to get metadata for ip: %s' % - req.remote_addr) + remote_address) raise webob.exc.HTTPNotFound() data = self.lookup(req.path_info, meta_data) if data is None: -- cgit From 83cf1f7140c20ea2188272b57e4e2c1a95f8ff9e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 21 Dec 2010 03:26:50 +0000 Subject: remove extra print statements --- nova/api/ec2/cloud.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 91b2f0064..ad6debb11 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -723,10 +723,8 @@ class CloudController(object): floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) # NOTE(vish): Perhaps we should just pass this on to compute and # let compute communicate with network. - print "in cloud get" network_topic = self.compute_api.get_network_topic(context, internal_id) - print "got the network topic", network_topic rpc.cast(context, network_topic, {"method": "associate_floating_ip", -- cgit From db938f975da64540ebb942e9dfd640db4dd7f939 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 21 Dec 2010 21:34:51 +0000 Subject: removed unused import and fix docstring --- nova/api/ec2/cloud.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'nova/api') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index ad6debb11..c845e6a2e 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -27,7 +27,6 @@ import datetime import logging import re import os -import time from nova import context import IPy @@ -706,7 +705,7 @@ class CloudController(object): def release_address(self, context, public_ip, **kwargs): floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) # NOTE(vish): We don't know which network host should get the ip - # when we allocate, so just send it to any one. This + # when we deallocate, so just send it to any one. This # will probably need to move into a network supervisor # at some point. rpc.cast(context, -- cgit From 21867297b673ec9fe055fb6c7e4a3dadcfa6fdd2 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 22 Dec 2010 12:39:59 -0600 Subject: Minor bug fix --- nova/api/__init__.py | 1 + nova/api/openstack/images.py | 1 + 2 files changed, 2 insertions(+) (limited to 'nova/api') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 80f9f2109..e081ec10b 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -24,6 +24,7 @@ Root WSGI middleware for all API controllers. :ec2api_subdomain: subdomain running the EC2 API (default: ec2) """ +import logging import routes import webob.dec diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index fe8d9d75f..d3312aba8 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -53,6 +53,7 @@ class Controller(wsgi.Controller): images = common.limited(images, req) except NotImplementedError: # Emulate detail() using repeated calls to show() + ctxt = req.environ['nova.context'] images = self._service.index(ctxt) images = common.limited(images, req) images = [self._service.show(ctxt, i['id']) for i in images] -- cgit From c4fb755b169895f9ffab6ab4d18f5227688b7ae4 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 22 Dec 2010 13:18:26 -0600 Subject: Abstracted auth and ratelimiting more --- nova/api/openstack/__init__.py | 56 ++++------------------------- nova/api/openstack/auth.py | 22 +++++++++--- nova/api/openstack/ratelimiting/__init__.py | 22 ++++++++++-- 3 files changed, 43 insertions(+), 57 deletions(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index cdc25e2b7..b18edc8e7 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -45,12 +45,12 @@ from nova.auth import manager FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', - 'nova.api.openstack.auth.BasicApiAuthManager', +flags.DEFINE_string('os_api_auth', + 'nova.api.openstack.auth.AuthMiddleware', 'The auth mechanism to use for the OpenStack API implemenation') flags.DEFINE_string('os_api_ratelimiting', - 'nova.api.openstack.ratelimiting.BasicRateLimiting', + 'nova.api.openstack.ratelimiting.RateLimitingMiddleware', 'Default ratelimiting implementation for the Openstack API') flags.DEFINE_bool('allow_admin_api', @@ -62,7 +62,10 @@ class API(wsgi.Middleware): """WSGI entry point for all OpenStack API requests.""" def __init__(self): - app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) + auth_middleware = utils.import_class(FLAGS.os_api_auth) + ratelimiting_middleware = \ + utils.import_class(FLAGS.os_api_ratelimiting) + app = auth_middleware(ratelimiting_middleware(APIRouter())) super(API, self).__init__(app) @webob.dec.wsgify @@ -76,51 +79,6 @@ class API(wsgi.Middleware): return faults.Fault(exc) -class AuthMiddleware(wsgi.Middleware): - """Authorize the openstack API request or return an HTTP Forbidden.""" - - def __init__(self, application): - self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() - super(AuthMiddleware, self).__init__(application) - - @webob.dec.wsgify - def __call__(self, req): - if not self.auth_driver.has_authentication(req): - return self.auth_driver.authenticate(req) - - user = self.auth_driver.get_user_by_authentication(req) - - if not user: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - req.environ['nova.context'] = context.RequestContext(user, user) - return self.application - - -class RateLimitingMiddleware(wsgi.Middleware): - """Rate limit incoming requests according to the OpenStack rate limits.""" - - def __init__(self, application, service_host=None): - """Create a rate limiting middleware that wraps the given application. - - By default, rate counters are stored in memory. If service_host is - specified, the middleware instead relies on the ratelimiting.WSGIApp - at the given host+port to keep rate counters. - """ - super(RateLimitingMiddleware, self).__init__(application) - self._limiting_driver = \ - utils.import_class(FLAGS.os_api_ratelimiting)(service_host) - - @webob.dec.wsgify - def __call__(self, req): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - return self._limiting_driver.limited_request(req, self.application) - - class APIRouter(wsgi.Router): """ Routes requests on the OpenStack API to the appropriate controller diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 26cb50dca..3850dd1f0 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -16,16 +16,28 @@ from nova.api.openstack import faults FLAGS = flags.FLAGS +class AuthMiddleware(wsgi.Middleware): + """Authorize the openstack API request or return an HTTP Forbidden.""" -class BasicApiAuthManager(object): - """ Implements a somewhat rudimentary version of OpenStack Auth""" - - def __init__(self, db_driver=None): + def __init__(self, application): if not db_driver: db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) self.auth = auth.manager.AuthManager() - super(BasicApiAuthManager, self).__init__() + super(AuthMiddleware, self).__init__(application) + + @webob.dec.wsgify + def __call__(self, req): + if not self.has_authentication(req): + return self.authenticate(req) + + user = self.get_user_by_authentication(req) + + if not user: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + req.environ['nova.context'] = context.RequestContext(user, user) + return self.application def has_authentication(self, req): return 'X-Auth-Token' in req.headers diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index 1bf44bc7b..9892e792e 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -14,11 +14,16 @@ PER_MINUTE = 60 PER_HOUR = 60 * 60 PER_DAY = 60 * 60 * 24 +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" -class BasicRateLimiting(object): - """ Implements Rate limits as per the Rackspace CloudServers API spec. """ + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. - def __init__(self, service_host): + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ if not service_host: #TODO(gundlach): These limits were based on limitations of Cloud #Servers. We should revisit them in Nova. @@ -31,6 +36,16 @@ class BasicRateLimiting(object): }) else: self.limiter = WSGIAppProxy(service_host) + super(RateLimitingMiddleware, self).__init__(application) + + @webob.dec.wsgify + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + return self.limited_request(req, self.application) def limited_request(self, req, application): """Rate limit the request. @@ -75,6 +90,7 @@ class BasicRateLimiting(object): return req.method return None + class Limiter(object): """Class providing rate limiting of arbitrary actions.""" -- cgit From e419c27a00a85b7daba42f580e332d31713ae271 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 22 Dec 2010 13:33:26 -0600 Subject: Moved some things for testing --- nova/api/openstack/auth.py | 1 + nova/api/openstack/ratelimiting/__init__.py | 1 + 2 files changed, 2 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 3850dd1f0..6c3c870a1 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -12,6 +12,7 @@ from nova import db from nova import flags from nova import manager from nova import utils +from nova import wsgi from nova.api.openstack import faults FLAGS = flags.FLAGS diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index 9892e792e..a2e0734ef 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -6,6 +6,7 @@ import urllib import webob.dec import webob.exc +from nova import wsgi from nova.api.openstack import faults # Convenience constants for the limits dictionary passed to Limiter(). -- cgit From 168cde072542f9f4df7e7eb26f6b632306c0b7d2 Mon Sep 17 00:00:00 2001 From: mdietz Date: Wed, 22 Dec 2010 19:52:13 +0000 Subject: Finished moving the middleware layers and fixed the API tests again --- nova/api/openstack/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 6c3c870a1..99cae2c75 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -20,7 +20,7 @@ FLAGS = flags.FLAGS class AuthMiddleware(wsgi.Middleware): """Authorize the openstack API request or return an HTTP Forbidden.""" - def __init__(self, application): + def __init__(self, application, db_driver=None): if not db_driver: db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) -- cgit From c2faf1c5e689ac5e81068a305a624e626e9a87b5 Mon Sep 17 00:00:00 2001 From: mdietz Date: Wed, 22 Dec 2010 20:06:22 +0000 Subject: Forgot the copyright info --- nova/api/openstack/auth.py | 18 +++++++++++++++++- nova/api/openstack/ratelimiting/__init__.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 99cae2c75..72ad4ffa9 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -1,4 +1,20 @@ -import datetime +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + import hashlib import json import time diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index a2e0734ef..8ca575b36 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -1,3 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + """Rate limiting of arbitrary actions.""" import httplib -- cgit From 775958e3a020b6b4b4c9fd4777aa72f7e9b0bdbc Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 22 Dec 2010 15:50:26 -0600 Subject: Accidentally yanked the datetime line in auth --- nova/api/openstack/__init__.py | 2 +- nova/api/openstack/auth.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index b18edc8e7..c49399f28 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -74,7 +74,7 @@ class API(wsgi.Middleware): return req.get_response(self.application) except Exception as ex: logging.warn(_("Caught error: %s") % str(ex)) - logging.debug(traceback.format_exc()) + logging.error(traceback.format_exc()) exc = webob.exc.HTTPInternalServerError(explanation=str(ex)) return faults.Fault(exc) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index 72ad4ffa9..c9d21ed49 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License.import datetime +import datetime import hashlib import json import time -- cgit From 12a9dc88f6ae947d005568dd2e644566cd1a9677 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Wed, 22 Dec 2010 21:14:06 -0600 Subject: And the common module --- nova/api/openstack/common.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'nova/api') diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 29e9a8623..83919cc23 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -1,3 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + def limited(items, req): """Return a slice of items according to requested offset and limit. -- cgit From ba6a99f926180d47870dcb18e4387d18cddad9b0 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 23 Dec 2010 11:58:13 -0600 Subject: Superfluous images include and added basic routes for shared ip groups --- nova/api/openstack/sharedipgroups.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index e805ca9f7..423ee61e1 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -19,4 +19,22 @@ from nova import wsgi class Controller(wsgi.Controller): - pass + """ The Shared IP Groups Controller for the Openstack API """ + + def index(self, req): + raise NotImplementedError + + def show(self, req, id): + raise NotImplementedError + + def update(self, req, id): + raise NotImplementedError + + def delete(self, req, id): + raise NotImplementedError + + def detail(self, req): + raise NotImplementedError + + def create(self, req): + raise NotImplementedError -- cgit From cb679a01e5905e4f7316f81de7c9ead9dc6536b8 Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 23 Dec 2010 13:17:53 -0600 Subject: Pep8 cleanup --- nova/api/openstack/auth.py | 1 + nova/api/openstack/common.py | 2 ++ nova/api/openstack/ratelimiting/__init__.py | 1 + nova/api/openstack/sharedipgroups.py | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) (limited to 'nova/api') diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index c9d21ed49..e24e58fd3 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -34,6 +34,7 @@ from nova.api.openstack import faults FLAGS = flags.FLAGS + class AuthMiddleware(wsgi.Middleware): """Authorize the openstack API request or return an HTTP Forbidden.""" diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 83919cc23..ac0572c96 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. + def limited(items, req): """Return a slice of items according to requested offset and limit. @@ -25,6 +26,7 @@ def limited(items, req): If limit is not specified, 0, or > 1000, defaults to 1000. """ + offset = int(req.GET.get('offset', 0)) limit = int(req.GET.get('limit', 0)) if not limit: diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index 8ca575b36..91a8b2e55 100644 --- a/nova/api/openstack/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -32,6 +32,7 @@ PER_MINUTE = 60 PER_HOUR = 60 * 60 PER_DAY = 60 * 60 * 24 + class RateLimitingMiddleware(wsgi.Middleware): """Rate limit incoming requests according to the OpenStack rate limits.""" diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index 423ee61e1..75d02905c 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py @@ -32,7 +32,7 @@ class Controller(wsgi.Controller): def delete(self, req, id): raise NotImplementedError - + def detail(self, req): raise NotImplementedError -- cgit