Changeset - d14328af601e
[Not reviewed]
default
0 4 0
Mads Kiilerich - 7 years ago 2019-01-07 02:08:38
mads@kiilerich.com
middleware: minor cleanup and alignment between VCSs to clarify how things work
4 files changed with 22 insertions and 25 deletions:
0 comments (0 inline, 0 general)
kallithea/lib/base.py
Show inline comments
 
@@ -37,97 +37,97 @@ 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
 
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.exceptions import UserCreationError
 
from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
 
from kallithea.model import meta
 

	
 
from kallithea.model.db import PullRequest, Repository, Ui, User, Setting
 
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(',')
 
        _first_ip = _ips[0].strip()
 
        log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
 
        return _first_ip
 
    return ip
 

	
 

	
 
def _get_ip_addr(environ):
 
    proxy_key = 'HTTP_X_REAL_IP'
 
    proxy_key2 = 'HTTP_X_FORWARDED_FOR'
 
    def_key = 'REMOTE_ADDR'
 

	
 
    ip = environ.get(proxy_key)
 
    if ip:
 
        return _filter_proxy(ip)
 

	
 
    ip = environ.get(proxy_key2)
 
    if ip:
 
        return _filter_proxy(ip)
 

	
 
    ip = environ.get(def_key, '0.0.0.0')
 
    return _filter_proxy(ip)
 

	
 

	
 
def _get_access_path(environ):
 
    path = environ.get('PATH_INFO')
 
    """Return PATH_INFO from environ ... using tg.original_request if available."""
 
    org_req = environ.get('tg.original_request')
 
    if org_req:
 
        path = org_req.environ.get('PATH_INFO')
 
    return path
 
    if org_req is not None:
 
        environ = org_req.environ
 
    return environ.get('PATH_INFO')
 

	
 

	
 
def log_in_user(user, remember, is_external_auth, ip_addr):
 
    """
 
    Log a `User` in and update session and cookies. If `remember` is True,
 
    the session cookie is set to expire in a year; otherwise, it expires at
 
    the end of the browser session.
 

	
 
    Returns populated `AuthUser` object.
 
    """
 
    # It should not be possible to explicitly log in as the default user.
 
    assert not user.is_default_user, user
 

	
 
    auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
 
    if auth_user is None:
 
        return None
 

	
 
    user.update_lastlogin()
 
    meta.Session().commit()
 

	
 
    # Start new session to prevent session fixation attacks.
 
    session.invalidate()
 
    session['authuser'] = cookie = auth_user.to_cookie()
 

	
 
@@ -189,49 +189,49 @@ class BasicAuth(paste.auth.basic.AuthBas
 

	
 
class BaseVCSController(object):
 
    """Base controller for handling Mercurial/Git protocol requests
 
    (coming from a VCS client, and not a browser).
 
    """
 

	
 
    scm_alias = None # 'hg' / 'git'
 

	
 
    def __init__(self, application, config):
 
        self.application = application
 
        self.config = config
 
        # base path of repo locations
 
        self.basepath = self.config['base_path']
 
        # authenticate this VCS request using the authentication modules
 
        self.authenticate = BasicAuth('', auth_modules.authenticate,
 
                                      config.get('auth_ret_code'))
 

	
 
    @classmethod
 
    def parse_request(cls, environ):
 
        """If request is parsed as a request for this VCS, return a namespace with the parsed request.
 
        If the request is unknown, return None.
 
        """
 
        raise NotImplementedError()
 

	
 
    def _authorize(self, environ, start_response, action, repo_name, ip_addr):
 
    def _authorize(self, environ, action, repo_name, ip_addr):
 
        """Authenticate and authorize user.
 

	
 
        Since we're dealing with a VCS client and not a browser, we only
 
        support HTTP basic authentication, either directly via raw header
 
        inspection, or by using container authentication to delegate the
 
        authentication to the web server.
 

	
 
        Returns (user, None) on successful authentication and authorization.
 
        Returns (None, wsgi_app) to send the wsgi_app response to the client.
 
        """
 
        # Use anonymous access if allowed for action on repo.
 
        default_user = User.get_default_user(cache=True)
 
        default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
 
        if default_authuser is None:
 
            log.debug('No anonymous access at all') # move on to proper user auth
 
        else:
 
            if self._check_permission(action, default_authuser, repo_name):
 
                return default_authuser, None
 
            log.debug('Not authorized to access this repository as anonymous user')
 

	
 
        username = None
 
        #==============================================================
 
        # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
 
        # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
kallithea/lib/middleware/simplegit.py
Show inline comments
 
@@ -7,141 +7,138 @@
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# 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 os
 
import re
 
import logging
 
import traceback
 

	
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
 
    HTTPNotAcceptable, HTTPBadRequest
 
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.base import BaseVCSController
 
from kallithea.lib.utils import make_ui, is_valid_repo
 
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):
 
        ip_addr = self._get_ip_addr(environ)
 
        # 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
 
        #======================================================================
 
        user, response_app = self._authorize(environ, start_response, parsed_request.action, parsed_request.repo_name, ip_addr)
 
        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
 
        server_url = get_server_url(environ)
 
        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': server_url,
 
            'server_url': get_server_url(environ),
 
        }
 

	
 
        #===================================================================
 
        # GIT REQUEST HANDLING
 
        #===================================================================
 
        #======================================================================
 
        # REQUEST HANDLING
 
        #======================================================================
 
        log.debug('HOOKS extras is %s', extras)
 
        _set_extras(extras or {})
 
        _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
