1
1
import asyncio
2
2
import functools
3
+ import os
3
4
import pathlib
5
+ import socket
4
6
import sys
5
- from typing import Generator , Optional
6
- from unittest import mock
7
- from unittest .mock import MagicMock
7
+ from stat import S_IFIFO , S_IMODE
8
+ from typing import Any , Generator , Optional
8
9
9
10
import pytest
10
11
import yarl
@@ -445,6 +446,56 @@ def mock_is_dir(self: pathlib.Path) -> bool:
445
446
assert r .status == 403
446
447
447
448
449
+ @pytest .mark .skipif (
450
+ sys .platform .startswith ("win32" ), reason = "Cannot remove read access on Windows"
451
+ )
452
+ async def test_static_file_without_read_permission (
453
+ tmp_path : pathlib .Path , aiohttp_client : AiohttpClient
454
+ ) -> None :
455
+ """Test static file without read permission receives forbidden response."""
456
+ my_file = tmp_path / "my_file.txt"
457
+ my_file .write_text ("secret" )
458
+ my_file .chmod (0o000 )
459
+
460
+ app = web .Application ()
461
+ app .router .add_static ("/" , str (tmp_path ))
462
+ client = await aiohttp_client (app )
463
+
464
+ r = await client .get (f"/{ my_file .name } " )
465
+ assert r .status == 403
466
+
467
+
468
+ async def test_static_file_with_mock_permission_error (
469
+ monkeypatch : pytest .MonkeyPatch ,
470
+ tmp_path : pathlib .Path ,
471
+ aiohttp_client : AiohttpClient ,
472
+ ) -> None :
473
+ """Test static file with mock permission errors receives forbidden response."""
474
+ my_file = tmp_path / "my_file.txt"
475
+ my_file .write_text ("secret" )
476
+ my_readable = tmp_path / "my_readable.txt"
477
+ my_readable .write_text ("info" )
478
+
479
+ real_open = pathlib .Path .open
480
+
481
+ def mock_open (self : pathlib .Path , * args : Any , ** kwargs : Any ) -> Any :
482
+ if my_file .samefile (self ):
483
+ raise PermissionError ()
484
+ return real_open (self , * args , ** kwargs )
485
+
486
+ monkeypatch .setattr ("pathlib.Path.open" , mock_open )
487
+
488
+ app = web .Application ()
489
+ app .router .add_static ("/" , str (tmp_path ))
490
+ client = await aiohttp_client (app )
491
+
492
+ # Test the mock only applies to my_file, then test the permission error.
493
+ r = await client .get (f"/{ my_readable .name } " )
494
+ assert r .status == 200
495
+ r = await client .get (f"/{ my_file .name } " )
496
+ assert r .status == 403
497
+
498
+
448
499
async def test_access_symlink_loop (
449
500
tmp_path : pathlib .Path , aiohttp_client : AiohttpClient
450
501
) -> None :
@@ -464,32 +515,54 @@ async def test_access_symlink_loop(
464
515
465
516
466
517
async def test_access_special_resource (
467
- tmp_path : pathlib . Path , aiohttp_client : AiohttpClient
518
+ tmp_path_factory : pytest . TempPathFactory , aiohttp_client : AiohttpClient
468
519
) -> None :
469
- # Tests the access to a resource that is neither a file nor a directory.
470
- # Checks that if a special resource is accessed (f.e. named pipe or UNIX
471
- # domain socket) then 404 HTTP status returned.
520
+ """Test access to non-regular files is forbidden using a UNIX domain socket."""
521
+ if not getattr (socket , "AF_UNIX" , None ):
522
+ pytest .skip ("UNIX domain sockets not supported" )
523
+
524
+ tmp_path = tmp_path_factory .mktemp ("special" )
525
+ my_special = tmp_path / "sock"
526
+ my_socket = socket .socket (socket .AF_UNIX )
527
+ my_socket .bind (str (my_special ))
528
+ assert my_special .is_socket ()
529
+
472
530
app = web .Application ()
531
+ app .router .add_static ("/" , str (tmp_path ))
473
532
474
- with mock . patch ( "pathlib.Path.__new__" ) as path_constructor :
475
- special = MagicMock ( )
476
- special . is_dir . return_value = False
477
- special . is_file . return_value = False
533
+ client = await aiohttp_client ( app )
534
+ r = await client . get ( f"/ { my_special . name } " )
535
+ assert r . status == 403
536
+ my_socket . close ()
478
537
479
- path = MagicMock ()
480
- path .joinpath .side_effect = lambda p : (special if p == "special" else path )
481
- path .resolve .return_value = path
482
- special .resolve .return_value = special
483
538
484
- path_constructor .return_value = path
539
+ async def test_access_mock_special_resource (
540
+ monkeypatch : pytest .MonkeyPatch ,
541
+ tmp_path : pathlib .Path ,
542
+ aiohttp_client : AiohttpClient ,
543
+ ) -> None :
544
+ """Test access to non-regular files is forbidden using a mock FIFO."""
545
+ my_special = tmp_path / "my_special"
546
+ my_special .touch ()
547
+
548
+ real_result = my_special .stat ()
549
+ real_stat = pathlib .Path .stat
550
+
551
+ def mock_stat (self : pathlib .Path ) -> os .stat_result :
552
+ s = real_stat (self )
553
+ if os .path .samestat (s , real_result ):
554
+ mock_mode = S_IFIFO | S_IMODE (s .st_mode )
555
+ s = os .stat_result ([mock_mode ] + list (s )[1 :])
556
+ return s
485
557
486
- # Register global static route:
487
- app .router .add_static ("/" , str (tmp_path ), show_index = True )
488
- client = await aiohttp_client (app )
558
+ monkeypatch .setattr ("pathlib.Path.stat" , mock_stat )
489
559
490
- # Request the root of the static directory.
491
- r = await client .get ("/special" )
492
- assert r .status == 403
560
+ app = web .Application ()
561
+ app .router .add_static ("/" , str (tmp_path ))
562
+ client = await aiohttp_client (app )
563
+
564
+ r = await client .get (f"/{ my_special .name } " )
565
+ assert r .status == 403
493
566
494
567
495
568
async def test_partially_applied_handler (aiohttp_client : AiohttpClient ) -> None :
0 commit comments