##// END OF EJS Templates
hgweb: more "http headers are native strs" cleanup...
Augie Fackler -
r34741:b2601c59 default
parent child Browse files
Show More
@@ -1,491 +1,491 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 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[r'SERVER_PORT']
168 port = req.env[r'SERVER_PORT']
169 port = port != default_port and (r':' + port) or r''
169 port = port != default_port and (r':' + port) or r''
170 urlbase = r'%s://%s%s' % (proto, req.env[r'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[r'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[r'REPO_NAME'] + r'/'
348 req.url += req.env[r'REPO_NAME'] + r'/'
349
349
350 if r'PATH_INFO' in req.env:
350 if r'PATH_INFO' in req.env:
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
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[r'QUERY_STRING'].partition(r'&')[0]
357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
358 query = query.partition(r';')[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 = pycompat.sysbytes(req.form.get(r'cmd', [r''])[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((r'Connection', r'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 r'cmd' not in req.form and args and args[0]:
389 if r'cmd' not in req.form and args and args[0]:
390 cmd = args.pop(0)
390 cmd = args.pop(0)
391 style = cmd.rfind('-')
391 style = cmd.rfind('-')
392 if style != -1:
392 if style != -1:
393 req.form['style'] = [cmd[:style]]
393 req.form['style'] = [cmd[:style]]
394 cmd = cmd[style + 1:]
394 cmd = cmd[style + 1:]
395
395
396 # avoid accepting e.g. style parameter as command
396 # avoid accepting e.g. style parameter as command
397 if util.safehasattr(webcommands, cmd):
397 if util.safehasattr(webcommands, cmd):
398 req.form[r'cmd'] = [cmd]
398 req.form[r'cmd'] = [cmd]
399
399
400 if cmd == 'static':
400 if cmd == 'static':
401 req.form['file'] = ['/'.join(args)]
401 req.form['file'] = ['/'.join(args)]
402 else:
402 else:
403 if args and args[0]:
403 if args and args[0]:
404 node = args.pop(0).replace('%2F', '/')
404 node = args.pop(0).replace('%2F', '/')
405 req.form['node'] = [node]
405 req.form['node'] = [node]
406 if args:
406 if args:
407 req.form['file'] = args
407 req.form['file'] = args
408
408
409 ua = req.env.get('HTTP_USER_AGENT', '')
409 ua = req.env.get('HTTP_USER_AGENT', '')
410 if cmd == 'rev' and 'mercurial' in ua:
410 if cmd == 'rev' and 'mercurial' in ua:
411 req.form['style'] = ['raw']
411 req.form['style'] = ['raw']
412
412
413 if cmd == 'archive':
413 if cmd == 'archive':
414 fn = req.form['node'][0]
414 fn = req.form['node'][0]
415 for type_, spec in rctx.archivespecs.iteritems():
415 for type_, spec in rctx.archivespecs.iteritems():
416 ext = spec[2]
416 ext = spec[2]
417 if fn.endswith(ext):
417 if fn.endswith(ext):
418 req.form['node'] = [fn[:-len(ext)]]
418 req.form['node'] = [fn[:-len(ext)]]
419 req.form['type'] = [type_]
419 req.form['type'] = [type_]
420
420
421 # process the web interface request
421 # process the web interface request
422
422
423 try:
423 try:
424 tmpl = rctx.templater(req)
424 tmpl = rctx.templater(req)
425 ctype = tmpl('mimetype', encoding=encoding.encoding)
425 ctype = tmpl('mimetype', encoding=encoding.encoding)
426 ctype = templater.stringify(ctype)
426 ctype = templater.stringify(ctype)
427
427
428 # check read permissions non-static content
428 # check read permissions non-static content
429 if cmd != 'static':
429 if cmd != 'static':
430 self.check_perm(rctx, req, None)
430 self.check_perm(rctx, req, None)
431
431
432 if cmd == '':
432 if cmd == '':
433 req.form[r'cmd'] = [tmpl.cache['default']]
433 req.form[r'cmd'] = [tmpl.cache['default']]
434 cmd = req.form[r'cmd'][0]
434 cmd = req.form[r'cmd'][0]
435
435
436 # 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
437 # be a nonce.
437 # be a nonce.
438 if rctx.configbool('web', 'cache') and not rctx.nonce:
438 if rctx.configbool('web', 'cache') and not rctx.nonce:
439 caching(self, req) # sets ETag header or raises NOT_MODIFIED
439 caching(self, req) # sets ETag header or raises NOT_MODIFIED
440 if cmd not in webcommands.__all__:
440 if cmd not in webcommands.__all__:
441 msg = 'no such method: %s' % cmd
441 msg = 'no such method: %s' % cmd
442 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
442 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
443 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
443 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
444 rctx.ctype = ctype
444 rctx.ctype = ctype
445 content = webcommands.rawfile(rctx, req, tmpl)
445 content = webcommands.rawfile(rctx, req, tmpl)
446 else:
446 else:
447 content = getattr(webcommands, cmd)(rctx, req, tmpl)
447 content = getattr(webcommands, cmd)(rctx, req, tmpl)
448 req.respond(HTTP_OK, ctype)
448 req.respond(HTTP_OK, ctype)
449
449
450 return content
450 return content
451
451
452 except (error.LookupError, error.RepoLookupError) as err:
452 except (error.LookupError, error.RepoLookupError) as err:
453 req.respond(HTTP_NOT_FOUND, ctype)
453 req.respond(HTTP_NOT_FOUND, ctype)
454 msg = str(err)
454 msg = str(err)
455 if (util.safehasattr(err, 'name') and
455 if (util.safehasattr(err, 'name') and
456 not isinstance(err, error.ManifestLookupError)):
456 not isinstance(err, error.ManifestLookupError)):
457 msg = 'revision not found: %s' % err.name
457 msg = 'revision not found: %s' % err.name
458 return tmpl('error', error=msg)
458 return tmpl('error', error=msg)
459 except (error.RepoError, error.RevlogError) as inst:
459 except (error.RepoError, error.RevlogError) as inst:
460 req.respond(HTTP_SERVER_ERROR, ctype)
460 req.respond(HTTP_SERVER_ERROR, ctype)
461 return tmpl('error', error=str(inst))
461 return tmpl('error', error=str(inst))
462 except ErrorResponse as inst:
462 except ErrorResponse as inst:
463 req.respond(inst, ctype)
463 req.respond(inst, ctype)
464 if inst.code == HTTP_NOT_MODIFIED:
464 if inst.code == HTTP_NOT_MODIFIED:
465 # Not allowed to return a body on a 304
465 # Not allowed to return a body on a 304
466 return ['']
466 return ['']
467 return tmpl('error', error=str(inst))
467 return tmpl('error', error=str(inst))
468
468
469 def check_perm(self, rctx, req, op):
469 def check_perm(self, rctx, req, op):
470 for permhook in permhooks:
470 for permhook in permhooks:
471 permhook(rctx, req, op)
471 permhook(rctx, req, op)
472
472
473 def getwebview(repo):
473 def getwebview(repo):
474 """The 'web.view' config controls changeset filter to hgweb. Possible
474 """The 'web.view' config controls changeset filter to hgweb. Possible
475 values are ``served``, ``visible`` and ``all``. Default is ``served``.
475 values are ``served``, ``visible`` and ``all``. Default is ``served``.
476 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
477 hgweb instance. The``visible`` filter includes secret changesets but
477 hgweb instance. The``visible`` filter includes secret changesets but
478 still excludes "hidden" one.
478 still excludes "hidden" one.
479
479
480 See the repoview module for details.
480 See the repoview module for details.
481
481
482 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
483 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."""
484 # experimental config: web.view
484 # experimental config: web.view
485 viewconfig = repo.ui.config('web', 'view', untrusted=True)
485 viewconfig = repo.ui.config('web', 'view', untrusted=True)
486 if viewconfig == 'all':
486 if viewconfig == 'all':
487 return repo.unfiltered()
487 return repo.unfiltered()
488 elif viewconfig in repoview.filtertable:
488 elif viewconfig in repoview.filtertable:
489 return repo.filtered(viewconfig)
489 return repo.filtered(viewconfig)
490 else:
490 else:
491 return repo.filtered('served')
491 return repo.filtered('served')
@@ -1,203 +1,203 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import cgi
10 import cgi
11 import struct
11 import struct
12
12
13 from .common import (
13 from .common import (
14 HTTP_OK,
14 HTTP_OK,
15 )
15 )
16
16
17 from .. import (
17 from .. import (
18 error,
18 error,
19 util,
19 util,
20 wireproto,
20 wireproto,
21 )
21 )
22 stringio = util.stringio
22 stringio = util.stringio
23
23
24 urlerr = util.urlerr
24 urlerr = util.urlerr
25 urlreq = util.urlreq
25 urlreq = util.urlreq
26
26
27 HGTYPE = 'application/mercurial-0.1'
27 HGTYPE = 'application/mercurial-0.1'
28 HGTYPE2 = 'application/mercurial-0.2'
28 HGTYPE2 = 'application/mercurial-0.2'
29 HGERRTYPE = 'application/hg-error'
29 HGERRTYPE = 'application/hg-error'
30
30
31 def decodevaluefromheaders(req, headerprefix):
31 def decodevaluefromheaders(req, headerprefix):
32 """Decode a long value from multiple HTTP request headers."""
32 """Decode a long value from multiple HTTP request headers."""
33 chunks = []
33 chunks = []
34 i = 1
34 i = 1
35 while True:
35 while True:
36 v = req.env.get('HTTP_%s_%d' % (
36 v = req.env.get('HTTP_%s_%d' % (
37 headerprefix.upper().replace('-', '_'), i))
37 headerprefix.upper().replace('-', '_'), i))
38 if v is None:
38 if v is None:
39 break
39 break
40 chunks.append(v)
40 chunks.append(v)
41 i += 1
41 i += 1
42
42
43 return ''.join(chunks)
43 return ''.join(chunks)
44
44
45 class webproto(wireproto.abstractserverproto):
45 class webproto(wireproto.abstractserverproto):
46 def __init__(self, req, ui):
46 def __init__(self, req, ui):
47 self.req = req
47 self.req = req
48 self.response = ''
48 self.response = ''
49 self.ui = ui
49 self.ui = ui
50 self.name = 'http'
50 self.name = 'http'
51
51
52 def getargs(self, args):
52 def getargs(self, args):
53 knownargs = self._args()
53 knownargs = self._args()
54 data = {}
54 data = {}
55 keys = args.split()
55 keys = args.split()
56 for k in keys:
56 for k in keys:
57 if k == '*':
57 if k == '*':
58 star = {}
58 star = {}
59 for key in knownargs.keys():
59 for key in knownargs.keys():
60 if key != 'cmd' and key not in keys:
60 if key != 'cmd' and key not in keys:
61 star[key] = knownargs[key][0]
61 star[key] = knownargs[key][0]
62 data['*'] = star
62 data['*'] = star
63 else:
63 else:
64 data[k] = knownargs[k][0]
64 data[k] = knownargs[k][0]
65 return [data[k] for k in keys]
65 return [data[k] for k in keys]
66 def _args(self):
66 def _args(self):
67 args = self.req.form.copy()
67 args = self.req.form.copy()
68 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
68 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
69 if postlen:
69 if postlen:
70 args.update(cgi.parse_qs(
70 args.update(cgi.parse_qs(
71 self.req.read(postlen), keep_blank_values=True))
71 self.req.read(postlen), keep_blank_values=True))
72 return args
72 return args
73
73
74 argvalue = decodevaluefromheaders(self.req, 'X-HgArg')
74 argvalue = decodevaluefromheaders(self.req, 'X-HgArg')
75 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
75 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
76 return args
76 return args
77 def getfile(self, fp):
77 def getfile(self, fp):
78 length = int(self.req.env['CONTENT_LENGTH'])
78 length = int(self.req.env[r'CONTENT_LENGTH'])
79 # If httppostargs is used, we need to read Content-Length
79 # If httppostargs is used, we need to read Content-Length
80 # minus the amount that was consumed by args.
80 # minus the amount that was consumed by args.
81 length -= int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
81 length -= int(self.req.env.get(r'HTTP_X_HGARGS_POST', 0))
82 for s in util.filechunkiter(self.req, limit=length):
82 for s in util.filechunkiter(self.req, limit=length):
83 fp.write(s)
83 fp.write(s)
84 def redirect(self):
84 def redirect(self):
85 self.oldio = self.ui.fout, self.ui.ferr
85 self.oldio = self.ui.fout, self.ui.ferr
86 self.ui.ferr = self.ui.fout = stringio()
86 self.ui.ferr = self.ui.fout = stringio()
87 def restore(self):
87 def restore(self):
88 val = self.ui.fout.getvalue()
88 val = self.ui.fout.getvalue()
89 self.ui.ferr, self.ui.fout = self.oldio
89 self.ui.ferr, self.ui.fout = self.oldio
90 return val
90 return val
91
91
92 def _client(self):
92 def _client(self):
93 return 'remote:%s:%s:%s' % (
93 return 'remote:%s:%s:%s' % (
94 self.req.env.get('wsgi.url_scheme') or 'http',
94 self.req.env.get('wsgi.url_scheme') or 'http',
95 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
95 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
96 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
96 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
97
97
98 def responsetype(self, v1compressible=False):
98 def responsetype(self, v1compressible=False):
99 """Determine the appropriate response type and compression settings.
99 """Determine the appropriate response type and compression settings.
100
100
101 The ``v1compressible`` argument states whether the response with
101 The ``v1compressible`` argument states whether the response with
102 application/mercurial-0.1 media types should be zlib compressed.
102 application/mercurial-0.1 media types should be zlib compressed.
103
103
104 Returns a tuple of (mediatype, compengine, engineopts).
104 Returns a tuple of (mediatype, compengine, engineopts).
105 """
105 """
106 # For now, if it isn't compressible in the old world, it's never
106 # For now, if it isn't compressible in the old world, it's never
107 # compressible. We can change this to send uncompressed 0.2 payloads
107 # compressible. We can change this to send uncompressed 0.2 payloads
108 # later.
108 # later.
109 if not v1compressible:
109 if not v1compressible:
110 return HGTYPE, None, None
110 return HGTYPE, None, None
111
111
112 # Determine the response media type and compression engine based
112 # Determine the response media type and compression engine based
113 # on the request parameters.
113 # on the request parameters.
114 protocaps = decodevaluefromheaders(self.req, 'X-HgProto').split(' ')
114 protocaps = decodevaluefromheaders(self.req, 'X-HgProto').split(' ')
115
115
116 if '0.2' in protocaps:
116 if '0.2' in protocaps:
117 # Default as defined by wire protocol spec.
117 # Default as defined by wire protocol spec.
118 compformats = ['zlib', 'none']
118 compformats = ['zlib', 'none']
119 for cap in protocaps:
119 for cap in protocaps:
120 if cap.startswith('comp='):
120 if cap.startswith('comp='):
121 compformats = cap[5:].split(',')
121 compformats = cap[5:].split(',')
122 break
122 break
123
123
124 # Now find an agreed upon compression format.
124 # Now find an agreed upon compression format.
125 for engine in wireproto.supportedcompengines(self.ui, self,
125 for engine in wireproto.supportedcompengines(self.ui, self,
126 util.SERVERROLE):
126 util.SERVERROLE):
127 if engine.wireprotosupport().name in compformats:
127 if engine.wireprotosupport().name in compformats:
128 opts = {}
128 opts = {}
129 level = self.ui.configint('server',
129 level = self.ui.configint('server',
130 '%slevel' % engine.name())
130 '%slevel' % engine.name())
131 if level is not None:
131 if level is not None:
132 opts['level'] = level
132 opts['level'] = level
133
133
134 return HGTYPE2, engine, opts
134 return HGTYPE2, engine, opts
135
135
136 # No mutually supported compression format. Fall back to the
136 # No mutually supported compression format. Fall back to the
137 # legacy protocol.
137 # legacy protocol.
138
138
139 # Don't allow untrusted settings because disabling compression or
139 # Don't allow untrusted settings because disabling compression or
140 # setting a very high compression level could lead to flooding
140 # setting a very high compression level could lead to flooding
141 # the server's network or CPU.
141 # the server's network or CPU.
142 opts = {'level': self.ui.configint('server', 'zliblevel')}
142 opts = {'level': self.ui.configint('server', 'zliblevel')}
143 return HGTYPE, util.compengines['zlib'], opts
143 return HGTYPE, util.compengines['zlib'], opts
144
144
145 def iscmd(cmd):
145 def iscmd(cmd):
146 return cmd in wireproto.commands
146 return cmd in wireproto.commands
147
147
148 def call(repo, req, cmd):
148 def call(repo, req, cmd):
149 p = webproto(req, repo.ui)
149 p = webproto(req, repo.ui)
150
150
151 def genversion2(gen, compress, engine, engineopts):
151 def genversion2(gen, compress, engine, engineopts):
152 # application/mercurial-0.2 always sends a payload header
152 # application/mercurial-0.2 always sends a payload header
153 # identifying the compression engine.
153 # identifying the compression engine.
154 name = engine.wireprotosupport().name
154 name = engine.wireprotosupport().name
155 assert 0 < len(name) < 256
155 assert 0 < len(name) < 256
156 yield struct.pack('B', len(name))
156 yield struct.pack('B', len(name))
157 yield name
157 yield name
158
158
159 if compress:
159 if compress:
160 for chunk in engine.compressstream(gen, opts=engineopts):
160 for chunk in engine.compressstream(gen, opts=engineopts):
161 yield chunk
161 yield chunk
162 else:
162 else:
163 for chunk in gen:
163 for chunk in gen:
164 yield chunk
164 yield chunk
165
165
166 rsp = wireproto.dispatch(repo, p, cmd)
166 rsp = wireproto.dispatch(repo, p, cmd)
167 if isinstance(rsp, bytes):
167 if isinstance(rsp, bytes):
168 req.respond(HTTP_OK, HGTYPE, body=rsp)
168 req.respond(HTTP_OK, HGTYPE, body=rsp)
169 return []
169 return []
170 elif isinstance(rsp, wireproto.streamres):
170 elif isinstance(rsp, wireproto.streamres):
171 if rsp.reader:
171 if rsp.reader:
172 gen = iter(lambda: rsp.reader.read(32768), '')
172 gen = iter(lambda: rsp.reader.read(32768), '')
173 else:
173 else:
174 gen = rsp.gen
174 gen = rsp.gen
175
175
176 # This code for compression should not be streamres specific. It
176 # This code for compression should not be streamres specific. It
177 # is here because we only compress streamres at the moment.
177 # is here because we only compress streamres at the moment.
178 mediatype, engine, engineopts = p.responsetype(rsp.v1compressible)
178 mediatype, engine, engineopts = p.responsetype(rsp.v1compressible)
179
179
180 if mediatype == HGTYPE and rsp.v1compressible:
180 if mediatype == HGTYPE and rsp.v1compressible:
181 gen = engine.compressstream(gen, engineopts)
181 gen = engine.compressstream(gen, engineopts)
182 elif mediatype == HGTYPE2:
182 elif mediatype == HGTYPE2:
183 gen = genversion2(gen, rsp.v1compressible, engine, engineopts)
183 gen = genversion2(gen, rsp.v1compressible, engine, engineopts)
184
184
185 req.respond(HTTP_OK, mediatype)
185 req.respond(HTTP_OK, mediatype)
186 return gen
186 return gen
187 elif isinstance(rsp, wireproto.pushres):
187 elif isinstance(rsp, wireproto.pushres):
188 val = p.restore()
188 val = p.restore()
189 rsp = '%d\n%s' % (rsp.res, val)
189 rsp = '%d\n%s' % (rsp.res, val)
190 req.respond(HTTP_OK, HGTYPE, body=rsp)
190 req.respond(HTTP_OK, HGTYPE, body=rsp)
191 return []
191 return []
192 elif isinstance(rsp, wireproto.pusherr):
192 elif isinstance(rsp, wireproto.pusherr):
193 # drain the incoming bundle
193 # drain the incoming bundle
194 req.drain()
194 req.drain()
195 p.restore()
195 p.restore()
196 rsp = '0\n%s\n' % rsp.res
196 rsp = '0\n%s\n' % rsp.res
197 req.respond(HTTP_OK, HGTYPE, body=rsp)
197 req.respond(HTTP_OK, HGTYPE, body=rsp)
198 return []
198 return []
199 elif isinstance(rsp, wireproto.ooberror):
199 elif isinstance(rsp, wireproto.ooberror):
200 rsp = rsp.message
200 rsp = rsp.message
201 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
201 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
202 return []
202 return []
203 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
203 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
@@ -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'] = self.server.prefix
128 env[r'PATH_INFO'] = path[len(self.server.prefix):]
128 env[r'PATH_INFO'] = 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'] = 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('Transfer-Encoding', 'chunked')
197 self.send_header(r'Transfer-Encoding', r'chunked')
198 else:
198 else:
199 self.send_header('Connection', '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 = socket._fileobject(self.request, "rb", self.rbufsize)
260 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
261 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
261 self.wfile = socket._fileobject(self.request, "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, 'a')
276 return open(opt, 'a')
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