##// END OF EJS Templates
http2: send an extra header to signal a non-broken client...
Augie Fackler -
r14991:4f396109 stable
parent child Browse files
Show More
@@ -1,302 +1,303 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater
10 from mercurial import ui, hg, hook, error, encoding, templater
11 from common import get_stat, ErrorResponse, permhooks, caching
11 from common import get_stat, ErrorResponse, permhooks, caching
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from request import wsgirequest
14 from request import wsgirequest
15 import webcommands, protocol, webutil
15 import webcommands, protocol, webutil
16
16
17 perms = {
17 perms = {
18 'changegroup': 'pull',
18 'changegroup': 'pull',
19 'changegroupsubset': 'pull',
19 'changegroupsubset': 'pull',
20 'getbundle': 'pull',
20 'getbundle': 'pull',
21 'stream_out': 'pull',
21 'stream_out': 'pull',
22 'listkeys': 'pull',
22 'listkeys': 'pull',
23 'unbundle': 'push',
23 'unbundle': 'push',
24 'pushkey': 'push',
24 'pushkey': 'push',
25 }
25 }
26
26
27 class hgweb(object):
27 class hgweb(object):
28 def __init__(self, repo, name=None, baseui=None):
28 def __init__(self, repo, name=None, baseui=None):
29 if isinstance(repo, str):
29 if isinstance(repo, str):
30 if baseui:
30 if baseui:
31 u = baseui.copy()
31 u = baseui.copy()
32 else:
32 else:
33 u = ui.ui()
33 u = ui.ui()
34 self.repo = hg.repository(u, repo)
34 self.repo = hg.repository(u, repo)
35 else:
35 else:
36 self.repo = repo
36 self.repo = repo
37
37
38 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
38 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
39 self.repo.ui.setconfig('ui', 'interactive', 'off')
39 self.repo.ui.setconfig('ui', 'interactive', 'off')
40 hook.redirect(True)
40 hook.redirect(True)
41 self.mtime = -1
41 self.mtime = -1
42 self.size = -1
42 self.size = -1
43 self.reponame = name
43 self.reponame = name
44 self.archives = 'zip', 'gz', 'bz2'
44 self.archives = 'zip', 'gz', 'bz2'
45 self.stripecount = 1
45 self.stripecount = 1
46 # a repo owner may set web.templates in .hg/hgrc to get any file
46 # a repo owner may set web.templates in .hg/hgrc to get any file
47 # readable by the user running the CGI script
47 # readable by the user running the CGI script
48 self.templatepath = self.config('web', 'templates')
48 self.templatepath = self.config('web', 'templates')
49
49
50 # The CGI scripts are often run by a user different from the repo owner.
50 # The CGI scripts are often run by a user different from the repo owner.
51 # Trust the settings from the .hg/hgrc files by default.
51 # Trust the settings from the .hg/hgrc files by default.
52 def config(self, section, name, default=None, untrusted=True):
52 def config(self, section, name, default=None, untrusted=True):
53 return self.repo.ui.config(section, name, default,
53 return self.repo.ui.config(section, name, default,
54 untrusted=untrusted)
54 untrusted=untrusted)
55
55
56 def configbool(self, section, name, default=False, untrusted=True):
56 def configbool(self, section, name, default=False, untrusted=True):
57 return self.repo.ui.configbool(section, name, default,
57 return self.repo.ui.configbool(section, name, default,
58 untrusted=untrusted)
58 untrusted=untrusted)
59
59
60 def configlist(self, section, name, default=None, untrusted=True):
60 def configlist(self, section, name, default=None, untrusted=True):
61 return self.repo.ui.configlist(section, name, default,
61 return self.repo.ui.configlist(section, name, default,
62 untrusted=untrusted)
62 untrusted=untrusted)
63
63
64 def refresh(self, request=None):
64 def refresh(self, request=None):
65 if request:
65 if request:
66 self.repo.ui.environ = request.env
66 self.repo.ui.environ = request.env
67 st = get_stat(self.repo.spath)
67 st = get_stat(self.repo.spath)
68 # compare changelog size in addition to mtime to catch
68 # compare changelog size in addition to mtime to catch
69 # rollbacks made less than a second ago
69 # rollbacks made less than a second ago
70 if st.st_mtime != self.mtime or st.st_size != self.size:
70 if st.st_mtime != self.mtime or st.st_size != self.size:
71 self.mtime = st.st_mtime
71 self.mtime = st.st_mtime
72 self.size = st.st_size
72 self.size = st.st_size
73 self.repo = hg.repository(self.repo.ui, self.repo.root)
73 self.repo = hg.repository(self.repo.ui, self.repo.root)
74 self.maxchanges = int(self.config("web", "maxchanges", 10))
74 self.maxchanges = int(self.config("web", "maxchanges", 10))
75 self.stripecount = int(self.config("web", "stripes", 1))
75 self.stripecount = int(self.config("web", "stripes", 1))
76 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
76 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
77 self.maxfiles = int(self.config("web", "maxfiles", 10))
77 self.maxfiles = int(self.config("web", "maxfiles", 10))
78 self.allowpull = self.configbool("web", "allowpull", True)
78 self.allowpull = self.configbool("web", "allowpull", True)
79 encoding.encoding = self.config("web", "encoding",
79 encoding.encoding = self.config("web", "encoding",
80 encoding.encoding)
80 encoding.encoding)
81
81
82 def run(self):
82 def run(self):
83 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
83 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
84 raise RuntimeError("This function is only intended to be "
84 raise RuntimeError("This function is only intended to be "
85 "called while running as a CGI script.")
85 "called while running as a CGI script.")
86 import mercurial.hgweb.wsgicgi as wsgicgi
86 import mercurial.hgweb.wsgicgi as wsgicgi
87 wsgicgi.launch(self)
87 wsgicgi.launch(self)
88
88
89 def __call__(self, env, respond):
89 def __call__(self, env, respond):
90 req = wsgirequest(env, respond)
90 req = wsgirequest(env, respond)
91 return self.run_wsgi(req)
91 return self.run_wsgi(req)
92
92
93 def run_wsgi(self, req):
93 def run_wsgi(self, req):
94
94
95 self.refresh(req)
95 self.refresh(req)
96
96
97 # work with CGI variables to create coherent structure
97 # work with CGI variables to create coherent structure
98 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
98 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
99
99
100 req.url = req.env['SCRIPT_NAME']
100 req.url = req.env['SCRIPT_NAME']
101 if not req.url.endswith('/'):
101 if not req.url.endswith('/'):
102 req.url += '/'
102 req.url += '/'
103 if 'REPO_NAME' in req.env:
103 if 'REPO_NAME' in req.env:
104 req.url += req.env['REPO_NAME'] + '/'
104 req.url += req.env['REPO_NAME'] + '/'
105
105
106 if 'PATH_INFO' in req.env:
106 if 'PATH_INFO' in req.env:
107 parts = req.env['PATH_INFO'].strip('/').split('/')
107 parts = req.env['PATH_INFO'].strip('/').split('/')
108 repo_parts = req.env.get('REPO_NAME', '').split('/')
108 repo_parts = req.env.get('REPO_NAME', '').split('/')
109 if parts[:len(repo_parts)] == repo_parts:
109 if parts[:len(repo_parts)] == repo_parts:
110 parts = parts[len(repo_parts):]
110 parts = parts[len(repo_parts):]
111 query = '/'.join(parts)
111 query = '/'.join(parts)
112 else:
112 else:
113 query = req.env['QUERY_STRING'].split('&', 1)[0]
113 query = req.env['QUERY_STRING'].split('&', 1)[0]
114 query = query.split(';', 1)[0]
114 query = query.split(';', 1)[0]
115
115
116 # process this if it's a protocol request
116 # process this if it's a protocol request
117 # protocol bits don't need to create any URLs
117 # protocol bits don't need to create any URLs
118 # and the clients always use the old URL structure
118 # and the clients always use the old URL structure
119
119
120 cmd = req.form.get('cmd', [''])[0]
120 cmd = req.form.get('cmd', [''])[0]
121 if protocol.iscmd(cmd):
121 if protocol.iscmd(cmd):
122 try:
122 try:
123 if query:
123 if query:
124 raise ErrorResponse(HTTP_NOT_FOUND)
124 raise ErrorResponse(HTTP_NOT_FOUND)
125 if cmd in perms:
125 if cmd in perms:
126 self.check_perm(req, perms[cmd])
126 self.check_perm(req, perms[cmd])
127 return protocol.call(self.repo, req, cmd)
127 return protocol.call(self.repo, req, cmd)
128 except ErrorResponse, inst:
128 except ErrorResponse, inst:
129 # A client that sends unbundle without 100-continue will
129 # A client that sends unbundle without 100-continue will
130 # break if we respond early.
130 # break if we respond early.
131 if (cmd == 'unbundle' and
131 if (cmd == 'unbundle' and
132 req.env.get('HTTP_EXPECT',
132 (req.env.get('HTTP_EXPECT',
133 '').lower() != '100-continue'):
133 '').lower() != '100-continue') or
134 req.env.get('X-HgHttp2', '')):
134 req.drain()
135 req.drain()
135 req.respond(inst, protocol.HGTYPE)
136 req.respond(inst, protocol.HGTYPE)
136 return '0\n%s\n' % inst.message
137 return '0\n%s\n' % inst.message
137
138
138 # translate user-visible url structure to internal structure
139 # translate user-visible url structure to internal structure
139
140
140 args = query.split('/', 2)
141 args = query.split('/', 2)
141 if 'cmd' not in req.form and args and args[0]:
142 if 'cmd' not in req.form and args and args[0]:
142
143
143 cmd = args.pop(0)
144 cmd = args.pop(0)
144 style = cmd.rfind('-')
145 style = cmd.rfind('-')
145 if style != -1:
146 if style != -1:
146 req.form['style'] = [cmd[:style]]
147 req.form['style'] = [cmd[:style]]
147 cmd = cmd[style + 1:]
148 cmd = cmd[style + 1:]
148
149
149 # avoid accepting e.g. style parameter as command
150 # avoid accepting e.g. style parameter as command
150 if hasattr(webcommands, cmd):
151 if hasattr(webcommands, cmd):
151 req.form['cmd'] = [cmd]
152 req.form['cmd'] = [cmd]
152 else:
153 else:
153 cmd = ''
154 cmd = ''
154
155
155 if cmd == 'static':
156 if cmd == 'static':
156 req.form['file'] = ['/'.join(args)]
157 req.form['file'] = ['/'.join(args)]
157 else:
158 else:
158 if args and args[0]:
159 if args and args[0]:
159 node = args.pop(0)
160 node = args.pop(0)
160 req.form['node'] = [node]
161 req.form['node'] = [node]
161 if args:
162 if args:
162 req.form['file'] = args
163 req.form['file'] = args
163
164
164 ua = req.env.get('HTTP_USER_AGENT', '')
165 ua = req.env.get('HTTP_USER_AGENT', '')
165 if cmd == 'rev' and 'mercurial' in ua:
166 if cmd == 'rev' and 'mercurial' in ua:
166 req.form['style'] = ['raw']
167 req.form['style'] = ['raw']
167
168
168 if cmd == 'archive':
169 if cmd == 'archive':
169 fn = req.form['node'][0]
170 fn = req.form['node'][0]
170 for type_, spec in self.archive_specs.iteritems():
171 for type_, spec in self.archive_specs.iteritems():
171 ext = spec[2]
172 ext = spec[2]
172 if fn.endswith(ext):
173 if fn.endswith(ext):
173 req.form['node'] = [fn[:-len(ext)]]
174 req.form['node'] = [fn[:-len(ext)]]
174 req.form['type'] = [type_]
175 req.form['type'] = [type_]
175
176
176 # process the web interface request
177 # process the web interface request
177
178
178 try:
179 try:
179 tmpl = self.templater(req)
180 tmpl = self.templater(req)
180 ctype = tmpl('mimetype', encoding=encoding.encoding)
181 ctype = tmpl('mimetype', encoding=encoding.encoding)
181 ctype = templater.stringify(ctype)
182 ctype = templater.stringify(ctype)
182
183
183 # check read permissions non-static content
184 # check read permissions non-static content
184 if cmd != 'static':
185 if cmd != 'static':
185 self.check_perm(req, None)
186 self.check_perm(req, None)
186
187
187 if cmd == '':
188 if cmd == '':
188 req.form['cmd'] = [tmpl.cache['default']]
189 req.form['cmd'] = [tmpl.cache['default']]
189 cmd = req.form['cmd'][0]
190 cmd = req.form['cmd'][0]
190
191
191 if self.configbool('web', 'cache', True):
192 if self.configbool('web', 'cache', True):
192 caching(self, req) # sets ETag header or raises NOT_MODIFIED
193 caching(self, req) # sets ETag header or raises NOT_MODIFIED
193 if cmd not in webcommands.__all__:
194 if cmd not in webcommands.__all__:
194 msg = 'no such method: %s' % cmd
195 msg = 'no such method: %s' % cmd
195 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
196 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
196 elif cmd == 'file' and 'raw' in req.form.get('style', []):
197 elif cmd == 'file' and 'raw' in req.form.get('style', []):
197 self.ctype = ctype
198 self.ctype = ctype
198 content = webcommands.rawfile(self, req, tmpl)
199 content = webcommands.rawfile(self, req, tmpl)
199 else:
200 else:
200 content = getattr(webcommands, cmd)(self, req, tmpl)
201 content = getattr(webcommands, cmd)(self, req, tmpl)
201 req.respond(HTTP_OK, ctype)
202 req.respond(HTTP_OK, ctype)
202
203
203 return content
204 return content
204
205
205 except error.LookupError, err:
206 except error.LookupError, err:
206 req.respond(HTTP_NOT_FOUND, ctype)
207 req.respond(HTTP_NOT_FOUND, ctype)
207 msg = str(err)
208 msg = str(err)
208 if 'manifest' not in msg:
209 if 'manifest' not in msg:
209 msg = 'revision not found: %s' % err.name
210 msg = 'revision not found: %s' % err.name
210 return tmpl('error', error=msg)
211 return tmpl('error', error=msg)
211 except (error.RepoError, error.RevlogError), inst:
212 except (error.RepoError, error.RevlogError), inst:
212 req.respond(HTTP_SERVER_ERROR, ctype)
213 req.respond(HTTP_SERVER_ERROR, ctype)
213 return tmpl('error', error=str(inst))
214 return tmpl('error', error=str(inst))
214 except ErrorResponse, inst:
215 except ErrorResponse, inst:
215 req.respond(inst, ctype)
216 req.respond(inst, ctype)
216 if inst.code == HTTP_NOT_MODIFIED:
217 if inst.code == HTTP_NOT_MODIFIED:
217 # Not allowed to return a body on a 304
218 # Not allowed to return a body on a 304
218 return ['']
219 return ['']
219 return tmpl('error', error=inst.message)
220 return tmpl('error', error=inst.message)
220
221
221 def templater(self, req):
222 def templater(self, req):
222
223
223 # determine scheme, port and server name
224 # determine scheme, port and server name
224 # this is needed to create absolute urls
225 # this is needed to create absolute urls
225
226
226 proto = req.env.get('wsgi.url_scheme')
227 proto = req.env.get('wsgi.url_scheme')
227 if proto == 'https':
228 if proto == 'https':
228 proto = 'https'
229 proto = 'https'
229 default_port = "443"
230 default_port = "443"
230 else:
231 else:
231 proto = 'http'
232 proto = 'http'
232 default_port = "80"
233 default_port = "80"
233
234
234 port = req.env["SERVER_PORT"]
235 port = req.env["SERVER_PORT"]
235 port = port != default_port and (":" + port) or ""
236 port = port != default_port and (":" + port) or ""
236 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
237 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
237 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
238 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
238 staticurl = self.config("web", "staticurl") or req.url + 'static/'
239 staticurl = self.config("web", "staticurl") or req.url + 'static/'
239 if not staticurl.endswith('/'):
240 if not staticurl.endswith('/'):
240 staticurl += '/'
241 staticurl += '/'
241
242
242 # some functions for the templater
243 # some functions for the templater
243
244
244 def header(**map):
245 def header(**map):
245 yield tmpl('header', encoding=encoding.encoding, **map)
246 yield tmpl('header', encoding=encoding.encoding, **map)
246
247
247 def footer(**map):
248 def footer(**map):
248 yield tmpl("footer", **map)
249 yield tmpl("footer", **map)
249
250
250 def motd(**map):
251 def motd(**map):
251 yield self.config("web", "motd", "")
252 yield self.config("web", "motd", "")
252
253
253 # figure out which style to use
254 # figure out which style to use
254
255
255 vars = {}
256 vars = {}
256 styles = (
257 styles = (
257 req.form.get('style', [None])[0],
258 req.form.get('style', [None])[0],
258 self.config('web', 'style'),
259 self.config('web', 'style'),
259 'paper',
260 'paper',
260 )
261 )
261 style, mapfile = templater.stylemap(styles, self.templatepath)
262 style, mapfile = templater.stylemap(styles, self.templatepath)
262 if style == styles[0]:
263 if style == styles[0]:
263 vars['style'] = style
264 vars['style'] = style
264
265
265 start = req.url[-1] == '?' and '&' or '?'
266 start = req.url[-1] == '?' and '&' or '?'
266 sessionvars = webutil.sessionvars(vars, start)
267 sessionvars = webutil.sessionvars(vars, start)
267
268
268 if not self.reponame:
269 if not self.reponame:
269 self.reponame = (self.config("web", "name")
270 self.reponame = (self.config("web", "name")
270 or req.env.get('REPO_NAME')
271 or req.env.get('REPO_NAME')
271 or req.url.strip('/') or self.repo.root)
272 or req.url.strip('/') or self.repo.root)
272
273
273 # create the templater
274 # create the templater
274
275
275 tmpl = templater.templater(mapfile,
276 tmpl = templater.templater(mapfile,
276 defaults={"url": req.url,
277 defaults={"url": req.url,
277 "logourl": logourl,
278 "logourl": logourl,
278 "staticurl": staticurl,
279 "staticurl": staticurl,
279 "urlbase": urlbase,
280 "urlbase": urlbase,
280 "repo": self.reponame,
281 "repo": self.reponame,
281 "header": header,
282 "header": header,
282 "footer": footer,
283 "footer": footer,
283 "motd": motd,
284 "motd": motd,
284 "sessionvars": sessionvars
285 "sessionvars": sessionvars
285 })
286 })
286 return tmpl
287 return tmpl
287
288
288 def archivelist(self, nodeid):
289 def archivelist(self, nodeid):
289 allowed = self.configlist("web", "allow_archive")
290 allowed = self.configlist("web", "allow_archive")
290 for i, spec in self.archive_specs.iteritems():
291 for i, spec in self.archive_specs.iteritems():
291 if i in allowed or self.configbool("web", "allow" + i):
292 if i in allowed or self.configbool("web", "allow" + i):
292 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
293 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
293
294
294 archive_specs = {
295 archive_specs = {
295 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
296 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
296 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
297 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
297 'zip': ('application/zip', 'zip', '.zip', None),
298 'zip': ('application/zip', 'zip', '.zip', None),
298 }
299 }
299
300
300 def check_perm(self, req, op):
301 def check_perm(self, req, op):
301 for hook in permhooks:
302 for hook in permhooks:
302 hook(self, req, op)
303 hook(self, req, op)
@@ -1,241 +1,242 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 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 from node import nullid
9 from node import nullid
10 from i18n import _
10 from i18n import _
11 import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
11 import changegroup, statichttprepo, error, httpconnection, url, util, wireproto
12 import os, urllib, urllib2, zlib, httplib
12 import os, urllib, urllib2, zlib, httplib
13 import errno, socket
13 import errno, socket
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 while chunk:
19 while chunk:
20 yield zd.decompress(chunk, 2**18)
20 yield zd.decompress(chunk, 2**18)
21 chunk = zd.unconsumed_tail
21 chunk = zd.unconsumed_tail
22 except httplib.HTTPException:
22 except httplib.HTTPException:
23 raise IOError(None, _('connection ended unexpectedly'))
23 raise IOError(None, _('connection ended unexpectedly'))
24 yield zd.flush()
24 yield zd.flush()
25
25
26 class httprepository(wireproto.wirerepository):
26 class httprepository(wireproto.wirerepository):
27 def __init__(self, ui, path):
27 def __init__(self, ui, path):
28 self.path = path
28 self.path = path
29 self.caps = None
29 self.caps = None
30 self.handler = None
30 self.handler = None
31 u = util.url(path)
31 u = util.url(path)
32 if u.query or u.fragment:
32 if u.query or u.fragment:
33 raise util.Abort(_('unsupported URL component: "%s"') %
33 raise util.Abort(_('unsupported URL component: "%s"') %
34 (u.query or u.fragment))
34 (u.query or u.fragment))
35
35
36 # urllib cannot handle URLs with embedded user or passwd
36 # urllib cannot handle URLs with embedded user or passwd
37 self._url, authinfo = u.authinfo()
37 self._url, authinfo = u.authinfo()
38
38
39 self.ui = ui
39 self.ui = ui
40 self.ui.debug('using %s\n' % self._url)
40 self.ui.debug('using %s\n' % self._url)
41
41
42 self.urlopener = url.opener(ui, authinfo)
42 self.urlopener = url.opener(ui, authinfo)
43
43
44 def __del__(self):
44 def __del__(self):
45 for h in self.urlopener.handlers:
45 for h in self.urlopener.handlers:
46 h.close()
46 h.close()
47 if hasattr(h, "close_all"):
47 if hasattr(h, "close_all"):
48 h.close_all()
48 h.close_all()
49
49
50 def url(self):
50 def url(self):
51 return self.path
51 return self.path
52
52
53 # look up capabilities only when needed
53 # look up capabilities only when needed
54
54
55 def _fetchcaps(self):
55 def _fetchcaps(self):
56 self.caps = set(self._call('capabilities').split())
56 self.caps = set(self._call('capabilities').split())
57
57
58 def get_caps(self):
58 def get_caps(self):
59 if self.caps is None:
59 if self.caps is None:
60 try:
60 try:
61 self._fetchcaps()
61 self._fetchcaps()
62 except error.RepoError:
62 except error.RepoError:
63 self.caps = set()
63 self.caps = set()
64 self.ui.debug('capabilities: %s\n' %
64 self.ui.debug('capabilities: %s\n' %
65 (' '.join(self.caps or ['none'])))
65 (' '.join(self.caps or ['none'])))
66 return self.caps
66 return self.caps
67
67
68 capabilities = property(get_caps)
68 capabilities = property(get_caps)
69
69
70 def lock(self):
70 def lock(self):
71 raise util.Abort(_('operation not supported over http'))
71 raise util.Abort(_('operation not supported over http'))
72
72
73 def _callstream(self, cmd, **args):
73 def _callstream(self, cmd, **args):
74 if cmd == 'pushkey':
74 if cmd == 'pushkey':
75 args['data'] = ''
75 args['data'] = ''
76 data = args.pop('data', None)
76 data = args.pop('data', None)
77 headers = args.pop('headers', {})
77 headers = args.pop('headers', {})
78
78
79 if data and self.ui.configbool('ui', 'usehttp2', False):
79 if data and self.ui.configbool('ui', 'usehttp2', False):
80 headers['Expect'] = '100-Continue'
80 headers['Expect'] = '100-Continue'
81 headers['X-HgHttp2'] = '1'
81
82
82 self.ui.debug("sending %s command\n" % cmd)
83 self.ui.debug("sending %s command\n" % cmd)
83 q = [('cmd', cmd)]
84 q = [('cmd', cmd)]
84 headersize = 0
85 headersize = 0
85 if len(args) > 0:
86 if len(args) > 0:
86 httpheader = self.capable('httpheader')
87 httpheader = self.capable('httpheader')
87 if httpheader:
88 if httpheader:
88 headersize = int(httpheader.split(',')[0])
89 headersize = int(httpheader.split(',')[0])
89 if headersize > 0:
90 if headersize > 0:
90 # The headers can typically carry more data than the URL.
91 # The headers can typically carry more data than the URL.
91 encargs = urllib.urlencode(sorted(args.items()))
92 encargs = urllib.urlencode(sorted(args.items()))
92 headerfmt = 'X-HgArg-%s'
93 headerfmt = 'X-HgArg-%s'
93 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
94 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
94 headernum = 0
95 headernum = 0
95 for i in xrange(0, len(encargs), contentlen):
96 for i in xrange(0, len(encargs), contentlen):
96 headernum += 1
97 headernum += 1
97 header = headerfmt % str(headernum)
98 header = headerfmt % str(headernum)
98 headers[header] = encargs[i:i + contentlen]
99 headers[header] = encargs[i:i + contentlen]
99 varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
100 varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
100 headers['Vary'] = ','.join(varyheaders)
101 headers['Vary'] = ','.join(varyheaders)
101 else:
102 else:
102 q += sorted(args.items())
103 q += sorted(args.items())
103 qs = '?%s' % urllib.urlencode(q)
104 qs = '?%s' % urllib.urlencode(q)
104 cu = "%s%s" % (self._url, qs)
105 cu = "%s%s" % (self._url, qs)
105 req = urllib2.Request(cu, data, headers)
106 req = urllib2.Request(cu, data, headers)
106 if data is not None:
107 if data is not None:
107 # len(data) is broken if data doesn't fit into Py_ssize_t
108 # len(data) is broken if data doesn't fit into Py_ssize_t
108 # add the header ourself to avoid OverflowError
109 # add the header ourself to avoid OverflowError
109 size = data.__len__()
110 size = data.__len__()
110 self.ui.debug("sending %s bytes\n" % size)
111 self.ui.debug("sending %s bytes\n" % size)
111 req.add_unredirected_header('Content-Length', '%d' % size)
112 req.add_unredirected_header('Content-Length', '%d' % size)
112 try:
113 try:
113 resp = self.urlopener.open(req)
114 resp = self.urlopener.open(req)
114 except urllib2.HTTPError, inst:
115 except urllib2.HTTPError, inst:
115 if inst.code == 401:
116 if inst.code == 401:
116 raise util.Abort(_('authorization failed'))
117 raise util.Abort(_('authorization failed'))
117 raise
118 raise
118 except httplib.HTTPException, inst:
119 except httplib.HTTPException, inst:
119 self.ui.debug('http error while sending %s command\n' % cmd)
120 self.ui.debug('http error while sending %s command\n' % cmd)
120 self.ui.traceback()
121 self.ui.traceback()
121 raise IOError(None, inst)
122 raise IOError(None, inst)
122 except IndexError:
123 except IndexError:
123 # this only happens with Python 2.3, later versions raise URLError
124 # this only happens with Python 2.3, later versions raise URLError
124 raise util.Abort(_('http error, possibly caused by proxy setting'))
125 raise util.Abort(_('http error, possibly caused by proxy setting'))
125 # record the url we got redirected to
126 # record the url we got redirected to
126 resp_url = resp.geturl()
127 resp_url = resp.geturl()
127 if resp_url.endswith(qs):
128 if resp_url.endswith(qs):
128 resp_url = resp_url[:-len(qs)]
129 resp_url = resp_url[:-len(qs)]
129 if self._url.rstrip('/') != resp_url.rstrip('/'):
130 if self._url.rstrip('/') != resp_url.rstrip('/'):
130 if not self.ui.quiet:
131 if not self.ui.quiet:
131 self.ui.warn(_('real URL is %s\n') % resp_url)
132 self.ui.warn(_('real URL is %s\n') % resp_url)
132 self._url = resp_url
133 self._url = resp_url
133 try:
134 try:
134 proto = resp.getheader('content-type')
135 proto = resp.getheader('content-type')
135 except AttributeError:
136 except AttributeError:
136 proto = resp.headers.get('content-type', '')
137 proto = resp.headers.get('content-type', '')
137
138
138 safeurl = util.hidepassword(self._url)
139 safeurl = util.hidepassword(self._url)
139 # accept old "text/plain" and "application/hg-changegroup" for now
140 # accept old "text/plain" and "application/hg-changegroup" for now
140 if not (proto.startswith('application/mercurial-') or
141 if not (proto.startswith('application/mercurial-') or
141 proto.startswith('text/plain') or
142 proto.startswith('text/plain') or
142 proto.startswith('application/hg-changegroup')):
143 proto.startswith('application/hg-changegroup')):
143 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
144 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
144 raise error.RepoError(
145 raise error.RepoError(
145 _("'%s' does not appear to be an hg repository:\n"
146 _("'%s' does not appear to be an hg repository:\n"
146 "---%%<--- (%s)\n%s\n---%%<---\n")
147 "---%%<--- (%s)\n%s\n---%%<---\n")
147 % (safeurl, proto or 'no content-type', resp.read()))
148 % (safeurl, proto or 'no content-type', resp.read()))
148
149
149 if proto.startswith('application/mercurial-'):
150 if proto.startswith('application/mercurial-'):
150 try:
151 try:
151 version = proto.split('-', 1)[1]
152 version = proto.split('-', 1)[1]
152 version_info = tuple([int(n) for n in version.split('.')])
153 version_info = tuple([int(n) for n in version.split('.')])
153 except ValueError:
154 except ValueError:
154 raise error.RepoError(_("'%s' sent a broken Content-Type "
155 raise error.RepoError(_("'%s' sent a broken Content-Type "
155 "header (%s)") % (safeurl, proto))
156 "header (%s)") % (safeurl, proto))
156 if version_info > (0, 1):
157 if version_info > (0, 1):
157 raise error.RepoError(_("'%s' uses newer protocol %s") %
158 raise error.RepoError(_("'%s' uses newer protocol %s") %
158 (safeurl, version))
159 (safeurl, version))
159
160
160 return resp
161 return resp
161
162
162 def _call(self, cmd, **args):
163 def _call(self, cmd, **args):
163 fp = self._callstream(cmd, **args)
164 fp = self._callstream(cmd, **args)
164 try:
165 try:
165 return fp.read()
166 return fp.read()
166 finally:
167 finally:
167 # if using keepalive, allow connection to be reused
168 # if using keepalive, allow connection to be reused
168 fp.close()
169 fp.close()
169
170
170 def _callpush(self, cmd, cg, **args):
171 def _callpush(self, cmd, cg, **args):
171 # have to stream bundle to a temp file because we do not have
172 # have to stream bundle to a temp file because we do not have
172 # http 1.1 chunked transfer.
173 # http 1.1 chunked transfer.
173
174
174 types = self.capable('unbundle')
175 types = self.capable('unbundle')
175 try:
176 try:
176 types = types.split(',')
177 types = types.split(',')
177 except AttributeError:
178 except AttributeError:
178 # servers older than d1b16a746db6 will send 'unbundle' as a
179 # servers older than d1b16a746db6 will send 'unbundle' as a
179 # boolean capability. They only support headerless/uncompressed
180 # boolean capability. They only support headerless/uncompressed
180 # bundles.
181 # bundles.
181 types = [""]
182 types = [""]
182 for x in types:
183 for x in types:
183 if x in changegroup.bundletypes:
184 if x in changegroup.bundletypes:
184 type = x
185 type = x
185 break
186 break
186
187
187 tempname = changegroup.writebundle(cg, None, type)
188 tempname = changegroup.writebundle(cg, None, type)
188 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
189 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
189 headers = {'Content-Type': 'application/mercurial-0.1'}
190 headers = {'Content-Type': 'application/mercurial-0.1'}
190
191
191 try:
192 try:
192 try:
193 try:
193 r = self._call(cmd, data=fp, headers=headers, **args)
194 r = self._call(cmd, data=fp, headers=headers, **args)
194 vals = r.split('\n', 1)
195 vals = r.split('\n', 1)
195 if len(vals) < 2:
196 if len(vals) < 2:
196 raise error.ResponseError(_("unexpected response:"), r)
197 raise error.ResponseError(_("unexpected response:"), r)
197 return vals
198 return vals
198 except socket.error, err:
199 except socket.error, err:
199 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
200 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
200 raise util.Abort(_('push failed: %s') % err.args[1])
201 raise util.Abort(_('push failed: %s') % err.args[1])
201 raise util.Abort(err.args[1])
202 raise util.Abort(err.args[1])
202 finally:
203 finally:
203 fp.close()
204 fp.close()
204 os.unlink(tempname)
205 os.unlink(tempname)
205
206
206 def _abort(self, exception):
207 def _abort(self, exception):
207 raise exception
208 raise exception
208
209
209 def _decompress(self, stream):
210 def _decompress(self, stream):
210 return util.chunkbuffer(zgenerator(stream))
211 return util.chunkbuffer(zgenerator(stream))
211
212
212 class httpsrepository(httprepository):
213 class httpsrepository(httprepository):
213 def __init__(self, ui, path):
214 def __init__(self, ui, path):
214 if not url.has_https:
215 if not url.has_https:
215 raise util.Abort(_('Python support for SSL and HTTPS '
216 raise util.Abort(_('Python support for SSL and HTTPS '
216 'is not installed'))
217 'is not installed'))
217 httprepository.__init__(self, ui, path)
218 httprepository.__init__(self, ui, path)
218
219
219 def instance(ui, path, create):
220 def instance(ui, path, create):
220 if create:
221 if create:
221 raise util.Abort(_('cannot create new http repository'))
222 raise util.Abort(_('cannot create new http repository'))
222 try:
223 try:
223 if path.startswith('https:'):
224 if path.startswith('https:'):
224 inst = httpsrepository(ui, path)
225 inst = httpsrepository(ui, path)
225 else:
226 else:
226 inst = httprepository(ui, path)
227 inst = httprepository(ui, path)
227 try:
228 try:
228 # Try to do useful work when checking compatibility.
229 # Try to do useful work when checking compatibility.
229 # Usually saves a roundtrip since we want the caps anyway.
230 # Usually saves a roundtrip since we want the caps anyway.
230 inst._fetchcaps()
231 inst._fetchcaps()
231 except error.RepoError:
232 except error.RepoError:
232 # No luck, try older compatibility check.
233 # No luck, try older compatibility check.
233 inst.between([(nullid, nullid)])
234 inst.between([(nullid, nullid)])
234 return inst
235 return inst
235 except error.RepoError, httpexception:
236 except error.RepoError, httpexception:
236 try:
237 try:
237 r = statichttprepo.instance(ui, "static-" + path, create)
238 r = statichttprepo.instance(ui, "static-" + path, create)
238 ui.note('(falling back to static-http)\n')
239 ui.note('(falling back to static-http)\n')
239 return r
240 return r
240 except error.RepoError:
241 except error.RepoError:
241 raise httpexception # use the original http RepoError instead
242 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now