Skip to content

Commit 2d82e69

Browse files
committed
Fix leakage contents
1 parent 98ddec3 commit 2d82e69

File tree

4 files changed

+73
-22
lines changed

4 files changed

+73
-22
lines changed

tests/app/contents_handler_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import json
2+
import pytest
3+
import tornado
4+
5+
6+
@pytest.fixture
7+
def preheat_mode():
8+
return False
9+
10+
11+
@pytest.fixture
12+
def contents_prefix(base_url):
13+
return base_url + "voila/api/contents"
14+
15+
16+
@pytest.fixture
17+
def voila_args(notebook_directory, voila_args_extra):
18+
return ["--VoilaTest.root_dir=%r" % notebook_directory, *voila_args_extra]
19+
20+
21+
@pytest.fixture
22+
def voila_args_extra():
23+
return ['--VoilaConfiguration.extension_language_mapping={".py": "python"}']
24+
25+
26+
async def test_contents_endpoint(http_server_client, contents_prefix):
27+
response = await http_server_client.fetch(contents_prefix)
28+
html_json = json.loads(response.body.decode("utf-8"))
29+
assert "content" in html_json
30+
assert len(html_json["content"]) > 0
31+
32+
33+
async def test_get_notebook(http_server_client, contents_prefix):
34+
response = await http_server_client.fetch(contents_prefix + "/autokill.ipynb")
35+
html_json = json.loads(response.body.decode("utf-8"))
36+
assert html_json["name"] == "autokill.ipynb"
37+
assert html_json["content"] is None
38+
39+
40+
async def test_get_not_allowed_file(http_server_client, contents_prefix):
41+
with pytest.raises(tornado.httpclient.HTTPClientError):
42+
await http_server_client.fetch(contents_prefix + "/print.xcpp")
43+
44+
45+
async def test_get_allowed_file(http_server_client, contents_prefix):
46+
response = await http_server_client.fetch(contents_prefix + "/print.py")
47+
html_json = json.loads(response.body.decode("utf-8"))
48+
assert html_json["name"] == "print.py"
49+
assert html_json["content"] is None

voila/app.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -680,13 +680,6 @@ def init_handlers(self) -> List:
680680
"no_cache_paths": ["/"],
681681
},
682682
),
683-
(
684-
url_path_join(
685-
self.server_url, r"/voila/api/contents%s" % path_regex
686-
),
687-
VoilaContentsHandler,
688-
tree_handler_conf,
689-
),
690683
(
691684
url_path_join(self.server_url, r"/voila/api/shutdown/(.*)"),
692685
VoilaShutdownKernelHandler,
@@ -762,6 +755,14 @@ def init_handlers(self) -> List:
762755
"prelaunch_hook": self.prelaunch_hook,
763756
},
764757
),
758+
# On serving a directory, expose the content handler.
759+
(
760+
url_path_join(
761+
self.server_url, r"/voila/api/contents%s" % path_regex
762+
),
763+
VoilaContentsHandler,
764+
tree_handler_conf,
765+
),
765766
]
766767
)
767768
return handlers

voila/configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class VoilaConfiguration(traitlets.config.Configurable):
3838
classic_tree = Bool(
3939
False,
4040
config=True,
41-
help=("Use the jinja2-based tree page instead of the new JupyterLab-based one"),
41+
help=("Use the jija2-based tree page instead of the new JupyterLab-based one"),
4242
)
4343
resources = Dict(
4444
allow_none=True,

voila/tornado/contentshandler.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ async def get(self, path=""):
3232
path = path or ""
3333
cm = self.contents_manager
3434

35-
type = self.get_query_argument("type", default=None)
36-
if type not in {None, "directory", "file", "notebook"}:
37-
# fall back to file if unknown type
38-
type = "file"
39-
4035
format = self.get_query_argument("format", default=None)
4136
if format not in {None, "text", "base64"}:
4237
raise web.HTTPError(400, "Format %r is invalid" % format)
@@ -51,23 +46,29 @@ async def get(self, path=""):
5146
model = await ensure_async(
5247
self.contents_manager.get(
5348
path=path,
54-
type=type,
49+
type=None,
5550
format=format,
5651
content=content,
5752
)
5853
)
5954

6055
validate_model(model, expect_content=content)
61-
if type is None or type == "directory":
6256

63-
def allowed_content(content):
64-
if content["type"] in ["directory", "notebook"]:
65-
return True
66-
__, ext = os.path.splitext(content.get("path"))
67-
return ext in self.allowed_extensions
57+
def allowed_content(content):
58+
if content["type"] in ["directory", "notebook"]:
59+
return True
60+
__, ext = os.path.splitext(content.get("path"))
61+
return ext in self.allowed_extensions
62+
63+
if not allowed_content(model):
64+
raise web.HTTPError(404, f"file or directory {path!r} does not exist")
6865

69-
model["content"] = sorted(model["content"], key=lambda i: i["name"])
70-
model["content"] = list(filter(allowed_content, model["content"]))
66+
if model["type"] == "directory":
67+
try:
68+
model["content"] = sorted(model["content"], key=lambda i: i["name"])
69+
model["content"] = list(filter(allowed_content, model["content"]))
70+
except Exception:
71+
model["content"] = None
7172
else:
7273
# Make sure we don't leak the file content.
7374
model["content"] = None

0 commit comments

Comments
 (0)