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