##// END OF EJS Templates
httppeer: improve protocol check...
Matt Mackall -
r18737:56f8522c default
parent child Browse files
Show More
@@ -1,248 +1,249 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from node import nullid
9 from node import nullid
10 from i18n import _
10 from i18n import _
11 import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
11 import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
12 import os, urllib, urllib2, zlib, httplib
12 import os, urllib, urllib2, zlib, httplib
13 import errno, socket
13 import errno, socket
14
14
15 def zgenerator(f):
15 def zgenerator(f):
16 zd = zlib.decompressobj()
16 zd = zlib.decompressobj()
17 try:
17 try:
18 for chunk in util.filechunkiter(f):
18 for chunk in util.filechunkiter(f):
19 while chunk:
19 while chunk:
20 yield zd.decompress(chunk, 2**18)
20 yield zd.decompress(chunk, 2**18)
21 chunk = zd.unconsumed_tail
21 chunk = zd.unconsumed_tail
22 except httplib.HTTPException:
22 except httplib.HTTPException:
23 raise IOError(None, _('connection ended unexpectedly'))
23 raise IOError(None, _('connection ended unexpectedly'))
24 yield zd.flush()
24 yield zd.flush()
25
25
26 class httppeer(wireproto.wirepeer):
26 class httppeer(wireproto.wirepeer):
27 def __init__(self, ui, path):
27 def __init__(self, ui, path):
28 self.path = path
28 self.path = path
29 self.caps = None
29 self.caps = None
30 self.handler = None
30 self.handler = None
31 self.urlopener = None
31 self.urlopener = None
32 u = util.url(path)
32 u = util.url(path)
33 if u.query or u.fragment:
33 if u.query or u.fragment:
34 raise util.Abort(_('unsupported URL component: "%s"') %
34 raise util.Abort(_('unsupported URL component: "%s"') %
35 (u.query or u.fragment))
35 (u.query or u.fragment))
36
36
37 # urllib cannot handle URLs with embedded user or passwd
37 # urllib cannot handle URLs with embedded user or passwd
38 self._url, authinfo = u.authinfo()
38 self._url, authinfo = u.authinfo()
39
39
40 self.ui = ui
40 self.ui = ui
41 self.ui.debug('using %s\n' % self._url)
41 self.ui.debug('using %s\n' % self._url)
42
42
43 self.urlopener = url.opener(ui, authinfo)
43 self.urlopener = url.opener(ui, authinfo)
44
44
45 def __del__(self):
45 def __del__(self):
46 if self.urlopener:
46 if self.urlopener:
47 for h in self.urlopener.handlers:
47 for h in self.urlopener.handlers:
48 h.close()
48 h.close()
49 getattr(h, "close_all", lambda : None)()
49 getattr(h, "close_all", lambda : None)()
50
50
51 def url(self):
51 def url(self):
52 return self.path
52 return self.path
53
53
54 # look up capabilities only when needed
54 # look up capabilities only when needed
55
55
56 def _fetchcaps(self):
56 def _fetchcaps(self):
57 self.caps = set(self._call('capabilities').split())
57 self.caps = set(self._call('capabilities').split())
58
58
59 def _capabilities(self):
59 def _capabilities(self):
60 if self.caps is None:
60 if self.caps is None:
61 try:
61 try:
62 self._fetchcaps()
62 self._fetchcaps()
63 except error.RepoError:
63 except error.RepoError:
64 self.caps = set()
64 self.caps = set()
65 self.ui.debug('capabilities: %s\n' %
65 self.ui.debug('capabilities: %s\n' %
66 (' '.join(self.caps or ['none'])))
66 (' '.join(self.caps or ['none'])))
67 return self.caps
67 return self.caps
68
68
69 def lock(self):
69 def lock(self):
70 raise util.Abort(_('operation not supported over http'))
70 raise util.Abort(_('operation not supported over http'))
71
71
72 def _callstream(self, cmd, **args):
72 def _callstream(self, cmd, **args):
73 if cmd == 'pushkey':
73 if cmd == 'pushkey':
74 args['data'] = ''
74 args['data'] = ''
75 data = args.pop('data', None)
75 data = args.pop('data', None)
76 size = 0
76 size = 0
77 if util.safehasattr(data, 'length'):
77 if util.safehasattr(data, 'length'):
78 size = data.length
78 size = data.length
79 elif data is not None:
79 elif data is not None:
80 size = len(data)
80 size = len(data)
81 headers = args.pop('headers', {})
81 headers = args.pop('headers', {})
82 if data is not None and 'Content-Type' not in headers:
82 if data is not None and 'Content-Type' not in headers:
83 headers['Content-Type'] = 'application/mercurial-0.1'
83 headers['Content-Type'] = 'application/mercurial-0.1'
84
84
85
85
86 if size and self.ui.configbool('ui', 'usehttp2', False):
86 if size and self.ui.configbool('ui', 'usehttp2', False):
87 headers['Expect'] = '100-Continue'
87 headers['Expect'] = '100-Continue'
88 headers['X-HgHttp2'] = '1'
88 headers['X-HgHttp2'] = '1'
89
89
90 self.ui.debug("sending %s command\n" % cmd)
90 self.ui.debug("sending %s command\n" % cmd)
91 q = [('cmd', cmd)]
91 q = [('cmd', cmd)]
92 headersize = 0
92 headersize = 0
93 if len(args) > 0:
93 if len(args) > 0:
94 httpheader = self.capable('httpheader')
94 httpheader = self.capable('httpheader')
95 if httpheader:
95 if httpheader:
96 headersize = int(httpheader.split(',')[0])
96 headersize = int(httpheader.split(',')[0])
97 if headersize > 0:
97 if headersize > 0:
98 # The headers can typically carry more data than the URL.
98 # The headers can typically carry more data than the URL.
99 encargs = urllib.urlencode(sorted(args.items()))
99 encargs = urllib.urlencode(sorted(args.items()))
100 headerfmt = 'X-HgArg-%s'
100 headerfmt = 'X-HgArg-%s'
101 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
101 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
102 headernum = 0
102 headernum = 0
103 for i in xrange(0, len(encargs), contentlen):
103 for i in xrange(0, len(encargs), contentlen):
104 headernum += 1
104 headernum += 1
105 header = headerfmt % str(headernum)
105 header = headerfmt % str(headernum)
106 headers[header] = encargs[i:i + contentlen]
106 headers[header] = encargs[i:i + contentlen]
107 varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
107 varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
108 headers['Vary'] = ','.join(varyheaders)
108 headers['Vary'] = ','.join(varyheaders)
109 else:
109 else:
110 q += sorted(args.items())
110 q += sorted(args.items())
111 qs = '?%s' % urllib.urlencode(q)
111 qs = '?%s' % urllib.urlencode(q)
112 cu = "%s%s" % (self._url, qs)
112 cu = "%s%s" % (self._url, qs)
113 req = urllib2.Request(cu, data, headers)
113 req = urllib2.Request(cu, data, headers)
114 if data is not None:
114 if data is not None:
115 self.ui.debug("sending %s bytes\n" % size)
115 self.ui.debug("sending %s bytes\n" % size)
116 req.add_unredirected_header('Content-Length', '%d' % size)
116 req.add_unredirected_header('Content-Length', '%d' % size)
117 try:
117 try:
118 resp = self.urlopener.open(req)
118 resp = self.urlopener.open(req)
119 except urllib2.HTTPError, inst:
119 except urllib2.HTTPError, inst:
120 if inst.code == 401:
120 if inst.code == 401:
121 raise util.Abort(_('authorization failed'))
121 raise util.Abort(_('authorization failed'))
122 raise
122 raise
123 except httplib.HTTPException, inst:
123 except httplib.HTTPException, inst:
124 self.ui.debug('http error while sending %s command\n' % cmd)
124 self.ui.debug('http error while sending %s command\n' % cmd)
125 self.ui.traceback()
125 self.ui.traceback()
126 raise IOError(None, inst)
126 raise IOError(None, inst)
127 except IndexError:
127 except IndexError:
128 # this only happens with Python 2.3, later versions raise URLError
128 # this only happens with Python 2.3, later versions raise URLError
129 raise util.Abort(_('http error, possibly caused by proxy setting'))
129 raise util.Abort(_('http error, possibly caused by proxy setting'))
130 # record the url we got redirected to
130 # record the url we got redirected to
131 resp_url = resp.geturl()
131 resp_url = resp.geturl()
132 if resp_url.endswith(qs):
132 if resp_url.endswith(qs):
133 resp_url = resp_url[:-len(qs)]
133 resp_url = resp_url[:-len(qs)]
134 if self._url.rstrip('/') != resp_url.rstrip('/'):
134 if self._url.rstrip('/') != resp_url.rstrip('/'):
135 if not self.ui.quiet:
135 if not self.ui.quiet:
136 self.ui.warn(_('real URL is %s\n') % resp_url)
136 self.ui.warn(_('real URL is %s\n') % resp_url)
137 self._url = resp_url
137 self._url = resp_url
138 try:
138 try:
139 proto = resp.getheader('content-type')
139 proto = resp.getheader('content-type')
140 except AttributeError:
140 except AttributeError:
141 proto = resp.headers.get('content-type', '')
141 proto = resp.headers.get('content-type', '')
142
142
143 safeurl = util.hidepassword(self._url)
143 safeurl = util.hidepassword(self._url)
144 if proto.startswith('application/hg-error'):
144 if proto.startswith('application/hg-error'):
145 raise error.OutOfBandError(resp.read())
145 raise error.OutOfBandError(resp.read())
146 # accept old "text/plain" and "application/hg-changegroup" for now
146 # accept old "text/plain" and "application/hg-changegroup" for now
147 if not (proto.startswith('application/mercurial-') or
147 if not (proto.startswith('application/mercurial-') or
148 proto.startswith('text/plain') or
148 (proto.startswith('text/plain')
149 and not resp.headers.get('content-length')) or
149 proto.startswith('application/hg-changegroup')):
150 proto.startswith('application/hg-changegroup')):
150 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
151 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
151 raise error.RepoError(
152 raise error.RepoError(
152 _("'%s' does not appear to be an hg repository:\n"
153 _("'%s' does not appear to be an hg repository:\n"
153 "---%%<--- (%s)\n%s\n---%%<---\n")
154 "---%%<--- (%s)\n%s\n---%%<---\n")
154 % (safeurl, proto or 'no content-type', resp.read()))
155 % (safeurl, proto or 'no content-type', resp.read()))
155
156
156 if proto.startswith('application/mercurial-'):
157 if proto.startswith('application/mercurial-'):
157 try:
158 try:
158 version = proto.split('-', 1)[1]
159 version = proto.split('-', 1)[1]
159 version_info = tuple([int(n) for n in version.split('.')])
160 version_info = tuple([int(n) for n in version.split('.')])
160 except ValueError:
161 except ValueError:
161 raise error.RepoError(_("'%s' sent a broken Content-Type "
162 raise error.RepoError(_("'%s' sent a broken Content-Type "
162 "header (%s)") % (safeurl, proto))
163 "header (%s)") % (safeurl, proto))
163 if version_info > (0, 1):
164 if version_info > (0, 1):
164 raise error.RepoError(_("'%s' uses newer protocol %s") %
165 raise error.RepoError(_("'%s' uses newer protocol %s") %
165 (safeurl, version))
166 (safeurl, version))
166
167
167 return resp
168 return resp
168
169
169 def _call(self, cmd, **args):
170 def _call(self, cmd, **args):
170 fp = self._callstream(cmd, **args)
171 fp = self._callstream(cmd, **args)
171 try:
172 try:
172 return fp.read()
173 return fp.read()
173 finally:
174 finally:
174 # if using keepalive, allow connection to be reused
175 # if using keepalive, allow connection to be reused
175 fp.close()
176 fp.close()
176
177
177 def _callpush(self, cmd, cg, **args):
178 def _callpush(self, cmd, cg, **args):
178 # have to stream bundle to a temp file because we do not have
179 # have to stream bundle to a temp file because we do not have
179 # http 1.1 chunked transfer.
180 # http 1.1 chunked transfer.
180
181
181 types = self.capable('unbundle')
182 types = self.capable('unbundle')
182 try:
183 try:
183 types = types.split(',')
184 types = types.split(',')
184 except AttributeError:
185 except AttributeError:
185 # servers older than d1b16a746db6 will send 'unbundle' as a
186 # servers older than d1b16a746db6 will send 'unbundle' as a
186 # boolean capability. They only support headerless/uncompressed
187 # boolean capability. They only support headerless/uncompressed
187 # bundles.
188 # bundles.
188 types = [""]
189 types = [""]
189 for x in types:
190 for x in types:
190 if x in changegroup.bundletypes:
191 if x in changegroup.bundletypes:
191 type = x
192 type = x
192 break
193 break
193
194
194 tempname = changegroup.writebundle(cg, None, type)
195 tempname = changegroup.writebundle(cg, None, type)
195 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
196 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
196 headers = {'Content-Type': 'application/mercurial-0.1'}
197 headers = {'Content-Type': 'application/mercurial-0.1'}
197
198
198 try:
199 try:
199 try:
200 try:
200 r = self._call(cmd, data=fp, headers=headers, **args)
201 r = self._call(cmd, data=fp, headers=headers, **args)
201 vals = r.split('\n', 1)
202 vals = r.split('\n', 1)
202 if len(vals) < 2:
203 if len(vals) < 2:
203 raise error.ResponseError(_("unexpected response:"), r)
204 raise error.ResponseError(_("unexpected response:"), r)
204 return vals
205 return vals
205 except socket.error, err:
206 except socket.error, err:
206 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
207 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
207 raise util.Abort(_('push failed: %s') % err.args[1])
208 raise util.Abort(_('push failed: %s') % err.args[1])
208 raise util.Abort(err.args[1])
209 raise util.Abort(err.args[1])
209 finally:
210 finally:
210 fp.close()
211 fp.close()
211 os.unlink(tempname)
212 os.unlink(tempname)
212
213
213 def _abort(self, exception):
214 def _abort(self, exception):
214 raise exception
215 raise exception
215
216
216 def _decompress(self, stream):
217 def _decompress(self, stream):
217 return util.chunkbuffer(zgenerator(stream))
218 return util.chunkbuffer(zgenerator(stream))
218
219
219 class httpspeer(httppeer):
220 class httpspeer(httppeer):
220 def __init__(self, ui, path):
221 def __init__(self, ui, path):
221 if not url.has_https:
222 if not url.has_https:
222 raise util.Abort(_('Python support for SSL and HTTPS '
223 raise util.Abort(_('Python support for SSL and HTTPS '
223 'is not installed'))
224 'is not installed'))
224 httppeer.__init__(self, ui, path)
225 httppeer.__init__(self, ui, path)
225
226
226 def instance(ui, path, create):
227 def instance(ui, path, create):
227 if create:
228 if create:
228 raise util.Abort(_('cannot create new http repository'))
229 raise util.Abort(_('cannot create new http repository'))
229 try:
230 try:
230 if path.startswith('https:'):
231 if path.startswith('https:'):
231 inst = httpspeer(ui, path)
232 inst = httpspeer(ui, path)
232 else:
233 else:
233 inst = httppeer(ui, path)
234 inst = httppeer(ui, path)
234 try:
235 try:
235 # Try to do useful work when checking compatibility.
236 # Try to do useful work when checking compatibility.
236 # Usually saves a roundtrip since we want the caps anyway.
237 # Usually saves a roundtrip since we want the caps anyway.
237 inst._fetchcaps()
238 inst._fetchcaps()
238 except error.RepoError:
239 except error.RepoError:
239 # No luck, try older compatibility check.
240 # No luck, try older compatibility check.
240 inst.between([(nullid, nullid)])
241 inst.between([(nullid, nullid)])
241 return inst
242 return inst
242 except error.RepoError, httpexception:
243 except error.RepoError, httpexception:
243 try:
244 try:
244 r = statichttprepo.instance(ui, "static-" + path, create)
245 r = statichttprepo.instance(ui, "static-" + path, create)
245 ui.note('(falling back to static-http)\n')
246 ui.note('(falling back to static-http)\n')
246 return r
247 return r
247 except error.RepoError:
248 except error.RepoError:
248 raise httpexception # use the original http RepoError instead
249 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now