Changeset - b88150a90804
[Not reviewed]
default
0 3 0
Mads Kiilerich - 7 years ago 2019-01-10 03:34:45
mads@kiilerich.com
middleware: unify Mercurial and Git _handle_request in the VCS base class

Finally, it is more clear in what the VCSs are different ... and what generic
setup code is needed.
3 files changed with 58 insertions and 114 deletions:
0 comments (0 inline, 0 general)
kallithea/lib/base.py
Show inline comments
 
@@ -28,53 +28,53 @@ Original author and date, and relevant c
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import datetime
 
import decorator
 
import logging
 
import time
 
import traceback
 
import warnings
 

	
 
import webob.exc
 
import paste.httpexceptions
 
import paste.auth.basic
 
import paste.httpheaders
 
from webhelpers.pylonslib import secure_form
 

	
 
from tg import config, tmpl_context as c, request, response, session, render_template
 
from tg import TGController
 
from tg.i18n import ugettext as _
 

	
 
from kallithea import __version__, BACKENDS
 

	
 
from kallithea.config.routing import url
 
from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict, \
 
    safe_str, safe_int
 
    safe_str, safe_int, get_server_url, _set_extras
 
from kallithea.lib import auth_modules
 
from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
 
from kallithea.lib.compat import json
 
from kallithea.lib.utils import get_repo_slug
 
from kallithea.lib.utils import get_repo_slug, is_valid_repo
 
from kallithea.lib.exceptions import UserCreationError
 
from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
 
from kallithea.model import meta
 

	
 
from kallithea.model.db import PullRequest, Repository, User, Setting
 
from kallithea.model.scm import ScmModel
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def render(template_path):
 
    return render_template({'url': url}, 'mako', template_path)
 

	
 

	
 
def _filter_proxy(ip):
 
    """
 
    HEADERS can have multiple ips inside the left-most being the original
 
    client, and each successive proxy that passed the request adding the IP
 
    address where it received the request from.
 

	
 
    :param ip:
 
    """
 
    if ',' in ip:
 
        _ips = ip.split(',')
 
