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