##// END OF EJS Templates
protocol: avoid sending outrageously large between requests
Matt Mackall -
r7342:1dcd2cc6 default
parent child Browse files
Show More
@@ -1,235 +1,238 b''
1 # httprepo.py - HTTP repository proxy classes for mercurial
1 # httprepo.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
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from node import bin, hex, nullid
9 from node import bin, hex, nullid
10 from i18n import _
10 from i18n import _
11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
12 import errno, socket, changegroup, statichttprepo
12 import errno, socket, changegroup, statichttprepo
13 import url
13 import url
14
14
15 def zgenerator(f):
15 def zgenerator(f):
16 zd = zlib.decompressobj()
16 zd = zlib.decompressobj()
17 try:
17 try:
18 for chunk in util.filechunkiter(f):
18 for chunk in util.filechunkiter(f):
19 yield zd.decompress(chunk)
19 yield zd.decompress(chunk)
20 except httplib.HTTPException:
20 except httplib.HTTPException:
21 raise IOError(None, _('connection ended unexpectedly'))
21 raise IOError(None, _('connection ended unexpectedly'))
22 yield zd.flush()
22 yield zd.flush()
23
23
24 class httprepository(repo.repository):
24 class httprepository(repo.repository):
25 def __init__(self, ui, path):
25 def __init__(self, ui, path):
26 self.path = path
26 self.path = path
27 self.caps = None
27 self.caps = None
28 self.handler = None
28 self.handler = None
29 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
29 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
30 if query or frag:
30 if query or frag:
31 raise util.Abort(_('unsupported URL component: "%s"') %
31 raise util.Abort(_('unsupported URL component: "%s"') %
32 (query or frag))
32 (query or frag))
33
33
34 # urllib cannot handle URLs with embedded user or passwd
34 # urllib cannot handle URLs with embedded user or passwd
35 self._url, authinfo = url.getauthinfo(path)
35 self._url, authinfo = url.getauthinfo(path)
36
36
37 self.ui = ui
37 self.ui = ui
38 self.ui.debug(_('using %s\n') % self._url)
38 self.ui.debug(_('using %s\n') % self._url)
39
39
40 self.urlopener = url.opener(ui, authinfo)
40 self.urlopener = url.opener(ui, authinfo)
41
41
42 def url(self):
42 def url(self):
43 return self.path
43 return self.path
44
44
45 # look up capabilities only when needed
45 # look up capabilities only when needed
46
46
47 def get_caps(self):
47 def get_caps(self):
48 if self.caps is None:
48 if self.caps is None:
49 try:
49 try:
50 self.caps = util.set(self.do_read('capabilities').split())
50 self.caps = util.set(self.do_read('capabilities').split())
51 except repo.RepoError:
51 except repo.RepoError:
52 self.caps = util.set()
52 self.caps = util.set()
53 self.ui.debug(_('capabilities: %s\n') %
53 self.ui.debug(_('capabilities: %s\n') %
54 (' '.join(self.caps or ['none'])))
54 (' '.join(self.caps or ['none'])))
55 return self.caps
55 return self.caps
56
56
57 capabilities = property(get_caps)
57 capabilities = property(get_caps)
58
58
59 def lock(self):
59 def lock(self):
60 raise util.Abort(_('operation not supported over http'))
60 raise util.Abort(_('operation not supported over http'))
61
61
62 def do_cmd(self, cmd, **args):
62 def do_cmd(self, cmd, **args):
63 data = args.pop('data', None)
63 data = args.pop('data', None)
64 headers = args.pop('headers', {})
64 headers = args.pop('headers', {})
65 self.ui.debug(_("sending %s command\n") % cmd)
65 self.ui.debug(_("sending %s command\n") % cmd)
66 q = {"cmd": cmd}
66 q = {"cmd": cmd}
67 q.update(args)
67 q.update(args)
68 qs = '?%s' % urllib.urlencode(q)
68 qs = '?%s' % urllib.urlencode(q)
69 cu = "%s%s" % (self._url, qs)
69 cu = "%s%s" % (self._url, qs)
70 try:
70 try:
71 if data:
71 if data:
72 self.ui.debug(_("sending %s bytes\n") % len(data))
72 self.ui.debug(_("sending %s bytes\n") % len(data))
73 resp = self.urlopener.open(urllib2.Request(cu, data, headers))
73 resp = self.urlopener.open(urllib2.Request(cu, data, headers))
74 except urllib2.HTTPError, inst:
74 except urllib2.HTTPError, inst:
75 if inst.code == 401:
75 if inst.code == 401:
76 raise util.Abort(_('authorization failed'))
76 raise util.Abort(_('authorization failed'))
77 raise
77 raise
78 except httplib.HTTPException, inst:
78 except httplib.HTTPException, inst:
79 self.ui.debug(_('http error while sending %s command\n') % cmd)
79 self.ui.debug(_('http error while sending %s command\n') % cmd)
80 self.ui.print_exc()
80 self.ui.print_exc()
81 raise IOError(None, inst)
81 raise IOError(None, inst)
82 except IndexError:
82 except IndexError:
83 # this only happens with Python 2.3, later versions raise URLError
83 # this only happens with Python 2.3, later versions raise URLError
84 raise util.Abort(_('http error, possibly caused by proxy setting'))
84 raise util.Abort(_('http error, possibly caused by proxy setting'))
85 # record the url we got redirected to
85 # record the url we got redirected to
86 resp_url = resp.geturl()
86 resp_url = resp.geturl()
87 if resp_url.endswith(qs):
87 if resp_url.endswith(qs):
88 resp_url = resp_url[:-len(qs)]
88 resp_url = resp_url[:-len(qs)]
89 if self._url != resp_url:
89 if self._url != resp_url:
90 self.ui.status(_('real URL is %s\n') % resp_url)
90 self.ui.status(_('real URL is %s\n') % resp_url)
91 self._url = resp_url
91 self._url = resp_url
92 try:
92 try:
93 proto = resp.getheader('content-type')
93 proto = resp.getheader('content-type')
94 except AttributeError:
94 except AttributeError:
95 proto = resp.headers['content-type']
95 proto = resp.headers['content-type']
96
96
97 # accept old "text/plain" and "application/hg-changegroup" for now
97 # accept old "text/plain" and "application/hg-changegroup" for now
98 if not (proto.startswith('application/mercurial-') or
98 if not (proto.startswith('application/mercurial-') or
99 proto.startswith('text/plain') or
99 proto.startswith('text/plain') or
100 proto.startswith('application/hg-changegroup')):
100 proto.startswith('application/hg-changegroup')):
101 self.ui.debug(_("Requested URL: '%s'\n") % cu)
101 self.ui.debug(_("Requested URL: '%s'\n") % cu)
102 raise repo.RepoError(_("'%s' does not appear to be an hg repository")
102 raise repo.RepoError(_("'%s' does not appear to be an hg repository")
103 % self._url)
103 % self._url)
104
104
105 if proto.startswith('application/mercurial-'):
105 if proto.startswith('application/mercurial-'):
106 try:
106 try:
107 version = proto.split('-', 1)[1]
107 version = proto.split('-', 1)[1]
108 version_info = tuple([int(n) for n in version.split('.')])
108 version_info = tuple([int(n) for n in version.split('.')])
109 except ValueError:
109 except ValueError:
110 raise repo.RepoError(_("'%s' sent a broken Content-Type "
110 raise repo.RepoError(_("'%s' sent a broken Content-Type "
111 "header (%s)") % (self._url, proto))
111 "header (%s)") % (self._url, proto))
112 if version_info > (0, 1):
112 if version_info > (0, 1):
113 raise repo.RepoError(_("'%s' uses newer protocol %s") %
113 raise repo.RepoError(_("'%s' uses newer protocol %s") %
114 (self._url, version))
114 (self._url, version))
115
115
116 return resp
116 return resp
117
117
118 def do_read(self, cmd, **args):
118 def do_read(self, cmd, **args):
119 fp = self.do_cmd(cmd, **args)
119 fp = self.do_cmd(cmd, **args)
120 try:
120 try:
121 return fp.read()
121 return fp.read()
122 finally:
122 finally:
123 # if using keepalive, allow connection to be reused
123 # if using keepalive, allow connection to be reused
124 fp.close()
124 fp.close()
125
125
126 def lookup(self, key):
126 def lookup(self, key):
127 self.requirecap('lookup', _('look up remote revision'))
127 self.requirecap('lookup', _('look up remote revision'))
128 d = self.do_cmd("lookup", key = key).read()
128 d = self.do_cmd("lookup", key = key).read()
129 success, data = d[:-1].split(' ', 1)
129 success, data = d[:-1].split(' ', 1)
130 if int(success):
130 if int(success):
131 return bin(data)
131 return bin(data)
132 raise repo.RepoError(data)
132 raise repo.RepoError(data)
133
133
134 def heads(self):
134 def heads(self):
135 d = self.do_read("heads")
135 d = self.do_read("heads")
136 try:
136 try:
137 return map(bin, d[:-1].split(" "))
137 return map(bin, d[:-1].split(" "))
138 except:
138 except:
139 raise util.UnexpectedOutput(_("unexpected response:"), d)
139 raise util.UnexpectedOutput(_("unexpected response:"), d)
140
140
141 def branches(self, nodes):
141 def branches(self, nodes):
142 n = " ".join(map(hex, nodes))
142 n = " ".join(map(hex, nodes))
143 d = self.do_read("branches", nodes=n)
143 d = self.do_read("branches", nodes=n)
144 try:
144 try:
145 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
145 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
146 return br
146 return br
147 except:
147 except:
148 raise util.UnexpectedOutput(_("unexpected response:"), d)
148 raise util.UnexpectedOutput(_("unexpected response:"), d)
149
149
150 def between(self, pairs):
150 def between(self, pairs):
151 n = " ".join(["-".join(map(hex, p)) for p in pairs])
151 batch = 8 # avoid giant requests
152 d = self.do_read("between", pairs=n)
152 r = []
153 try:
153 for i in xrange(0, len(pairs), batch):
154 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
154 n = " ".join(["-".join(map(hex, p)) for p in pairs[i:i + batch]])
155 return p
155 d = self.do_read("between", pairs=n)
156 except:
156 try:
157 raise util.UnexpectedOutput(_("unexpected response:"), d)
157 r += [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
158 except:
159 raise util.UnexpectedOutput(_("unexpected response:"), d)
160 return r
158
161
159 def changegroup(self, nodes, kind):
162 def changegroup(self, nodes, kind):
160 n = " ".join(map(hex, nodes))
163 n = " ".join(map(hex, nodes))
161 f = self.do_cmd("changegroup", roots=n)
164 f = self.do_cmd("changegroup", roots=n)
162 return util.chunkbuffer(zgenerator(f))
165 return util.chunkbuffer(zgenerator(f))
163
166
164 def changegroupsubset(self, bases, heads, source):
167 def changegroupsubset(self, bases, heads, source):
165 self.requirecap('changegroupsubset', _('look up remote changes'))
168 self.requirecap('changegroupsubset', _('look up remote changes'))
166 baselst = " ".join([hex(n) for n in bases])
169 baselst = " ".join([hex(n) for n in bases])
167 headlst = " ".join([hex(n) for n in heads])
170 headlst = " ".join([hex(n) for n in heads])
168 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
171 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
169 return util.chunkbuffer(zgenerator(f))
172 return util.chunkbuffer(zgenerator(f))
170
173
171 def unbundle(self, cg, heads, source):
174 def unbundle(self, cg, heads, source):
172 # have to stream bundle to a temp file because we do not have
175 # have to stream bundle to a temp file because we do not have
173 # http 1.1 chunked transfer.
176 # http 1.1 chunked transfer.
174
177
175 type = ""
178 type = ""
176 types = self.capable('unbundle')
179 types = self.capable('unbundle')
177 # servers older than d1b16a746db6 will send 'unbundle' as a
180 # servers older than d1b16a746db6 will send 'unbundle' as a
178 # boolean capability
181 # boolean capability
179 try:
182 try:
180 types = types.split(',')
183 types = types.split(',')
181 except AttributeError:
184 except AttributeError:
182 types = [""]
185 types = [""]
183 if types:
186 if types:
184 for x in types:
187 for x in types:
185 if x in changegroup.bundletypes:
188 if x in changegroup.bundletypes:
186 type = x
189 type = x
187 break
190 break
188
191
189 tempname = changegroup.writebundle(cg, None, type)
192 tempname = changegroup.writebundle(cg, None, type)
190 fp = url.httpsendfile(tempname, "rb")
193 fp = url.httpsendfile(tempname, "rb")
191 try:
194 try:
192 try:
195 try:
193 resp = self.do_read(
196 resp = self.do_read(
194 'unbundle', data=fp,
197 'unbundle', data=fp,
195 headers={'Content-Type': 'application/octet-stream'},
198 headers={'Content-Type': 'application/octet-stream'},
196 heads=' '.join(map(hex, heads)))
199 heads=' '.join(map(hex, heads)))
197 resp_code, output = resp.split('\n', 1)
200 resp_code, output = resp.split('\n', 1)
198 try:
201 try:
199 ret = int(resp_code)
202 ret = int(resp_code)
200 except ValueError, err:
203 except ValueError, err:
201 raise util.UnexpectedOutput(
204 raise util.UnexpectedOutput(
202 _('push failed (unexpected response):'), resp)
205 _('push failed (unexpected response):'), resp)
203 self.ui.write(output)
206 self.ui.write(output)
204 return ret
207 return ret
205 except socket.error, err:
208 except socket.error, err:
206 if err[0] in (errno.ECONNRESET, errno.EPIPE):
209 if err[0] in (errno.ECONNRESET, errno.EPIPE):
207 raise util.Abort(_('push failed: %s') % err[1])
210 raise util.Abort(_('push failed: %s') % err[1])
208 raise util.Abort(err[1])
211 raise util.Abort(err[1])
209 finally:
212 finally:
210 fp.close()
213 fp.close()
211 os.unlink(tempname)
214 os.unlink(tempname)
212
215
213 def stream_out(self):
216 def stream_out(self):
214 return self.do_cmd('stream_out')
217 return self.do_cmd('stream_out')
215
218
216 class httpsrepository(httprepository):
219 class httpsrepository(httprepository):
217 def __init__(self, ui, path):
220 def __init__(self, ui, path):
218 if not url.has_https:
221 if not url.has_https:
219 raise util.Abort(_('Python support for SSL and HTTPS '
222 raise util.Abort(_('Python support for SSL and HTTPS '
220 'is not installed'))
223 'is not installed'))
221 httprepository.__init__(self, ui, path)
224 httprepository.__init__(self, ui, path)
222
225
223 def instance(ui, path, create):
226 def instance(ui, path, create):
224 if create:
227 if create:
225 raise util.Abort(_('cannot create new http repository'))
228 raise util.Abort(_('cannot create new http repository'))
226 try:
229 try:
227 if path.startswith('https:'):
230 if path.startswith('https:'):
228 inst = httpsrepository(ui, path)
231 inst = httpsrepository(ui, path)
229 else:
232 else:
230 inst = httprepository(ui, path)
233 inst = httprepository(ui, path)
231 inst.between([(nullid, nullid)])
234 inst.between([(nullid, nullid)])
232 return inst
235 return inst
233 except repo.RepoError:
236 except repo.RepoError:
234 ui.note('(falling back to static-http)\n')
237 ui.note('(falling back to static-http)\n')
235 return statichttprepo.instance(ui, "static-" + path, create)
238 return statichttprepo.instance(ui, "static-" + path, create)
General Comments 0
You need to be logged in to leave comments. Login now