kallithea/lib/middleware/simplehg.py
Show inline comments
 
@@ -12,50 +12,49 @@
 
# 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, HTTPForbidden, HTTPInternalServerError, \
 
    HTTPNotAcceptable, HTTPBadRequest
 
from webob.exc import HTTPNotFound, HTTPInternalServerError, HTTPBadRequest
 

	
 
from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
 
    _set_extras
 
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
 

	
 
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)
 
@@ -116,85 +115,85 @@ class SimpleHg(BaseVCSController):
 
            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):
 
        ip_addr = self._get_ip_addr(environ)
 
        # 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
 
        #======================================================================
 
        user, response_app = self._authorize(environ, start_response, parsed_request.action, parsed_request.repo_name, ip_addr)
 
        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
 
        server_url = get_server_url(environ)
 
        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': server_url,
 
            'server_url': get_server_url(environ),
 
        }
 

	
 
        #======================================================================
 
        # MERCURIAL REQUEST HANDLING
 
        # REQUEST HANDLING
 
        #======================================================================
 
        log.debug('HOOKS extras is %s', extras)
 
        _set_extras(extras or {})
 
        _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
kallithea/lib/utils.py
Show inline comments
 
@@ -247,48 +247,49 @@ def is_valid_repo_uri(repo_type, url, ui
 
        if url.startswith('http') or url.startswith('git'):
 
            # initially check if it's at least the proper URL
 
            # or does it pass basic auth
 
            GitRepository._check_url(url)
 
        elif url.startswith('svn+http'):
 
            raise NotImplementedError()
 
        elif url.startswith('hg+http'):
 
            raise NotImplementedError()
 
        else:
 
            raise Exception('URI %s not allowed' % (url))
 

	
 

	
 
def is_valid_repo(repo_name, base_path, scm=None):
 
    """
 
    Returns True if given path is a valid repository False otherwise.
 
    If scm param is given also compare if given scm is the same as expected
 
    from scm parameter
 

	
 
    :param repo_name:
 
    :param base_path:
 
    :param scm:
 

	
 
    :return True: if given path is a valid repository
 
    """
 
    # TODO: paranoid security checks?
 
    full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
 

	
 
    try:
 
        scm_ = get_scm(full_path)
 
        if scm:
 
            return scm_[0] == scm
 
        return True
 
    except VCSError:
 
        return False
 

	
 

	
 
def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
 
    """
 
    Returns True if given path is a repository group False otherwise
 

	
 
    :param repo_name:
 
    :param base_path:
 
    """
 
    full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
 

	
 
    # check if it's not a repo
 
    if is_valid_repo(repo_group_name, base_path):
 
        return False
 

	
0 comments (0 inline, 0 general)