Ejemplo n.º 1
0
def hgwebinit_run_wsgi_wrapper(orig, obj, req):
    """Handles hgwebdir_mod requests, looking for pushes to non-existent repos.
    If one is detected, the user is first authorized and then prompted to init.
    Following that we simply hand the request off ot the next handler in the
    chain - typically hgwebdir_mod itself."""
    try:
        tmpl = obj.templater(req)
        ctype = tmpl("mimetype", encoding=encoding.encoding)
        ctype = templater.stringify(ctype)

        obj.refresh()

        # Do our stuff...
        if should_create_repo(obj, req):
            # Ah, but is this user allowed to create repos?
            if create_allowed(obj.ui, req):
                virtual = req.env.get("PATH_INFO", "").strip("/")

                paths = {}
                for name, value in obj.ui.configitems("paths"):
                    paths[name] = value

                local = local_path_for_repo(virtual, paths)

                if obj.ui.configbool("web", "implicit_init", False):
                    # Go ahead and init if implicit creation is enabled
                    hg.repository(obj.ui, path=local, create=True)
                else:
                    # Find out what the client wants.
                    # Only the capabilities and init commands are supported.
                    cmd = req.form.get("cmd", [""])[0]
                    if protocol.iscmd(cmd) and cmd in ("capabilities", "init"):
                        repo = emptyrepo(baseui=obj.ui)
                        return protocol.call(repo, req, cmd)

                # force refresh
                obj.lastrefresh = 0

    except ErrorResponse, err:
        req.respond(err, ctype)
        return tmpl("error", error=err.message or "")
    def run_wsgi(self, req):
        path = req.env['PATH_INFO'].replace('\\', '/').strip('/')

        u = util.url(self.serverurl)
        # Forward HTTP basic authorization headers through the layers
        authheader = req.env.get('HTTP_AUTHORIZATION')
        if authheader and authheader.lower().startswith('basic '):
            userpasswd = authheader[6:].decode('base64')
            if ':' in userpasswd:
                u.user, u.passwd = userpasswd.split(':', 1)

        proto = protocol.webproto(req, self.ui)
        # MIME and HTTP allows multiple headers by the same name - we only
        # use and care about one
        args = dict((k, v[0]) for k, v in proto._args().items())
        cmd = args.pop('cmd', None)

        self.ui.write("%s@%s  cmd: %s  args: %s\n" %
                      (u.user, path or '/', cmd, ' '.join('%s=%s' % (k, v)
                       for k, v in sorted(args.items()))))

        if not cmd:
            if self.index:
                req.respond(common.HTTP_OK,
                            'text/html' if self.index.endswith('.html') else
                            'text/plain')
                return file(self.index)
            self.ui.warn(_('no command in request\n'))
            req.respond(common.HTTP_BAD_REQUEST, protocol.HGTYPE)
            return []

        # Simple path validation - probably only sufficient on Linux
        if ':' in path or path.startswith('.') or '/.' in path:
            self.ui.warn(_('bad request path %r\n') % path)
            req.respond(common.HTTP_BAD_REQUEST, protocol.HGTYPE)
            return []

        # Bounce early on missing credentials
        if not (self.anonymous or u.user and u.passwd):
            er = common.ErrorResponse(common.HTTP_UNAUTHORIZED,
                                      'Authentication is mandatory',
                                      self.authheaders)
            req.respond(er, protocol.HGTYPE)
            return ['HTTP authentication required']

        u.path = posixpath.join(u.path or '', req.env['PATH_INFO']).strip('/')
        url = str(u)

        repopath = os.path.join(self.cachepath, path)
        path = path or '/'

        try:
            # Reuse auth if possible - checking remotely is expensive
            peer, ts = peercache.get((u.user, u.passwd, path), (None, None))
            if peer is not None and time.time() > ts + self.ttl:
                self.ui.note(_('%s@%s expired, age %s\n') %
                             (u.user, path, time.time() - ts))
                peer = None
                peercache[(u.user, u.passwd, path)] = (peer, ts)
            # peer is now None or valid

            try:
                repo = hg.repository(self.ui, path=repopath)
            except error.RepoError as e:
                hg.peer(self.ui, {}, url) # authenticate / authorize first
                if os.path.exists(repopath) or not self.clone:
                    self.ui.warn(_("error with path %r: %s\n") % (path, e))
                    req.respond(common.HTTP_NOT_FOUND, protocol.HGTYPE)
                    return ['repository %s not found in proxy' % path]
                self.ui.warn(_("%r not found locally - cloning\n") % path)
                try:
                    repodir = os.path.dirname(repopath)
                    if not os.path.exists(repodir):
                        os.makedirs(repodir)
                    peer, destpeer = hg.clone(self.ui, {}, url, repopath,
                                              stream=True, update=False)
                except Exception as e:
                    self.ui.warn(_("error cloning %r: %s\n") % (path, e))
                    req.respond(common.HTTP_NOT_FOUND, protocol.HGTYPE)
                    return ['repository %s not available' % path]
                repo = destpeer.local()

            if cmd in ['capabilities', 'batch', 'lookup', 'branchmap'] and not peer:
                # new session on expired repo - do auth and pull again
                self.ui.note(_('%s@%s - pulling\n') % (u.user, path))
                t0 = time.time()
                peer = hg.peer(self.ui, {}, url)
                with repo.lock():
                    try:
                        r = pull(repo, peer)
                    except error.RepoError as e:
                        self.ui.debug('got %s on pull - running recover\n' % (e,))
                        repo.recover()
                        # should also run hg.verify(repo) ... but too expensive
                        r = pull(repo, peer)
                self.ui.debug('pull got %r after %s\n' % (r, time.time() - t0))
                peercache[(u.user, u.passwd, path)] = (peer, time.time())
            elif ts is None: # never authenticated
                self.ui.note('%s@%s - authenticating\n' % (u.user, path))
                peer = hg.peer(self.ui, {}, url)
                self.ui.debug('%s@%s - authenticated\n' % (u.user, path))
                peercache[(u.user, u.passwd, path)] = (peer, time.time())
            # user is now auth'ed for this session

            # fetch largefiles whenever they are referenced
            # (creating fake/combined batch statlfile responses is too complex)
            shas = []
            if cmd in ['statlfile', 'getlfile']:
                shas.append(args['sha'])
            if cmd == 'batch':
                for x in args['cmds'].split(';'):
                    if x.startswith('statlfile sha='):
                        shas.append(x[14:])
            missingshas = [sha for sha in shas
                           if not lfutil.findfile(repo, sha)]
            if missingshas:
                self.ui.debug('%s@%s - missing %s\n' %
                              (u.user, path, ' '.join(missingshas)))
                if not peer:
                    peer = hg.peer(self.ui, {}, url)
                store = openstore(repo, peer, False)
                existsremotely = store.exists(missingshas)
                for sha, available in sorted(existsremotely.iteritems()):
                    if not available:
                        self.ui.warn('%s@%s - %s not available remotely\n' %
                                     (u.user, path, sha))
                        continue
                    self.ui.write('%s@%s - fetching %s\n' % (u.user, path, sha))
                    gotit = store._gethash(sha, sha)
                    if not gotit:
                        self.ui.warn(_('failed to get %s for %s@%s remotely\n'
                                       ) % (sha, u.user, path))
                peercache[(u.user, u.passwd, path)] = (peer, time.time())

            # Forward write commands to the remote server.
            # Lookup and listkeys are also forwarded so we get
            # local tags, bookmarks and phases from the server
            if cmd in ['putlfile', 'unbundle', 'pushkey', 'lookup', 'listkeys']:
                size = req.env.get('CONTENT_LENGTH')
                self.ui.debug('reading %s bytes content before forwarding\n'
                              % size)
                data = None
                if req.env['REQUEST_METHOD'] == 'POST' or size is not None:
                    data = req.read(int(size or 0))

                if not peer:
                    peer = hg.peer(self.ui, {}, url)
                self.ui.note(_('calling %s remotely\n') % cmd)
                with repo.lock():
                    r = peer._call(cmd, data=data, **args)
                    if cmd == 'unbundle':
                        self.ui.debug('fetching pushed changes back\n')
                        # we could perhaps just have pulled from data ... but it
                        # could be tricky to make sure the repo stays in sync ...
                        pull(repo, peer)
                peercache[(u.user, u.passwd, path)] = (peer, time.time())
                req.respond(common.HTTP_OK, protocol.HGTYPE)
                return [r]

            # Now serve it locally
            return protocol.call(repo, req, cmd)

        except urllib2.HTTPError as inst:
            self.ui.warn(_('HTTPError connecting to server: %s\n') % inst)
            req.respond(inst.code, protocol.HGTYPE)
            return ['HTTP error']
        except error.Abort as e: # hg.peer will abort when it gets 401
            if e.args not in [('http authorization required',),
                              ('authorization failed',)]:
                raise
            self.ui.warn('%s@%s error: %r\n' % (u.user, path, e.args[0]))
            er = common.ErrorResponse(
                common.HTTP_UNAUTHORIZED
                if e.args == ('http authorization required',)
                else common.HTTP_BAD_REQUEST,
                'Authentication is required',
                self.authheaders)
            req.respond(er, protocol.HGTYPE)
            return ['HTTP authentication required']
        except Exception as e:
            msg = 'Internal proxy server error - please contact the administrator: %s' % e
            self.ui.warn('%s\n' % msg) # TODO: log traceback?
            req.respond(common.ErrorResponse(common.HTTP_SERVER_ERROR, msg), 'text/plain')
            return [msg]
