mercurial/httprepo.py
changeset 7270 2db33c1a5654
parent 7269 95a53961d7a6
child 7279 1f0f84660dea
equal deleted inserted replaced
7269:95a53961d7a6 7270:2db33c1a5654
     8 
     8 
     9 from node import bin, hex, nullid
     9 from node import bin, hex, nullid
    10 from i18n import _
    10 from i18n import _
    11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
    11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
    12 import errno, keepalive, socket, changegroup, statichttprepo
    12 import errno, keepalive, socket, changegroup, statichttprepo
    13 
    13 import url
    14 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
       
    15     def __init__(self, ui):
       
    16         urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
       
    17         self.ui = ui
       
    18 
       
    19     def find_user_password(self, realm, authuri):
       
    20         authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
       
    21             self, realm, authuri)
       
    22         user, passwd = authinfo
       
    23         if user and passwd:
       
    24             return (user, passwd)
       
    25 
       
    26         if not self.ui.interactive:
       
    27             raise util.Abort(_('http authorization required'))
       
    28 
       
    29         self.ui.write(_("http authorization required\n"))
       
    30         self.ui.status(_("realm: %s\n") % realm)
       
    31         if user:
       
    32             self.ui.status(_("user: %s\n") % user)
       
    33         else:
       
    34             user = self.ui.prompt(_("user:"), default=None)
       
    35 
       
    36         if not passwd:
       
    37             passwd = self.ui.getpass()
       
    38 
       
    39         self.add_password(realm, authuri, user, passwd)
       
    40         return (user, passwd)
       
    41 
       
    42 class proxyhandler(urllib2.ProxyHandler):
       
    43     def __init__(self, ui):
       
    44         proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
       
    45         # XXX proxyauthinfo = None
       
    46 
       
    47         if proxyurl:
       
    48             # proxy can be proper url or host[:port]
       
    49             if not (proxyurl.startswith('http:') or
       
    50                     proxyurl.startswith('https:')):
       
    51                 proxyurl = 'http://' + proxyurl + '/'
       
    52             snpqf = urlparse.urlsplit(proxyurl)
       
    53             proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
       
    54             hpup = netlocsplit(proxynetloc)
       
    55 
       
    56             proxyhost, proxyport, proxyuser, proxypasswd = hpup
       
    57             if not proxyuser:
       
    58                 proxyuser = ui.config("http_proxy", "user")
       
    59                 proxypasswd = ui.config("http_proxy", "passwd")
       
    60 
       
    61             # see if we should use a proxy for this url
       
    62             no_list = [ "localhost", "127.0.0.1" ]
       
    63             no_list.extend([p.lower() for
       
    64                             p in ui.configlist("http_proxy", "no")])
       
    65             no_list.extend([p.strip().lower() for
       
    66                             p in os.getenv("no_proxy", '').split(',')
       
    67                             if p.strip()])
       
    68             # "http_proxy.always" config is for running tests on localhost
       
    69             if ui.configbool("http_proxy", "always"):
       
    70                 self.no_list = []
       
    71             else:
       
    72                 self.no_list = no_list
       
    73 
       
    74             proxyurl = urlparse.urlunsplit((
       
    75                 proxyscheme, netlocunsplit(proxyhost, proxyport,
       
    76                                            proxyuser, proxypasswd or ''),
       
    77                 proxypath, proxyquery, proxyfrag))
       
    78             proxies = {'http': proxyurl, 'https': proxyurl}
       
    79             ui.debug(_('proxying through http://%s:%s\n') %
       
    80                       (proxyhost, proxyport))
       
    81         else:
       
    82             proxies = {}
       
    83 
       
    84         # urllib2 takes proxy values from the environment and those
       
    85         # will take precedence if found, so drop them
       
    86         for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
       
    87             try:
       
    88                 if env in os.environ:
       
    89                     del os.environ[env]
       
    90             except OSError:
       
    91                 pass
       
    92 
       
    93         urllib2.ProxyHandler.__init__(self, proxies)
       
    94         self.ui = ui
       
    95 
       
    96     def proxy_open(self, req, proxy, type):
       
    97         host = req.get_host().split(':')[0]
       
    98         if host in self.no_list:
       
    99             return None
       
   100         return urllib2.ProxyHandler.proxy_open(self, req, proxy, type)
       
   101 
       
   102 def netlocsplit(netloc):
       
   103     '''split [user[:passwd]@]host[:port] into 4-tuple.'''
       
   104 
       
   105     a = netloc.find('@')
       
   106     if a == -1:
       
   107         user, passwd = None, None
       
   108     else:
       
   109         userpass, netloc = netloc[:a], netloc[a+1:]
       
   110         c = userpass.find(':')
       
   111         if c == -1:
       
   112             user, passwd = urllib.unquote(userpass), None
       
   113         else:
       
   114             user = urllib.unquote(userpass[:c])
       
   115             passwd = urllib.unquote(userpass[c+1:])
       
   116     c = netloc.find(':')
       
   117     if c == -1:
       
   118         host, port = netloc, None
       
   119     else:
       
   120         host, port = netloc[:c], netloc[c+1:]
       
   121     return host, port, user, passwd
       
   122 
       
   123 def netlocunsplit(host, port, user=None, passwd=None):
       
   124     '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
       
   125     if port:
       
   126         hostport = host + ':' + port
       
   127     else:
       
   128         hostport = host
       
   129     if user:
       
   130         if passwd:
       
   131             userpass = urllib.quote(user) + ':' + urllib.quote(passwd)
       
   132         else:
       
   133             userpass = urllib.quote(user)
       
   134         return userpass + '@' + hostport
       
   135     return hostport
       
   136 
       
   137 # work around a bug in Python < 2.4.2
       
   138 # (it leaves a "\n" at the end of Proxy-authorization headers)
       
   139 class request(urllib2.Request):
       
   140     def add_header(self, key, val):
       
   141         if key.lower() == 'proxy-authorization':
       
   142             val = val.strip()
       
   143         return urllib2.Request.add_header(self, key, val)
       
   144 
       
   145 class httpsendfile(file):
       
   146     def __len__(self):
       
   147         return os.fstat(self.fileno()).st_size
       
   148 
       
   149 def _gen_sendfile(connection):
       
   150     def _sendfile(self, data):
       
   151         # send a file
       
   152         if isinstance(data, httpsendfile):
       
   153             # if auth required, some data sent twice, so rewind here
       
   154             data.seek(0)
       
   155             for chunk in util.filechunkiter(data):
       
   156                 connection.send(self, chunk)
       
   157         else:
       
   158             connection.send(self, data)
       
   159     return _sendfile
       
   160 
       
   161 class httpconnection(keepalive.HTTPConnection):
       
   162     # must be able to send big bundle as stream.
       
   163     send = _gen_sendfile(keepalive.HTTPConnection)
       
   164 
       
   165 class httphandler(keepalive.HTTPHandler):
       
   166     def http_open(self, req):
       
   167         return self.do_open(httpconnection, req)
       
   168 
       
   169     def __del__(self):
       
   170         self.close_all()
       
   171 
       
   172 has_https = hasattr(urllib2, 'HTTPSHandler')
       
   173 if has_https:
       
   174     class httpsconnection(httplib.HTTPSConnection):
       
   175         response_class = keepalive.HTTPResponse
       
   176         # must be able to send big bundle as stream.
       
   177         send = _gen_sendfile(httplib.HTTPSConnection)
       
   178 
       
   179     class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
       
   180         def https_open(self, req):
       
   181             return self.do_open(httpsconnection, req)
       
   182 
       
   183 # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if
       
   184 # it doesn't know about the auth type requested.  This can happen if
       
   185 # somebody is using BasicAuth and types a bad password.
       
   186 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
       
   187     def http_error_auth_reqed(self, auth_header, host, req, headers):
       
   188         try:
       
   189             return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
       
   190                         self, auth_header, host, req, headers)
       
   191         except ValueError, inst:
       
   192             arg = inst.args[0]
       
   193             if arg.startswith("AbstractDigestAuthHandler doesn't know "):
       
   194                 return
       
   195             raise
       
   196 
    14 
   197 def zgenerator(f):
    15 def zgenerator(f):
   198     zd = zlib.decompressobj()
    16     zd = zlib.decompressobj()
   199     try:
    17     try:
   200         for chunk in util.filechunkiter(f):
    18         for chunk in util.filechunkiter(f):
   201             yield zd.decompress(chunk)
    19             yield zd.decompress(chunk)
   202     except httplib.HTTPException, inst:
    20     except httplib.HTTPException, inst:
   203         raise IOError(None, _('connection ended unexpectedly'))
    21         raise IOError(None, _('connection ended unexpectedly'))
   204     yield zd.flush()
    22     yield zd.flush()
   205 
       
   206 _safe = ('abcdefghijklmnopqrstuvwxyz'
       
   207          'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
       
   208          '0123456789' '_.-/')
       
   209 _safeset = None
       
   210 _hex = None
       
   211 def quotepath(path):
       
   212     '''quote the path part of a URL
       
   213 
       
   214     This is similar to urllib.quote, but it also tries to avoid
       
   215     quoting things twice (inspired by wget):
       
   216 
       
   217     >>> quotepath('abc def')
       
   218     'abc%20def'
       
   219     >>> quotepath('abc%20def')
       
   220     'abc%20def'
       
   221     >>> quotepath('abc%20 def')
       
   222     'abc%20%20def'
       
   223     >>> quotepath('abc def%20')
       
   224     'abc%20def%20'
       
   225     >>> quotepath('abc def%2')
       
   226     'abc%20def%252'
       
   227     >>> quotepath('abc def%')
       
   228     'abc%20def%25'
       
   229     '''
       
   230     global _safeset, _hex
       
   231     if _safeset is None:
       
   232         _safeset = util.set(_safe)
       
   233         _hex = util.set('abcdefABCDEF0123456789')
       
   234     l = list(path)
       
   235     for i in xrange(len(l)):
       
   236         c = l[i]
       
   237         if c == '%' and i + 2 < len(l) and (l[i+1] in _hex and l[i+2] in _hex):
       
   238             pass
       
   239         elif c not in _safeset:
       
   240             l[i] = '%%%02X' % ord(c)
       
   241     return ''.join(l)
       
   242 
    23 
   243 class httprepository(repo.repository):
    24 class httprepository(repo.repository):
   244     def __init__(self, ui, path):
    25     def __init__(self, ui, path):
   245         self.path = path
    26         self.path = path
   246         self.caps = None
    27         self.caps = None
   247         self.handler = None
    28         self.handler = None
   248         scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
    29         scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
   249         if query or frag:
    30         if query or frag:
   250             raise util.Abort(_('unsupported URL component: "%s"') %
    31             raise util.Abort(_('unsupported URL component: "%s"') %
   251                              (query or frag))
    32                              (query or frag))
   252         if not urlpath:
       
   253             urlpath = '/'
       
   254         urlpath = quotepath(urlpath)
       
   255         host, port, user, passwd = netlocsplit(netloc)
       
   256 
    33 
   257         # urllib cannot handle URLs with embedded user or passwd
    34         # urllib cannot handle URLs with embedded user or passwd
   258         self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
    35         self._url, authinfo = url.getauthinfo(path)
   259                                          urlpath, '', ''))
    36 
   260         self.ui = ui
    37         self.ui = ui
   261         self.ui.debug(_('using %s\n') % self._url)
    38         self.ui.debug(_('using %s\n') % self._url)
   262 
    39 
   263         handlers = [httphandler()]
    40         self.urlopener = url.opener(ui, authinfo)
   264         if has_https:
       
   265             handlers.append(httpshandler())
       
   266 
       
   267         handlers.append(proxyhandler(ui))
       
   268 
       
   269         passmgr = passwordmgr(ui)
       
   270         if user:
       
   271             ui.debug(_('http auth: user %s, password %s\n') %
       
   272                      (user, passwd and '*' * len(passwd) or 'not set'))
       
   273             netloc = host
       
   274             if port:
       
   275                 netloc += ':' + port
       
   276             # Python < 2.4.3 uses only the netloc to search for a password
       
   277             passmgr.add_password(None, (self._url, netloc), user, passwd or '')
       
   278 
       
   279         handlers.extend((urllib2.HTTPBasicAuthHandler(passmgr),
       
   280                          httpdigestauthhandler(passmgr)))
       
   281         opener = urllib2.build_opener(*handlers)
       
   282 
       
   283         # 1.0 here is the _protocol_ version
       
   284         opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
       
   285         opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
       
   286         urllib2.install_opener(opener)
       
   287 
    41 
   288     def url(self):
    42     def url(self):
   289         return self.path
    43         return self.path
   290 
    44 
   291     # look up capabilities only when needed
    45     # look up capabilities only when needed
   314         qs = '?%s' % urllib.urlencode(q)
    68         qs = '?%s' % urllib.urlencode(q)
   315         cu = "%s%s" % (self._url, qs)
    69         cu = "%s%s" % (self._url, qs)
   316         try:
    70         try:
   317             if data:
    71             if data:
   318                 self.ui.debug(_("sending %s bytes\n") % len(data))
    72                 self.ui.debug(_("sending %s bytes\n") % len(data))
   319             resp = urllib2.urlopen(request(cu, data, headers))
    73             resp = self.urlopener.open(urllib2.Request(cu, data, headers))
   320         except urllib2.HTTPError, inst:
    74         except urllib2.HTTPError, inst:
   321             if inst.code == 401:
    75             if inst.code == 401:
   322                 raise util.Abort(_('authorization failed'))
    76                 raise util.Abort(_('authorization failed'))
   323             raise
    77             raise
   324         except httplib.HTTPException, inst:
    78         except httplib.HTTPException, inst:
   431                 if x in changegroup.bundletypes:
   185                 if x in changegroup.bundletypes:
   432                     type = x
   186                     type = x
   433                     break
   187                     break
   434 
   188 
   435         tempname = changegroup.writebundle(cg, None, type)
   189         tempname = changegroup.writebundle(cg, None, type)
   436         fp = httpsendfile(tempname, "rb")
   190         fp = url.httpsendfile(tempname, "rb")
   437         try:
   191         try:
   438             try:
   192             try:
   439                 resp = self.do_read(
   193                 resp = self.do_read(
   440                      'unbundle', data=fp,
   194                      'unbundle', data=fp,
   441                      headers={'Content-Type': 'application/octet-stream'},
   195                      headers={'Content-Type': 'application/octet-stream'},