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