@@ -285,57 +285,105 @@ class BaseVCSController(object):
 
        :param repo_name: repository name
 
        """
 
        if action == 'push':
 
            if not HasPermissionAnyMiddleware('repository.write',
 
                                              'repository.admin')(authuser,
 
                                                                  repo_name):
 
                return False
 

	
 
        else:
 
            #any other action need at least read permission
 
            if not HasPermissionAnyMiddleware('repository.read',
 
                                              'repository.write',
 
                                              'repository.admin')(authuser,
 
                                                                  repo_name):
 
                return False
 

	
 
        return True
 

	
 
    def _get_ip_addr(self, environ):
 
        return _get_ip_addr(environ)
 

	
 
    def __call__(self, environ, start_response):
 
        start = time.time()
 
        try:
 
            # try parsing a request for this VCS - if it fails, call the wrapped app
 
            parsed_request = self.parse_request(environ)
 
            if parsed_request is None:
 
                return self.application(environ, start_response)
 
            return self._handle_request(parsed_request, environ, start_response)
 

	
 
            # skip passing error to error controller
 
            environ['pylons.status_code_redirect'] = True
 

	
 
            # quick check if repo exists...
 
            if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
 
                raise webob.exc.HTTPNotFound()
 

	
 
            if parsed_request.action is None:
 
                # Note: the client doesn't get the helpful error message
 
                raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
 

	
 
            #======================================================================
 
            # CHECK PERMISSIONS
 
            #======================================================================
 
            ip_addr = self._get_ip_addr(environ)
 
            user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
 
            if response_app is not None:
 
                return response_app(environ, start_response)
 

	
 
            # extras are injected into Mercurial UI object and later available
 
            # in hooks executed by Kallithea
 
            from kallithea import CONFIG
 
            extras = {
 
                'ip': ip_addr,
 
                'username': user.username,
 
                'action': parsed_request.action,
 
                'repository': parsed_request.repo_name,
 
                'scm': self.scm_alias,
 
                'config': CONFIG['__file__'],
 
                'server_url': get_server_url(environ),
 
            }
 

	
 
            #======================================================================
 
            # REQUEST HANDLING
 
            #======================================================================
 
            log.debug('HOOKS extras is %s', extras)
 
            _set_extras(extras)
 

	
 
            try:
 
                log.info('%s action on %s repo "%s" by "%s" from %s',
 
                         parsed_request.action, self.scm_alias, parsed_request.repo_name, safe_str(user.username), ip_addr)
 
                app = self._make_app(parsed_request)
 
                return app(environ, start_response)
 
            except Exception:
 
                log.error(traceback.format_exc())
 
                raise webob.exc.HTTPInternalServerError()
 

	
 
        except webob.exc.HTTPException as e:
 
            return e(environ, start_response)
 
        finally:
 
            log = logging.getLogger('kallithea.' + self.__class__.__name__)
 
            log.debug('Request time: %.3fs', time.time() - start)
 
            log_ = logging.getLogger('kallithea.' + self.__class__.__name__)
 
            log_.debug('Request time: %.3fs', time.time() - start)
 
            meta.Session.remove()
 

	
 

	
 
class BaseController(TGController):
 

	
 
    def _before(self, *args, **kwargs):
 
        """
 
        _before is called before controller methods and after __call__
 
        """
 
        if request.needs_csrf_check:
 
            # CSRF protection: Whenever a request has ambient authority (whether
 
            # through a session cookie or its origin IP address), it must include
 
            # the correct token, unless the HTTP method is GET or HEAD (and thus
 
            # guaranteed to be side effect free. In practice, the only situation
 
            # where we allow side effects without ambient authority is when the
 
            # authority comes from an API key; and that is handled above.
 
            token = request.POST.get(secure_form.token_key)
 
            if not token or token != secure_form.authentication_token():
 
                log.error('CSRF check failed')
 
                raise webob.exc.HTTPForbidden()
 

	
 
        c.kallithea_version = __version__
 
        rc_config = Setting.get_app_settings()
 

	
kallithea/lib/middleware/simplegit.py
Show inline comments
 
@@ -9,142 +9,90 @@
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.middleware.simplegit
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
SimpleGit middleware for handling Git protocol requests (push/clone etc.)
 
It's implemented with basic auth function
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Apr 28, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 

	
 
"""
 

	
 

	
 
import re
 
import logging
 
import traceback
 

	
 
from webob.exc import HTTPNotFound, HTTPInternalServerError, HTTPBadRequest
 

	
 
from kallithea.model.db import Ui, Repository
 
from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
 
    _set_extras
 
from kallithea.lib.utils2 import safe_unicode
 
from kallithea.lib.base import BaseVCSController
 
from kallithea.lib.utils import make_ui, is_valid_repo
 
from kallithea.lib.utils import make_ui
 
from kallithea.lib.hooks import log_pull_action
 
from kallithea.lib.middleware.pygrack import make_wsgi_app
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)$')
 

	
 

	
 
cmd_mapping = {
 
    'git-receive-pack': 'push',
 
    'git-upload-pack': 'pull',
 
}
 

	
 

	
 
