Skip to content

Commit 55cdf5f

Browse files
Revert "Revert "Block access to Via in the web app (#1739)" (#1742)" (#1745)
This reverts commit 5a39924.
1 parent 5a39924 commit 55cdf5f

17 files changed

Lines changed: 650 additions & 557 deletions
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import pytest
2-
3-
from tests.conftest import assert_cache_control
4-
5-
61
class TestViewPDFAPI:
7-
@pytest.mark.usefixtures("checkmate_pass")
8-
def test_caching_is_disabled(self, test_app):
2+
def test_pdf_shows_restricted_page(self, test_app):
93
response = test_app.get("/pdf?url=http://example.com/foo.pdf")
104

11-
assert_cache_control(
12-
response.headers, ["max-age=0", "must-revalidate", "no-cache", "no-store"]
13-
)
5+
assert response.status_code == 200
6+
assert "Access to Via is now" in response.text
7+
assert "restricted" in response.text
8+
assert "http://example.com/foo.pdf" in response.text
Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,10 @@
1-
from urllib.parse import quote_plus
2-
3-
import httpretty
4-
import pytest
5-
from h_matchers import Any
6-
7-
from tests.conftest import assert_cache_control
8-
9-
101
class TestRouteByContent:
11-
DEFAULT_OPTIONS = { # noqa: RUF012
12-
"via.client.ignoreOtherConfiguration": "1",
13-
"via.client.openSidebar": "1",
14-
"via.external_link_mode": "new-tab",
15-
}
16-
17-
@pytest.mark.usefixtures("html_response", "checkmate_pass")
18-
def test_proxy_html(self, test_app):
19-
target_url = "http://example.com"
20-
21-
response = test_app.get(f"/route?url={target_url}")
22-
23-
assert response.status_code == 302
24-
query = dict(self.DEFAULT_OPTIONS)
25-
assert response.location == Any.url.matching(
26-
f"https://viahtml.hypothes.is/proxy/{target_url}/"
27-
).with_query(query)
28-
29-
@pytest.mark.usefixtures("pdf_response", "checkmate_pass")
30-
def test_proxy_pdf(self, test_app):
2+
def test_route_shows_restricted_page(self, test_app):
313
target_url = "http://example.com"
324

335
response = test_app.get(f"/route?url={target_url}")
346

35-
assert response.status_code == 302
36-
query = dict(self.DEFAULT_OPTIONS)
37-
query["via.sec"] = Any.string()
38-
query["url"] = target_url
39-
assert response.location == Any.url.matching(
40-
f"http://localhost/pdf?url={quote_plus(target_url)}"
41-
).with_query(query)
42-
assert_cache_control(
43-
response.headers, ["public", "max-age=300", "stale-while-revalidate=86400"]
44-
)
45-
46-
@pytest.fixture
47-
def html_response(self):
48-
httpretty.register_uri(
49-
httpretty.GET,
50-
"http://example.com",
51-
status=204,
52-
adding_headers={"Content-Type": "text/html"},
53-
)
54-
55-
@pytest.fixture
56-
def pdf_response(self):
57-
httpretty.register_uri(
58-
httpretty.GET,
59-
"http://example.com",
60-
status=204,
61-
adding_headers={"Content-Type": "application/pdf"},
62-
)
7+
assert response.status_code == 200
8+
assert "Access to Via is now" in response.text
9+
assert "restricted" in response.text
10+
assert target_url in response.text

tests/functional/static_content_test.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import re
44

5+
import importlib_resources
56
import pytest
67
from h_matchers import Any
78

89
from tests.conftest import assert_cache_control
10+
from via.cache_buster import PathCacheBuster
911

1012

1113
class TestStaticContent:
@@ -38,12 +40,18 @@ def test_immutable_contents(self, test_app):
3840
def get_salt(self, test_app):
3941
"""Get the salt value being used by the app.
4042
41-
The most sure fire way to get the exact salt value being used is to
42-
actually make a call with immutable assets and then scrape the HTML
43-
for the salt value.
43+
We use the proxy route to scrape for the salt since the PDF viewer
44+
is now restricted. Any page that includes static assets will work.
4445
"""
45-
response = test_app.get("/pdf?url=http://example.com")
46+
# Use a URL that will hit the proxy route and return the restricted page
47+
response = test_app.get("/https://example.com")
4648
static_match = re.search("/static/([^/]+)/", response.text)
47-
assert static_match
49+
if not static_match:
50+
# The restricted template doesn't reference /static/ paths,
51+
# so read the salt directly from stdout capture during app startup.
52+
# Fall back to getting it from the cache buster.
53+
static_path = str(importlib_resources.files("via") / "static")
54+
cache_buster = PathCacheBuster(static_path)
55+
return cache_buster.salt
4856

4957
return static_match.group(1)
Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,8 @@
1-
import pytest
2-
3-
from tests.functional.matchers import temporary_redirect_to
4-
5-
6-
@pytest.mark.parametrize(
7-
"url,expected_redirect_location", # noqa: PT006
8-
[
9-
# When you submit the form on the front page it redirects you to the
10-
# page that will proxy the URL that you entered.
11-
(
12-
"https://example.com/foo/",
13-
"http://localhost/https://example.com/foo/",
14-
),
15-
(
16-
"http://example.com/foo/",
17-
"http://localhost/http://example.com/foo/",
18-
),
19-
# The submitted URL is normalized to strip leading/trailing spaces and
20-
# add a protocol.
21-
(
22-
"example.com/foo/",
23-
"http://localhost/https://example.com/foo/",
24-
),
25-
# If you submit an empty form it just reloads the front page again.
26-
("", "http://localhost/"),
27-
],
28-
)
29-
def test_index(test_app, url, expected_redirect_location):
30-
form = test_app.get("/").form
31-
32-
form.set("url", url)
33-
response = form.submit()
34-
35-
assert response == temporary_redirect_to(expected_redirect_location)
1+
def test_index_shows_page_when_not_signed_urls_required(test_app):
2+
"""When signed_urls_required is False (default test config), index page is served."""
3+
response = test_app.get("/")
4+
5+
assert response.status_code == 200
6+
# The normal index page is shown (not restricted) because
7+
# signed_urls_required=False means all requests are considered valid.
8+
assert "hypothes.is" in response.text

tests/unit/via/services/secure_link_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ def test_request_is_valid_if_signatures_not_required(
5050

5151
service._via_secure_url.verify.assert_not_called() # noqa: SLF001
5252

53+
def test_request_has_valid_token(self, service, pyramid_request):
54+
assert service.request_has_valid_token(pyramid_request)
55+
56+
def test_request_has_valid_token_can_fail(self, service, pyramid_request):
57+
service._via_secure_url.verify.side_effect = TokenException # noqa: SLF001
58+
59+
assert not service.request_has_valid_token(pyramid_request)
60+
61+
@pytest.mark.usefixtures("with_signed_urls_not_required")
62+
def test_request_has_valid_token_always_checks_signature(
63+
self, service, pyramid_request
64+
):
65+
"""request_has_valid_token always verifies the token, even when signed URLs aren't required."""
66+
service._via_secure_url.verify.side_effect = TokenException # noqa: SLF001
67+
68+
assert not service.request_has_valid_token(pyramid_request)
69+
70+
service._via_secure_url.verify.assert_called_once_with(pyramid_request.url) # noqa: SLF001
71+
5372
def test_sign_url(self, service):
5473
result = service.sign_url(sentinel.url)
5574

tests/unit/via/views/index_test.py

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,102 @@
1+
from unittest.mock import create_autospec
2+
13
import pytest
2-
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
4+
from pyramid.httpexceptions import HTTPBadRequest, HTTPFound, HTTPNotFound
35

46
from tests.unit.matchers import temporary_redirect_to
57
from via.resources import QueryURLResource
6-
from via.views.exceptions import BadURL
78
from via.views.index import IndexViews
89

910

10-
class TestIndexViews:
11-
def test_get(self, views):
12-
assert views.get() == {}
13-
14-
def test_post(self, views, pyramid_request):
15-
pyramid_request.params["url"] = "//site.org?q1=value1&q2=value2"
11+
class TestIndexGet:
12+
def test_it_returns_restricted_page_when_not_lms(
13+
self, context, pyramid_request, secure_link_service
14+
):
15+
secure_link_service.request_has_valid_token.return_value = False
16+
views = IndexViews(context, pyramid_request)
1617

17-
redirect = views.post()
18+
result = views.get()
1819

19-
assert isinstance(redirect, HTTPFound)
20+
assert result == {"target_url": None}
2021
assert (
21-
redirect.location
22-
== "http://example.com/https://site.org?q1=value1&q2=value2"
22+
pyramid_request.override_renderer == "via:templates/restricted.html.jinja2"
2323
)
2424

25-
def test_post_with_no_url(self, views, pyramid_request):
26-
assert "url" not in pyramid_request.params
25+
def test_it_returns_page_when_lms(
26+
self, context, pyramid_request, secure_link_service
27+
):
28+
secure_link_service.request_has_valid_token.return_value = True
29+
views = IndexViews(context, pyramid_request)
2730

28-
redirect = views.post()
31+
result = views.get()
2932

30-
assert redirect == temporary_redirect_to(
31-
pyramid_request.route_url(route_name="index")
32-
)
33+
assert result == {}
3334

34-
def test_post_raises_if_url_invalid(self, views, pyramid_request):
35-
# Set a `url` that causes `urlparse` to throw.
36-
pyramid_request.params["url"] = "http://::12.34.56.78]/"
35+
def test_it_returns_not_found_when_front_page_disabled(
36+
self, context, pyramid_request, secure_link_service
37+
):
38+
secure_link_service.request_has_valid_token.return_value = True
39+
pyramid_request.registry.settings["enable_front_page"] = False
40+
views = IndexViews(context, pyramid_request)
3741

38-
with pytest.raises(BadURL):
39-
views.post()
42+
result = views.get()
4043

41-
@pytest.mark.usefixtures("disable_front_page")
42-
@pytest.mark.parametrize("view", ["get", "post"])
43-
def test_it_404s_if_the_front_page_isnt_enabled(self, view, views):
44-
view = getattr(views, view)
44+
assert isinstance(result, HTTPNotFound)
4545

46-
response = view()
46+
@pytest.fixture
47+
def context(self):
48+
return create_autospec(QueryURLResource, spec_set=True, instance=True)
4749

48-
assert isinstance(response, HTTPNotFound)
4950

50-
@pytest.fixture
51-
def disable_front_page(self, pyramid_settings):
52-
pyramid_settings["enable_front_page"] = False
51+
class TestIndexPost:
52+
def test_it_returns_restricted_page_when_not_lms(
53+
self, context, pyramid_request, secure_link_service
54+
):
55+
secure_link_service.request_has_valid_token.return_value = False
56+
views = IndexViews(context, pyramid_request)
5357

54-
@pytest.fixture
55-
def views(self, context, pyramid_request):
56-
return IndexViews(context, pyramid_request)
58+
result = views.post()
59+
60+
assert result == {"target_url": None}
61+
62+
def test_it_returns_not_found_when_front_page_disabled(
63+
self, context, pyramid_request, secure_link_service
64+
):
65+
secure_link_service.request_has_valid_token.return_value = True
66+
pyramid_request.registry.settings["enable_front_page"] = False
67+
views = IndexViews(context, pyramid_request)
68+
69+
result = views.post()
70+
71+
assert isinstance(result, HTTPNotFound)
72+
73+
def test_it_redirects_when_lms(self, context, pyramid_request, secure_link_service):
74+
secure_link_service.request_has_valid_token.return_value = True
75+
context.url_from_query.return_value = "http://example.com/page?q=1"
76+
views = IndexViews(context, pyramid_request)
77+
78+
result = views.post()
79+
80+
assert isinstance(result, HTTPFound)
81+
assert result == temporary_redirect_to(
82+
pyramid_request.route_url(
83+
route_name="proxy",
84+
url="http://example.com/page",
85+
_query="q=1",
86+
)
87+
)
88+
89+
def test_it_redirects_to_index_on_bad_url(
90+
self, context, pyramid_request, secure_link_service
91+
):
92+
secure_link_service.request_has_valid_token.return_value = True
93+
context.url_from_query.side_effect = HTTPBadRequest("bad url")
94+
views = IndexViews(context, pyramid_request)
95+
96+
result = views.post()
97+
98+
assert isinstance(result, HTTPFound)
5799

58100
@pytest.fixture
59-
def context(self, pyramid_request):
60-
return QueryURLResource(pyramid_request)
101+
def context(self):
102+
return create_autospec(QueryURLResource, spec_set=True, instance=True)

tests/unit/via/views/proxy_test.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,47 @@ def test_it(self):
1414

1515

1616
class TestProxy:
17-
def test_it(
18-
self, context, pyramid_request, url_details_service, via_client_service
17+
def test_it_returns_restricted_page_when_not_lms(
18+
self, context, pyramid_request, secure_link_service
1919
):
20-
url_details_service.get_url_details.return_value = (
21-
sentinel.mime_type,
22-
sentinel.status_code,
23-
)
20+
secure_link_service.request_has_valid_token.return_value = False
2421
url = context.url_from_path.return_value = "/https://example.org?a=1"
2522

2623
result = proxy(context, pyramid_request)
2724

28-
url_details_service.get_url_details.assert_called_once_with(url)
29-
via_client_service.url_for.assert_called_once_with(
30-
url, sentinel.mime_type, pyramid_request.params
25+
assert result == {"target_url": url}
26+
assert (
27+
pyramid_request.override_renderer == "via:templates/restricted.html.jinja2"
28+
)
29+
30+
def test_it_returns_restricted_none_url_on_error_when_not_lms(
31+
self, context, pyramid_request, secure_link_service
32+
):
33+
secure_link_service.request_has_valid_token.return_value = False
34+
context.url_from_path.side_effect = Exception("bad url")
35+
36+
result = proxy(context, pyramid_request)
37+
38+
assert result == {"target_url": None}
39+
40+
def test_it_proxies_when_lms(
41+
self,
42+
context,
43+
pyramid_request,
44+
secure_link_service,
45+
url_details_service,
46+
via_client_service, # noqa: ARG002
47+
):
48+
secure_link_service.request_has_valid_token.return_value = True
49+
context.url_from_path.return_value = "http://example.com/page"
50+
url_details_service.get_url_details.return_value = ("text/html", 200)
51+
52+
result = proxy(context, pyramid_request)
53+
54+
url_details_service.get_url_details.assert_called_once_with(
55+
"http://example.com/page"
3156
)
32-
assert result == {"src": via_client_service.url_for.return_value}
57+
assert "src" in result
3358

3459
@pytest.fixture
3560
def context(self):

0 commit comments

Comments
 (0)