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]
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()