Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/internal/nginx.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const internalNginx = {
{ certificate_id: host.certificate_id },
{ ssl_forced: host.ssl_forced },
{ caching_enabled: host.caching_enabled },
{ asset_cache_ttl: host.asset_cache_ttl },
{ block_exploits: host.block_exploits },
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
{ http2_support: host.http2_support },
Expand Down
37 changes: 37 additions & 0 deletions backend/migrations/20260825180010_proxy_host_asset_cache_ttl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { migrate as logger } from "../logger.js";

const migrateName = "proxy_host_asset_cache_ttl";

/**
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);

return knex.schema
.alterTable("proxy_host", (table) => {
table.integer("asset_cache_ttl").notNullable().unsigned().defaultTo(1800);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};

/**
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);

return knex.schema
.alterTable("proxy_host", (table) => {
table.dropColumn("asset_cache_ttl");
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
});
};

export { up, down };
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"type": "module",
"scripts": {
"lint": "biome lint",
"test": "node --test test/**/*.test.js",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js",
"regenerate-config": "node scripts/regenerate-config"
Expand Down
7 changes: 7 additions & 0 deletions backend/schema/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@
"type": "boolean",
"example": true
},
"asset_cache_ttl": {
"description": "Asset cache lifetime in seconds",
"type": "integer",
"minimum": 1,
"maximum": 31536000,
"example": 1800
},
"email": {
"description": "Email address",
"type": "string",
Expand Down
4 changes: 4 additions & 0 deletions backend/schema/components/proxy-host-object.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"certificate_id",
"ssl_forced",
"caching_enabled",
"asset_cache_ttl",
"block_exploits",
"advanced_config",
"meta",
Expand Down Expand Up @@ -65,6 +66,9 @@
"caching_enabled": {
"$ref": "../common.json#/properties/caching_enabled"
},
"asset_cache_ttl": {
"$ref": "../common.json#/properties/asset_cache_ttl"
},
"block_exploits": {
"$ref": "../common.json#/properties/block_exploits"
},
Expand Down
1 change: 1 addition & 0 deletions backend/schema/paths/nginx/proxy-hosts/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"certificate_id": 1,
"ssl_forced": false,
"caching_enabled": false,
"asset_cache_ttl": 1800,
"block_exploits": false,
"advanced_config": "",
"meta": {
Expand Down
1 change: 1 addition & 0 deletions backend/schema/paths/nginx/proxy-hosts/hostID/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"asset_cache_ttl": 1800,
"block_exploits": false,
"advanced_config": "",
"meta": {
Expand Down
4 changes: 4 additions & 0 deletions backend/schema/paths/nginx/proxy-hosts/hostID/put.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
"caching_enabled": {
"$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled"
},
"asset_cache_ttl": {
"$ref": "../../../../components/proxy-host-object.json#/properties/asset_cache_ttl"
},
"allow_websocket_upgrade": {
"$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
},
Expand Down Expand Up @@ -112,6 +115,7 @@
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"asset_cache_ttl": 1800,
"block_exploits": false,
"advanced_config": "",
"meta": {
Expand Down
4 changes: 4 additions & 0 deletions backend/schema/paths/nginx/proxy-hosts/post.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
"caching_enabled": {
"$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled"
},
"asset_cache_ttl": {
"$ref": "../../../components/proxy-host-object.json#/properties/asset_cache_ttl"
},
"allow_websocket_upgrade": {
"$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade"
},
Expand Down Expand Up @@ -112,6 +115,7 @@
"certificate_id": 0,
"ssl_forced": false,
"caching_enabled": false,
"asset_cache_ttl": 1800,
"block_exploits": false,
"advanced_config": "",
"meta": {},
Expand Down
10 changes: 8 additions & 2 deletions backend/templates/_assets.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{% if caching_enabled == 1 or caching_enabled == true -%}
# Asset Caching
include conf.d/include/assets.conf;
{% endif %}
location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ {
proxy_cache_valid any {{ asset_cache_ttl | default: 1800 }}s;
proxy_cache_valid 404 1m;
expires {{ asset_cache_ttl | default: 1800 }}s;

include conf.d/include/assets-common.conf;
}
{% endif %}
74 changes: 74 additions & 0 deletions backend/test/asset-cache-templates.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import test from "node:test";
import utils from "../lib/utils.js";
import internalNginx from "../internal/nginx.js";

const render = async (templateName, data) => {
const template = fs.readFileSync(new URL(`../templates/${templateName}`, import.meta.url), "utf8");
return utils.getRenderEngine().parseAndRender(template, data);
};

test("renders a numeric asset cache lifetime", async () => {
const config = await render("_assets.conf", {
asset_cache_ttl: 21600,
caching_enabled: true,
certificate: null,
});

assert.match(config, /proxy_cache_valid any 21600s;/);
assert.match(config, /expires 21600s;/);
assert.match(config, /include conf\.d\/include\/assets-common\.conf;/);
});

test("uses the proxy host cache lifetime inside custom locations", async () => {
const config = await render("_location.conf", {
access_list_id: 0,
advanced_config: "",
allow_websocket_upgrade: false,
asset_cache_ttl: 3600,
block_exploits: false,
caching_enabled: true,
certificate: null,
forward_host: "127.0.0.1",
forward_path: "",
forward_port: 80,
forward_scheme: "http",
hsts_enabled: false,
path: "/assets",
ssl_forced: false,
});

assert.match(config, /location \/assets \{/);
assert.match(config, /proxy_cache_valid any 3600s;/);
});

test("retains the 30-minute lifetime when no value is provided", async () => {
const config = await render("_assets.conf", { caching_enabled: true });
assert.match(config, /proxy_cache_valid any 1800s;/);
assert.match(config, /expires 1800s;/);
});

test("does not emit a cache location when caching is disabled", async () => {
const config = await render("_assets.conf", { caching_enabled: false, asset_cache_ttl: 3600 });
assert.equal(config.trim(), "");
});

test("propagates the host lifetime when rendering its custom locations", async () => {
const config = await internalNginx.renderLocations({
asset_cache_ttl: 86400,
caching_enabled: true,
locations: [
{
path: "/custom",
forward_scheme: "http",
forward_host: "127.0.0.1",
forward_port: 8080,
advanced_config: "",
},
],
});
assert.match(config, /location \/custom \{/);
assert.match(config, /proxy_cache_valid any 86400s;/);
assert.match(config, /expires 86400s;/);
});
24 changes: 24 additions & 0 deletions docker/rootfs/etc/nginx/conf.d/include/assets-common.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
if_modified_since off;

# use the public cache
proxy_cache public-cache;
proxy_cache_key $host$request_uri;

# ignore these headers for media
proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires;

# strip this header to avoid If-Modified-Since requests
proxy_hide_header Last-Modified;
proxy_hide_header Cache-Control;
proxy_hide_header Vary;

proxy_cache_bypass 0;
proxy_no_cache 0;

proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404;
proxy_connect_timeout 5s;
proxy_read_timeout 45s;

access_log off;

include conf.d/include/proxy.conf;
24 changes: 1 addition & 23 deletions docker/rootfs/etc/nginx/conf.d/include/assets.conf
Original file line number Diff line number Diff line change
@@ -1,31 +1,9 @@
location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ {
if_modified_since off;

# use the public cache
proxy_cache public-cache;
proxy_cache_key $host$request_uri;

# ignore these headers for media
proxy_ignore_headers Set-Cookie Cache-Control Expires X-Accel-Expires;

# cache 200s and also 404s (not ideal but there are a few 404 images for some reason)
proxy_cache_valid any 30m;
proxy_cache_valid 404 1m;

# strip this header to avoid If-Modified-Since requests
proxy_hide_header Last-Modified;
proxy_hide_header Cache-Control;
proxy_hide_header Vary;

proxy_cache_bypass 0;
proxy_no_cache 0;

proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504 http_404;
proxy_connect_timeout 5s;
proxy_read_timeout 45s;

expires @30m;
access_log off;

include conf.d/include/proxy.conf;
include conf.d/include/assets-common.conf;
}
1 change: 1 addition & 0 deletions frontend/src/api/backend/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export interface ProxyHost {
certificateId: number;
sslForced: boolean;
cachingEnabled: boolean;
assetCacheTtl: number;
blockExploits: boolean;
advancedConfig: string;
meta: Record<string, any>;
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/components/Form/ProxyCacheOptionsFields.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { fireEvent, render, waitFor } from "@testing-library/react";
import { Form, Formik } from "formik";
import { describe, expect, it } from "vitest";
import { ProxyCacheOptionsFields } from "./ProxyCacheOptionsFields";

const renderFields = (overrides = {}) =>
render(
<Formik
initialValues={{
assetCacheTtl: 1800,
cachingEnabled: false,
...overrides,
}}
onSubmit={() => undefined}
>
<Form>
<ProxyCacheOptionsFields color="bg-lime" />
</Form>
</Formik>,
);

describe("ProxyCacheOptionsFields", () => {
it("shows the cache lifetime only while asset caching is enabled", async () => {
renderFields();

expect(document.getElementById("assetCacheTtl")).toBeNull();
fireEvent.click(document.getElementById("cachingEnabled") as HTMLElement);

await waitFor(() => {
expect(document.getElementById("assetCacheTtl")).not.toBeNull();
});

fireEvent.click(document.getElementById("cachingEnabled") as HTMLElement);
await waitFor(() => {
expect(document.getElementById("assetCacheTtl")).toBeNull();
});
});
});
Loading