Skip to content
This repository was archived by the owner on Aug 25, 2024. It is now read-only.

Commit ed5c1e2

Browse files
committed
service: http: routes: Default to setting no-cache on all respones
Signed-off-by: John Andersen <[email protected]>
1 parent 9b6e82a commit ed5c1e2

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

service/http/dffml_service_http/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ class ServerConfig(TLSCMDConfig, MultiCommCMDConfig):
233233
"Domains to allow CORS for (see keys in defaults dict for aiohttp_cors.setup)",
234234
default_factory=lambda: [],
235235
)
236+
allow_caching: bool = field(
237+
"Allow caching of HTTP responses", action="store_true", default=False,
238+
)
236239
models: Model = field(
237240
"Models configured on start",
238241
default_factory=lambda: AsyncContextManagerList(),

service/http/dffml_service_http/routes.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
SECRETS_TOKEN_BITS = 384
4545
SECRETS_TOKEN_BYTES = int(SECRETS_TOKEN_BITS / 8)
4646

47+
# Headers required to set no-cache for confidential web pages
48+
DISALLOW_CACHING = {
49+
"Pragma": "no-cache",
50+
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
51+
"Expires": "-1",
52+
}
4753

4854
OK = {"error": None}
4955
SOURCE_NOT_LOADED = {"error": "Source not loaded"}
@@ -448,22 +454,31 @@ async def error_middleware(self, request, handler):
448454
new_handler = await self.get_registered_handler(request)
449455
if new_handler is not None:
450456
handler = new_handler
451-
return await handler(request)
457+
response = await handler(request)
452458
except web.HTTPException as error:
453459
response = {"error": error.reason}
454460
if error.text is not None:
455461
response["error"] = error.text
456-
return web.json_response(response, status=error.status)
462+
response = web.json_response(response, status=error.status)
457463
except Exception as error: # pragma: no cov
458464
self.logger.error(
459465
"ERROR handling %s: %s",
460466
request,
461467
traceback.format_exc().strip(),
462468
)
463-
return web.json_response(
469+
response = web.json_response(
464470
{"error": "Internal Server Error"},
465471
status=HTTPStatus.INTERNAL_SERVER_ERROR,
466472
)
473+
# Disable request caching unless allowed explicitly
474+
if not self.allow_caching:
475+
self.set_no_cache(response)
476+
return response
477+
478+
def set_no_cache(self, response):
479+
# Set headers required to set no-cache for confidential web pages
480+
for header, value in DISALLOW_CACHING.items():
481+
response.headers[header] = value
467482

468483
async def service_upload(self, request):
469484
if self.upload_dir is None:

service/http/dffml_service_http/util/testing.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from dffml.source.memory import MemorySource, MemorySourceConfig
2323

2424
from dffml_service_http.cli import Server
25+
from dffml_service_http.routes import DISALLOW_CACHING
2526

2627

2728
@config
@@ -123,16 +124,27 @@ async def tearDown(self):
123124
def url(self):
124125
return f"http://{self.cli.addr}:{self.cli.port}"
125126

127+
def check_allow_caching(self, r):
128+
for header, should_be in DISALLOW_CACHING.items():
129+
if not header in r.headers:
130+
raise Exception(f"No cache header {header} not in {r.headers}")
131+
if r.headers[header] != should_be:
132+
raise Exception(
133+
f"No cache header {header} should have been {should_be!r} but was {r.headers[header]!r}"
134+
)
135+
126136
@contextlib.asynccontextmanager
127137
async def get(self, path):
128138
async with self.session.get(self.url + path) as r:
139+
self.check_allow_caching(r)
129140
if r.status != HTTPStatus.OK:
130141
raise ServerException((await r.json())["error"])
131142
yield r
132143

133144
@contextlib.asynccontextmanager
134145
async def post(self, path, *args, **kwargs):
135146
async with self.session.post(self.url + path, *args, **kwargs) as r:
147+
self.check_allow_caching(r)
136148
if r.status != HTTPStatus.OK:
137149
raise ServerException((await r.json())["error"])
138150
yield r

service/http/tests/test_routes.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
MODEL_NOT_LOADED,
3131
MODEL_NO_SOURCES,
3232
HTTPChannelConfig,
33+
DISALLOW_CACHING,
3334
)
3435
from dffml_service_http.util.testing import (
3536
ServerRunner,
@@ -59,6 +60,27 @@ async def test_not_found_handler(self):
5960
async with self.get("/non-existant"):
6061
pass # pramga: no cov
6162

63+
def set_no_cache_do_not_set_any_headers(self, response):
64+
pass
65+
66+
async def test_check_allow_caching_header_not_found(self):
67+
self.cli.set_no_cache = self.set_no_cache_do_not_set_any_headers
68+
with self.assertRaisesRegex(Exception, "No cache header .* not in"):
69+
async with self.get("/non-existant"):
70+
pass # pramga: no cov
71+
72+
def set_no_cache_bad_values_for_headers(self, response):
73+
for header, value in DISALLOW_CACHING.items():
74+
response.headers[header] = "BAD!"
75+
76+
async def test_check_allow_caching_header_not_correct(self):
77+
self.cli.set_no_cache = self.set_no_cache_bad_values_for_headers
78+
with self.assertRaisesRegex(
79+
Exception, "No cache header .* should have been .* but was"
80+
):
81+
async with self.get("/non-existant"):
82+
pass # pramga: no cov
83+
6284

6385
class TestRoutesServiceUpload(TestRoutesRunning, AsyncTestCase):
6486
async def test_success(self):

0 commit comments

Comments
 (0)