# relay.py - serve repos over an httprelay service # see http://code.google.com/p/httprelay/ # # Copyright 2010 Peter Arrenbrecht # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. '''serve repos over httprelay for when both parties are behind firewalls''' import urllib, urllib2, base64 from StringIO import StringIO from mercurial.i18n import _ from mercurial import cmdutil, hg, util, error from mercurial.hgweb.hgweb_mod import hgweb from mercurial.hgweb.request import wsgirequest class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): def http_error_default(self, req, fp, code, msg, headers): result = urllib2.HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result def _submit(host, url, headers=None, data=None): if url.startswith("/"): uri = host + url else: uri = host + "/" + url request = urllib2.Request(uri) if headers: for k, v in headers.iteritems(): request.add_header(k, v) if data: request.add_data(data) opener = urllib2.build_opener(DefaultErrorHandler()) return opener.open(request) class Response: pass def _internal(ui, hgwebobj, method, path, headers, data): ui.status("%s %s\n" % (method, path)) pathcomps = path.split("?", 1) if len(pathcomps) > 1: pathinfo, querystring = pathcomps else: pathinfo = path querystring = "" env = { "wsgi.version": (1,0), "wsgi.input": StringIO(data), "wsgi.errors": None, "wsgi.multithread": False, "wsgi.multiprocess": False, "wsgi.run_once": False, "REQUEST_METHOD": method, "QUERY_STRING": querystring, "PATH_INFO": pathinfo, "REQUEST_URI": path, "SCRIPT_NAME": "", "SERVER_PORT": "?", "SERVER_NAME": "httprelay", } for k,v in headers.iteritems(): k = k.replace("-", "_").upper() env[k] = v env["HTTP_" + k] = v rsp = Response() rsp.headers = {} rsp.status = None rsp.body = None def start(status, headers, exc_info=None): rsp.headers = dict(headers) rsp.status = status rsp.body = "".join(hgwebobj.run_wsgi(wsgirequest(env, start))) return rsp def _handle(ui, hgwebobj, host, req): ui.note(_("querying local server\n")) info, rest = req.split("\n", 1) _magic, id, method, path = info.split(" ", 3) headers, data = rest.split("\n-\n", 1) headers = dict(h.split(": ", 1) for h in headers.splitlines()) rsp = _internal(ui, hgwebobj, method, path, headers, data) ui.note(_("forwarding response to relay host\n")) headers = rsp.headers body = rsp.body type = headers.get("Content-Type").lower() if not type or not type.startswith("text/"): ui.note(_("Encoding\n")) body = base64.b64encode(body) headers = "".join("%s: %s\n" % (k, v) for k, v in headers.iteritems()) data = urllib.urlencode([("headers", headers), ("body", body)]) _submit(host, "/httprelay/fwd/%s" % id, data=data) def relay(ui, repo, **opts): """export the repository via httprelay (use when behind a firewall) Start a local HTTP repository browser and pull server. This server communicates with a relay service hosted on Google's AppEngine (appspot.com), instead of listening on a local port (see http://code.google.com/p/httprelay/). Specify the relay host with the --host argument. This is typically "http://our-secret.our-app.appspot.com/", where "our-app" is the name of your Google AppEngine instance running the httprelay service, and "our-secret" is an arbitrary string the that client will have to use to access the reflected server. With --allow-push, the server will accept incoming pushes without SSL from anyone (--config 'web.allow_push=*' --config 'web.push_ssl=false'). Otherwise "hg relay" behaves very much like "hg serve". """ baseui = repo and repo.baseui or ui optlist = ("name templates style" " accesslog errorlog webdir_conf certificate encoding") def setcfg(section, option, value): baseui.setconfig(section, option, value) if (repo is not None) and (repo.ui != baseui): repo.ui.setconfig(section, option, value) for o in optlist.split(): if opts.get(o, None): setcfg("web", o, str(opts[o])) if opts.get("allow_push"): setcfg("web", "allow_push", "*") setcfg("web", "push_ssl", "false") if repo is None and not ui.config("web", "webdir_conf"): raise error.RepoError(_("There is no Mercurial repository here" " (.hg not found)")) host = opts.get("host") if not host: raise error.Abort(_("Must specify --host")) if host.endswith("/"): host = host[:-1] class service(object): def init(self): util.set_signal_handler() self.hgwebobj = hgweb(hg.repository(repo.ui, repo.root)) if not ui.verbose: return ui.status(_('relaying via %s\n') % host) def run(self): hgwebobj = self.hgwebobj while True: ui.note(_("polling relay host\n")) req = _submit(host, "/httprelay/poll").read() if req and req.startswith("httprelay "): _handle(ui, hgwebobj, host, req) service = service() cmdutil.service(opts, initfn=service.init, runfn=service.run) cmdtable = { 'relay': (relay, [('H', 'host', '', _('URL of the httprelay service host')), ('A', 'accesslog', '', _('name of access log file to write to')), ('E', 'errorlog', '', _('name of error log file to write to')), ('n', 'name', '', _('name to show in web pages (default: working directory)')), ('t', 'templates', '', _('web templates to use')), ('', 'style', '', _('template style to use')), ('d', 'daemon', None, _('run server in background')), ('', 'daemon-pipefds', '', _('used internally by daemon mode')), ('', 'pid-file', '', _('name of file to write process ID to')), ('', 'allow-push', None, _('allow pushing')), ], _('[OPTION]...')), }