Ejemplo n.º 3
0
    def process_request(self, request):
        """
        Process the request object and returns output.
        """

        protocol = mercurial.hgweb.protocol
        request.stdin.seek(0)
        env = dict(request.environ)

        # the request object *should* be WSGI compliant as Mercurial
        # supports it, but we have kind of completely wrap around it so
        # might as well emulate it if it's missing.

        if 'wsgi.version' not in env:
            # 'REQUEST_URI' is missing but seems to be unused
            env['REMOTE_HOST'] = env['REMOTE_ADDR']
            # emulate wsgi environment
            env['wsgi.version'] = (1, 0)
            # environment variable has https
            env['wsgi.url_scheme'] = \
                request.base.split(':')[0]  # self.url_scheme
            env['wsgi.input'] = request.stdin # self.rfile
            env['wsgi.errors'] = StringIO() #_error_logger(self)
            env['wsgi.multithread'] = True  # XXX guess
            env['wsgi.multiprocess'] = True  # same as above
            env['wsgi.run_once'] = True

        headers_set = []
        headers_sent = []

        def write(data):
            if not headers_set:
                raise AssertionError("write() before start_response()")

            elif not headers_sent:
                # Before the first output, send the stored headers
                status, response_headers = headers_sent[:] = headers_set
                status, code = status.split(' ', 1)
                request.response.setStatus(status, code)
                for header in response_headers:
                    # let zope deal with the header.
                    request.response.setHeader(*header)

            out.write(data)
            out.flush()

        def start_response(status, response_headers, exc_info=None):
            if exc_info:
                try:
                    if headers_sent:
                        # Re-raise original exception if headers sent
                        raise exc_info[0], exc_info[1], exc_info[2]
                finally:
                    exc_info = None     # avoid dangling circular ref
            elif headers_set:
                raise AssertionError("Headers already set!")

            headers_set[:] = [status, response_headers]
            return write

        # XXX this is a hgweb object already.  While run_wsgi has what
        # we need, Zope/repoze/Plone handles some of the request stuff
        # already, so we just skip some of the parsing steps.

        out = StringIO()
        req = wsgirequest(env, start_response)

        self.refresh()

        req.url = req.env['SCRIPT_NAME']
        if not req.url.endswith('/'):
            req.url += '/'
        if 'REPO_NAME' in req.env:
            req.url += req.env['REPO_NAME'] + '/'

        if 'PATH_INFO' in req.env:
            parts = req.env['PATH_INFO'].strip('/').split('/')
            repo_parts = req.env.get('REPO_NAME', '').split('/')
            if parts[:len(repo_parts)] == repo_parts:
                parts = parts[len(repo_parts):]
            query = '/'.join(parts)
        else:
            query = req.env['QUERY_STRING'].split('&', 1)[0]
            query = query.split(';', 1)[0]

        # process this if it's a protocol request
        # protocol bits don't need to create any URLs
        # and the clients always use the old URL structure

        cmd = req.form.get('cmd', [''])[0]
        if cmd:
            if not protocol.iscmd(cmd):
                raise UnsupportedCommandError('%s is unsupported' % cmd)
            # XXX not sure why?
            #if query:
            #    raise ErrorResponse(HTTP_NOT_FOUND)
            try:
                if cmd in perms:
                    try:
                        self.check_perm(req, perms[cmd])
                    except ErrorResponse, inst:
                        if cmd == 'unbundle':
                            req.drain()
                        raise
                content = protocol.call(self.repo, req, cmd)
            except ErrorResponse, inst:
                req.respond(inst, protocol.HGTYPE)
                # XXX doing write here because the other methods expect
                # output.
                write('0\n')
                if not inst.message:
                    return []
                write('%s\n' % inst.message,)
                return out.getvalue()