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