diff --git a/.version b/.version index 965a689ec0..8bbab56270 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.13.4 +2.13.5 diff --git a/README.md b/README.md index 2a4c817120..5ba3650bf7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-
+
diff --git a/backend/certbot/dns-plugins.json b/backend/certbot/dns-plugins.json
index daf2d0a8a4..102734d2a0 100644
--- a/backend/certbot/dns-plugins.json
+++ b/backend/certbot/dns-plugins.json
@@ -26,8 +26,8 @@
"azure": {
"name": "Azure",
"package_name": "certbot-dns-azure",
- "version": "~=1.2.0",
- "dependencies": "",
+ "version": "~=2.6.1",
+ "dependencies": "azure-mgmt-dns==8.2.0",
"credentials": "# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine.\n# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it.\n# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required.\n\n# Using a service principal (option 1)\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n\n# Using used assigned MSI (option 2)\n# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\n\n# Using system assigned MSI (option 3)\n# dns_azure_msi_system_assigned = true\n\n# Zones (at least one always required)\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1\ndns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2",
"full_plugin_name": "dns-azure"
},
@@ -482,7 +482,7 @@
"porkbun": {
"name": "Porkbun",
"package_name": "certbot-dns-porkbun",
- "version": "~=0.9",
+ "version": "~=0.11.0",
"dependencies": "",
"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
"full_plugin_name": "dns-porkbun"
diff --git a/backend/internal/remote-version.js b/backend/internal/remote-version.js
new file mode 100644
index 0000000000..dd9c927797
--- /dev/null
+++ b/backend/internal/remote-version.js
@@ -0,0 +1,84 @@
+import https from "node:https";
+import { ProxyAgent } from "proxy-agent";
+import { debug, remoteVersion as logger } from "../logger.js";
+import pjson from "../package.json" with { type: "json" };
+
+const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest";
+
+const internalRemoteVersion = {
+ cache_timeout: 1000 * 60 * 15, // 15 minutes
+ last_result: null,
+ last_fetch_time: null,
+
+ /**
+ * Fetch the latest version info, using a cached result if within the cache timeout period.
+ * @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info
+ */
+ get: async () => {
+ if (
+ !internalRemoteVersion.last_result ||
+ !internalRemoteVersion.last_fetch_time ||
+ Date.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout
+ ) {
+ const raw = await internalRemoteVersion.fetchUrl(VERSION_URL);
+ const data = JSON.parse(raw);
+ internalRemoteVersion.last_result = data;
+ internalRemoteVersion.last_fetch_time = Date.now();
+ } else {
+ debug(logger, "Using cached remote version result");
+ }
+
+ const latestVersion = internalRemoteVersion.last_result.tag_name;
+ const version = pjson.version.split("-").shift().split(".");
+ const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;
+ return {
+ current: currentVersion,
+ latest: latestVersion,
+ update_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion),
+ };
+ },
+
+ fetchUrl: (url) => {
+ const agent = new ProxyAgent();
+ const headers = {
+ "User-Agent": `NginxProxyManager v${pjson.version}`,
+ };
+
+ return new Promise((resolve, reject) => {
+ logger.info(`Fetching ${url}`);
+ return https
+ .get(url, { agent, headers }, (res) => {
+ res.setEncoding("utf8");
+ let raw_data = "";
+ res.on("data", (chunk) => {
+ raw_data += chunk;
+ });
+ res.on("end", () => {
+ resolve(raw_data);
+ });
+ })
+ .on("error", (err) => {
+ reject(err);
+ });
+ });
+ },
+
+ compareVersions: (current, latest) => {
+ const cleanCurrent = current.replace(/^v/, "");
+ const cleanLatest = latest.replace(/^v/, "");
+
+ const currentParts = cleanCurrent.split(".").map(Number);
+ const latestParts = cleanLatest.split(".").map(Number);
+
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
+ const curr = currentParts[i] || 0;
+ const lat = latestParts[i] || 0;
+
+ if (lat > curr) return true;
+ if (lat < curr) return false;
+ }
+ return false;
+ },
+};
+
+export default internalRemoteVersion;
diff --git a/backend/logger.js b/backend/logger.js
index 7bf4ee0525..2b60dbff7b 100644
--- a/backend/logger.js
+++ b/backend/logger.js
@@ -15,6 +15,7 @@ const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
+const remoteVersion = new signale.Signale({ scope: "Remote Version", ...opts });
const debug = (logger, ...args) => {
if (isDebugMode()) {
@@ -22,4 +23,4 @@ const debug = (logger, ...args) => {
}
};
-export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges };
+export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };
diff --git a/backend/routes/main.js b/backend/routes/main.js
index 7bc4323d8b..94682cfba4 100644
--- a/backend/routes/main.js
+++ b/backend/routes/main.js
@@ -14,6 +14,7 @@ import schemaRoutes from "./schema.js";
import settingsRoutes from "./settings.js";
import tokensRoutes from "./tokens.js";
import usersRoutes from "./users.js";
+import versionRoutes from "./version.js";
const router = express.Router({
caseSensitive: true,
@@ -46,6 +47,7 @@ router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes);
router.use("/reports", reportsRoutes);
router.use("/settings", settingsRoutes);
+router.use("/version", versionRoutes);
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
router.use("/nginx/dead-hosts", deadHostsRoutes);
diff --git a/backend/routes/version.js b/backend/routes/version.js
new file mode 100644
index 0000000000..266e56fff7
--- /dev/null
+++ b/backend/routes/version.js
@@ -0,0 +1,40 @@
+import express from "express";
+import internalRemoteVersion from "../internal/remote-version.js";
+import { debug, express as logger } from "../logger.js";
+
+const router = express.Router({
+ caseSensitive: true,
+ strict: true,
+ mergeParams: true,
+});
+
+/**
+ * /api/version/check
+ */
+router
+ .route("/check")
+ .options((_, res) => {
+ res.sendStatus(204);
+ })
+
+ /**
+ * GET /api/version/check
+ *
+ * Check for available updates
+ */
+ .get(async (req, res, _next) => {
+ try {
+ const data = await internalRemoteVersion.get();
+ res.status(200).send(data);
+ } catch (error) {
+ debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);
+ // Send 200 even though there's an error to avoid triggering update checks repeatedly
+ res.status(200).send({
+ current: null,
+ latest: null,
+ update_available: false,
+ });
+ }
+ });
+
+export default router;
diff --git a/backend/schema/components/check-version-object.json b/backend/schema/components/check-version-object.json
new file mode 100644
index 0000000000..ef2ffac418
--- /dev/null
+++ b/backend/schema/components/check-version-object.json
@@ -0,0 +1,23 @@
+{
+ "type": "object",
+ "description": "Check Version object",
+ "additionalProperties": false,
+ "required": ["current", "latest", "update_available"],
+ "properties": {
+ "current": {
+ "type": ["string", "null"],
+ "description": "Current version string",
+ "example": "v2.10.1"
+ },
+ "latest": {
+ "type": ["string", "null"],
+ "description": "Latest version string",
+ "example": "v2.13.4"
+ },
+ "update_available": {
+ "type": "boolean",
+ "description": "Whether there's an update available",
+ "example": true
+ }
+ }
+}
diff --git a/backend/schema/paths/version/check/get.json b/backend/schema/paths/version/check/get.json
new file mode 100644
index 0000000000..cbc576a7f5
--- /dev/null
+++ b/backend/schema/paths/version/check/get.json
@@ -0,0 +1,26 @@
+{
+ "operationId": "checkVersion",
+ "summary": "Returns any new version data from github",
+ "tags": ["public"],
+ "responses": {
+ "200": {
+ "description": "200 response",
+ "content": {
+ "application/json": {
+ "examples": {
+ "default": {
+ "value": {
+ "current": "v2.12.0",
+ "latest": "v2.13.4",
+ "update_available": true
+ }
+ }
+ },
+ "schema": {
+ "$ref": "../../../components/check-version-object.json"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json
index e7234d4ec3..9afb51f293 100644
--- a/backend/schema/swagger.json
+++ b/backend/schema/swagger.json
@@ -293,6 +293,11 @@
"$ref": "./paths/tokens/post.json"
}
},
+ "/version/check": {
+ "get": {
+ "$ref": "./paths/version/check/get.json"
+ }
+ },
"/users": {
"get": {
"$ref": "./paths/users/get.json"
diff --git a/backend/yarn.lock b/backend/yarn.lock
index f5db77966d..b6f7a94885 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -1430,9 +1430,9 @@ isexe@^2.0.0:
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-yaml@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
- integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
+ integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml
index ce8913fe2b..ba2c5d3fed 100644
--- a/docker/docker-compose.ci.yml
+++ b/docker/docker-compose.ci.yml
@@ -24,9 +24,13 @@ services:
interval: 10s
timeout: 3s
expose:
- - "80-81/tcp"
+ - "80/tcp"
+ - "81/tcp"
- "443/tcp"
- - "1500-1503/tcp"
+ - "1500/tcp"
+ - "1501/tcp"
+ - "1502/tcp"
+ - "1503/tcp"
networks:
fulltest:
aliases:
diff --git a/frontend/.gitignore b/frontend/.gitignore
index 8b7e50214d..a9b91bc1a0 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -1,3 +1,5 @@
+src/locale/lang
+
# Logs
logs
*.log
diff --git a/frontend/check-locales.cjs b/frontend/check-locales.cjs
index 2429cbc223..bd871169fb 100755
--- a/frontend/check-locales.cjs
+++ b/frontend/check-locales.cjs
@@ -8,12 +8,16 @@
const allLocales = [
["en", "en-US"],
- ["es", "es-ES"],
["de", "de-DE"],
+ ["es", "es-ES"],
+ ["it", "it-IT"],
+ ["ja", "ja-JP"],
+ ["nl", "nl-NL"],
+ ["pl", "pl-PL"],
["ru", "ru-RU"],
["sk", "sk-SK"],
+ ["vi", "vi-VN"],
["zh", "zh-CN"],
- ["pl", "pl-PL"],
];
const ignoreUnused = [
diff --git a/frontend/index.html b/frontend/index.html
index 8081804472..c6a2012266 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -5,6 +5,7 @@
+