httprepo.py
334 lines
| 11.9 KiB
| text/x-python
|
PythonLexer
/ mercurial / httprepo.py
|
r1089 | # httprepo.py - HTTP repository proxy classes for mercurial | ||
# | ||||
# Copyright 2005 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms | ||||
# of the GNU General Public License, incorporated herein by reference. | ||||
from node import * | ||||
from remoterepo import * | ||||
|
r1400 | from i18n import gettext as _ | ||
|
r1251 | from demandload import * | ||
|
r2015 | demandload(globals(), "hg os urllib urllib2 urlparse zlib util httplib") | ||
|
r2467 | demandload(globals(), "errno keepalive tempfile socket") | ||
|
r1089 | |||
|
r2447 | class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm): | ||
|
r2281 | def __init__(self, ui): | ||
|
r2447 | urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self) | ||
|
r2281 | self.ui = ui | ||
def find_user_password(self, realm, authuri): | ||||
|
r2447 | authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password( | ||
|
r2281 | self, realm, authuri) | ||
|
r2556 | user, passwd = authinfo | ||
if user and passwd: | ||||
return (user, passwd) | ||||
|
r2281 | |||
|
r2446 | if not self.ui.interactive: | ||
|
r2337 | raise util.Abort(_('http authorization required')) | ||
|
r2281 | self.ui.write(_("http authorization required\n")) | ||
self.ui.status(_("realm: %s\n") % realm) | ||||
|
r2556 | if user: | ||
self.ui.status(_("user: %s\n") % user) | ||||
else: | ||||
user = self.ui.prompt(_("user:"), default=None) | ||||
if not passwd: | ||||
passwd = self.ui.getpass() | ||||
|
r2281 | |||
self.add_password(realm, authuri, user, passwd) | ||||
return (user, passwd) | ||||
|
r2337 | def netlocsplit(netloc): | ||
'''split [user[:passwd]@]host[:port] into 4-tuple.''' | ||||
a = netloc.find('@') | ||||
if a == -1: | ||||
user, passwd = None, None | ||||
else: | ||||
userpass, netloc = netloc[:a], netloc[a+1:] | ||||
c = userpass.find(':') | ||||
if c == -1: | ||||
user, passwd = urllib.unquote(userpass), None | ||||
else: | ||||
user = urllib.unquote(userpass[:c]) | ||||
passwd = urllib.unquote(userpass[c+1:]) | ||||
c = netloc.find(':') | ||||
if c == -1: | ||||
host, port = netloc, None | ||||
else: | ||||
host, port = netloc[:c], netloc[c+1:] | ||||
return host, port, user, passwd | ||||
def netlocunsplit(host, port, user=None, passwd=None): | ||||
'''turn host, port, user, passwd into [user[:passwd]@]host[:port].''' | ||||
if port: | ||||
hostport = host + ':' + port | ||||
else: | ||||
hostport = host | ||||
if user: | ||||
if passwd: | ||||
userpass = urllib.quote(user) + ':' + urllib.quote(passwd) | ||||
else: | ||||
userpass = urllib.quote(user) | ||||
return userpass + '@' + hostport | ||||
return hostport | ||||
|
r2465 | class httpconnection(keepalive.HTTPConnection): | ||
# must be able to send big bundle as stream. | ||||
def send(self, data): | ||||
if isinstance(data, str): | ||||
keepalive.HTTPConnection.send(self, data) | ||||
else: | ||||
# if auth required, some data sent twice, so rewind here | ||||
data.seek(0) | ||||
for chunk in util.filechunkiter(data): | ||||
keepalive.HTTPConnection.send(self, chunk) | ||||
|
r2569 | class basehttphandler(keepalive.HTTPHandler): | ||
|
r2465 | def http_open(self, req): | ||
return self.do_open(httpconnection, req) | ||||
|
r2569 | has_https = hasattr(urllib2, 'HTTPSHandler') | ||
if has_https: | ||||
class httpsconnection(httplib.HTTPSConnection): | ||||
response_class = keepalive.HTTPResponse | ||||
# must be able to send big bundle as stream. | ||||
|
r2557 | |||
|
r2569 | def send(self, data): | ||
if isinstance(data, str): | ||||
httplib.HTTPSConnection.send(self, data) | ||||
else: | ||||
# if auth required, some data sent twice, so rewind here | ||||
data.seek(0) | ||||
for chunk in util.filechunkiter(data): | ||||
httplib.HTTPSConnection.send(self, chunk) | ||||
|
r2557 | |||
|
r2569 | class httphandler(basehttphandler, urllib2.HTTPSHandler): | ||
def https_open(self, req): | ||||
return self.do_open(httpsconnection, req) | ||||
else: | ||||
class httphandler(basehttphandler): | ||||
pass | ||||
|
r2557 | |||
|
r1089 | class httprepository(remoterepository): | ||
def __init__(self, ui, path): | ||||
|
r2442 | self.caps = None | ||
|
r2337 | scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) | ||
if query or frag: | ||||
raise util.Abort(_('unsupported URL component: "%s"') % | ||||
(query or frag)) | ||||
if not urlpath: urlpath = '/' | ||||
host, port, user, passwd = netlocsplit(netloc) | ||||
# urllib cannot handle URLs with embedded user or passwd | ||||
self.url = urlparse.urlunsplit((scheme, netlocunsplit(host, port), | ||||
urlpath, '', '')) | ||||
|
r1089 | self.ui = ui | ||
|
r2337 | |||
proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy') | ||||
proxyauthinfo = None | ||||
|
r2465 | handler = httphandler() | ||
|
r2337 | |||
if proxyurl: | ||||
# proxy can be proper url or host[:port] | ||||
if not (proxyurl.startswith('http:') or | ||||
proxyurl.startswith('https:')): | ||||
proxyurl = 'http://' + proxyurl + '/' | ||||
snpqf = urlparse.urlsplit(proxyurl) | ||||
proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf | ||||
hpup = netlocsplit(proxynetloc) | ||||
proxyhost, proxyport, proxyuser, proxypasswd = hpup | ||||
if not proxyuser: | ||||
proxyuser = ui.config("http_proxy", "user") | ||||
proxypasswd = ui.config("http_proxy", "passwd") | ||||
|
r1089 | |||
|
r2337 | # see if we should use a proxy for this url | ||
no_list = [ "localhost", "127.0.0.1" ] | ||||
|
r2501 | no_list.extend([p.lower() for | ||
p in ui.configlist("http_proxy", "no")]) | ||||
|
r2337 | no_list.extend([p.strip().lower() for | ||
p in os.getenv("no_proxy", '').split(',') | ||||
if p.strip()]) | ||||
# "http_proxy.always" config is for running tests on localhost | ||||
if (not ui.configbool("http_proxy", "always") and | ||||
host.lower() in no_list): | ||||
ui.debug(_('disabling proxy for %s\n') % host) | ||||
else: | ||||
proxyurl = urlparse.urlunsplit(( | ||||
proxyscheme, netlocunsplit(proxyhost, proxyport, | ||||
proxyuser, proxypasswd or ''), | ||||
proxypath, proxyquery, proxyfrag)) | ||||
handler = urllib2.ProxyHandler({scheme: proxyurl}) | ||||
ui.debug(_('proxying through %s\n') % proxyurl) | ||||
|
r1089 | |||
|
r2337 | # urllib2 takes proxy values from the environment and those | ||
# will take precedence if found, so drop them | ||||
|
r1089 | for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]: | ||
try: | ||||
if os.environ.has_key(env): | ||||
del os.environ[env] | ||||
except OSError: | ||||
pass | ||||
|
r2337 | passmgr = passwordmgr(ui) | ||
if user: | ||||
|
r2556 | ui.debug(_('http auth: user %s, password %s\n') % | ||
(user, passwd and '*' * len(passwd) or 'not set')) | ||||
|
r2337 | passmgr.add_password(None, host, user, passwd or '') | ||
|
r1089 | |||
|
r2337 | opener = urllib2.build_opener( | ||
handler, | ||||
urllib2.HTTPBasicAuthHandler(passmgr), | ||||
urllib2.HTTPDigestAuthHandler(passmgr)) | ||||
|
r2281 | |||
|
r1359 | # 1.0 here is the _protocol_ version | ||
opener.addheaders = [('User-agent', 'mercurial/proto-1.0')] | ||||
|
r1089 | urllib2.install_opener(opener) | ||
|
r2442 | # look up capabilities only when needed | ||
def get_caps(self): | ||||
if self.caps is None: | ||||
try: | ||||
self.caps = self.do_read('capabilities').split() | ||||
except hg.RepoError: | ||||
self.caps = () | ||||
|
r2465 | self.ui.debug(_('capabilities: %s\n') % | ||
(' '.join(self.caps or ['none']))) | ||||
|
r2442 | return self.caps | ||
capabilities = property(get_caps) | ||||
|
r1870 | def lock(self): | ||
raise util.Abort(_('operation not supported over http')) | ||||
|
r1089 | def do_cmd(self, cmd, **args): | ||
|
r2465 | data = args.pop('data', None) | ||
headers = args.pop('headers', {}) | ||||
|
r1402 | self.ui.debug(_("sending %s command\n") % cmd) | ||
|
r1089 | q = {"cmd": cmd} | ||
q.update(args) | ||||
qs = urllib.urlencode(q) | ||||
cu = "%s?%s" % (self.url, qs) | ||||
|
r2294 | try: | ||
|
r2465 | resp = urllib2.urlopen(urllib2.Request(cu, data, headers)) | ||
|
r2467 | except urllib2.HTTPError, inst: | ||
if inst.code == 401: | ||||
raise util.Abort(_('authorization failed')) | ||||
raise | ||||
|
r2294 | except httplib.HTTPException, inst: | ||
|
r2336 | self.ui.debug(_('http error while sending %s command\n') % cmd) | ||
self.ui.print_exc() | ||||
raise IOError(None, inst) | ||||
|
r2435 | try: | ||
proto = resp.getheader('content-type') | ||||
except AttributeError: | ||||
proto = resp.headers['content-type'] | ||||
|
r1089 | |||
# accept old "text/plain" and "application/hg-changegroup" for now | ||||
if not proto.startswith('application/mercurial') and \ | ||||
not proto.startswith('text/plain') and \ | ||||
not proto.startswith('application/hg-changegroup'): | ||||
|
r1402 | raise hg.RepoError(_("'%s' does not appear to be an hg repository") % | ||
|
r1251 | self.url) | ||
|
r1089 | |||
if proto.startswith('application/mercurial'): | ||||
version = proto[22:] | ||||
if float(version) > 0.1: | ||||
|
r1402 | raise hg.RepoError(_("'%s' uses newer protocol %s") % | ||
|
r1251 | (self.url, version)) | ||
|
r1089 | |||
return resp | ||||
|
r2435 | def do_read(self, cmd, **args): | ||
fp = self.do_cmd(cmd, **args) | ||||
try: | ||||
return fp.read() | ||||
finally: | ||||
# if using keepalive, allow connection to be reused | ||||
fp.close() | ||||
|
r1089 | def heads(self): | ||
|
r2435 | d = self.do_read("heads") | ||
|
r1089 | try: | ||
return map(bin, d[:-1].split(" ")) | ||||
except: | ||||
|
r1402 | self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") | ||
|
r1089 | raise | ||
def branches(self, nodes): | ||||
n = " ".join(map(hex, nodes)) | ||||
|
r2435 | d = self.do_read("branches", nodes=n) | ||
|
r1089 | try: | ||
br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ] | ||||
return br | ||||
except: | ||||
|
r1402 | self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") | ||
|
r1089 | raise | ||
def between(self, pairs): | ||||
n = "\n".join(["-".join(map(hex, p)) for p in pairs]) | ||||
|
r2435 | d = self.do_read("between", pairs=n) | ||
|
r1089 | try: | ||
p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ] | ||||
return p | ||||
except: | ||||
|
r1402 | self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") | ||
|
r1089 | raise | ||
|
r1736 | def changegroup(self, nodes, kind): | ||
|
r1089 | n = " ".join(map(hex, nodes)) | ||
f = self.do_cmd("changegroup", roots=n) | ||||
bytes = 0 | ||||
|
r1376 | def zgenerator(f): | ||
zd = zlib.decompressobj() | ||||
|
r2015 | try: | ||
for chnk in f: | ||||
yield zd.decompress(chnk) | ||||
except httplib.HTTPException, inst: | ||||
raise IOError(None, _('connection ended unexpectedly')) | ||||
|
r1376 | yield zd.flush() | ||
|
r1089 | |||
|
r1376 | return util.chunkbuffer(zgenerator(util.filechunkiter(f))) | ||
|
r1089 | |||
|
r2439 | def unbundle(self, cg, heads, source): | ||
|
r2465 | # have to stream bundle to a temp file because we do not have | ||
# http 1.1 chunked transfer. | ||||
fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') | ||||
fp = os.fdopen(fd, 'wb+') | ||||
try: | ||||
for chunk in util.filechunkiter(cg): | ||||
fp.write(chunk) | ||||
length = fp.tell() | ||||
try: | ||||
|
r2467 | rfp = self.do_cmd( | ||
'unbundle', data=fp, | ||||
headers={'content-length': length, | ||||
'content-type': 'application/octet-stream'}, | ||||
heads=' '.join(map(hex, heads))) | ||||
try: | ||||
ret = int(rfp.readline()) | ||||
self.ui.write(rfp.read()) | ||||
return ret | ||||
finally: | ||||
rfp.close() | ||||
except socket.error, err: | ||||
if err[0] in (errno.ECONNRESET, errno.EPIPE): | ||||
raise util.Abort(_('push failed: %s'), err[1]) | ||||
raise util.Abort(err[1]) | ||||
|
r2465 | finally: | ||
fp.close() | ||||
os.unlink(tempname) | ||||
|
r2439 | |||
|
r1089 | class httpsrepository(httprepository): | ||
|
r2569 | def __init__(self, ui, path): | ||
if not has_https: | ||||
raise util.Abort(_('Python support for SSL and HTTPS ' | ||||
'is not installed')) | ||||
httprepository.__init__(self, ui, path) | ||||