##// END OF EJS Templates
httppeer: move a variable to avoid a bad reference before assignment warning...
Matt Harbison -
r52632:78f789a4 default
parent child Browse files
Show More
@@ -1,668 +1,669
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005, 2006 Olivia Mackall <olivia@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
9
10 import errno
10 import errno
11 import io
11 import io
12 import os
12 import os
13 import socket
13 import socket
14 import struct
14 import struct
15
15
16 from concurrent import futures
16 from concurrent import futures
17 from .i18n import _
17 from .i18n import _
18 from . import (
18 from . import (
19 bundle2,
19 bundle2,
20 error,
20 error,
21 httpconnection,
21 httpconnection,
22 pycompat,
22 pycompat,
23 statichttprepo,
23 statichttprepo,
24 url as urlmod,
24 url as urlmod,
25 util,
25 util,
26 wireprotov1peer,
26 wireprotov1peer,
27 )
27 )
28 from .utils import urlutil
28 from .utils import urlutil
29
29
30 httplib = util.httplib
30 httplib = util.httplib
31 urlerr = util.urlerr
31 urlerr = util.urlerr
32 urlreq = util.urlreq
32 urlreq = util.urlreq
33
33
34
34
35 def encodevalueinheaders(value, header, limit):
35 def encodevalueinheaders(value, header, limit):
36 """Encode a string value into multiple HTTP headers.
36 """Encode a string value into multiple HTTP headers.
37
37
38 ``value`` will be encoded into 1 or more HTTP headers with the names
38 ``value`` will be encoded into 1 or more HTTP headers with the names
39 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
40 name + value will be at most ``limit`` bytes long.
40 name + value will be at most ``limit`` bytes long.
41
41
42 Returns an iterable of 2-tuples consisting of header names and
42 Returns an iterable of 2-tuples consisting of header names and
43 values as native strings.
43 values as native strings.
44 """
44 """
45 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
45 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
46 # not bytes. This function always takes bytes in as arguments.
46 # not bytes. This function always takes bytes in as arguments.
47 fmt = pycompat.strurl(header) + r'-%s'
47 fmt = pycompat.strurl(header) + r'-%s'
48 # Note: it is *NOT* a bug that the last bit here is a bytestring
48 # Note: it is *NOT* a bug that the last bit here is a bytestring
49 # and not a unicode: we're just getting the encoded length anyway,
49 # and not a unicode: we're just getting the encoded length anyway,
50 # and using an r-string to make it portable between Python 2 and 3
50 # and using an r-string to make it portable between Python 2 and 3
51 # doesn't work because then the \r is a literal backslash-r
51 # doesn't work because then the \r is a literal backslash-r
52 # instead of a carriage return.
52 # instead of a carriage return.
53 valuelen = limit - len(fmt % '000') - len(b': \r\n')
53 valuelen = limit - len(fmt % '000') - len(b': \r\n')
54 result = []
54 result = []
55
55
56 n = 0
56 n = 0
57 for i in range(0, len(value), valuelen):
57 for i in range(0, len(value), valuelen):
58 n += 1
58 n += 1
59 result.append((fmt % str(n), pycompat.strurl(value[i : i + valuelen])))
59 result.append((fmt % str(n), pycompat.strurl(value[i : i + valuelen])))
60
60
61 return result
61 return result
62
62
63
63
64 class _multifile:
64 class _multifile:
65 def __init__(self, *fileobjs):
65 def __init__(self, *fileobjs):
66 for f in fileobjs:
66 for f in fileobjs:
67 if not hasattr(f, 'length'):
67 if not hasattr(f, 'length'):
68 raise ValueError(
68 raise ValueError(
69 b'_multifile only supports file objects that '
69 b'_multifile only supports file objects that '
70 b'have a length but this one does not:',
70 b'have a length but this one does not:',
71 type(f),
71 type(f),
72 f,
72 f,
73 )
73 )
74 self._fileobjs = fileobjs
74 self._fileobjs = fileobjs
75 self._index = 0
75 self._index = 0
76
76
77 @property
77 @property
78 def length(self):
78 def length(self):
79 return sum(f.length for f in self._fileobjs)
79 return sum(f.length for f in self._fileobjs)
80
80
81 def read(self, amt=None):
81 def read(self, amt=None):
82 if amt <= 0:
82 if amt <= 0:
83 return b''.join(f.read() for f in self._fileobjs)
83 return b''.join(f.read() for f in self._fileobjs)
84 parts = []
84 parts = []
85 while amt and self._index < len(self._fileobjs):
85 while amt and self._index < len(self._fileobjs):
86 parts.append(self._fileobjs[self._index].read(amt))
86 parts.append(self._fileobjs[self._index].read(amt))
87 got = len(parts[-1])
87 got = len(parts[-1])
88 if got < amt:
88 if got < amt:
89 self._index += 1
89 self._index += 1
90 amt -= got
90 amt -= got
91 return b''.join(parts)
91 return b''.join(parts)
92
92
93 def seek(self, offset, whence=os.SEEK_SET):
93 def seek(self, offset, whence=os.SEEK_SET):
94 if whence != os.SEEK_SET:
94 if whence != os.SEEK_SET:
95 raise NotImplementedError(
95 raise NotImplementedError(
96 b'_multifile does not support anything other'
96 b'_multifile does not support anything other'
97 b' than os.SEEK_SET for whence on seek()'
97 b' than os.SEEK_SET for whence on seek()'
98 )
98 )
99 if offset != 0:
99 if offset != 0:
100 raise NotImplementedError(
100 raise NotImplementedError(
101 b'_multifile only supports seeking to start, but that '
101 b'_multifile only supports seeking to start, but that '
102 b'could be fixed if you need it'
102 b'could be fixed if you need it'
103 )
103 )
104 for f in self._fileobjs:
104 for f in self._fileobjs:
105 f.seek(0)
105 f.seek(0)
106 self._index = 0
106 self._index = 0
107
107
108
108
109 def makev1commandrequest(
109 def makev1commandrequest(
110 ui,
110 ui,
111 requestbuilder,
111 requestbuilder,
112 caps,
112 caps,
113 capablefn,
113 capablefn,
114 repobaseurl,
114 repobaseurl,
115 cmd,
115 cmd,
116 args,
116 args,
117 remotehidden=False,
117 remotehidden=False,
118 ):
118 ):
119 """Make an HTTP request to run a command for a version 1 client.
119 """Make an HTTP request to run a command for a version 1 client.
120
120
121 ``caps`` is a set of known server capabilities. The value may be
121 ``caps`` is a set of known server capabilities. The value may be
122 None if capabilities are not yet known.
122 None if capabilities are not yet known.
123
123
124 ``capablefn`` is a function to evaluate a capability.
124 ``capablefn`` is a function to evaluate a capability.
125
125
126 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
126 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
127 raw data to pass to it.
127 raw data to pass to it.
128 """
128 """
129 if cmd == b'pushkey':
129 if cmd == b'pushkey':
130 args[b'data'] = b''
130 args[b'data'] = b''
131 data = args.pop(b'data', None)
131 data = args.pop(b'data', None)
132 headers = args.pop(b'headers', {})
132 headers = args.pop(b'headers', {})
133
133
134 ui.debug(b"sending %s command\n" % cmd)
134 ui.debug(b"sending %s command\n" % cmd)
135 q = [(b'cmd', cmd)]
135 q = [(b'cmd', cmd)]
136 if remotehidden:
136 if remotehidden:
137 q.append(('access-hidden', '1'))
137 q.append(('access-hidden', '1'))
138 headersize = 0
138 headersize = 0
139 # Important: don't use self.capable() here or else you end up
139 # Important: don't use self.capable() here or else you end up
140 # with infinite recursion when trying to look up capabilities
140 # with infinite recursion when trying to look up capabilities
141 # for the first time.
141 # for the first time.
142 postargsok = caps is not None and b'httppostargs' in caps
142 postargsok = caps is not None and b'httppostargs' in caps
143
143
144 # Send arguments via POST.
144 # Send arguments via POST.
145 if postargsok and args:
145 if postargsok and args:
146 strargs = urlreq.urlencode(sorted(args.items()))
146 strargs = urlreq.urlencode(sorted(args.items()))
147 if not data:
147 if not data:
148 data = strargs
148 data = strargs
149 else:
149 else:
150 if isinstance(data, bytes):
150 if isinstance(data, bytes):
151 i = io.BytesIO(data)
151 i = io.BytesIO(data)
152 i.length = len(data)
152 i.length = len(data)
153 data = i
153 data = i
154 argsio = io.BytesIO(strargs)
154 argsio = io.BytesIO(strargs)
155 argsio.length = len(strargs)
155 argsio.length = len(strargs)
156 data = _multifile(argsio, data)
156 data = _multifile(argsio, data)
157 headers['X-HgArgs-Post'] = len(strargs)
157 headers['X-HgArgs-Post'] = len(strargs)
158 elif args:
158 elif args:
159 # Calling self.capable() can infinite loop if we are calling
159 # Calling self.capable() can infinite loop if we are calling
160 # "capabilities". But that command should never accept wire
160 # "capabilities". But that command should never accept wire
161 # protocol arguments. So this should never happen.
161 # protocol arguments. So this should never happen.
162 assert cmd != b'capabilities'
162 assert cmd != b'capabilities'
163 httpheader = capablefn(b'httpheader')
163 httpheader = capablefn(b'httpheader')
164 if httpheader:
164 if httpheader:
165 headersize = int(httpheader.split(b',', 1)[0])
165 headersize = int(httpheader.split(b',', 1)[0])
166
166
167 # Send arguments via HTTP headers.
167 # Send arguments via HTTP headers.
168 if headersize > 0:
168 if headersize > 0:
169 # The headers can typically carry more data than the URL.
169 # The headers can typically carry more data than the URL.
170 encoded_args = urlreq.urlencode(sorted(args.items()))
170 encoded_args = urlreq.urlencode(sorted(args.items()))
171 for header, value in encodevalueinheaders(
171 for header, value in encodevalueinheaders(
172 encoded_args, b'X-HgArg', headersize
172 encoded_args, b'X-HgArg', headersize
173 ):
173 ):
174 headers[header] = value
174 headers[header] = value
175 # Send arguments via query string (Mercurial <1.9).
175 # Send arguments via query string (Mercurial <1.9).
176 else:
176 else:
177 q += sorted(args.items())
177 q += sorted(args.items())
178
178
179 qs = b'?%s' % urlreq.urlencode(q)
179 qs = b'?%s' % urlreq.urlencode(q)
180 cu = b"%s%s" % (repobaseurl, qs)
180 cu = b"%s%s" % (repobaseurl, qs)
181 size = 0
181 size = 0
182 if hasattr(data, 'length'):
182 if hasattr(data, 'length'):
183 size = data.length
183 size = data.length
184 elif data is not None:
184 elif data is not None:
185 size = len(data)
185 size = len(data)
186 if data is not None and 'Content-Type' not in headers:
186 if data is not None and 'Content-Type' not in headers:
187 headers['Content-Type'] = 'application/mercurial-0.1'
187 headers['Content-Type'] = 'application/mercurial-0.1'
188
188
189 # Tell the server we accept application/mercurial-0.2 and multiple
189 # Tell the server we accept application/mercurial-0.2 and multiple
190 # compression formats if the server is capable of emitting those
190 # compression formats if the server is capable of emitting those
191 # payloads.
191 # payloads.
192 # Note: Keep this set empty by default, as client advertisement of
192 # Note: Keep this set empty by default, as client advertisement of
193 # protocol parameters should only occur after the handshake.
193 # protocol parameters should only occur after the handshake.
194 protoparams = set()
194 protoparams = set()
195
195
196 mediatypes = set()
196 mediatypes = set()
197 if caps is not None:
197 if caps is not None:
198 mt = capablefn(b'httpmediatype')
198 mt = capablefn(b'httpmediatype')
199 if mt:
199 if mt:
200 protoparams.add(b'0.1')
200 protoparams.add(b'0.1')
201 mediatypes = set(mt.split(b','))
201 mediatypes = set(mt.split(b','))
202
202
203 protoparams.add(b'partial-pull')
203 protoparams.add(b'partial-pull')
204
204
205 if b'0.2tx' in mediatypes:
205 if b'0.2tx' in mediatypes:
206 protoparams.add(b'0.2')
206 protoparams.add(b'0.2')
207
207
208 if b'0.2tx' in mediatypes and capablefn(b'compression'):
208 if b'0.2tx' in mediatypes and capablefn(b'compression'):
209 # We /could/ compare supported compression formats and prune
209 # We /could/ compare supported compression formats and prune
210 # non-mutually supported or error if nothing is mutually supported.
210 # non-mutually supported or error if nothing is mutually supported.
211 # For now, send the full list to the server and have it error.
211 # For now, send the full list to the server and have it error.
212 comps = [
212 comps = [
213 e.wireprotosupport().name
213 e.wireprotosupport().name
214 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
214 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
215 ]
215 ]
216 protoparams.add(b'comp=%s' % b','.join(comps))
216 protoparams.add(b'comp=%s' % b','.join(comps))
217
217
218 if protoparams:
218 if protoparams:
219 protoheaders = encodevalueinheaders(
219 protoheaders = encodevalueinheaders(
220 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
220 b' '.join(sorted(protoparams)), b'X-HgProto', headersize or 1024
221 )
221 )
222 for header, value in protoheaders:
222 for header, value in protoheaders:
223 headers[header] = value
223 headers[header] = value
224
224
225 varyheaders = []
225 varyheaders = []
226 for header in headers:
226 for header in headers:
227 if header.lower().startswith('x-hg'):
227 if header.lower().startswith('x-hg'):
228 varyheaders.append(header)
228 varyheaders.append(header)
229
229
230 if varyheaders:
230 if varyheaders:
231 headers['Vary'] = ','.join(sorted(varyheaders))
231 headers['Vary'] = ','.join(sorted(varyheaders))
232
232
233 req = requestbuilder(pycompat.strurl(cu), data, headers)
233 req = requestbuilder(pycompat.strurl(cu), data, headers)
234
234
235 if data is not None:
235 if data is not None:
236 ui.debug(b"sending %d bytes\n" % size)
236 ui.debug(b"sending %d bytes\n" % size)
237 req.add_unredirected_header('Content-Length', '%d' % size)
237 req.add_unredirected_header('Content-Length', '%d' % size)
238
238
239 return req, cu, qs
239 return req, cu, qs
240
240
241
241
242 def sendrequest(ui, opener, req):
242 def sendrequest(ui, opener, req):
243 """Send a prepared HTTP request.
243 """Send a prepared HTTP request.
244
244
245 Returns the response object.
245 Returns the response object.
246 """
246 """
247 dbg = ui.debug
247 dbg = ui.debug
248 line = b'devel-peer-request: %s\n'
249
248 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
250 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
249 line = b'devel-peer-request: %s\n'
250 dbg(
251 dbg(
251 line
252 line
252 % b'%s %s'
253 % b'%s %s'
253 % (
254 % (
254 pycompat.bytesurl(req.get_method()),
255 pycompat.bytesurl(req.get_method()),
255 pycompat.bytesurl(req.get_full_url()),
256 pycompat.bytesurl(req.get_full_url()),
256 )
257 )
257 )
258 )
258 hgargssize = None
259 hgargssize = None
259
260
260 for header, value in sorted(req.header_items()):
261 for header, value in sorted(req.header_items()):
261 header = pycompat.bytesurl(header)
262 header = pycompat.bytesurl(header)
262 value = pycompat.bytesurl(value)
263 value = pycompat.bytesurl(value)
263 if header.startswith(b'X-hgarg-'):
264 if header.startswith(b'X-hgarg-'):
264 if hgargssize is None:
265 if hgargssize is None:
265 hgargssize = 0
266 hgargssize = 0
266 hgargssize += len(value)
267 hgargssize += len(value)
267 else:
268 else:
268 dbg(line % b' %s %s' % (header, value))
269 dbg(line % b' %s %s' % (header, value))
269
270
270 if hgargssize is not None:
271 if hgargssize is not None:
271 dbg(
272 dbg(
272 line
273 line
273 % b' %d bytes of commands arguments in headers'
274 % b' %d bytes of commands arguments in headers'
274 % hgargssize
275 % hgargssize
275 )
276 )
276 data = req.data
277 data = req.data
277 if data is not None:
278 if data is not None:
278 length = getattr(data, 'length', None)
279 length = getattr(data, 'length', None)
279 if length is None:
280 if length is None:
280 length = len(data)
281 length = len(data)
281 dbg(line % b' %d bytes of data' % length)
282 dbg(line % b' %d bytes of data' % length)
282
283
283 start = util.timer()
284 start = util.timer()
284
285
285 res = None
286 res = None
286 try:
287 try:
287 res = opener.open(req)
288 res = opener.open(req)
288 except urlerr.httperror as inst:
289 except urlerr.httperror as inst:
289 if inst.code == 401:
290 if inst.code == 401:
290 raise error.Abort(_(b'authorization failed'))
291 raise error.Abort(_(b'authorization failed'))
291 raise
292 raise
292 except httplib.HTTPException as inst:
293 except httplib.HTTPException as inst:
293 ui.debug(
294 ui.debug(
294 b'http error requesting %s\n'
295 b'http error requesting %s\n'
295 % urlutil.hidepassword(req.get_full_url())
296 % urlutil.hidepassword(req.get_full_url())
296 )
297 )
297 ui.traceback()
298 ui.traceback()
298 raise IOError(None, inst)
299 raise IOError(None, inst)
299 finally:
300 finally:
300 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
301 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
301 code = res.code if res else -1
302 code = res.code if res else -1
302 dbg(
303 dbg(
303 line
304 line
304 % b' finished in %.4f seconds (%d)'
305 % b' finished in %.4f seconds (%d)'
305 % (util.timer() - start, code)
306 % (util.timer() - start, code)
306 )
307 )
307
308
308 # Insert error handlers for common I/O failures.
309 # Insert error handlers for common I/O failures.
309 urlmod.wrapresponse(res)
310 urlmod.wrapresponse(res)
310
311
311 return res
312 return res
312
313
313
314
314 class RedirectedRepoError(error.RepoError):
315 class RedirectedRepoError(error.RepoError):
315 def __init__(self, msg, respurl):
316 def __init__(self, msg, respurl):
316 super(RedirectedRepoError, self).__init__(msg)
317 super(RedirectedRepoError, self).__init__(msg)
317 self.respurl = respurl
318 self.respurl = respurl
318
319
319
320
320 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
321 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
321 # record the url we got redirected to
322 # record the url we got redirected to
322 redirected = False
323 redirected = False
323 respurl = pycompat.bytesurl(resp.geturl())
324 respurl = pycompat.bytesurl(resp.geturl())
324 if respurl.endswith(qs):
325 if respurl.endswith(qs):
325 respurl = respurl[: -len(qs)]
326 respurl = respurl[: -len(qs)]
326 qsdropped = False
327 qsdropped = False
327 else:
328 else:
328 qsdropped = True
329 qsdropped = True
329
330
330 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
331 if baseurl.rstrip(b'/') != respurl.rstrip(b'/'):
331 redirected = True
332 redirected = True
332 if not ui.quiet:
333 if not ui.quiet:
333 ui.warn(_(b'real URL is %s\n') % respurl)
334 ui.warn(_(b'real URL is %s\n') % respurl)
334
335
335 try:
336 try:
336 proto = pycompat.bytesurl(resp.getheader('content-type', ''))
337 proto = pycompat.bytesurl(resp.getheader('content-type', ''))
337 except AttributeError:
338 except AttributeError:
338 proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
339 proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
339
340
340 safeurl = urlutil.hidepassword(baseurl)
341 safeurl = urlutil.hidepassword(baseurl)
341 if proto.startswith(b'application/hg-error'):
342 if proto.startswith(b'application/hg-error'):
342 raise error.OutOfBandError(resp.read())
343 raise error.OutOfBandError(resp.read())
343
344
344 # Pre 1.0 versions of Mercurial used text/plain and
345 # Pre 1.0 versions of Mercurial used text/plain and
345 # application/hg-changegroup. We don't support such old servers.
346 # application/hg-changegroup. We don't support such old servers.
346 if not proto.startswith(b'application/mercurial-'):
347 if not proto.startswith(b'application/mercurial-'):
347 ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
348 ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
348 msg = _(
349 msg = _(
349 b"'%s' does not appear to be an hg repository:\n"
350 b"'%s' does not appear to be an hg repository:\n"
350 b"---%%<--- (%s)\n%s\n---%%<---\n"
351 b"---%%<--- (%s)\n%s\n---%%<---\n"
351 ) % (safeurl, proto or b'no content-type', resp.read(1024))
352 ) % (safeurl, proto or b'no content-type', resp.read(1024))
352
353
353 # Some servers may strip the query string from the redirect. We
354 # Some servers may strip the query string from the redirect. We
354 # raise a special error type so callers can react to this specially.
355 # raise a special error type so callers can react to this specially.
355 if redirected and qsdropped:
356 if redirected and qsdropped:
356 raise RedirectedRepoError(msg, respurl)
357 raise RedirectedRepoError(msg, respurl)
357 else:
358 else:
358 raise error.RepoError(msg)
359 raise error.RepoError(msg)
359
360
360 try:
361 try:
361 subtype = proto.split(b'-', 1)[1]
362 subtype = proto.split(b'-', 1)[1]
362
363
363 version_info = tuple([int(n) for n in subtype.split(b'.')])
364 version_info = tuple([int(n) for n in subtype.split(b'.')])
364 except ValueError:
365 except ValueError:
365 raise error.RepoError(
366 raise error.RepoError(
366 _(b"'%s' sent a broken Content-Type header (%s)") % (safeurl, proto)
367 _(b"'%s' sent a broken Content-Type header (%s)") % (safeurl, proto)
367 )
368 )
368
369
369 # TODO consider switching to a decompression reader that uses
370 # TODO consider switching to a decompression reader that uses
370 # generators.
371 # generators.
371 if version_info == (0, 1):
372 if version_info == (0, 1):
372 if compressible:
373 if compressible:
373 resp = util.compengines[b'zlib'].decompressorreader(resp)
374 resp = util.compengines[b'zlib'].decompressorreader(resp)
374
375
375 elif version_info == (0, 2):
376 elif version_info == (0, 2):
376 # application/mercurial-0.2 always identifies the compression
377 # application/mercurial-0.2 always identifies the compression
377 # engine in the payload header.
378 # engine in the payload header.
378 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
379 elen = struct.unpack(b'B', util.readexactly(resp, 1))[0]
379 ename = util.readexactly(resp, elen)
380 ename = util.readexactly(resp, elen)
380 engine = util.compengines.forwiretype(ename)
381 engine = util.compengines.forwiretype(ename)
381
382
382 resp = engine.decompressorreader(resp)
383 resp = engine.decompressorreader(resp)
383 else:
384 else:
384 raise error.RepoError(
385 raise error.RepoError(
385 _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
386 _(b"'%s' uses newer protocol %s") % (safeurl, subtype)
386 )
387 )
387
388
388 return respurl, proto, resp
389 return respurl, proto, resp
389
390
390
391
391 class httppeer(wireprotov1peer.wirepeer):
392 class httppeer(wireprotov1peer.wirepeer):
392 def __init__(
393 def __init__(
393 self, ui, path, url, opener, requestbuilder, caps, remotehidden=False
394 self, ui, path, url, opener, requestbuilder, caps, remotehidden=False
394 ):
395 ):
395 super().__init__(ui, path=path, remotehidden=remotehidden)
396 super().__init__(ui, path=path, remotehidden=remotehidden)
396 self._url = url
397 self._url = url
397 self._caps = caps
398 self._caps = caps
398 self.limitedarguments = caps is not None and b'httppostargs' not in caps
399 self.limitedarguments = caps is not None and b'httppostargs' not in caps
399 self._urlopener = opener
400 self._urlopener = opener
400 self._requestbuilder = requestbuilder
401 self._requestbuilder = requestbuilder
401 self._remotehidden = remotehidden
402 self._remotehidden = remotehidden
402
403
403 def __del__(self):
404 def __del__(self):
404 for h in self._urlopener.handlers:
405 for h in self._urlopener.handlers:
405 h.close()
406 h.close()
406 getattr(h, "close_all", lambda: None)()
407 getattr(h, "close_all", lambda: None)()
407
408
408 # Begin of ipeerconnection interface.
409 # Begin of ipeerconnection interface.
409
410
410 def url(self):
411 def url(self):
411 return self.path.loc
412 return self.path.loc
412
413
413 def local(self):
414 def local(self):
414 return None
415 return None
415
416
416 def canpush(self):
417 def canpush(self):
417 return True
418 return True
418
419
419 def close(self):
420 def close(self):
420 try:
421 try:
421 reqs, sent, recv = (
422 reqs, sent, recv = (
422 self._urlopener.requestscount,
423 self._urlopener.requestscount,
423 self._urlopener.sentbytescount,
424 self._urlopener.sentbytescount,
424 self._urlopener.receivedbytescount,
425 self._urlopener.receivedbytescount,
425 )
426 )
426 except AttributeError:
427 except AttributeError:
427 return
428 return
428 self.ui.note(
429 self.ui.note(
429 _(
430 _(
430 b'(sent %d HTTP requests and %d bytes; '
431 b'(sent %d HTTP requests and %d bytes; '
431 b'received %d bytes in responses)\n'
432 b'received %d bytes in responses)\n'
432 )
433 )
433 % (reqs, sent, recv)
434 % (reqs, sent, recv)
434 )
435 )
435
436
436 # End of ipeerconnection interface.
437 # End of ipeerconnection interface.
437
438
438 # Begin of ipeercommands interface.
439 # Begin of ipeercommands interface.
439
440
440 def capabilities(self):
441 def capabilities(self):
441 return self._caps
442 return self._caps
442
443
443 def _finish_inline_clone_bundle(self, stream):
444 def _finish_inline_clone_bundle(self, stream):
444 # HTTP streams must hit the end to process the last empty
445 # HTTP streams must hit the end to process the last empty
445 # chunk of Chunked-Encoding so the connection can be reused.
446 # chunk of Chunked-Encoding so the connection can be reused.
446 chunk = stream.read(1)
447 chunk = stream.read(1)
447 if chunk:
448 if chunk:
448 self._abort(error.ResponseError(_(b"unexpected response:"), chunk))
449 self._abort(error.ResponseError(_(b"unexpected response:"), chunk))
449
450
450 # End of ipeercommands interface.
451 # End of ipeercommands interface.
451
452
452 def _callstream(self, cmd, _compressible=False, **args):
453 def _callstream(self, cmd, _compressible=False, **args):
453 args = pycompat.byteskwargs(args)
454 args = pycompat.byteskwargs(args)
454
455
455 req, cu, qs = makev1commandrequest(
456 req, cu, qs = makev1commandrequest(
456 self.ui,
457 self.ui,
457 self._requestbuilder,
458 self._requestbuilder,
458 self._caps,
459 self._caps,
459 self.capable,
460 self.capable,
460 self._url,
461 self._url,
461 cmd,
462 cmd,
462 args,
463 args,
463 self._remotehidden,
464 self._remotehidden,
464 )
465 )
465
466
466 resp = sendrequest(self.ui, self._urlopener, req)
467 resp = sendrequest(self.ui, self._urlopener, req)
467
468
468 self._url, ct, resp = parsev1commandresponse(
469 self._url, ct, resp = parsev1commandresponse(
469 self.ui, self._url, cu, qs, resp, _compressible
470 self.ui, self._url, cu, qs, resp, _compressible
470 )
471 )
471
472
472 return resp
473 return resp
473
474
474 def _call(self, cmd, **args):
475 def _call(self, cmd, **args):
475 fp = self._callstream(cmd, **args)
476 fp = self._callstream(cmd, **args)
476 try:
477 try:
477 return fp.read()
478 return fp.read()
478 finally:
479 finally:
479 # if using keepalive, allow connection to be reused
480 # if using keepalive, allow connection to be reused
480 fp.close()
481 fp.close()
481
482
482 def _callpush(self, cmd, cg, **args):
483 def _callpush(self, cmd, cg, **args):
483 # have to stream bundle to a temp file because we do not have
484 # have to stream bundle to a temp file because we do not have
484 # http 1.1 chunked transfer.
485 # http 1.1 chunked transfer.
485
486
486 types = self.capable(b'unbundle')
487 types = self.capable(b'unbundle')
487 try:
488 try:
488 types = types.split(b',')
489 types = types.split(b',')
489 except AttributeError:
490 except AttributeError:
490 # servers older than d1b16a746db6 will send 'unbundle' as a
491 # servers older than d1b16a746db6 will send 'unbundle' as a
491 # boolean capability. They only support headerless/uncompressed
492 # boolean capability. They only support headerless/uncompressed
492 # bundles.
493 # bundles.
493 types = [b""]
494 types = [b""]
494 for x in types:
495 for x in types:
495 if x in bundle2.bundletypes:
496 if x in bundle2.bundletypes:
496 type = x
497 type = x
497 break
498 break
498
499
499 tempname = bundle2.writebundle(self.ui, cg, None, type)
500 tempname = bundle2.writebundle(self.ui, cg, None, type)
500 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
501 fp = httpconnection.httpsendfile(self.ui, tempname, b"rb")
501 headers = {'Content-Type': 'application/mercurial-0.1'}
502 headers = {'Content-Type': 'application/mercurial-0.1'}
502
503
503 try:
504 try:
504 r = self._call(cmd, data=fp, headers=headers, **args)
505 r = self._call(cmd, data=fp, headers=headers, **args)
505 vals = r.split(b'\n', 1)
506 vals = r.split(b'\n', 1)
506 if len(vals) < 2:
507 if len(vals) < 2:
507 raise error.ResponseError(_(b"unexpected response:"), r)
508 raise error.ResponseError(_(b"unexpected response:"), r)
508 return vals
509 return vals
509 except urlerr.httperror:
510 except urlerr.httperror:
510 # Catch and re-raise these so we don't try and treat them
511 # Catch and re-raise these so we don't try and treat them
511 # like generic socket errors. They lack any values in
512 # like generic socket errors. They lack any values in
512 # .args on Python 3 which breaks our socket.error block.
513 # .args on Python 3 which breaks our socket.error block.
513 raise
514 raise
514 except socket.error as err:
515 except socket.error as err:
515 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
516 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
516 raise error.Abort(_(b'push failed: %s') % err.args[1])
517 raise error.Abort(_(b'push failed: %s') % err.args[1])
517 raise error.Abort(err.args[1])
518 raise error.Abort(err.args[1])
518 finally:
519 finally:
519 fp.close()
520 fp.close()
520 os.unlink(tempname)
521 os.unlink(tempname)
521
522
522 def _calltwowaystream(self, cmd, fp, **args):
523 def _calltwowaystream(self, cmd, fp, **args):
523 # dump bundle to disk
524 # dump bundle to disk
524 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
525 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
525 try:
526 try:
526 with os.fdopen(fd, "wb") as fh:
527 with os.fdopen(fd, "wb") as fh:
527 d = fp.read(4096)
528 d = fp.read(4096)
528 while d:
529 while d:
529 fh.write(d)
530 fh.write(d)
530 d = fp.read(4096)
531 d = fp.read(4096)
531 # start http push
532 # start http push
532 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
533 with httpconnection.httpsendfile(self.ui, filename, b"rb") as fp_:
533 headers = {'Content-Type': 'application/mercurial-0.1'}
534 headers = {'Content-Type': 'application/mercurial-0.1'}
534 return self._callstream(cmd, data=fp_, headers=headers, **args)
535 return self._callstream(cmd, data=fp_, headers=headers, **args)
535 finally:
536 finally:
536 os.unlink(filename)
537 os.unlink(filename)
537
538
538 def _callcompressable(self, cmd, **args):
539 def _callcompressable(self, cmd, **args):
539 return self._callstream(cmd, _compressible=True, **args)
540 return self._callstream(cmd, _compressible=True, **args)
540
541
541 def _abort(self, exception):
542 def _abort(self, exception):
542 raise exception
543 raise exception
543
544
544
545
545 class queuedcommandfuture(futures.Future):
546 class queuedcommandfuture(futures.Future):
546 """Wraps result() on command futures to trigger submission on call."""
547 """Wraps result() on command futures to trigger submission on call."""
547
548
548 def result(self, timeout=None):
549 def result(self, timeout=None):
549 if self.done():
550 if self.done():
550 return futures.Future.result(self, timeout)
551 return futures.Future.result(self, timeout)
551
552
552 self._peerexecutor.sendcommands()
553 self._peerexecutor.sendcommands()
553
554
554 # sendcommands() will restore the original __class__ and self.result
555 # sendcommands() will restore the original __class__ and self.result
555 # will resolve to Future.result.
556 # will resolve to Future.result.
556 return self.result(timeout)
557 return self.result(timeout)
557
558
558
559
559 def performhandshake(ui, url, opener, requestbuilder):
560 def performhandshake(ui, url, opener, requestbuilder):
560 # The handshake is a request to the capabilities command.
561 # The handshake is a request to the capabilities command.
561
562
562 caps = None
563 caps = None
563
564
564 def capable(x):
565 def capable(x):
565 raise error.ProgrammingError(b'should not be called')
566 raise error.ProgrammingError(b'should not be called')
566
567
567 args = {}
568 args = {}
568
569
569 req, requrl, qs = makev1commandrequest(
570 req, requrl, qs = makev1commandrequest(
570 ui, requestbuilder, caps, capable, url, b'capabilities', args
571 ui, requestbuilder, caps, capable, url, b'capabilities', args
571 )
572 )
572 resp = sendrequest(ui, opener, req)
573 resp = sendrequest(ui, opener, req)
573
574
574 # The server may redirect us to the repo root, stripping the
575 # The server may redirect us to the repo root, stripping the
575 # ?cmd=capabilities query string from the URL. The server would likely
576 # ?cmd=capabilities query string from the URL. The server would likely
576 # return HTML in this case and ``parsev1commandresponse()`` would raise.
577 # return HTML in this case and ``parsev1commandresponse()`` would raise.
577 # We catch this special case and re-issue the capabilities request against
578 # We catch this special case and re-issue the capabilities request against
578 # the new URL.
579 # the new URL.
579 #
580 #
580 # We should ideally not do this, as a redirect that drops the query
581 # We should ideally not do this, as a redirect that drops the query
581 # string from the URL is arguably a server bug. (Garbage in, garbage out).
582 # string from the URL is arguably a server bug. (Garbage in, garbage out).
582 # However, Mercurial clients for several years appeared to handle this
583 # However, Mercurial clients for several years appeared to handle this
583 # issue without behavior degradation. And according to issue 5860, it may
584 # issue without behavior degradation. And according to issue 5860, it may
584 # be a longstanding bug in some server implementations. So we allow a
585 # be a longstanding bug in some server implementations. So we allow a
585 # redirect that drops the query string to "just work."
586 # redirect that drops the query string to "just work."
586 try:
587 try:
587 respurl, ct, resp = parsev1commandresponse(
588 respurl, ct, resp = parsev1commandresponse(
588 ui, url, requrl, qs, resp, compressible=False
589 ui, url, requrl, qs, resp, compressible=False
589 )
590 )
590 except RedirectedRepoError as e:
591 except RedirectedRepoError as e:
591 req, requrl, qs = makev1commandrequest(
592 req, requrl, qs = makev1commandrequest(
592 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
593 ui, requestbuilder, caps, capable, e.respurl, b'capabilities', args
593 )
594 )
594 resp = sendrequest(ui, opener, req)
595 resp = sendrequest(ui, opener, req)
595 respurl, ct, resp = parsev1commandresponse(
596 respurl, ct, resp = parsev1commandresponse(
596 ui, url, requrl, qs, resp, compressible=False
597 ui, url, requrl, qs, resp, compressible=False
597 )
598 )
598
599
599 try:
600 try:
600 rawdata = resp.read()
601 rawdata = resp.read()
601 finally:
602 finally:
602 resp.close()
603 resp.close()
603
604
604 if not ct.startswith(b'application/mercurial-'):
605 if not ct.startswith(b'application/mercurial-'):
605 raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
606 raise error.ProgrammingError(b'unexpected content-type: %s' % ct)
606
607
607 info = {b'v1capabilities': set(rawdata.split())}
608 info = {b'v1capabilities': set(rawdata.split())}
608
609
609 return respurl, info
610 return respurl, info
610
611
611
612
612 def _make_peer(
613 def _make_peer(
613 ui, path, opener=None, requestbuilder=urlreq.request, remotehidden=False
614 ui, path, opener=None, requestbuilder=urlreq.request, remotehidden=False
614 ):
615 ):
615 """Construct an appropriate HTTP peer instance.
616 """Construct an appropriate HTTP peer instance.
616
617
617 ``opener`` is an ``url.opener`` that should be used to establish
618 ``opener`` is an ``url.opener`` that should be used to establish
618 connections, perform HTTP requests.
619 connections, perform HTTP requests.
619
620
620 ``requestbuilder`` is the type used for constructing HTTP requests.
621 ``requestbuilder`` is the type used for constructing HTTP requests.
621 It exists as an argument so extensions can override the default.
622 It exists as an argument so extensions can override the default.
622 """
623 """
623 if path.url.query or path.url.fragment:
624 if path.url.query or path.url.fragment:
624 msg = _(b'unsupported URL component: "%s"')
625 msg = _(b'unsupported URL component: "%s"')
625 msg %= path.url.query or path.url.fragment
626 msg %= path.url.query or path.url.fragment
626 raise error.Abort(msg)
627 raise error.Abort(msg)
627
628
628 # urllib cannot handle URLs with embedded user or passwd.
629 # urllib cannot handle URLs with embedded user or passwd.
629 url, authinfo = path.url.authinfo()
630 url, authinfo = path.url.authinfo()
630 ui.debug(b'using %s\n' % url)
631 ui.debug(b'using %s\n' % url)
631
632
632 opener = opener or urlmod.opener(ui, authinfo)
633 opener = opener or urlmod.opener(ui, authinfo)
633
634
634 respurl, info = performhandshake(ui, url, opener, requestbuilder)
635 respurl, info = performhandshake(ui, url, opener, requestbuilder)
635
636
636 return httppeer(
637 return httppeer(
637 ui,
638 ui,
638 path,
639 path,
639 respurl,
640 respurl,
640 opener,
641 opener,
641 requestbuilder,
642 requestbuilder,
642 info[b'v1capabilities'],
643 info[b'v1capabilities'],
643 remotehidden=remotehidden,
644 remotehidden=remotehidden,
644 )
645 )
645
646
646
647
647 def make_peer(
648 def make_peer(
648 ui, path, create, intents=None, createopts=None, remotehidden=False
649 ui, path, create, intents=None, createopts=None, remotehidden=False
649 ):
650 ):
650 if create:
651 if create:
651 raise error.Abort(_(b'cannot create new http repository'))
652 raise error.Abort(_(b'cannot create new http repository'))
652 try:
653 try:
653 if path.url.scheme == b'https' and not urlmod.has_https:
654 if path.url.scheme == b'https' and not urlmod.has_https:
654 raise error.Abort(
655 raise error.Abort(
655 _(b'Python support for SSL and HTTPS is not installed')
656 _(b'Python support for SSL and HTTPS is not installed')
656 )
657 )
657
658
658 inst = _make_peer(ui, path, remotehidden=remotehidden)
659 inst = _make_peer(ui, path, remotehidden=remotehidden)
659
660
660 return inst
661 return inst
661 except error.RepoError as httpexception:
662 except error.RepoError as httpexception:
662 try:
663 try:
663 path = path.copy(new_raw_location=b"static-" + path.rawloc)
664 path = path.copy(new_raw_location=b"static-" + path.rawloc)
664 r = statichttprepo.make_peer(ui, path, create)
665 r = statichttprepo.make_peer(ui, path, create)
665 ui.note(_(b'(falling back to static-http)\n'))
666 ui.note(_(b'(falling back to static-http)\n'))
666 return r
667 return r
667 except error.RepoError:
668 except error.RepoError:
668 raise httpexception # use the original http RepoError instead
669 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now