##// END OF EJS Templates
hgweb: rewrite most obviously-native-strings to be native strings...
Augie Fackler -
r34705:c5138087 default
parent child Browse files
Show More
@@ -1,95 +1,97
1 # hgweb/__init__.py - web interface to a mercurial repository
1 # hgweb/__init__.py - web interface to a mercurial 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 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005 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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12
12
13 from ..i18n import _
13 from ..i18n import _
14
14
15 from .. import (
15 from .. import (
16 error,
16 error,
17 pycompat,
17 util,
18 util,
18 )
19 )
19
20
20 from . import (
21 from . import (
21 hgweb_mod,
22 hgweb_mod,
22 hgwebdir_mod,
23 hgwebdir_mod,
23 server,
24 server,
24 )
25 )
25
26
26 def hgweb(config, name=None, baseui=None):
27 def hgweb(config, name=None, baseui=None):
27 '''create an hgweb wsgi object
28 '''create an hgweb wsgi object
28
29
29 config can be one of:
30 config can be one of:
30 - repo object (single repo view)
31 - repo object (single repo view)
31 - path to repo (single repo view)
32 - path to repo (single repo view)
32 - path to config file (multi-repo view)
33 - path to config file (multi-repo view)
33 - dict of virtual:real pairs (multi-repo view)
34 - dict of virtual:real pairs (multi-repo view)
34 - list of virtual:real tuples (multi-repo view)
35 - list of virtual:real tuples (multi-repo view)
35 '''
36 '''
36
37
37 if ((isinstance(config, str) and not os.path.isdir(config)) or
38 if ((isinstance(config, str) and not os.path.isdir(config)) or
38 isinstance(config, dict) or isinstance(config, list)):
39 isinstance(config, dict) or isinstance(config, list)):
39 # create a multi-dir interface
40 # create a multi-dir interface
40 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
41 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
41 return hgweb_mod.hgweb(config, name=name, baseui=baseui)
42 return hgweb_mod.hgweb(config, name=name, baseui=baseui)
42
43
43 def hgwebdir(config, baseui=None):
44 def hgwebdir(config, baseui=None):
44 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
45 return hgwebdir_mod.hgwebdir(config, baseui=baseui)
45
46
46 class httpservice(object):
47 class httpservice(object):
47 def __init__(self, ui, app, opts):
48 def __init__(self, ui, app, opts):
48 self.ui = ui
49 self.ui = ui
49 self.app = app
50 self.app = app
50 self.opts = opts
51 self.opts = opts
51
52
52 def init(self):
53 def init(self):
53 util.setsignalhandler()
54 util.setsignalhandler()
54 self.httpd = server.create_server(self.ui, self.app)
55 self.httpd = server.create_server(self.ui, self.app)
55
56
56 if self.opts['port'] and not self.ui.verbose:
57 if self.opts['port'] and not self.ui.verbose:
57 return
58 return
58
59
59 if self.httpd.prefix:
60 if self.httpd.prefix:
60 prefix = self.httpd.prefix.strip('/') + '/'
61 prefix = self.httpd.prefix.strip('/') + '/'
61 else:
62 else:
62 prefix = ''
63 prefix = ''
63
64
64 port = ':%d' % self.httpd.port
65 port = r':%d' % self.httpd.port
65 if port == ':80':
66 if port == r':80':
66 port = ''
67 port = r''
67
68
68 bindaddr = self.httpd.addr
69 bindaddr = self.httpd.addr
69 if bindaddr == '0.0.0.0':
70 if bindaddr == r'0.0.0.0':
70 bindaddr = '*'
71 bindaddr = r'*'
71 elif ':' in bindaddr: # IPv6
72 elif r':' in bindaddr: # IPv6
72 bindaddr = '[%s]' % bindaddr
73 bindaddr = r'[%s]' % bindaddr
73
74
74 fqaddr = self.httpd.fqaddr
75 fqaddr = self.httpd.fqaddr
75 if ':' in fqaddr:
76 if r':' in fqaddr:
76 fqaddr = '[%s]' % fqaddr
77 fqaddr = r'[%s]' % fqaddr
77 if self.opts['port']:
78 if self.opts['port']:
78 write = self.ui.status
79 write = self.ui.status
79 else:
80 else:
80 write = self.ui.write
81 write = self.ui.write
81 write(_('listening at http://%s%s/%s (bound to %s:%d)\n') %
82 write(_('listening at http://%s%s/%s (bound to %s:%d)\n') %
82 (fqaddr, port, prefix, bindaddr, self.httpd.port))
83 (pycompat.sysbytes(fqaddr), pycompat.sysbytes(port),
84 prefix, pycompat.sysbytes(bindaddr), self.httpd.port))
83 self.ui.flush() # avoid buffering of status message
85 self.ui.flush() # avoid buffering of status message
84
86
85 def run(self):
87 def run(self):
86 self.httpd.serve_forever()
88 self.httpd.serve_forever()
87
89
88 def createapp(baseui, repo, webconf):
90 def createapp(baseui, repo, webconf):
89 if webconf:
91 if webconf:
90 return hgwebdir_mod.hgwebdir(webconf, baseui=baseui)
92 return hgwebdir_mod.hgwebdir(webconf, baseui=baseui)
91 else:
93 else:
92 if not repo:
94 if not repo:
93 raise error.RepoError(_("there is no Mercurial repository"
95 raise error.RepoError(_("there is no Mercurial repository"
94 " here (.hg not found)"))
96 " here (.hg not found)"))
95 return hgweb_mod.hgweb(repo, baseui=baseui)
97 return hgweb_mod.hgweb(repo, baseui=baseui)
@@ -1,492 +1,491
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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 cspvalues,
23 permhooks,
23 permhooks,
24 )
24 )
25 from .request import wsgirequest
25 from .request import wsgirequest
26
26
27 from .. import (
27 from .. import (
28 encoding,
28 encoding,
29 error,
29 error,
30 hg,
30 hg,
31 hook,
31 hook,
32 profiling,
32 profiling,
33 pycompat,
33 pycompat,
34 repoview,
34 repoview,
35 templatefilters,
35 templatefilters,
36 templater,
36 templater,
37 ui as uimod,
37 ui as uimod,
38 util,
38 util,
39 )
39 )
40
40
41 from . import (
41 from . import (
42 protocol,
42 protocol,
43 webcommands,
43 webcommands,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 perms = {
48 perms = {
49 'changegroup': 'pull',
49 'changegroup': 'pull',
50 'changegroupsubset': 'pull',
50 'changegroupsubset': 'pull',
51 'getbundle': 'pull',
51 'getbundle': 'pull',
52 'stream_out': 'pull',
52 'stream_out': 'pull',
53 'listkeys': 'pull',
53 'listkeys': 'pull',
54 'unbundle': 'push',
54 'unbundle': 'push',
55 'pushkey': 'push',
55 'pushkey': 'push',
56 }
56 }
57
57
58 archivespecs = util.sortdict((
58 archivespecs = util.sortdict((
59 ('zip', ('application/zip', 'zip', '.zip', None)),
59 ('zip', ('application/zip', 'zip', '.zip', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 ))
62 ))
63
63
64 def getstyle(req, configfn, templatepath):
64 def getstyle(req, configfn, templatepath):
65 fromreq = req.form.get('style', [None])[0]
65 fromreq = req.form.get('style', [None])[0]
66 if fromreq is not None:
66 if fromreq is not None:
67 fromreq = pycompat.sysbytes(fromreq)
67 fromreq = pycompat.sysbytes(fromreq)
68 styles = (
68 styles = (
69 fromreq,
69 fromreq,
70 configfn('web', 'style'),
70 configfn('web', 'style'),
71 'paper',
71 'paper',
72 )
72 )
73 return styles, templater.stylemap(styles, templatepath)
73 return styles, templater.stylemap(styles, templatepath)
74
74
75 def makebreadcrumb(url, prefix=''):
75 def makebreadcrumb(url, prefix=''):
76 '''Return a 'URL breadcrumb' list
76 '''Return a 'URL breadcrumb' list
77
77
78 A 'URL breadcrumb' is a list of URL-name pairs,
78 A 'URL breadcrumb' is a list of URL-name pairs,
79 corresponding to each of the path items on a URL.
79 corresponding to each of the path items on a URL.
80 This can be used to create path navigation entries.
80 This can be used to create path navigation entries.
81 '''
81 '''
82 if url.endswith('/'):
82 if url.endswith('/'):
83 url = url[:-1]
83 url = url[:-1]
84 if prefix:
84 if prefix:
85 url = '/' + prefix + url
85 url = '/' + prefix + url
86 relpath = url
86 relpath = url
87 if relpath.startswith('/'):
87 if relpath.startswith('/'):
88 relpath = relpath[1:]
88 relpath = relpath[1:]
89
89
90 breadcrumb = []
90 breadcrumb = []
91 urlel = url
91 urlel = url
92 pathitems = [''] + relpath.split('/')
92 pathitems = [''] + relpath.split('/')
93 for pathel in reversed(pathitems):
93 for pathel in reversed(pathitems):
94 if not pathel or not urlel:
94 if not pathel or not urlel:
95 break
95 break
96 breadcrumb.append({'url': urlel, 'name': pathel})
96 breadcrumb.append({'url': urlel, 'name': pathel})
97 urlel = os.path.dirname(urlel)
97 urlel = os.path.dirname(urlel)
98 return reversed(breadcrumb)
98 return reversed(breadcrumb)
99
99
100 class requestcontext(object):
100 class requestcontext(object):
101 """Holds state/context for an individual request.
101 """Holds state/context for an individual request.
102
102
103 Servers can be multi-threaded. Holding state on the WSGI application
103 Servers can be multi-threaded. Holding state on the WSGI application
104 is prone to race conditions. Instances of this class exist to hold
104 is prone to race conditions. Instances of this class exist to hold
105 mutable and race-free state for requests.
105 mutable and race-free state for requests.
106 """
106 """
107 def __init__(self, app, repo):
107 def __init__(self, app, repo):
108 self.repo = repo
108 self.repo = repo
109 self.reponame = app.reponame
109 self.reponame = app.reponame
110
110
111 self.archivespecs = archivespecs
111 self.archivespecs = archivespecs
112
112
113 self.maxchanges = self.configint('web', 'maxchanges')
113 self.maxchanges = self.configint('web', 'maxchanges')
114 self.stripecount = self.configint('web', 'stripes')
114 self.stripecount = self.configint('web', 'stripes')
115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
116 self.maxfiles = self.configint('web', 'maxfiles')
116 self.maxfiles = self.configint('web', 'maxfiles')
117 self.allowpull = self.configbool('web', 'allowpull')
117 self.allowpull = self.configbool('web', 'allowpull')
118
118
119 # we use untrusted=False to prevent a repo owner from using
119 # we use untrusted=False to prevent a repo owner from using
120 # web.templates in .hg/hgrc to get access to any file readable
120 # web.templates in .hg/hgrc to get access to any file readable
121 # by the user running the CGI script
121 # by the user running the CGI script
122 self.templatepath = self.config('web', 'templates', untrusted=False)
122 self.templatepath = self.config('web', 'templates', untrusted=False)
123
123
124 # This object is more expensive to build than simple config values.
124 # This object is more expensive to build than simple config values.
125 # It is shared across requests. The app will replace the object
125 # It is shared across requests. The app will replace the object
126 # if it is updated. Since this is a reference and nothing should
126 # if it is updated. Since this is a reference and nothing should
127 # modify the underlying object, it should be constant for the lifetime
127 # modify the underlying object, it should be constant for the lifetime
128 # of the request.
128 # of the request.
129 self.websubtable = app.websubtable
129 self.websubtable = app.websubtable
130
130
131 self.csp, self.nonce = cspvalues(self.repo.ui)
131 self.csp, self.nonce = cspvalues(self.repo.ui)
132
132
133 # Trust the settings from the .hg/hgrc files by default.
133 # Trust the settings from the .hg/hgrc files by default.
134 def config(self, section, name, default=uimod._unset, untrusted=True):
134 def config(self, section, name, default=uimod._unset, untrusted=True):
135 return self.repo.ui.config(section, name, default,
135 return self.repo.ui.config(section, name, default,
136 untrusted=untrusted)
136 untrusted=untrusted)
137
137
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
139 return self.repo.ui.configbool(section, name, default,
139 return self.repo.ui.configbool(section, name, default,
140 untrusted=untrusted)
140 untrusted=untrusted)
141
141
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
142 def configint(self, section, name, default=uimod._unset, untrusted=True):
143 return self.repo.ui.configint(section, name, default,
143 return self.repo.ui.configint(section, name, default,
144 untrusted=untrusted)
144 untrusted=untrusted)
145
145
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
147 return self.repo.ui.configlist(section, name, default,
147 return self.repo.ui.configlist(section, name, default,
148 untrusted=untrusted)
148 untrusted=untrusted)
149
149
150 def archivelist(self, nodeid):
150 def archivelist(self, nodeid):
151 allowed = self.configlist('web', 'allow_archive')
151 allowed = self.configlist('web', 'allow_archive')
152 for typ, spec in self.archivespecs.iteritems():
152 for typ, spec in self.archivespecs.iteritems():
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
155
155
156 def templater(self, req):
156 def templater(self, req):
157 # determine scheme, port and server name
157 # determine scheme, port and server name
158 # this is needed to create absolute urls
158 # this is needed to create absolute urls
159
159
160 proto = req.env.get('wsgi.url_scheme')
160 proto = req.env.get('wsgi.url_scheme')
161 if proto == 'https':
161 if proto == 'https':
162 proto = 'https'
162 proto = 'https'
163 default_port = '443'
163 default_port = '443'
164 else:
164 else:
165 proto = 'http'
165 proto = 'http'
166 default_port = '80'
166 default_port = '80'
167
167
168 port = req.env['SERVER_PORT']
168 port = req.env[r'SERVER_PORT']
169 port = port != default_port and (':' + port) or ''
169 port = port != default_port and (r':' + port) or r''
170 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
170 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
171 logourl = self.config('web', 'logourl')
171 logourl = self.config('web', 'logourl')
172 logoimg = self.config('web', 'logoimg')
172 logoimg = self.config('web', 'logoimg')
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
174 if not staticurl.endswith('/'):
174 if not staticurl.endswith('/'):
175 staticurl += '/'
175 staticurl += '/'
176
176
177 # some functions for the templater
177 # some functions for the templater
178
178
179 def motd(**map):
179 def motd(**map):
180 yield self.config('web', 'motd')
180 yield self.config('web', 'motd')
181
181
182 # figure out which style to use
182 # figure out which style to use
183
183
184 vars = {}
184 vars = {}
185 styles, (style, mapfile) = getstyle(req, self.config,
185 styles, (style, mapfile) = getstyle(req, self.config,
186 self.templatepath)
186 self.templatepath)
187 if style == styles[0]:
187 if style == styles[0]:
188 vars['style'] = style
188 vars['style'] = style
189
189
190 start = r'&' if req.url[-1] == r'?' else r'?'
190 start = r'&' if req.url[-1] == r'?' else r'?'
191 sessionvars = webutil.sessionvars(vars, start)
191 sessionvars = webutil.sessionvars(vars, start)
192
192
193 if not self.reponame:
193 if not self.reponame:
194 self.reponame = (self.config('web', 'name', '')
194 self.reponame = (self.config('web', 'name', '')
195 or req.env.get('REPO_NAME')
195 or req.env.get('REPO_NAME')
196 or req.url.strip('/') or self.repo.root)
196 or req.url.strip('/') or self.repo.root)
197
197
198 def websubfilter(text):
198 def websubfilter(text):
199 return templatefilters.websub(text, self.websubtable)
199 return templatefilters.websub(text, self.websubtable)
200
200
201 # create the templater
201 # create the templater
202
202
203 defaults = {
203 defaults = {
204 'url': req.url,
204 'url': req.url,
205 'logourl': logourl,
205 'logourl': logourl,
206 'logoimg': logoimg,
206 'logoimg': logoimg,
207 'staticurl': staticurl,
207 'staticurl': staticurl,
208 'urlbase': urlbase,
208 'urlbase': urlbase,
209 'repo': self.reponame,
209 'repo': self.reponame,
210 'encoding': encoding.encoding,
210 'encoding': encoding.encoding,
211 'motd': motd,
211 'motd': motd,
212 'sessionvars': sessionvars,
212 'sessionvars': sessionvars,
213 'pathdef': makebreadcrumb(req.url),
213 'pathdef': makebreadcrumb(req.url),
214 'style': style,
214 'style': style,
215 'nonce': self.nonce,
215 'nonce': self.nonce,
216 }
216 }
217 tmpl = templater.templater.frommapfile(mapfile,
217 tmpl = templater.templater.frommapfile(mapfile,
218 filters={'websub': websubfilter},
218 filters={'websub': websubfilter},
219 defaults=defaults)
219 defaults=defaults)
220 return tmpl
220 return tmpl
221
221
222
222
223 class hgweb(object):
223 class hgweb(object):
224 """HTTP server for individual repositories.
224 """HTTP server for individual repositories.
225
225
226 Instances of this class serve HTTP responses for a particular
226 Instances of this class serve HTTP responses for a particular
227 repository.
227 repository.
228
228
229 Instances are typically used as WSGI applications.
229 Instances are typically used as WSGI applications.
230
230
231 Some servers are multi-threaded. On these servers, there may
231 Some servers are multi-threaded. On these servers, there may
232 be multiple active threads inside __call__.
232 be multiple active threads inside __call__.
233 """
233 """
234 def __init__(self, repo, name=None, baseui=None):
234 def __init__(self, repo, name=None, baseui=None):
235 if isinstance(repo, str):
235 if isinstance(repo, str):
236 if baseui:
236 if baseui:
237 u = baseui.copy()
237 u = baseui.copy()
238 else:
238 else:
239 u = uimod.ui.load()
239 u = uimod.ui.load()
240 r = hg.repository(u, repo)
240 r = hg.repository(u, repo)
241 else:
241 else:
242 # we trust caller to give us a private copy
242 # we trust caller to give us a private copy
243 r = repo
243 r = repo
244
244
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
249 # resolve file patterns relative to repo root
249 # resolve file patterns relative to repo root
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
252 # displaying bundling progress bar while serving feel wrong and may
252 # displaying bundling progress bar while serving feel wrong and may
253 # break some wsgi implementation.
253 # break some wsgi implementation.
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
257 self._lastrepo = self._repos[0]
257 self._lastrepo = self._repos[0]
258 hook.redirect(True)
258 hook.redirect(True)
259 self.reponame = name
259 self.reponame = name
260
260
261 def _webifyrepo(self, repo):
261 def _webifyrepo(self, repo):
262 repo = getwebview(repo)
262 repo = getwebview(repo)
263 self.websubtable = webutil.getwebsubs(repo)
263 self.websubtable = webutil.getwebsubs(repo)
264 return repo
264 return repo
265
265
266 @contextlib.contextmanager
266 @contextlib.contextmanager
267 def _obtainrepo(self):
267 def _obtainrepo(self):
268 """Obtain a repo unique to the caller.
268 """Obtain a repo unique to the caller.
269
269
270 Internally we maintain a stack of cachedlocalrepo instances
270 Internally we maintain a stack of cachedlocalrepo instances
271 to be handed out. If one is available, we pop it and return it,
271 to be handed out. If one is available, we pop it and return it,
272 ensuring it is up to date in the process. If one is not available,
272 ensuring it is up to date in the process. If one is not available,
273 we clone the most recently used repo instance and return it.
273 we clone the most recently used repo instance and return it.
274
274
275 It is currently possible for the stack to grow without bounds
275 It is currently possible for the stack to grow without bounds
276 if the server allows infinite threads. However, servers should
276 if the server allows infinite threads. However, servers should
277 have a thread limit, thus establishing our limit.
277 have a thread limit, thus establishing our limit.
278 """
278 """
279 if self._repos:
279 if self._repos:
280 cached = self._repos.pop()
280 cached = self._repos.pop()
281 r, created = cached.fetch()
281 r, created = cached.fetch()
282 else:
282 else:
283 cached = self._lastrepo.copy()
283 cached = self._lastrepo.copy()
284 r, created = cached.fetch()
284 r, created = cached.fetch()
285 if created:
285 if created:
286 r = self._webifyrepo(r)
286 r = self._webifyrepo(r)
287
287
288 self._lastrepo = cached
288 self._lastrepo = cached
289 self.mtime = cached.mtime
289 self.mtime = cached.mtime
290 try:
290 try:
291 yield r
291 yield r
292 finally:
292 finally:
293 self._repos.append(cached)
293 self._repos.append(cached)
294
294
295 def run(self):
295 def run(self):
296 """Start a server from CGI environment.
296 """Start a server from CGI environment.
297
297
298 Modern servers should be using WSGI and should avoid this
298 Modern servers should be using WSGI and should avoid this
299 method, if possible.
299 method, if possible.
300 """
300 """
301 if not encoding.environ.get('GATEWAY_INTERFACE',
301 if not encoding.environ.get('GATEWAY_INTERFACE',
302 '').startswith("CGI/1."):
302 '').startswith("CGI/1."):
303 raise RuntimeError("This function is only intended to be "
303 raise RuntimeError("This function is only intended to be "
304 "called while running as a CGI script.")
304 "called while running as a CGI script.")
305 wsgicgi.launch(self)
305 wsgicgi.launch(self)
306
306
307 def __call__(self, env, respond):
307 def __call__(self, env, respond):
308 """Run the WSGI application.
308 """Run the WSGI application.
309
309
310 This may be called by multiple threads.
310 This may be called by multiple threads.
311 """
311 """
312 req = wsgirequest(env, respond)
312 req = wsgirequest(env, respond)
313 return self.run_wsgi(req)
313 return self.run_wsgi(req)
314
314
315 def run_wsgi(self, req):
315 def run_wsgi(self, req):
316 """Internal method to run the WSGI application.
316 """Internal method to run the WSGI application.
317
317
318 This is typically only called by Mercurial. External consumers
318 This is typically only called by Mercurial. External consumers
319 should be using instances of this class as the WSGI application.
319 should be using instances of this class as the WSGI application.
320 """
320 """
321 with self._obtainrepo() as repo:
321 with self._obtainrepo() as repo:
322 profile = repo.ui.configbool('profiling', 'enabled')
322 profile = repo.ui.configbool('profiling', 'enabled')
323 with profiling.profile(repo.ui, enabled=profile):
323 with profiling.profile(repo.ui, enabled=profile):
324 for r in self._runwsgi(req, repo):
324 for r in self._runwsgi(req, repo):
325 yield r
325 yield r
326
326
327 def _runwsgi(self, req, repo):
327 def _runwsgi(self, req, repo):
328 rctx = requestcontext(self, repo)
328 rctx = requestcontext(self, repo)
329
329
330 # This state is global across all threads.
330 # This state is global across all threads.
331 encoding.encoding = rctx.config('web', 'encoding')
331 encoding.encoding = rctx.config('web', 'encoding')
332 rctx.repo.ui.environ = req.env
332 rctx.repo.ui.environ = req.env
333
333
334 if rctx.csp:
334 if rctx.csp:
335 # hgwebdir may have added CSP header. Since we generate our own,
335 # hgwebdir may have added CSP header. Since we generate our own,
336 # replace it.
336 # replace it.
337 req.headers = [h for h in req.headers
337 req.headers = [h for h in req.headers
338 if h[0] != 'Content-Security-Policy']
338 if h[0] != 'Content-Security-Policy']
339 req.headers.append(('Content-Security-Policy', rctx.csp))
339 req.headers.append(('Content-Security-Policy', rctx.csp))
340
340
341 # work with CGI variables to create coherent structure
341 # work with CGI variables to create coherent structure
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
343
343
344 req.url = req.env['SCRIPT_NAME']
344 req.url = req.env[r'SCRIPT_NAME']
345 if not req.url.endswith('/'):
345 if not req.url.endswith('/'):
346 req.url += '/'
346 req.url += '/'
347 if req.env.get('REPO_NAME'):
347 if req.env.get('REPO_NAME'):
348 req.url += req.env['REPO_NAME'] + '/'
348 req.url += req.env[r'REPO_NAME'] + r'/'
349
349
350 if 'PATH_INFO' in req.env:
350 if r'PATH_INFO' in req.env:
351 parts = req.env['PATH_INFO'].strip('/').split('/')
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
352 repo_parts = req.env.get('REPO_NAME', '').split('/')
352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
353 if parts[:len(repo_parts)] == repo_parts:
353 if parts[:len(repo_parts)] == repo_parts:
354 parts = parts[len(repo_parts):]
354 parts = parts[len(repo_parts):]
355 query = '/'.join(parts)
355 query = '/'.join(parts)
356 else:
356 else:
357 query = req.env['QUERY_STRING'].partition('&')[0]
357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
358 query = query.partition(';')[0]
358 query = query.partition(r';')[0]
359
359
360 # process this if it's a protocol request
360 # process this if it's a protocol request
361 # protocol bits don't need to create any URLs
361 # protocol bits don't need to create any URLs
362 # and the clients always use the old URL structure
362 # and the clients always use the old URL structure
363
363
364 cmd = req.form.get('cmd', [''])[0]
364 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
365 if protocol.iscmd(cmd):
365 if protocol.iscmd(cmd):
366 try:
366 try:
367 if query:
367 if query:
368 raise ErrorResponse(HTTP_NOT_FOUND)
368 raise ErrorResponse(HTTP_NOT_FOUND)
369 if cmd in perms:
369 if cmd in perms:
370 self.check_perm(rctx, req, perms[cmd])
370 self.check_perm(rctx, req, perms[cmd])
371 return protocol.call(rctx.repo, req, cmd)
371 return protocol.call(rctx.repo, req, cmd)
372 except ErrorResponse as inst:
372 except ErrorResponse as inst:
373 # A client that sends unbundle without 100-continue will
373 # A client that sends unbundle without 100-continue will
374 # break if we respond early.
374 # break if we respond early.
375 if (cmd == 'unbundle' and
375 if (cmd == 'unbundle' and
376 (req.env.get('HTTP_EXPECT',
376 (req.env.get('HTTP_EXPECT',
377 '').lower() != '100-continue') or
377 '').lower() != '100-continue') or
378 req.env.get('X-HgHttp2', '')):
378 req.env.get('X-HgHttp2', '')):
379 req.drain()
379 req.drain()
380 else:
380 else:
381 req.headers.append(('Connection', 'Close'))
381 req.headers.append(('Connection', 'Close'))
382 req.respond(inst, protocol.HGTYPE,
382 req.respond(inst, protocol.HGTYPE,
383 body='0\n%s\n' % inst)
383 body='0\n%s\n' % inst)
384 return ''
384 return ''
385
385
386 # translate user-visible url structure to internal structure
386 # translate user-visible url structure to internal structure
387
387
388 args = query.split('/', 2)
388 args = query.split('/', 2)
389 if 'cmd' not in req.form and args and args[0]:
389 if r'cmd' not in req.form and args and args[0]:
390
391 cmd = args.pop(0)
390 cmd = args.pop(0)
392 style = cmd.rfind('-')
391 style = cmd.rfind('-')
393 if style != -1:
392 if style != -1:
394 req.form['style'] = [cmd[:style]]
393 req.form['style'] = [cmd[:style]]
395 cmd = cmd[style + 1:]
394 cmd = cmd[style + 1:]
396
395
397 # avoid accepting e.g. style parameter as command
396 # avoid accepting e.g. style parameter as command
398 if util.safehasattr(webcommands, cmd):
397 if util.safehasattr(webcommands, cmd):
399 req.form['cmd'] = [cmd]
398 req.form[r'cmd'] = [cmd]
400
399
401 if cmd == 'static':
400 if cmd == 'static':
402 req.form['file'] = ['/'.join(args)]
401 req.form['file'] = ['/'.join(args)]
403 else:
402 else:
404 if args and args[0]:
403 if args and args[0]:
405 node = args.pop(0).replace('%2F', '/')
404 node = args.pop(0).replace('%2F', '/')
406 req.form['node'] = [node]
405 req.form['node'] = [node]
407 if args:
406 if args:
408 req.form['file'] = args
407 req.form['file'] = args
409
408
410 ua = req.env.get('HTTP_USER_AGENT', '')
409 ua = req.env.get('HTTP_USER_AGENT', '')
411 if cmd == 'rev' and 'mercurial' in ua:
410 if cmd == 'rev' and 'mercurial' in ua:
412 req.form['style'] = ['raw']
411 req.form['style'] = ['raw']
413
412
414 if cmd == 'archive':
413 if cmd == 'archive':
415 fn = req.form['node'][0]
414 fn = req.form['node'][0]
416 for type_, spec in rctx.archivespecs.iteritems():
415 for type_, spec in rctx.archivespecs.iteritems():
417 ext = spec[2]
416 ext = spec[2]
418 if fn.endswith(ext):
417 if fn.endswith(ext):
419 req.form['node'] = [fn[:-len(ext)]]
418 req.form['node'] = [fn[:-len(ext)]]
420 req.form['type'] = [type_]
419 req.form['type'] = [type_]
421
420
422 # process the web interface request
421 # process the web interface request
423
422
424 try:
423 try:
425 tmpl = rctx.templater(req)
424 tmpl = rctx.templater(req)
426 ctype = tmpl('mimetype', encoding=encoding.encoding)
425 ctype = tmpl('mimetype', encoding=encoding.encoding)
427 ctype = templater.stringify(ctype)
426 ctype = templater.stringify(ctype)
428
427
429 # check read permissions non-static content
428 # check read permissions non-static content
430 if cmd != 'static':
429 if cmd != 'static':
431 self.check_perm(rctx, req, None)
430 self.check_perm(rctx, req, None)
432
431
433 if cmd == '':
432 if cmd == '':
434 req.form['cmd'] = [tmpl.cache['default']]
433 req.form[r'cmd'] = [tmpl.cache['default']]
435 cmd = req.form['cmd'][0]
434 cmd = req.form[r'cmd'][0]
436
435
437 # Don't enable caching if using a CSP nonce because then it wouldn't
436 # Don't enable caching if using a CSP nonce because then it wouldn't
438 # be a nonce.
437 # be a nonce.
439 if rctx.configbool('web', 'cache') and not rctx.nonce:
438 if rctx.configbool('web', 'cache') and not rctx.nonce:
440 caching(self, req) # sets ETag header or raises NOT_MODIFIED
439 caching(self, req) # sets ETag header or raises NOT_MODIFIED
441 if cmd not in webcommands.__all__:
440 if cmd not in webcommands.__all__:
442 msg = 'no such method: %s' % cmd
441 msg = 'no such method: %s' % cmd
443 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
442 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
444 elif cmd == 'file' and 'raw' in req.form.get('style', []):
443 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
445 rctx.ctype = ctype
444 rctx.ctype = ctype
446 content = webcommands.rawfile(rctx, req, tmpl)
445 content = webcommands.rawfile(rctx, req, tmpl)
447 else:
446 else:
448 content = getattr(webcommands, cmd)(rctx, req, tmpl)
447 content = getattr(webcommands, cmd)(rctx, req, tmpl)
449 req.respond(HTTP_OK, ctype)
448 req.respond(HTTP_OK, ctype)
450
449
451 return content
450 return content
452
451
453 except (error.LookupError, error.RepoLookupError) as err:
452 except (error.LookupError, error.RepoLookupError) as err:
454 req.respond(HTTP_NOT_FOUND, ctype)
453 req.respond(HTTP_NOT_FOUND, ctype)
455 msg = str(err)
454 msg = str(err)
456 if (util.safehasattr(err, 'name') and
455 if (util.safehasattr(err, 'name') and
457 not isinstance(err, error.ManifestLookupError)):
456 not isinstance(err, error.ManifestLookupError)):
458 msg = 'revision not found: %s' % err.name
457 msg = 'revision not found: %s' % err.name
459 return tmpl('error', error=msg)
458 return tmpl('error', error=msg)
460 except (error.RepoError, error.RevlogError) as inst:
459 except (error.RepoError, error.RevlogError) as inst:
461 req.respond(HTTP_SERVER_ERROR, ctype)
460 req.respond(HTTP_SERVER_ERROR, ctype)
462 return tmpl('error', error=str(inst))
461 return tmpl('error', error=str(inst))
463 except ErrorResponse as inst:
462 except ErrorResponse as inst:
464 req.respond(inst, ctype)
463 req.respond(inst, ctype)
465 if inst.code == HTTP_NOT_MODIFIED:
464 if inst.code == HTTP_NOT_MODIFIED:
466 # Not allowed to return a body on a 304
465 # Not allowed to return a body on a 304
467 return ['']
466 return ['']
468 return tmpl('error', error=str(inst))
467 return tmpl('error', error=str(inst))
469
468
470 def check_perm(self, rctx, req, op):
469 def check_perm(self, rctx, req, op):
471 for permhook in permhooks:
470 for permhook in permhooks:
472 permhook(rctx, req, op)
471 permhook(rctx, req, op)
473
472
474 def getwebview(repo):
473 def getwebview(repo):
475 """The 'web.view' config controls changeset filter to hgweb. Possible
474 """The 'web.view' config controls changeset filter to hgweb. Possible
476 values are ``served``, ``visible`` and ``all``. Default is ``served``.
475 values are ``served``, ``visible`` and ``all``. Default is ``served``.
477 The ``served`` filter only shows changesets that can be pulled from the
476 The ``served`` filter only shows changesets that can be pulled from the
478 hgweb instance. The``visible`` filter includes secret changesets but
477 hgweb instance. The``visible`` filter includes secret changesets but
479 still excludes "hidden" one.
478 still excludes "hidden" one.
480
479
481 See the repoview module for details.
480 See the repoview module for details.
482
481
483 The option has been around undocumented since Mercurial 2.5, but no
482 The option has been around undocumented since Mercurial 2.5, but no
484 user ever asked about it. So we better keep it undocumented for now."""
483 user ever asked about it. So we better keep it undocumented for now."""
485 # experimental config: web.view
484 # experimental config: web.view
486 viewconfig = repo.ui.config('web', 'view', untrusted=True)
485 viewconfig = repo.ui.config('web', 'view', untrusted=True)
487 if viewconfig == 'all':
486 if viewconfig == 'all':
488 return repo.unfiltered()
487 return repo.unfiltered()
489 elif viewconfig in repoview.filtertable:
488 elif viewconfig in repoview.filtertable:
490 return repo.filtered(viewconfig)
489 return repo.filtered(viewconfig)
491 else:
490 else:
492 return repo.filtered('served')
491 return repo.filtered('served')
General Comments 0
You need to be logged in to leave comments. Login now