class SimpleGit(BaseVCSController):
 

	
 
    scm_alias = 'git'
 

	
 
    @classmethod
 
    def parse_request(cls, environ):
 
        path_info = environ.get('PATH_INFO', '')
 
        m = GIT_PROTO_PAT.match(path_info)
 
        if m is None:
 
            return None
 

	
 
        class parsed_request(object):
 
            # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol
 
            repo_name = safe_unicode(m.group(1).rstrip('/'))
 
            cmd = m.group(2)
 

	
 
            query_string = environ['QUERY_STRING']
 
            if cmd == 'info/refs' and query_string.startswith('service='):
 
                service = query_string.split('=', 1)[1]
 
                action = cmd_mapping.get(service)
 
            else:
 
                service = None
 
                action = cmd_mapping.get(cmd)
 

	
 
        return parsed_request
 

	
 
    def _handle_request(self, parsed_request, environ, start_response):
 
        # skip passing error to error controller
 
        environ['pylons.status_code_redirect'] = True
 

	
 
        # quick check if repo exists...
 
        if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
 
            raise HTTPNotFound()
 

	
 
        if parsed_request.action is None:
 
            # Note: the client doesn't get the helpful error message
 
            raise HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
 

	
 
        #======================================================================
 
        # CHECK PERMISSIONS
 
        #======================================================================
 
        ip_addr = self._get_ip_addr(environ)
 
        user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
 
        if response_app is not None:
 
            return response_app(environ, start_response)
 

	
 
        # extras are injected into Mercurial UI object and later available
 
        # in hooks executed by Kallithea
 
        from kallithea import CONFIG
 
        extras = {
 
            'ip': ip_addr,
 
            'username': user.username,
 
            'action': parsed_request.action,
 
            'repository': parsed_request.repo_name,
 
            'scm': self.scm_alias,
 
            'config': CONFIG['__file__'],
 
            'server_url': get_server_url(environ),
 
        }
 

	
 
        #======================================================================
 
        # REQUEST HANDLING
 
        #======================================================================
 
        log.debug('HOOKS extras is %s', extras)
 
        _set_extras(extras)
 

	
 
        try:
 
            log.info('%s action on %s repo "%s" by "%s" from %s',
 
                     parsed_request.action, self.scm_alias, parsed_request.repo_name, safe_str(user.username), ip_addr)
 
            app = self._make_app(parsed_request)
 
            return app(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise HTTPInternalServerError()
 

	
 
    def _make_app(self, parsed_request):
 
        """
 
        Return a pygrack wsgi application.
 
        """
 
        pygrack_app = make_wsgi_app(parsed_request.repo_name, self.basepath)
 

	
 
        def wrapper_app(environ, start_response):
 
            if (parsed_request.cmd == 'info/refs' and
 
                parsed_request.service == 'git-upload-pack'
 
                ):
 
                baseui = make_ui()
 
                repo = Repository.get_by_repo_name(parsed_request.repo_name)
 
                scm_repo = repo.scm_instance
 
                # Run hooks, like Mercurial outgoing.pull_logger does
 
                log_pull_action(ui=baseui, repo=scm_repo._repo)
 
            # Note: push hooks are handled by post-receive hook
 

	
 
            return pygrack_app(environ, start_response)
 

	
 
        return wrapper_app
kallithea/lib/middleware/simplehg.py
Show inline comments
 
@@ -9,58 +9,54 @@
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.middleware.simplehg
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
SimpleHg middleware for handling Mercurial protocol requests (push/clone etc.).
 
It's implemented with basic auth function
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Apr 28, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 

	
 
"""
 

	
 

	
 
import os
 
import logging
 
import traceback
 
import urllib
 

	
 
from webob.exc import HTTPNotFound, HTTPInternalServerError, HTTPBadRequest
 

	
 
from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
 
    _set_extras
 
from kallithea.lib.utils2 import safe_str, safe_unicode
 
from kallithea.lib.base import BaseVCSController
 
from kallithea.lib.utils import make_ui, is_valid_repo
 
from kallithea.lib.vcs.utils.hgcompat import RepoError, hgweb_mod
 
from kallithea.lib.utils import make_ui
 
from kallithea.lib.vcs.utils.hgcompat import hgweb_mod
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def get_header_hgarg(environ):
 
    """Decode the special Mercurial encoding of big requests over multiple headers.
 
    >>> get_header_hgarg({})
 
    ''
 
    >>> get_header_hgarg({'HTTP_X_HGARG_0': ' ', 'HTTP_X_HGARG_1': 'a','HTTP_X_HGARG_2': '','HTTP_X_HGARG_3': 'b+c %20'})
 
    'ab+c %20'
 
    """
 
    chunks = []
 
    i = 1
 
    while True:
 
        v = environ.get('HTTP_X_HGARG_%d' % i)
 
        if v is None:
 
            break
 
        chunks.append(v)
 
        i += 1
 
    return ''.join(chunks)
 

	
 

	
 
cmd_mapping = {
 
    # 'batch' is not in this list - it is handled explicitly
 
@@ -114,86 +110,38 @@ class SimpleHg(BaseVCSController):
 
            action = None
 
            for qry in query_string.split('&'):
 
                parts = qry.split('=', 1)
 
                if len(parts) == 2 and parts[0] == 'cmd':
 
                    cmd = parts[1]
 
                    if cmd == 'batch':
 
                        hgarg = get_header_hgarg(environ)
 
                        if not hgarg.startswith('cmds='):
 
                            action = 'push' # paranoid and safe
 
                            break
 
                        action = 'pull'
 
                        for cmd_arg in hgarg[5:].split(';'):
 
                            cmd, _args = urllib.unquote_plus(cmd_arg).split(' ', 1)
 
                            op = cmd_mapping.get(cmd, 'push')
 
                            if op != 'pull':
 
                                assert op == 'push'
 
                                action = 'push'
 
                                break
 
                    else:
 
                        action = cmd_mapping.get(cmd, 'push')
 
                    break # only process one cmd
 

	
 
        return parsed_request
 

	
 
    def _handle_request(self, parsed_request, environ, start_response):
 
        # skip passing error to error controller
 
        environ['pylons.status_code_redirect'] = True
 

	
 
        # quick check if repo exists...
 
        if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
 
            raise HTTPNotFound()
 

	
 
        if parsed_request.action is None:
 
            # Note: the client doesn't get the helpful error message
 
            raise HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
 

	
 
        #======================================================================
 
        # CHECK PERMISSIONS
 
        #======================================================================
 
        ip_addr = self._get_ip_addr(environ)
 
        user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
 
        if response_app is not None:
 
            return response_app(environ, start_response)
 

	
 
        # extras are injected into Mercurial UI object and later available
 
        # in hooks executed by Kallithea
 
        from kallithea import CONFIG
 
        extras = {
 
            'ip': ip_addr,
 
            'username': user.username,
 
            'action': parsed_request.action,
 
            'repository': parsed_request.repo_name,
 
            'scm': self.scm_alias,
 
            'config': CONFIG['__file__'],
 
            'server_url': get_server_url(environ),
 
        }
 

	
 
        #======================================================================
 
        # REQUEST HANDLING
 
        #======================================================================
 
        log.debug('HOOKS extras is %s', extras)
 
        _set_extras(extras)
 

	
 
        try:
 
            log.info('%s action on %s repo "%s" by "%s" from %s',
 
                     parsed_request.action, self.scm_alias, parsed_request.repo_name, safe_str(user.username), ip_addr)
 
            app = self._make_app(parsed_request)
 
            return app(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise HTTPInternalServerError()
 

	
 
    def _make_app(self, parsed_request):
 
        """
 
        Make an hgweb wsgi application.
 
        """
 
        str_repo_name = safe_str(parsed_request.repo_name)
 
        repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
 
        baseui = make_ui(repo_path=repo_path)
 
        hgweb_app = hgweb_mod.hgweb(repo_path, name=str_repo_name, baseui=baseui)
 

	
 
        def wrapper_app(environ, start_response):
 
            environ['REPO_NAME'] = str_repo_name # used by hgweb_mod.hgweb
 
            return hgweb_app(environ, start_response)
 

	
 
        return wrapper_app
0 comments (0 inline, 0 general)