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