##// END OF EJS Templates
hgweb: ensure all wsgi environment values are str...
Gregory Szorc -
r36820:7fc80c98 default
parent child Browse files
Show More
@@ -1,472 +1,473 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 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 formatter,
30 formatter,
31 hg,
31 hg,
32 hook,
32 hook,
33 profiling,
33 profiling,
34 pycompat,
34 pycompat,
35 repoview,
35 repoview,
36 templatefilters,
36 templatefilters,
37 templater,
37 templater,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 wireprotoserver,
40 wireprotoserver,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 webcommands,
44 webcommands,
45 webutil,
45 webutil,
46 wsgicgi,
46 wsgicgi,
47 )
47 )
48
48
49 archivespecs = util.sortdict((
49 archivespecs = util.sortdict((
50 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('zip', ('application/zip', 'zip', '.zip', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 ))
53 ))
54
54
55 def getstyle(req, configfn, templatepath):
55 def getstyle(req, configfn, templatepath):
56 fromreq = req.form.get('style', [None])[0]
56 fromreq = req.form.get('style', [None])[0]
57 styles = (
57 styles = (
58 fromreq,
58 fromreq,
59 configfn('web', 'style'),
59 configfn('web', 'style'),
60 'paper',
60 'paper',
61 )
61 )
62 return styles, templater.stylemap(styles, templatepath)
62 return styles, templater.stylemap(styles, templatepath)
63
63
64 def makebreadcrumb(url, prefix=''):
64 def makebreadcrumb(url, prefix=''):
65 '''Return a 'URL breadcrumb' list
65 '''Return a 'URL breadcrumb' list
66
66
67 A 'URL breadcrumb' is a list of URL-name pairs,
67 A 'URL breadcrumb' is a list of URL-name pairs,
68 corresponding to each of the path items on a URL.
68 corresponding to each of the path items on a URL.
69 This can be used to create path navigation entries.
69 This can be used to create path navigation entries.
70 '''
70 '''
71 if url.endswith('/'):
71 if url.endswith('/'):
72 url = url[:-1]
72 url = url[:-1]
73 if prefix:
73 if prefix:
74 url = '/' + prefix + url
74 url = '/' + prefix + url
75 relpath = url
75 relpath = url
76 if relpath.startswith('/'):
76 if relpath.startswith('/'):
77 relpath = relpath[1:]
77 relpath = relpath[1:]
78
78
79 breadcrumb = []
79 breadcrumb = []
80 urlel = url
80 urlel = url
81 pathitems = [''] + relpath.split('/')
81 pathitems = [''] + relpath.split('/')
82 for pathel in reversed(pathitems):
82 for pathel in reversed(pathitems):
83 if not pathel or not urlel:
83 if not pathel or not urlel:
84 break
84 break
85 breadcrumb.append({'url': urlel, 'name': pathel})
85 breadcrumb.append({'url': urlel, 'name': pathel})
86 urlel = os.path.dirname(urlel)
86 urlel = os.path.dirname(urlel)
87 return reversed(breadcrumb)
87 return reversed(breadcrumb)
88
88
89 class requestcontext(object):
89 class requestcontext(object):
90 """Holds state/context for an individual request.
90 """Holds state/context for an individual request.
91
91
92 Servers can be multi-threaded. Holding state on the WSGI application
92 Servers can be multi-threaded. Holding state on the WSGI application
93 is prone to race conditions. Instances of this class exist to hold
93 is prone to race conditions. Instances of this class exist to hold
94 mutable and race-free state for requests.
94 mutable and race-free state for requests.
95 """
95 """
96 def __init__(self, app, repo):
96 def __init__(self, app, repo):
97 self.repo = repo
97 self.repo = repo
98 self.reponame = app.reponame
98 self.reponame = app.reponame
99
99
100 self.archivespecs = archivespecs
100 self.archivespecs = archivespecs
101
101
102 self.maxchanges = self.configint('web', 'maxchanges')
102 self.maxchanges = self.configint('web', 'maxchanges')
103 self.stripecount = self.configint('web', 'stripes')
103 self.stripecount = self.configint('web', 'stripes')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 self.maxfiles = self.configint('web', 'maxfiles')
105 self.maxfiles = self.configint('web', 'maxfiles')
106 self.allowpull = self.configbool('web', 'allow-pull')
106 self.allowpull = self.configbool('web', 'allow-pull')
107
107
108 # we use untrusted=False to prevent a repo owner from using
108 # we use untrusted=False to prevent a repo owner from using
109 # web.templates in .hg/hgrc to get access to any file readable
109 # web.templates in .hg/hgrc to get access to any file readable
110 # by the user running the CGI script
110 # by the user running the CGI script
111 self.templatepath = self.config('web', 'templates', untrusted=False)
111 self.templatepath = self.config('web', 'templates', untrusted=False)
112
112
113 # This object is more expensive to build than simple config values.
113 # This object is more expensive to build than simple config values.
114 # It is shared across requests. The app will replace the object
114 # It is shared across requests. The app will replace the object
115 # if it is updated. Since this is a reference and nothing should
115 # if it is updated. Since this is a reference and nothing should
116 # modify the underlying object, it should be constant for the lifetime
116 # modify the underlying object, it should be constant for the lifetime
117 # of the request.
117 # of the request.
118 self.websubtable = app.websubtable
118 self.websubtable = app.websubtable
119
119
120 self.csp, self.nonce = cspvalues(self.repo.ui)
120 self.csp, self.nonce = cspvalues(self.repo.ui)
121
121
122 # Trust the settings from the .hg/hgrc files by default.
122 # Trust the settings from the .hg/hgrc files by default.
123 def config(self, section, name, default=uimod._unset, untrusted=True):
123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 return self.repo.ui.config(section, name, default,
124 return self.repo.ui.config(section, name, default,
125 untrusted=untrusted)
125 untrusted=untrusted)
126
126
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 return self.repo.ui.configbool(section, name, default,
128 return self.repo.ui.configbool(section, name, default,
129 untrusted=untrusted)
129 untrusted=untrusted)
130
130
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 return self.repo.ui.configint(section, name, default,
132 return self.repo.ui.configint(section, name, default,
133 untrusted=untrusted)
133 untrusted=untrusted)
134
134
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 return self.repo.ui.configlist(section, name, default,
136 return self.repo.ui.configlist(section, name, default,
137 untrusted=untrusted)
137 untrusted=untrusted)
138
138
139 def archivelist(self, nodeid):
139 def archivelist(self, nodeid):
140 allowed = self.configlist('web', 'allow_archive')
140 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems():
141 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144
144
145 def templater(self, req):
145 def templater(self, req):
146 # determine scheme, port and server name
146 # determine scheme, port and server name
147 # this is needed to create absolute urls
147 # this is needed to create absolute urls
148
148
149 proto = req.env.get('wsgi.url_scheme')
149 proto = req.env.get('wsgi.url_scheme')
150 if proto == 'https':
150 if proto == 'https':
151 proto = 'https'
151 proto = 'https'
152 default_port = '443'
152 default_port = '443'
153 else:
153 else:
154 proto = 'http'
154 proto = 'http'
155 default_port = '80'
155 default_port = '80'
156
156
157 port = req.env[r'SERVER_PORT']
157 port = req.env[r'SERVER_PORT']
158 port = port != default_port and (r':' + port) or r''
158 port = port != default_port and (r':' + port) or r''
159 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
159 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
160 logourl = self.config('web', 'logourl')
160 logourl = self.config('web', 'logourl')
161 logoimg = self.config('web', 'logoimg')
161 logoimg = self.config('web', 'logoimg')
162 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
162 staticurl = (self.config('web', 'staticurl')
163 or pycompat.sysbytes(req.url) + 'static/')
163 if not staticurl.endswith('/'):
164 if not staticurl.endswith('/'):
164 staticurl += '/'
165 staticurl += '/'
165
166
166 # some functions for the templater
167 # some functions for the templater
167
168
168 def motd(**map):
169 def motd(**map):
169 yield self.config('web', 'motd')
170 yield self.config('web', 'motd')
170
171
171 # figure out which style to use
172 # figure out which style to use
172
173
173 vars = {}
174 vars = {}
174 styles, (style, mapfile) = getstyle(req, self.config,
175 styles, (style, mapfile) = getstyle(req, self.config,
175 self.templatepath)
176 self.templatepath)
176 if style == styles[0]:
177 if style == styles[0]:
177 vars['style'] = style
178 vars['style'] = style
178
179
179 start = '&' if req.url[-1] == r'?' else '?'
180 start = '&' if req.url[-1] == r'?' else '?'
180 sessionvars = webutil.sessionvars(vars, start)
181 sessionvars = webutil.sessionvars(vars, start)
181
182
182 if not self.reponame:
183 if not self.reponame:
183 self.reponame = (self.config('web', 'name', '')
184 self.reponame = (self.config('web', 'name', '')
184 or req.env.get('REPO_NAME')
185 or req.env.get('REPO_NAME')
185 or req.url.strip('/') or self.repo.root)
186 or req.url.strip(r'/') or self.repo.root)
186
187
187 def websubfilter(text):
188 def websubfilter(text):
188 return templatefilters.websub(text, self.websubtable)
189 return templatefilters.websub(text, self.websubtable)
189
190
190 # create the templater
191 # create the templater
191 # TODO: export all keywords: defaults = templatekw.keywords.copy()
192 # TODO: export all keywords: defaults = templatekw.keywords.copy()
192 defaults = {
193 defaults = {
193 'url': req.url,
194 'url': pycompat.sysbytes(req.url),
194 'logourl': logourl,
195 'logourl': logourl,
195 'logoimg': logoimg,
196 'logoimg': logoimg,
196 'staticurl': staticurl,
197 'staticurl': staticurl,
197 'urlbase': urlbase,
198 'urlbase': urlbase,
198 'repo': self.reponame,
199 'repo': self.reponame,
199 'encoding': encoding.encoding,
200 'encoding': encoding.encoding,
200 'motd': motd,
201 'motd': motd,
201 'sessionvars': sessionvars,
202 'sessionvars': sessionvars,
202 'pathdef': makebreadcrumb(req.url),
203 'pathdef': makebreadcrumb(pycompat.sysbytes(req.url)),
203 'style': style,
204 'style': style,
204 'nonce': self.nonce,
205 'nonce': self.nonce,
205 }
206 }
206 tres = formatter.templateresources(self.repo.ui, self.repo)
207 tres = formatter.templateresources(self.repo.ui, self.repo)
207 tmpl = templater.templater.frommapfile(mapfile,
208 tmpl = templater.templater.frommapfile(mapfile,
208 filters={'websub': websubfilter},
209 filters={'websub': websubfilter},
209 defaults=defaults,
210 defaults=defaults,
210 resources=tres)
211 resources=tres)
211 return tmpl
212 return tmpl
212
213
213
214
214 class hgweb(object):
215 class hgweb(object):
215 """HTTP server for individual repositories.
216 """HTTP server for individual repositories.
216
217
217 Instances of this class serve HTTP responses for a particular
218 Instances of this class serve HTTP responses for a particular
218 repository.
219 repository.
219
220
220 Instances are typically used as WSGI applications.
221 Instances are typically used as WSGI applications.
221
222
222 Some servers are multi-threaded. On these servers, there may
223 Some servers are multi-threaded. On these servers, there may
223 be multiple active threads inside __call__.
224 be multiple active threads inside __call__.
224 """
225 """
225 def __init__(self, repo, name=None, baseui=None):
226 def __init__(self, repo, name=None, baseui=None):
226 if isinstance(repo, str):
227 if isinstance(repo, str):
227 if baseui:
228 if baseui:
228 u = baseui.copy()
229 u = baseui.copy()
229 else:
230 else:
230 u = uimod.ui.load()
231 u = uimod.ui.load()
231 r = hg.repository(u, repo)
232 r = hg.repository(u, repo)
232 else:
233 else:
233 # we trust caller to give us a private copy
234 # we trust caller to give us a private copy
234 r = repo
235 r = repo
235
236
236 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 # resolve file patterns relative to repo root
241 # resolve file patterns relative to repo root
241 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 # displaying bundling progress bar while serving feel wrong and may
244 # displaying bundling progress bar while serving feel wrong and may
244 # break some wsgi implementation.
245 # break some wsgi implementation.
245 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 self._lastrepo = self._repos[0]
249 self._lastrepo = self._repos[0]
249 hook.redirect(True)
250 hook.redirect(True)
250 self.reponame = name
251 self.reponame = name
251
252
252 def _webifyrepo(self, repo):
253 def _webifyrepo(self, repo):
253 repo = getwebview(repo)
254 repo = getwebview(repo)
254 self.websubtable = webutil.getwebsubs(repo)
255 self.websubtable = webutil.getwebsubs(repo)
255 return repo
256 return repo
256
257
257 @contextlib.contextmanager
258 @contextlib.contextmanager
258 def _obtainrepo(self):
259 def _obtainrepo(self):
259 """Obtain a repo unique to the caller.
260 """Obtain a repo unique to the caller.
260
261
261 Internally we maintain a stack of cachedlocalrepo instances
262 Internally we maintain a stack of cachedlocalrepo instances
262 to be handed out. If one is available, we pop it and return it,
263 to be handed out. If one is available, we pop it and return it,
263 ensuring it is up to date in the process. If one is not available,
264 ensuring it is up to date in the process. If one is not available,
264 we clone the most recently used repo instance and return it.
265 we clone the most recently used repo instance and return it.
265
266
266 It is currently possible for the stack to grow without bounds
267 It is currently possible for the stack to grow without bounds
267 if the server allows infinite threads. However, servers should
268 if the server allows infinite threads. However, servers should
268 have a thread limit, thus establishing our limit.
269 have a thread limit, thus establishing our limit.
269 """
270 """
270 if self._repos:
271 if self._repos:
271 cached = self._repos.pop()
272 cached = self._repos.pop()
272 r, created = cached.fetch()
273 r, created = cached.fetch()
273 else:
274 else:
274 cached = self._lastrepo.copy()
275 cached = self._lastrepo.copy()
275 r, created = cached.fetch()
276 r, created = cached.fetch()
276 if created:
277 if created:
277 r = self._webifyrepo(r)
278 r = self._webifyrepo(r)
278
279
279 self._lastrepo = cached
280 self._lastrepo = cached
280 self.mtime = cached.mtime
281 self.mtime = cached.mtime
281 try:
282 try:
282 yield r
283 yield r
283 finally:
284 finally:
284 self._repos.append(cached)
285 self._repos.append(cached)
285
286
286 def run(self):
287 def run(self):
287 """Start a server from CGI environment.
288 """Start a server from CGI environment.
288
289
289 Modern servers should be using WSGI and should avoid this
290 Modern servers should be using WSGI and should avoid this
290 method, if possible.
291 method, if possible.
291 """
292 """
292 if not encoding.environ.get('GATEWAY_INTERFACE',
293 if not encoding.environ.get('GATEWAY_INTERFACE',
293 '').startswith("CGI/1."):
294 '').startswith("CGI/1."):
294 raise RuntimeError("This function is only intended to be "
295 raise RuntimeError("This function is only intended to be "
295 "called while running as a CGI script.")
296 "called while running as a CGI script.")
296 wsgicgi.launch(self)
297 wsgicgi.launch(self)
297
298
298 def __call__(self, env, respond):
299 def __call__(self, env, respond):
299 """Run the WSGI application.
300 """Run the WSGI application.
300
301
301 This may be called by multiple threads.
302 This may be called by multiple threads.
302 """
303 """
303 req = wsgirequest(env, respond)
304 req = wsgirequest(env, respond)
304 return self.run_wsgi(req)
305 return self.run_wsgi(req)
305
306
306 def run_wsgi(self, req):
307 def run_wsgi(self, req):
307 """Internal method to run the WSGI application.
308 """Internal method to run the WSGI application.
308
309
309 This is typically only called by Mercurial. External consumers
310 This is typically only called by Mercurial. External consumers
310 should be using instances of this class as the WSGI application.
311 should be using instances of this class as the WSGI application.
311 """
312 """
312 with self._obtainrepo() as repo:
313 with self._obtainrepo() as repo:
313 profile = repo.ui.configbool('profiling', 'enabled')
314 profile = repo.ui.configbool('profiling', 'enabled')
314 with profiling.profile(repo.ui, enabled=profile):
315 with profiling.profile(repo.ui, enabled=profile):
315 for r in self._runwsgi(req, repo):
316 for r in self._runwsgi(req, repo):
316 yield r
317 yield r
317
318
318 def _runwsgi(self, req, repo):
319 def _runwsgi(self, req, repo):
319 rctx = requestcontext(self, repo)
320 rctx = requestcontext(self, repo)
320
321
321 # This state is global across all threads.
322 # This state is global across all threads.
322 encoding.encoding = rctx.config('web', 'encoding')
323 encoding.encoding = rctx.config('web', 'encoding')
323 rctx.repo.ui.environ = req.env
324 rctx.repo.ui.environ = req.env
324
325
325 if rctx.csp:
326 if rctx.csp:
326 # hgwebdir may have added CSP header. Since we generate our own,
327 # hgwebdir may have added CSP header. Since we generate our own,
327 # replace it.
328 # replace it.
328 req.headers = [h for h in req.headers
329 req.headers = [h for h in req.headers
329 if h[0] != 'Content-Security-Policy']
330 if h[0] != 'Content-Security-Policy']
330 req.headers.append(('Content-Security-Policy', rctx.csp))
331 req.headers.append(('Content-Security-Policy', rctx.csp))
331
332
332 # work with CGI variables to create coherent structure
333 # work with CGI variables to create coherent structure
333 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334
335
335 req.url = req.env[r'SCRIPT_NAME']
336 req.url = req.env[r'SCRIPT_NAME']
336 if not req.url.endswith('/'):
337 if not req.url.endswith(r'/'):
337 req.url += '/'
338 req.url += r'/'
338 if req.env.get('REPO_NAME'):
339 if req.env.get('REPO_NAME'):
339 req.url += req.env[r'REPO_NAME'] + r'/'
340 req.url += req.env[r'REPO_NAME'] + r'/'
340
341
341 if r'PATH_INFO' in req.env:
342 if r'PATH_INFO' in req.env:
342 parts = req.env[r'PATH_INFO'].strip('/').split('/')
343 parts = req.env[r'PATH_INFO'].strip(r'/').split(r'/')
343 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
344 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
344 if parts[:len(repo_parts)] == repo_parts:
345 if parts[:len(repo_parts)] == repo_parts:
345 parts = parts[len(repo_parts):]
346 parts = parts[len(repo_parts):]
346 query = '/'.join(parts)
347 query = r'/'.join(parts)
347 else:
348 else:
348 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
349 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
349 query = query.partition(r';')[0]
350 query = query.partition(r';')[0]
350
351
351 # Route it to a wire protocol handler if it looks like a wire protocol
352 # Route it to a wire protocol handler if it looks like a wire protocol
352 # request.
353 # request.
353 protohandler = wireprotoserver.parsehttprequest(rctx, req, query,
354 protohandler = wireprotoserver.parsehttprequest(rctx, req, query,
354 self.check_perm)
355 self.check_perm)
355
356
356 if protohandler:
357 if protohandler:
357 try:
358 try:
358 if query:
359 if query:
359 raise ErrorResponse(HTTP_NOT_FOUND)
360 raise ErrorResponse(HTTP_NOT_FOUND)
360
361
361 return protohandler['dispatch']()
362 return protohandler['dispatch']()
362 except ErrorResponse as inst:
363 except ErrorResponse as inst:
363 return protohandler['handleerror'](inst)
364 return protohandler['handleerror'](inst)
364
365
365 # translate user-visible url structure to internal structure
366 # translate user-visible url structure to internal structure
366
367
367 args = query.split('/', 2)
368 args = query.split(r'/', 2)
368 if 'cmd' not in req.form and args and args[0]:
369 if 'cmd' not in req.form and args and args[0]:
369 cmd = args.pop(0)
370 cmd = args.pop(0)
370 style = cmd.rfind('-')
371 style = cmd.rfind('-')
371 if style != -1:
372 if style != -1:
372 req.form['style'] = [cmd[:style]]
373 req.form['style'] = [cmd[:style]]
373 cmd = cmd[style + 1:]
374 cmd = cmd[style + 1:]
374
375
375 # avoid accepting e.g. style parameter as command
376 # avoid accepting e.g. style parameter as command
376 if util.safehasattr(webcommands, cmd):
377 if util.safehasattr(webcommands, cmd):
377 req.form['cmd'] = [cmd]
378 req.form['cmd'] = [cmd]
378
379
379 if cmd == 'static':
380 if cmd == 'static':
380 req.form['file'] = ['/'.join(args)]
381 req.form['file'] = ['/'.join(args)]
381 else:
382 else:
382 if args and args[0]:
383 if args and args[0]:
383 node = args.pop(0).replace('%2F', '/')
384 node = args.pop(0).replace('%2F', '/')
384 req.form['node'] = [node]
385 req.form['node'] = [node]
385 if args:
386 if args:
386 req.form['file'] = args
387 req.form['file'] = args
387
388
388 ua = req.env.get('HTTP_USER_AGENT', '')
389 ua = req.env.get('HTTP_USER_AGENT', '')
389 if cmd == 'rev' and 'mercurial' in ua:
390 if cmd == 'rev' and 'mercurial' in ua:
390 req.form['style'] = ['raw']
391 req.form['style'] = ['raw']
391
392
392 if cmd == 'archive':
393 if cmd == 'archive':
393 fn = req.form['node'][0]
394 fn = req.form['node'][0]
394 for type_, spec in rctx.archivespecs.iteritems():
395 for type_, spec in rctx.archivespecs.iteritems():
395 ext = spec[2]
396 ext = spec[2]
396 if fn.endswith(ext):
397 if fn.endswith(ext):
397 req.form['node'] = [fn[:-len(ext)]]
398 req.form['node'] = [fn[:-len(ext)]]
398 req.form['type'] = [type_]
399 req.form['type'] = [type_]
399 else:
400 else:
400 cmd = req.form.get('cmd', [''])[0]
401 cmd = req.form.get('cmd', [''])[0]
401
402
402 # process the web interface request
403 # process the web interface request
403
404
404 try:
405 try:
405 tmpl = rctx.templater(req)
406 tmpl = rctx.templater(req)
406 ctype = tmpl('mimetype', encoding=encoding.encoding)
407 ctype = tmpl('mimetype', encoding=encoding.encoding)
407 ctype = templater.stringify(ctype)
408 ctype = templater.stringify(ctype)
408
409
409 # check read permissions non-static content
410 # check read permissions non-static content
410 if cmd != 'static':
411 if cmd != 'static':
411 self.check_perm(rctx, req, None)
412 self.check_perm(rctx, req, None)
412
413
413 if cmd == '':
414 if cmd == '':
414 req.form['cmd'] = [tmpl.cache['default']]
415 req.form['cmd'] = [tmpl.cache['default']]
415 cmd = req.form['cmd'][0]
416 cmd = req.form['cmd'][0]
416
417
417 # Don't enable caching if using a CSP nonce because then it wouldn't
418 # Don't enable caching if using a CSP nonce because then it wouldn't
418 # be a nonce.
419 # be a nonce.
419 if rctx.configbool('web', 'cache') and not rctx.nonce:
420 if rctx.configbool('web', 'cache') and not rctx.nonce:
420 caching(self, req) # sets ETag header or raises NOT_MODIFIED
421 caching(self, req) # sets ETag header or raises NOT_MODIFIED
421 if cmd not in webcommands.__all__:
422 if cmd not in webcommands.__all__:
422 msg = 'no such method: %s' % cmd
423 msg = 'no such method: %s' % cmd
423 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
424 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
424 elif cmd == 'file' and 'raw' in req.form.get('style', []):
425 elif cmd == 'file' and 'raw' in req.form.get('style', []):
425 rctx.ctype = ctype
426 rctx.ctype = ctype
426 content = webcommands.rawfile(rctx, req, tmpl)
427 content = webcommands.rawfile(rctx, req, tmpl)
427 else:
428 else:
428 content = getattr(webcommands, cmd)(rctx, req, tmpl)
429 content = getattr(webcommands, cmd)(rctx, req, tmpl)
429 req.respond(HTTP_OK, ctype)
430 req.respond(HTTP_OK, ctype)
430
431
431 return content
432 return content
432
433
433 except (error.LookupError, error.RepoLookupError) as err:
434 except (error.LookupError, error.RepoLookupError) as err:
434 req.respond(HTTP_NOT_FOUND, ctype)
435 req.respond(HTTP_NOT_FOUND, ctype)
435 msg = pycompat.bytestr(err)
436 msg = pycompat.bytestr(err)
436 if (util.safehasattr(err, 'name') and
437 if (util.safehasattr(err, 'name') and
437 not isinstance(err, error.ManifestLookupError)):
438 not isinstance(err, error.ManifestLookupError)):
438 msg = 'revision not found: %s' % err.name
439 msg = 'revision not found: %s' % err.name
439 return tmpl('error', error=msg)
440 return tmpl('error', error=msg)
440 except (error.RepoError, error.RevlogError) as inst:
441 except (error.RepoError, error.RevlogError) as inst:
441 req.respond(HTTP_SERVER_ERROR, ctype)
442 req.respond(HTTP_SERVER_ERROR, ctype)
442 return tmpl('error', error=pycompat.bytestr(inst))
443 return tmpl('error', error=pycompat.bytestr(inst))
443 except ErrorResponse as inst:
444 except ErrorResponse as inst:
444 req.respond(inst, ctype)
445 req.respond(inst, ctype)
445 if inst.code == HTTP_NOT_MODIFIED:
446 if inst.code == HTTP_NOT_MODIFIED:
446 # Not allowed to return a body on a 304
447 # Not allowed to return a body on a 304
447 return ['']
448 return ['']
448 return tmpl('error', error=pycompat.bytestr(inst))
449 return tmpl('error', error=pycompat.bytestr(inst))
449
450
450 def check_perm(self, rctx, req, op):
451 def check_perm(self, rctx, req, op):
451 for permhook in permhooks:
452 for permhook in permhooks:
452 permhook(rctx, req, op)
453 permhook(rctx, req, op)
453
454
454 def getwebview(repo):
455 def getwebview(repo):
455 """The 'web.view' config controls changeset filter to hgweb. Possible
456 """The 'web.view' config controls changeset filter to hgweb. Possible
456 values are ``served``, ``visible`` and ``all``. Default is ``served``.
457 values are ``served``, ``visible`` and ``all``. Default is ``served``.
457 The ``served`` filter only shows changesets that can be pulled from the
458 The ``served`` filter only shows changesets that can be pulled from the
458 hgweb instance. The``visible`` filter includes secret changesets but
459 hgweb instance. The``visible`` filter includes secret changesets but
459 still excludes "hidden" one.
460 still excludes "hidden" one.
460
461
461 See the repoview module for details.
462 See the repoview module for details.
462
463
463 The option has been around undocumented since Mercurial 2.5, but no
464 The option has been around undocumented since Mercurial 2.5, but no
464 user ever asked about it. So we better keep it undocumented for now."""
465 user ever asked about it. So we better keep it undocumented for now."""
465 # experimental config: web.view
466 # experimental config: web.view
466 viewconfig = repo.ui.config('web', 'view', untrusted=True)
467 viewconfig = repo.ui.config('web', 'view', untrusted=True)
467 if viewconfig == 'all':
468 if viewconfig == 'all':
468 return repo.unfiltered()
469 return repo.unfiltered()
469 elif viewconfig in repoview.filtertable:
470 elif viewconfig in repoview.filtertable:
470 return repo.filtered(viewconfig)
471 return repo.filtered(viewconfig)
471 else:
472 else:
472 return repo.filtered('served')
473 return repo.filtered('served')
@@ -1,347 +1,347 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
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 errno
11 import errno
12 import os
12 import os
13 import socket
13 import socket
14 import sys
14 import sys
15 import traceback
15 import traceback
16
16
17 from ..i18n import _
17 from ..i18n import _
18
18
19 from .. import (
19 from .. import (
20 encoding,
20 encoding,
21 error,
21 error,
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25
25
26 httpservermod = util.httpserver
26 httpservermod = util.httpserver
27 socketserver = util.socketserver
27 socketserver = util.socketserver
28 urlerr = util.urlerr
28 urlerr = util.urlerr
29 urlreq = util.urlreq
29 urlreq = util.urlreq
30
30
31 from . import (
31 from . import (
32 common,
32 common,
33 )
33 )
34
34
35 def _splitURI(uri):
35 def _splitURI(uri):
36 """Return path and query that has been split from uri
36 """Return path and query that has been split from uri
37
37
38 Just like CGI environment, the path is unquoted, the query is
38 Just like CGI environment, the path is unquoted, the query is
39 not.
39 not.
40 """
40 """
41 if r'?' in uri:
41 if r'?' in uri:
42 path, query = uri.split(r'?', 1)
42 path, query = uri.split(r'?', 1)
43 else:
43 else:
44 path, query = uri, r''
44 path, query = uri, r''
45 return urlreq.unquote(path), query
45 return urlreq.unquote(path), query
46
46
47 class _error_logger(object):
47 class _error_logger(object):
48 def __init__(self, handler):
48 def __init__(self, handler):
49 self.handler = handler
49 self.handler = handler
50 def flush(self):
50 def flush(self):
51 pass
51 pass
52 def write(self, str):
52 def write(self, str):
53 self.writelines(str.split('\n'))
53 self.writelines(str.split('\n'))
54 def writelines(self, seq):
54 def writelines(self, seq):
55 for msg in seq:
55 for msg in seq:
56 self.handler.log_error("HG error: %s", msg)
56 self.handler.log_error("HG error: %s", msg)
57
57
58 class _httprequesthandler(httpservermod.basehttprequesthandler):
58 class _httprequesthandler(httpservermod.basehttprequesthandler):
59
59
60 url_scheme = 'http'
60 url_scheme = 'http'
61
61
62 @staticmethod
62 @staticmethod
63 def preparehttpserver(httpserver, ui):
63 def preparehttpserver(httpserver, ui):
64 """Prepare .socket of new HTTPServer instance"""
64 """Prepare .socket of new HTTPServer instance"""
65
65
66 def __init__(self, *args, **kargs):
66 def __init__(self, *args, **kargs):
67 self.protocol_version = r'HTTP/1.1'
67 self.protocol_version = r'HTTP/1.1'
68 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
68 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
69
69
70 def _log_any(self, fp, format, *args):
70 def _log_any(self, fp, format, *args):
71 fp.write(pycompat.sysbytes(
71 fp.write(pycompat.sysbytes(
72 r"%s - - [%s] %s" % (self.client_address[0],
72 r"%s - - [%s] %s" % (self.client_address[0],
73 self.log_date_time_string(),
73 self.log_date_time_string(),
74 format % args)) + '\n')
74 format % args)) + '\n')
75 fp.flush()
75 fp.flush()
76
76
77 def log_error(self, format, *args):
77 def log_error(self, format, *args):
78 self._log_any(self.server.errorlog, format, *args)
78 self._log_any(self.server.errorlog, format, *args)
79
79
80 def log_message(self, format, *args):
80 def log_message(self, format, *args):
81 self._log_any(self.server.accesslog, format, *args)
81 self._log_any(self.server.accesslog, format, *args)
82
82
83 def log_request(self, code=r'-', size=r'-'):
83 def log_request(self, code=r'-', size=r'-'):
84 xheaders = []
84 xheaders = []
85 if util.safehasattr(self, 'headers'):
85 if util.safehasattr(self, 'headers'):
86 xheaders = [h for h in self.headers.items()
86 xheaders = [h for h in self.headers.items()
87 if h[0].startswith(r'x-')]
87 if h[0].startswith(r'x-')]
88 self.log_message(r'"%s" %s %s%s',
88 self.log_message(r'"%s" %s %s%s',
89 self.requestline, str(code), str(size),
89 self.requestline, str(code), str(size),
90 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
90 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
91
91
92 def do_write(self):
92 def do_write(self):
93 try:
93 try:
94 self.do_hgweb()
94 self.do_hgweb()
95 except socket.error as inst:
95 except socket.error as inst:
96 if inst[0] != errno.EPIPE:
96 if inst[0] != errno.EPIPE:
97 raise
97 raise
98
98
99 def do_POST(self):
99 def do_POST(self):
100 try:
100 try:
101 self.do_write()
101 self.do_write()
102 except Exception:
102 except Exception:
103 self._start_response("500 Internal Server Error", [])
103 self._start_response("500 Internal Server Error", [])
104 self._write("Internal Server Error")
104 self._write("Internal Server Error")
105 self._done()
105 self._done()
106 tb = r"".join(traceback.format_exception(*sys.exc_info()))
106 tb = r"".join(traceback.format_exception(*sys.exc_info()))
107 # We need a native-string newline to poke in the log
107 # We need a native-string newline to poke in the log
108 # message, because we won't get a newline when using an
108 # message, because we won't get a newline when using an
109 # r-string. This is the easy way out.
109 # r-string. This is the easy way out.
110 newline = chr(10)
110 newline = chr(10)
111 self.log_error(r"Exception happened during processing "
111 self.log_error(r"Exception happened during processing "
112 r"request '%s':%s%s", self.path, newline, tb)
112 r"request '%s':%s%s", self.path, newline, tb)
113
113
114 def do_GET(self):
114 def do_GET(self):
115 self.do_POST()
115 self.do_POST()
116
116
117 def do_hgweb(self):
117 def do_hgweb(self):
118 self.sent_headers = False
118 self.sent_headers = False
119 path, query = _splitURI(self.path)
119 path, query = _splitURI(self.path)
120
120
121 env = {}
121 env = {}
122 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
122 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
123 env[r'REQUEST_METHOD'] = self.command
123 env[r'REQUEST_METHOD'] = self.command
124 env[r'SERVER_NAME'] = self.server.server_name
124 env[r'SERVER_NAME'] = self.server.server_name
125 env[r'SERVER_PORT'] = str(self.server.server_port)
125 env[r'SERVER_PORT'] = str(self.server.server_port)
126 env[r'REQUEST_URI'] = self.path
126 env[r'REQUEST_URI'] = self.path
127 env[r'SCRIPT_NAME'] = self.server.prefix
127 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
128 env[r'PATH_INFO'] = path[len(self.server.prefix):]
128 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix):])
129 env[r'REMOTE_HOST'] = self.client_address[0]
129 env[r'REMOTE_HOST'] = self.client_address[0]
130 env[r'REMOTE_ADDR'] = self.client_address[0]
130 env[r'REMOTE_ADDR'] = self.client_address[0]
131 if query:
131 if query:
132 env[r'QUERY_STRING'] = query
132 env[r'QUERY_STRING'] = query
133
133
134 if pycompat.ispy3:
134 if pycompat.ispy3:
135 if self.headers.get_content_type() is None:
135 if self.headers.get_content_type() is None:
136 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
136 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
137 else:
137 else:
138 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
138 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
139 length = self.headers.get('content-length')
139 length = self.headers.get('content-length')
140 else:
140 else:
141 if self.headers.typeheader is None:
141 if self.headers.typeheader is None:
142 env[r'CONTENT_TYPE'] = self.headers.type
142 env[r'CONTENT_TYPE'] = self.headers.type
143 else:
143 else:
144 env[r'CONTENT_TYPE'] = self.headers.typeheader
144 env[r'CONTENT_TYPE'] = self.headers.typeheader
145 length = self.headers.getheader('content-length')
145 length = self.headers.getheader('content-length')
146 if length:
146 if length:
147 env[r'CONTENT_LENGTH'] = length
147 env[r'CONTENT_LENGTH'] = length
148 for header in [h for h in self.headers.keys()
148 for header in [h for h in self.headers.keys()
149 if h not in ('content-type', 'content-length')]:
149 if h not in ('content-type', 'content-length')]:
150 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
150 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
151 hval = self.headers.get(header)
151 hval = self.headers.get(header)
152 hval = hval.replace(r'\n', r'').strip()
152 hval = hval.replace(r'\n', r'').strip()
153 if hval:
153 if hval:
154 env[hkey] = hval
154 env[hkey] = hval
155 env[r'SERVER_PROTOCOL'] = self.request_version
155 env[r'SERVER_PROTOCOL'] = self.request_version
156 env[r'wsgi.version'] = (1, 0)
156 env[r'wsgi.version'] = (1, 0)
157 env[r'wsgi.url_scheme'] = self.url_scheme
157 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
158 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
158 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
159 self.rfile = common.continuereader(self.rfile, self.wfile.write)
159 self.rfile = common.continuereader(self.rfile, self.wfile.write)
160
160
161 env[r'wsgi.input'] = self.rfile
161 env[r'wsgi.input'] = self.rfile
162 env[r'wsgi.errors'] = _error_logger(self)
162 env[r'wsgi.errors'] = _error_logger(self)
163 env[r'wsgi.multithread'] = isinstance(self.server,
163 env[r'wsgi.multithread'] = isinstance(self.server,
164 socketserver.ThreadingMixIn)
164 socketserver.ThreadingMixIn)
165 env[r'wsgi.multiprocess'] = isinstance(self.server,
165 env[r'wsgi.multiprocess'] = isinstance(self.server,
166 socketserver.ForkingMixIn)
166 socketserver.ForkingMixIn)
167 env[r'wsgi.run_once'] = 0
167 env[r'wsgi.run_once'] = 0
168
168
169 self.saved_status = None
169 self.saved_status = None
170 self.saved_headers = []
170 self.saved_headers = []
171 self.length = None
171 self.length = None
172 self._chunked = None
172 self._chunked = None
173 for chunk in self.server.application(env, self._start_response):
173 for chunk in self.server.application(env, self._start_response):
174 self._write(chunk)
174 self._write(chunk)
175 if not self.sent_headers:
175 if not self.sent_headers:
176 self.send_headers()
176 self.send_headers()
177 self._done()
177 self._done()
178
178
179 def send_headers(self):
179 def send_headers(self):
180 if not self.saved_status:
180 if not self.saved_status:
181 raise AssertionError("Sending headers before "
181 raise AssertionError("Sending headers before "
182 "start_response() called")
182 "start_response() called")
183 saved_status = self.saved_status.split(None, 1)
183 saved_status = self.saved_status.split(None, 1)
184 saved_status[0] = int(saved_status[0])
184 saved_status[0] = int(saved_status[0])
185 self.send_response(*saved_status)
185 self.send_response(*saved_status)
186 self.length = None
186 self.length = None
187 self._chunked = False
187 self._chunked = False
188 for h in self.saved_headers:
188 for h in self.saved_headers:
189 self.send_header(*h)
189 self.send_header(*h)
190 if h[0].lower() == 'content-length':
190 if h[0].lower() == 'content-length':
191 self.length = int(h[1])
191 self.length = int(h[1])
192 if (self.length is None and
192 if (self.length is None and
193 saved_status[0] != common.HTTP_NOT_MODIFIED):
193 saved_status[0] != common.HTTP_NOT_MODIFIED):
194 self._chunked = (not self.close_connection and
194 self._chunked = (not self.close_connection and
195 self.request_version == "HTTP/1.1")
195 self.request_version == "HTTP/1.1")
196 if self._chunked:
196 if self._chunked:
197 self.send_header(r'Transfer-Encoding', r'chunked')
197 self.send_header(r'Transfer-Encoding', r'chunked')
198 else:
198 else:
199 self.send_header(r'Connection', r'close')
199 self.send_header(r'Connection', r'close')
200 self.end_headers()
200 self.end_headers()
201 self.sent_headers = True
201 self.sent_headers = True
202
202
203 def _start_response(self, http_status, headers, exc_info=None):
203 def _start_response(self, http_status, headers, exc_info=None):
204 code, msg = http_status.split(None, 1)
204 code, msg = http_status.split(None, 1)
205 code = int(code)
205 code = int(code)
206 self.saved_status = http_status
206 self.saved_status = http_status
207 bad_headers = ('connection', 'transfer-encoding')
207 bad_headers = ('connection', 'transfer-encoding')
208 self.saved_headers = [h for h in headers
208 self.saved_headers = [h for h in headers
209 if h[0].lower() not in bad_headers]
209 if h[0].lower() not in bad_headers]
210 return self._write
210 return self._write
211
211
212 def _write(self, data):
212 def _write(self, data):
213 if not self.saved_status:
213 if not self.saved_status:
214 raise AssertionError("data written before start_response() called")
214 raise AssertionError("data written before start_response() called")
215 elif not self.sent_headers:
215 elif not self.sent_headers:
216 self.send_headers()
216 self.send_headers()
217 if self.length is not None:
217 if self.length is not None:
218 if len(data) > self.length:
218 if len(data) > self.length:
219 raise AssertionError("Content-length header sent, but more "
219 raise AssertionError("Content-length header sent, but more "
220 "bytes than specified are being written.")
220 "bytes than specified are being written.")
221 self.length = self.length - len(data)
221 self.length = self.length - len(data)
222 elif self._chunked and data:
222 elif self._chunked and data:
223 data = '%x\r\n%s\r\n' % (len(data), data)
223 data = '%x\r\n%s\r\n' % (len(data), data)
224 self.wfile.write(data)
224 self.wfile.write(data)
225 self.wfile.flush()
225 self.wfile.flush()
226
226
227 def _done(self):
227 def _done(self):
228 if self._chunked:
228 if self._chunked:
229 self.wfile.write('0\r\n\r\n')
229 self.wfile.write('0\r\n\r\n')
230 self.wfile.flush()
230 self.wfile.flush()
231
231
232 class _httprequesthandlerssl(_httprequesthandler):
232 class _httprequesthandlerssl(_httprequesthandler):
233 """HTTPS handler based on Python's ssl module"""
233 """HTTPS handler based on Python's ssl module"""
234
234
235 url_scheme = 'https'
235 url_scheme = 'https'
236
236
237 @staticmethod
237 @staticmethod
238 def preparehttpserver(httpserver, ui):
238 def preparehttpserver(httpserver, ui):
239 try:
239 try:
240 from .. import sslutil
240 from .. import sslutil
241 sslutil.modernssl
241 sslutil.modernssl
242 except ImportError:
242 except ImportError:
243 raise error.Abort(_("SSL support is unavailable"))
243 raise error.Abort(_("SSL support is unavailable"))
244
244
245 certfile = ui.config('web', 'certificate')
245 certfile = ui.config('web', 'certificate')
246
246
247 # These config options are currently only meant for testing. Use
247 # These config options are currently only meant for testing. Use
248 # at your own risk.
248 # at your own risk.
249 cafile = ui.config('devel', 'servercafile')
249 cafile = ui.config('devel', 'servercafile')
250 reqcert = ui.configbool('devel', 'serverrequirecert')
250 reqcert = ui.configbool('devel', 'serverrequirecert')
251
251
252 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
252 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
253 ui,
253 ui,
254 certfile=certfile,
254 certfile=certfile,
255 cafile=cafile,
255 cafile=cafile,
256 requireclientcert=reqcert)
256 requireclientcert=reqcert)
257
257
258 def setup(self):
258 def setup(self):
259 self.connection = self.request
259 self.connection = self.request
260 self.rfile = self.request.makefile(r"rb", self.rbufsize)
260 self.rfile = self.request.makefile(r"rb", self.rbufsize)
261 self.wfile = self.request.makefile(r"wb", self.wbufsize)
261 self.wfile = self.request.makefile(r"wb", self.wbufsize)
262
262
263 try:
263 try:
264 import threading
264 import threading
265 threading.activeCount() # silence pyflakes and bypass demandimport
265 threading.activeCount() # silence pyflakes and bypass demandimport
266 _mixin = socketserver.ThreadingMixIn
266 _mixin = socketserver.ThreadingMixIn
267 except ImportError:
267 except ImportError:
268 if util.safehasattr(os, "fork"):
268 if util.safehasattr(os, "fork"):
269 _mixin = socketserver.ForkingMixIn
269 _mixin = socketserver.ForkingMixIn
270 else:
270 else:
271 class _mixin(object):
271 class _mixin(object):
272 pass
272 pass
273
273
274 def openlog(opt, default):
274 def openlog(opt, default):
275 if opt and opt != '-':
275 if opt and opt != '-':
276 return open(opt, 'ab')
276 return open(opt, 'ab')
277 return default
277 return default
278
278
279 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
279 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
280
280
281 # SO_REUSEADDR has broken semantics on windows
281 # SO_REUSEADDR has broken semantics on windows
282 if pycompat.iswindows:
282 if pycompat.iswindows:
283 allow_reuse_address = 0
283 allow_reuse_address = 0
284
284
285 def __init__(self, ui, app, addr, handler, **kwargs):
285 def __init__(self, ui, app, addr, handler, **kwargs):
286 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
286 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
287 self.daemon_threads = True
287 self.daemon_threads = True
288 self.application = app
288 self.application = app
289
289
290 handler.preparehttpserver(self, ui)
290 handler.preparehttpserver(self, ui)
291
291
292 prefix = ui.config('web', 'prefix')
292 prefix = ui.config('web', 'prefix')
293 if prefix:
293 if prefix:
294 prefix = '/' + prefix.strip('/')
294 prefix = '/' + prefix.strip('/')
295 self.prefix = prefix
295 self.prefix = prefix
296
296
297 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
297 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
298 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
298 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
299 self.accesslog = alog
299 self.accesslog = alog
300 self.errorlog = elog
300 self.errorlog = elog
301
301
302 self.addr, self.port = self.socket.getsockname()[0:2]
302 self.addr, self.port = self.socket.getsockname()[0:2]
303 self.fqaddr = socket.getfqdn(addr[0])
303 self.fqaddr = socket.getfqdn(addr[0])
304
304
305 class IPv6HTTPServer(MercurialHTTPServer):
305 class IPv6HTTPServer(MercurialHTTPServer):
306 address_family = getattr(socket, 'AF_INET6', None)
306 address_family = getattr(socket, 'AF_INET6', None)
307 def __init__(self, *args, **kwargs):
307 def __init__(self, *args, **kwargs):
308 if self.address_family is None:
308 if self.address_family is None:
309 raise error.RepoError(_('IPv6 is not available on this system'))
309 raise error.RepoError(_('IPv6 is not available on this system'))
310 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
310 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
311
311
312 def create_server(ui, app):
312 def create_server(ui, app):
313
313
314 if ui.config('web', 'certificate'):
314 if ui.config('web', 'certificate'):
315 handler = _httprequesthandlerssl
315 handler = _httprequesthandlerssl
316 else:
316 else:
317 handler = _httprequesthandler
317 handler = _httprequesthandler
318
318
319 if ui.configbool('web', 'ipv6'):
319 if ui.configbool('web', 'ipv6'):
320 cls = IPv6HTTPServer
320 cls = IPv6HTTPServer
321 else:
321 else:
322 cls = MercurialHTTPServer
322 cls = MercurialHTTPServer
323
323
324 # ugly hack due to python issue5853 (for threaded use)
324 # ugly hack due to python issue5853 (for threaded use)
325 try:
325 try:
326 import mimetypes
326 import mimetypes
327 mimetypes.init()
327 mimetypes.init()
328 except UnicodeDecodeError:
328 except UnicodeDecodeError:
329 # Python 2.x's mimetypes module attempts to decode strings
329 # Python 2.x's mimetypes module attempts to decode strings
330 # from Windows' ANSI APIs as ascii (fail), then re-encode them
330 # from Windows' ANSI APIs as ascii (fail), then re-encode them
331 # as ascii (clown fail), because the default Python Unicode
331 # as ascii (clown fail), because the default Python Unicode
332 # codec is hardcoded as ascii.
332 # codec is hardcoded as ascii.
333
333
334 sys.argv # unwrap demand-loader so that reload() works
334 sys.argv # unwrap demand-loader so that reload() works
335 reload(sys) # resurrect sys.setdefaultencoding()
335 reload(sys) # resurrect sys.setdefaultencoding()
336 oldenc = sys.getdefaultencoding()
336 oldenc = sys.getdefaultencoding()
337 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
337 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
338 mimetypes.init()
338 mimetypes.init()
339 sys.setdefaultencoding(oldenc)
339 sys.setdefaultencoding(oldenc)
340
340
341 address = ui.config('web', 'address')
341 address = ui.config('web', 'address')
342 port = util.getport(ui.config('web', 'port'))
342 port = util.getport(ui.config('web', 'port'))
343 try:
343 try:
344 return cls(ui, app, (address, port), handler)
344 return cls(ui, app, (address, port), handler)
345 except socket.error as inst:
345 except socket.error as inst:
346 raise error.Abort(_("cannot start server at '%s:%d': %s")
346 raise error.Abort(_("cannot start server at '%s:%d': %s")
347 % (address, port, encoding.strtolocal(inst.args[1])))
347 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now