1
2
3
4
5
6
7
8
9
10
11
12
13
14 import os.path
15 import re
16 import sys
17 import tempfile
18 import textwrap
19 import unittest
20 from subprocess import PIPE, Popen
21
22
23 import trac.tests.compat
24 from trac.config import ConfigurationError
25 from trac.core import Component, ComponentManager, TracError, implements
26 from trac.db.api import DatabaseManager
27 from trac.perm import PermissionError
28 from trac.resource import ResourceNotFound
29 from trac.test import EnvironmentStub, MockRequest
30 from trac.util import create_file
31 from trac.util.compat import close_fds
32 from trac.web.api import (HTTPForbidden, HTTPInternalError, HTTPNotFound,
33 IRequestFilter, IRequestHandler, RequestDone)
34 from trac.web.auth import IAuthenticator
35 from trac.web.main import FakeSession, RequestDispatcher, Session, \
36 get_environments
37
38
40
41 implements(IRequestHandler)
42
43 filename = 'test_stub.html'
44
45 template = textwrap.dedent("""\
46 <!DOCTYPE html
47 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
48 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
49 <html xmlns="http://www.w3.org/1999/xhtml"
50 xmlns:py="http://genshi.edgewall.org/">
51 <body>
52 <h1>${greeting}</h1>
53 </body>
54 </html>
55 """)
56
59
61 return self.filename, {'greeting': 'Hello World'}, None
62
63
65
71
74
76 class SuccessfulAuthenticator1(Component):
77 implements(IAuthenticator)
78 def authenticate(self, req):
79 return 'user1'
80 class SuccessfulAuthenticator2(Component):
81 implements(IAuthenticator)
82 def authenticate(self, req):
83 return 'user2'
84 self.assertEqual(2, len(self.request_dispatcher.authenticators))
85 self.assertIsInstance(self.request_dispatcher.authenticators[0],
86 SuccessfulAuthenticator1)
87 self.assertIsInstance(self.request_dispatcher.authenticators[1],
88 SuccessfulAuthenticator2)
89 self.assertEqual('user1',
90 self.request_dispatcher.authenticate(self.req))
91
93 class UnsuccessfulAuthenticator(Component):
94 implements(IAuthenticator)
95 def authenticate(self, req):
96 return None
97 class SuccessfulAuthenticator(Component):
98 implements(IAuthenticator)
99 def authenticate(self, req):
100 return 'user'
101 self.assertEqual(2, len(self.request_dispatcher.authenticators))
102 self.assertIsInstance(self.request_dispatcher.authenticators[0],
103 UnsuccessfulAuthenticator)
104 self.assertIsInstance(self.request_dispatcher.authenticators[1],
105 SuccessfulAuthenticator)
106 self.assertEqual('user',
107 self.request_dispatcher.authenticate(self.req))
108
114 class SuccessfulAuthenticator(Component):
115 implements(IAuthenticator)
116 def authenticate(self, req):
117 return 'user'
118 self.assertEqual(2, len(self.request_dispatcher.authenticators))
119 self.assertIsInstance(self.request_dispatcher.authenticators[0],
120 RaisingAuthenticator)
121 self.assertIsInstance(self.request_dispatcher.authenticators[1],
122 SuccessfulAuthenticator)
123 self.assertEqual('anonymous',
124 self.request_dispatcher.authenticate(self.req))
125 self.assertEqual(1, len(self.req.chrome['warnings']))
126
128 class Authenticator(Component):
129 implements(IAuthenticator)
130 def authenticate(self, req):
131 authenticated[0] += 1
132 return 'admin'
133 class AuthenticateRequestHandler(Component):
134 implements(IRequestHandler)
135 def match_request(self, req):
136 return bool(req.perm)
137 def process_request(self, req):
138 req.authname
139 req.send('')
140
141 self.env.config.set('trac', 'default_handler',
142 'AuthenticateRequestHandler')
143 authenticated = [0]
144 req = MockRequest(self.env)
145 self.request_dispatcher.set_default_callbacks(req)
146
147 self.assertEqual(1, len(self.request_dispatcher.authenticators))
148 self.assertIsInstance(self.request_dispatcher.authenticators[0],
149 Authenticator)
150 self.assertRaises(RequestDone, self.request_dispatcher.dispatch, req)
151 self.assertEqual(1, authenticated[0])
152
153
155
157 """EnvironmentError exception is raised when dispatching request
158 with optimizations enabled.
159 """
160 proc = Popen((sys.executable, '-O', '-c',
161 'from trac.web.main import dispatch_request; '
162 'dispatch_request({}, None)'), stdin=PIPE,
163 stdout=PIPE, stderr=PIPE, close_fds=close_fds)
164
165 stdout, stderr = proc.communicate()
166 for f in (proc.stdin, proc.stdout, proc.stderr):
167 f.close()
168 self.assertEqual(1, proc.returncode)
169 self.assertIn("EnvironmentError: Python with optimizations is not "
170 "supported.", stderr)
171
172
174
175 dirs = ('mydir1', 'mydir2', '.hidden_dir')
176 files = ('myfile1', 'myfile2', '.dot_file')
177
179 self.parent_dir = tempfile.mkdtemp(prefix='trac-')
180 self.tracignore = os.path.join(self.parent_dir, '.tracignore')
181 for dname in self.dirs:
182 os.mkdir(os.path.join(self.parent_dir, dname))
183 for fname in self.files:
184 create_file(os.path.join(self.parent_dir, fname))
185 self.environ = {
186 'trac.env_paths': [],
187 'trac.env_parent_dir': self.parent_dir,
188 }
189
191 for fname in self.files:
192 os.unlink(os.path.join(self.parent_dir, fname))
193 for dname in self.dirs:
194 os.rmdir(os.path.join(self.parent_dir, dname))
195 if os.path.exists(self.tracignore):
196 os.unlink(self.tracignore)
197 os.rmdir(self.parent_dir)
198
200 return dict((project, os.path.normpath(os.path.join(self.parent_dir,
201 project)))
202 for project in projects)
203
207
212
217
221
226
227
229
238 def process_request(self, req):
239 pass
240
243
245 """TracError in pre_process_request is trapped and an
246 HTTPInternalError is raised.
247 """
248 class RequestFilter(Component):
249 implements(IRequestFilter)
250 def pre_process_request(self, req, handler):
251 raise TracError("Raised in pre_process_request")
252 def post_process_request(self, req, template, data, content_type):
253 return template, data, content_type
254 req = MockRequest(self.env)
255
256 try:
257 RequestDispatcher(self.env).dispatch(req)
258 except HTTPInternalError as e:
259 self.assertEqual("500 Trac Error (Raised in pre_process_request)",
260 unicode(e))
261 else:
262 self.fail("HTTPInternalError not raised")
263
264
266
275 def process_request(self, req):
276 raise req.exc_class("Raised in process_request")
277
280
282 """TracError in process_request is trapped and an HTTPForbidden
283 error is raised.
284 """
285 req = MockRequest(self.env)
286 req.exc_class = PermissionError
287
288 try:
289 RequestDispatcher(self.env).dispatch(req)
290 except HTTPForbidden as e:
291 self.assertEqual(
292 "403 Forbidden (Raised in process_request "
293 "privileges are required to perform this operation. You "
294 "don't have the required permissions.)", unicode(e))
295 else:
296 self.fail("HTTPForbidden not raised")
297
312
327
329 """NotImplementedError in process_request is trapped and an
330 HTTPInternalError is raised.
331 """
332 req = MockRequest(self.env)
333 req.exc_class = NotImplementedError
334
335 try:
336 RequestDispatcher(self.env).dispatch(req)
337 except HTTPInternalError as e:
338 self.assertEqual("500 Not Implemented Error (Raised in "
339 "process_request)", unicode(e))
340 else:
341 self.fail("HTTPInternalError not raised")
342
343
344 -class PostProcessRequestTestCase(unittest.TestCase):
345 """Test cases for handling of the optional `method` argument in
346 RequestDispatcher._post_process_request."""
347
349 self.env = EnvironmentStub()
350 self.req = MockRequest(self.env)
351 self.request_dispatcher = RequestDispatcher(self.env)
352 self.compmgr = ComponentManager()
353 self.env.clear_component_registry()
354
355 - def tearDown(self):
357
359 """IRequestHandler doesn't return `method` and no IRequestFilters
360 are registered. The `method` is set to `None`.
361 """
362 args = ('template.html', {}, 'text/html')
363 resp = self.request_dispatcher._post_process_request(self.req, *args)
364 self.assertEqual(0, len(self.request_dispatcher.filters))
365 self.assertEqual(4, len(resp))
366 self.assertEqual(args + (None,), resp)
367
369 """IRequestHandler returns `method` and no IRequestFilters
370 are registered. The `method` is forwarded.
371 """
372 args = ('template.html', {}, 'text/html', 'xhtml')
373 resp = self.request_dispatcher._post_process_request(self.req, *args)
374 self.assertEqual(0, len(self.request_dispatcher.filters))
375 self.assertEqual(4, len(resp))
376 self.assertEqual(args, resp)
377
379 """IRequestHandler doesn't return `method` and IRequestFilter doesn't
380 accept `method` as an argument. The `method` is set to `None`.
381 """
382 class RequestFilter(Component):
383 implements(IRequestFilter)
384 def pre_process_request(self, req, handler):
385 return handler
386 def post_process_request(self, req, template, data, content_type):
387 return template, data, content_type
388 args = ('template.html', {}, 'text/html')
389 resp = self.request_dispatcher._post_process_request(self.req, *args)
390 self.assertEqual(1, len(self.request_dispatcher.filters))
391 self.assertEqual(4, len(resp))
392 self.assertEqual(args + (None,), resp)
393
395 """IRequestHandler returns `method` and IRequestFilter doesn't accept
396 the argument. The `method` argument is forwarded over IRequestFilter
397 implementations that don't accept the argument.
398 """
399 class RequestFilter(Component):
400 implements(IRequestFilter)
401 def pre_process_request(self, req, handler):
402 return handler
403 def post_process_request(self, req, template, data, content_type):
404 return template, data, content_type
405 args = ('template.html', {}, 'text/html', 'xhtml')
406 resp = self.request_dispatcher._post_process_request(self.req, *args)
407 self.assertEqual(1, len(self.request_dispatcher.filters))
408 self.assertEqual(4, len(resp))
409 self.assertEqual(args, resp)
410
412 """IRequestHandler doesn't return `method` and IRequestFilter accepts
413 `method` as an argument. The `method` is set to `None`.
414 """
415 class RequestFilter(Component):
416 implements(IRequestFilter)
417 def pre_process_request(self, req, handler):
418 return handler
419 def post_process_request(self, req, template, data,
420 content_type, method=None):
421 return template, data, content_type, method
422 args = ('template.html', {}, 'text/html')
423 resp = self.request_dispatcher._post_process_request(self.req, *args)
424 self.assertEqual(1, len(self.request_dispatcher.filters))
425 self.assertEqual(4, len(resp))
426 self.assertEqual(args[:3] + (None,), resp)
427
429 """IRequestHandler returns `method` and IRequestFilter accepts
430 the argument. The `method` argument is passed through IRequestFilter
431 implementations.
432 """
433 class RequestFilter(Component):
434 implements(IRequestFilter)
435 def pre_process_request(self, req, handler):
436 return handler
437 def post_process_request(self, req, template, data,
438 content_type, method=None):
439 return template, data, content_type, method
440 args = ('template.html', {}, 'text/html', 'xhtml')
441 resp = self.request_dispatcher._post_process_request(self.req, *args)
442 self.assertEqual(1, len(self.request_dispatcher.filters))
443 self.assertEqual(4, len(resp))
444 self.assertEqual(args, resp)
445
447 """IRequestFilter adds `method` not returned by IRequestHandler.
448 """
449 class RequestFilter(Component):
450 implements(IRequestFilter)
451 def pre_process_request(self, req, handler):
452 return handler
453 def post_process_request(self, req, template, data,
454 content_type, method=None):
455 return template, data, content_type, 'xml'
456 args = ('template.html', {}, 'text/html')
457 resp = self.request_dispatcher._post_process_request(self.req, *args)
458 self.assertEqual(1, len(self.request_dispatcher.filters))
459 self.assertEqual(4, len(resp))
460 self.assertEqual(args[:3] + ('xml',), resp)
461
463 """IRequestFilter modifies `method` returned by IRequestHandler.
464 """
465 class RequestFilter(Component):
466 implements(IRequestFilter)
467 def pre_process_request(self, req, handler):
468 return handler
469 def post_process_request(self, req, template, data,
470 content_type, method=None):
471 return template, data, content_type, 'xml'
472 args = ('template.html', {}, 'text/html', 'xhtml')
473 resp = self.request_dispatcher._post_process_request(self.req, *args)
474 self.assertEqual(1, len(self.request_dispatcher.filters))
475 self.assertEqual(4, len(resp))
476 self.assertEqual(args[:3] + ('xml',), resp)
477
479 """The post_process_request method can redirect during exception
480 handling from an exception raised in process_request.
481 """
482 class RedirectOnPermissionErrorStub(Component):
483 implements(IRequestHandler, IRequestFilter)
484
485 def match_request(self, req):
486 return re.match(r'/perm-error', req.path_info)
487
488 def process_request(self, req):
489 req.entered_process_request = True
490 raise PermissionError("No permission to view")
491
492 def pre_process_request(self, req, handler):
493 return handler
494
495 def post_process_request(self, req, template, data, content_type):
496 if (template, data, content_type) == (None, None, None):
497 req.entered_post_process_request = True
498 req.redirect(req.href('/redirect-target'))
499 return template, data, content_type
500
501 dispatcher = RequestDispatcher(self.env)
502 req = MockRequest(self.env, method='GET', path_info='/perm-error')
503 req.entered_process_request = False
504 req.entered_post_process_request = False
505
506 try:
507 dispatcher.dispatch(req)
508 except RequestDone:
509 pass
510 else:
511 self.fail("RequestDone not raised")
512
513 self.assertTrue(req.entered_process_request)
514 self.assertTrue(req.entered_post_process_request)
515
516
518
529
532
539
540 - def _content(self):
541 yield 'line1,'
542 yield 'line2,'
543 yield 'line3\n'
544
552
554 """Session is returned when database is reachable."""
555 sid, name, email = self._insert_session()
556 req = MockRequest(self.env, path_info='/test-stub',
557 cookie='trac_session=%s;' % sid)
558 request_dispatcher = RequestDispatcher(self.env)
559 request_dispatcher.set_default_callbacks(req)
560
561 self.assertRaises(RequestDone, request_dispatcher.dispatch, req)
562
563 self.assertIsInstance(req.session, Session)
564 self.assertEqual(sid, req.session.sid)
565 self.assertEqual(name, req.session['name'])
566 self.assertEqual(email, req.session['email'])
567 self.assertFalse(req.session.authenticated)
568 self.assertEqual('200 Ok', req.status_sent[0])
569 self.assertIn('<h1>Hello World</h1>', req.response_sent.getvalue())
570
572 """Fake session is returned when database is not reachable."""
573 sid = self._insert_session()[0]
574 request_dispatcher = RequestDispatcher(self.env)
575
576 def get_session(req):
577 """Simulates an unreachable database."""
578 _get_connector = DatabaseManager.get_connector
579
580 def get_connector(self):
581 raise TracError("Database not reachable")
582
583 DatabaseManager.get_connector = get_connector
584 DatabaseManager(self.env).shutdown()
585 session = request_dispatcher._get_session(req)
586 DatabaseManager.get_connector = _get_connector
587 return session
588
589 req = MockRequest(self.env, path_info='/test-stub',
590 cookie='trac_session=%s;' % sid)
591 req.callbacks['session'] = get_session
592
593 self.assertRaises(RequestDone, request_dispatcher.dispatch, req)
594
595 self.assertIsInstance(req.session, FakeSession)
596 self.assertIsNone(req.session.sid)
597 self.assertNotIn('name', req.session)
598 self.assertNotIn('email', req.session)
599 self.assertFalse(req.session.authenticated)
600 self.assertEqual('200 Ok', req.status_sent[0])
601 self.assertIn('<h1>Hello World</h1>', req.response_sent.getvalue())
602
617
619 """Send file using xsendfile header."""
620 self.env.config.set('trac', 'use_xsendfile', True)
621 self.env.config.set('trac', 'xsendfile_header', 'X-Accel-Redirect')
622
623 req = MockRequest(self.env)
624 request_dispatcher = RequestDispatcher(self.env)
625 request_dispatcher.set_default_callbacks(req)
626
627
628 self.assertRaises(RequestDone, req.send_file, self.filename)
629 self.assertEqual(['200 Ok'], req.status_sent)
630 self.assertEqual('text/plain', req.headers_sent['Content-Type'])
631 self.assertEqual(self.filename, req.headers_sent['X-Accel-Redirect'])
632 self.assertNotIn('X-Sendfile', req.headers_sent)
633 self.assertEqual(None, req._response)
634 self.assertEqual('', req.response_sent.getvalue())
635
648
656
664
666
667 content_type = 'not-allowed'
668 self.env.config.set('http-headers', 'Content-Type', content_type)
669
670 custom1 = '\x00custom1'
671 self.env.config.set('http-headers', 'X-Custom-1', custom1)
672
673 custom2 = 'Custom2-!#$%&\'*+.^_`|~'
674 self.env.config.set('http-headers', custom2, 'custom2')
675
676 self.env.config.set('http-headers', 'X-Custom-(3)', 'custom3')
677
678 req = MockRequest(self.env, method='POST')
679 request_dispatcher = RequestDispatcher(self.env)
680 request_dispatcher.set_default_callbacks(req)
681 self.assertRaises(RequestDone, getattr(req, method), self._content())
682
683 self.assertNotEqual('not-allowed', req.headers_sent.get('Content-Type'))
684 self.assertNotIn('x-custom-1', req.headers_sent)
685 self.assertIn(custom2.lower(), req.headers_sent)
686 self.assertNotIn('x-custom-(3)', req.headers_sent)
687
689 self._test_configurable_headers('send')
690
692 self._test_configurable_headers('send_error')
693
695 """Headers in request not overridden by configurable headers."""
696 self.env.config.set('http-headers', 'X-XSS-Protection', '1; mode=block')
697 request_dispatcher = RequestDispatcher(self.env)
698 req1 = MockRequest(self.env)
699 request_dispatcher.set_default_callbacks(req1)
700
701 self.assertRaises(RequestDone, req1.send, self._content())
702
703 self.assertNotIn('X-XSS-protection', req1.headers_sent)
704 self.assertIn('x-xss-protection', req1.headers_sent)
705 self.assertEqual('1; mode=block', req1.headers_sent['x-xss-protection'])
706
707 req2 = MockRequest(self.env, method='POST')
708 request_dispatcher.set_default_callbacks(req2)
709
710 self.assertRaises(RequestDone, req2.send, self._content())
711
712 self.assertNotIn('x-xss-protection', req2.headers_sent)
713 self.assertIn('X-XSS-Protection', req2.headers_sent)
714 self.assertEqual('0', req2.headers_sent['X-XSS-Protection'])
715
716
718
726
729
731 class HdfdumpRequestHandler(Component):
732 implements(IRequestHandler)
733 def match_request(self, req):
734 return True
735 def process_request(self, req):
736 data = {'name': 'value'}
737 return 'error.html', data, None
738
739 self.env.config.set('trac', 'default_handler', 'HdfdumpRequestHandler')
740 self.assertRaises(RequestDone, self.request_dispatcher.dispatch,
741 self.req)
742 self.assertIn("{'name': 'value'}\n",
743 self.req.response_sent.getvalue())
744 self.assertEqual('text/plain;charset=utf-8',
745 self.req.headers_sent['Content-Type'])
746
747
759
760
761 if __name__ == '__main__':
762 unittest.main(defaultTest='test_suite')
763