diff options
Diffstat (limited to 'extra')
52 files changed, 2185 insertions, 0 deletions
diff --git a/extra/beta/update-version.js b/extra/beta/update-version.js new file mode 100644 index 0000000..9ab0015 --- /dev/null +++ b/extra/beta/update-version.js @@ -0,0 +1,84 @@ +const pkg = require("../../package.json"); +const fs = require("fs"); +const childProcess = require("child_process"); +const util = require("../../src/util"); + +util.polyfill(); + +const version = process.env.RELEASE_BETA_VERSION; + +console.log("Beta Version: " + version); + +if (!version || !version.includes("-beta.")) { + console.error("invalid version, beta version only"); + process.exit(1); +} + +const exists = tagExists(version); + +if (! exists) { + // Process package.json + pkg.version = version; + fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); + + // Also update package-lock.json + const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; + childProcess.spawnSync(npm, [ "install" ]); + + commit(version); + tag(version); + +} else { + console.log("version tag exists, please delete the tag or use another tag"); + process.exit(1); +} + +/** + * Commit updated files + * @param {string} version Version to update to + * @returns {void} + * @throws Error committing files + */ +function commit(version) { + let msg = "Update to " + version; + + let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]); + let stdout = res.stdout.toString().trim(); + console.log(stdout); + + if (stdout.includes("no changes added to commit")) { + throw new Error("commit error"); + } + + res = childProcess.spawnSync("git", [ "push", "origin", "master" ]); + console.log(res.stdout.toString().trim()); +} + +/** + * Create a tag with the specified version + * @param {string} version Tag to create + * @returns {void} + */ +function tag(version) { + let res = childProcess.spawnSync("git", [ "tag", version ]); + console.log(res.stdout.toString().trim()); + + res = childProcess.spawnSync("git", [ "push", "origin", version ]); + console.log(res.stdout.toString().trim()); +} + +/** + * Check if a tag exists for the specified version + * @param {string} version Version to check + * @returns {boolean} Does the tag already exist + * @throws Version is not valid + */ +function tagExists(version) { + if (! version) { + throw new Error("invalid version"); + } + + let res = childProcess.spawnSync("git", [ "tag", "-l", version ]); + + return res.stdout.toString().trim() === version; +} diff --git a/extra/build-healthcheck.js b/extra/build-healthcheck.js new file mode 100644 index 0000000..e4c8026 --- /dev/null +++ b/extra/build-healthcheck.js @@ -0,0 +1,27 @@ +const childProcess = require("child_process"); +const fs = require("fs"); +const platform = process.argv[2]; + +if (!platform) { + console.error("No platform??"); + process.exit(1); +} + +if (platform === "linux/arm/v7") { + console.log("Arch: armv7"); + if (fs.existsSync("./extra/healthcheck-armv7")) { + fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck"); + console.log("Already built in the host, skip."); + process.exit(0); + } else { + console.log("prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build."); + } +} else { + if (fs.existsSync("./extra/healthcheck-armv7")) { + fs.rmSync("./extra/healthcheck-armv7"); + } +} + +const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8"); +console.log(output); + diff --git a/extra/check-knex-filenames.mjs b/extra/check-knex-filenames.mjs new file mode 100644 index 0000000..4911fc5 --- /dev/null +++ b/extra/check-knex-filenames.mjs @@ -0,0 +1,72 @@ +import fs from "fs"; +const dir = "./db/knex_migrations"; + +// Get the file list (ending with .js) from the directory +const files = fs.readdirSync(dir).filter((file) => file !== "README.md"); + +// They are wrong, but they had been merged, so allowed. +const exceptionList = [ + "2024-08-24-000-add-cache-bust.js", + "2024-10-1315-rabbitmq-monitor.js", +]; + +// Correct format: YYYY-MM-DD-HHmm-description.js + +for (const file of files) { + if (exceptionList.includes(file)) { + continue; + } + + // Check ending with .js + if (!file.endsWith(".js")) { + console.error(`It should end with .js: ${file}`); + process.exit(1); + } + + const parts = file.split("-"); + + // Should be at least 5 parts + if (parts.length < 5) { + console.error(`Invalid format: ${file}`); + process.exit(1); + } + + // First part should be a year >= 2024 + const year = parseInt(parts[0], 10); + if (isNaN(year) || year < 2023) { + console.error(`Invalid year: ${file}`); + process.exit(1); + } + + // Second part should be a month + const month = parseInt(parts[1], 10); + if (isNaN(month) || month < 1 || month > 12) { + console.error(`Invalid month: ${file}`); + process.exit(1); + } + + // Third part should be a day + const day = parseInt(parts[2], 10); + if (isNaN(day) || day < 1 || day > 31) { + console.error(`Invalid day: ${file}`); + process.exit(1); + } + + // Fourth part should be HHmm + const time = parts[3]; + + // Check length is 4 + if (time.length !== 4) { + console.error(`Invalid time: ${file}`); + process.exit(1); + } + + const hour = parseInt(time.substring(0, 2), 10); + const minute = parseInt(time.substring(2), 10); + if (isNaN(hour) || hour < 0 || hour > 23 || isNaN(minute) || minute < 0 || minute > 59) { + console.error(`Invalid time: ${file}`); + process.exit(1); + } +} + +console.log("All knex filenames are correct."); diff --git a/extra/check-lang-json.js b/extra/check-lang-json.js new file mode 100644 index 0000000..dfda348 --- /dev/null +++ b/extra/check-lang-json.js @@ -0,0 +1,27 @@ +// For #5231 + +const fs = require("fs"); + +let path = "./src/lang"; + +// list directories in the lang directory +let jsonFileList = fs.readdirSync(path); + +for (let jsonFile of jsonFileList) { + if (!jsonFile.endsWith(".json")) { + continue; + } + + let jsonPath = path + "/" + jsonFile; + let originalContent = fs.readFileSync(jsonPath, "utf8"); + let langData = JSON.parse(originalContent); + + let formattedContent = JSON.stringify(langData, null, 4) + "\n"; + + if (originalContent !== formattedContent) { + console.error(`File ${jsonFile} is not formatted correctly.`); + process.exit(1); + } +} + +console.log("All lang json files are formatted correctly."); diff --git a/extra/checkout-pr.js b/extra/checkout-pr.js new file mode 100644 index 0000000..0328770 --- /dev/null +++ b/extra/checkout-pr.js @@ -0,0 +1,33 @@ +const childProcess = require("child_process"); + +if (!process.env.UPTIME_KUMA_GH_REPO) { + console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)"); + process.exit(1); +} + +let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":"); + +if (inputArray.length !== 2) { + console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)"); +} + +let name = inputArray[0]; +let branch = inputArray[1]; + +console.log("Checkout pr"); + +// Checkout the pr +let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]); + +console.log(result.stdout.toString()); +console.error(result.stderr.toString()); + +result = childProcess.spawnSync("git", [ "fetch", name, branch ]); + +console.log(result.stdout.toString()); +console.error(result.stderr.toString()); + +result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]); + +console.log(result.stdout.toString()); +console.error(result.stderr.toString()); diff --git a/extra/close-incorrect-issue.js b/extra/close-incorrect-issue.js new file mode 100644 index 0000000..9bb01b1 --- /dev/null +++ b/extra/close-incorrect-issue.js @@ -0,0 +1,57 @@ +const github = require("@actions/github"); + +(async () => { + try { + const token = process.argv[2]; + const issueNumber = process.argv[3]; + const username = process.argv[4]; + + const client = github.getOctokit(token).rest; + + const issue = { + owner: "louislam", + repo: "uptime-kuma", + number: issueNumber, + }; + + const labels = ( + await client.issues.listLabelsOnIssue({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number + }) + ).data.map(({ name }) => name); + + if (labels.length === 0) { + console.log("Bad format here"); + + await client.issues.addLabels({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + labels: [ "invalid-format" ] + }); + + // Add the issue closing comment + await client.issues.createComment({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context nessesary for a good discussions.` + }); + + // Close the issue + await client.issues.update({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + state: "closed" + }); + } else { + console.log("Pass!"); + } + } catch (e) { + console.log(e); + } + +})(); diff --git a/extra/compile-install-script.ps1 b/extra/compile-install-script.ps1 new file mode 100644 index 0000000..dd44798 --- /dev/null +++ b/extra/compile-install-script.ps1 @@ -0,0 +1,2 @@ +# Must enable File Sharing in Docker Desktop +docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh diff --git a/extra/deploy-demo-server.js b/extra/deploy-demo-server.js new file mode 100644 index 0000000..2cc196b --- /dev/null +++ b/extra/deploy-demo-server.js @@ -0,0 +1,60 @@ +require("dotenv").config(); +const { NodeSSH } = require("node-ssh"); +const readline = require("readline"); +const rl = readline.createInterface({ input: process.stdin, + output: process.stdout }); +const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); + +(async () => { + try { + console.log("SSH to demo server"); + const ssh = new NodeSSH(); + await ssh.connect({ + host: process.env.UPTIME_KUMA_DEMO_HOST, + port: process.env.UPTIME_KUMA_DEMO_PORT, + username: process.env.UPTIME_KUMA_DEMO_USERNAME, + privateKeyPath: process.env.UPTIME_KUMA_DEMO_PRIVATE_KEY_PATH + }); + + let cwd = process.env.UPTIME_KUMA_DEMO_CWD; + let result; + + const version = await prompt("Enter Version: "); + + result = await ssh.execCommand("git fetch --all", { + cwd, + }); + console.log(result.stdout + result.stderr); + + await prompt("Press any key to continue..."); + + result = await ssh.execCommand(`git checkout ${version} --force`, { + cwd, + }); + console.log(result.stdout + result.stderr); + + result = await ssh.execCommand("npm run download-dist", { + cwd, + }); + console.log(result.stdout + result.stderr); + + result = await ssh.execCommand("npm install --production", { + cwd, + }); + console.log(result.stdout + result.stderr); + + /* + result = await ssh.execCommand("pm2 restart 1", { + cwd, + }); + console.log(result.stdout + result.stderr);*/ + + } catch (e) { + console.log(e); + } finally { + rl.close(); + } +})(); + +// When done reading prompt, exit program +rl.on("close", () => process.exit(0)); diff --git a/extra/download-apprise.mjs b/extra/download-apprise.mjs new file mode 100644 index 0000000..3d31f7c --- /dev/null +++ b/extra/download-apprise.mjs @@ -0,0 +1,57 @@ +// Go to http://ftp.debian.org/debian/pool/main/a/apprise/ using fetch api, where it is a apache directory listing page +// Use cheerio to parse the html and get the latest version of Apprise +// call curl to download the latest version of Apprise +// Target file: the latest version of Apprise, which the format is apprise_{VERSION}_all.deb + +import * as cheerio from "cheerio"; +import semver from "semver"; +import * as childProcess from "child_process"; + +const baseURL = "http://ftp.debian.org/debian/pool/main/a/apprise/"; +const response = await fetch(baseURL); + +if (!response.ok) { + throw new Error("Failed to fetch page of Apprise Debian repository."); +} + +const html = await response.text(); + +const $ = cheerio.load(html); + +// Get all the links in the page +const linkElements = $("a"); + +// Filter the links which match apprise_{VERSION}_all.deb +const links = []; +const pattern = /apprise_(.*?)_all.deb/; + +for (let i = 0; i < linkElements.length; i++) { + const link = linkElements[i]; + if (link.attribs.href.match(pattern) && !link.attribs.href.includes("~")) { + links.push({ + filename: link.attribs.href, + version: link.attribs.href.match(pattern)[1], + }); + } +} + +console.log(links); + +// semver compare and download +let latestLink = { + filename: "", + version: "0.0.0", +}; + +for (const link of links) { + if (semver.gt(link.version, latestLink.version)) { + latestLink = link; + } +} + +const downloadURL = baseURL + latestLink.filename; +console.log(`Downloading ${downloadURL}...`); +let result = childProcess.spawnSync("curl", [ downloadURL, "--output", "apprise.deb" ]); +console.log(result.stdout?.toString()); +console.error(result.stderr?.toString()); +process.exit(result.status !== null ? result.status : 1); diff --git a/extra/download-dist.js b/extra/download-dist.js new file mode 100644 index 0000000..b339ac9 --- /dev/null +++ b/extra/download-dist.js @@ -0,0 +1,69 @@ +console.log("Downloading dist"); +const https = require("https"); +const tar = require("tar"); + +const packageJSON = require("../package.json"); +const fs = require("fs"); +const version = packageJSON.version; + +const filename = "dist.tar.gz"; + +const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`; +download(url); + +/** + * Downloads the latest version of the dist from a GitHub release. + * @param {string} url The URL to download from. + * @returns {void} + * + * Generated by Trelent + */ +function download(url) { + console.log(url); + + https.get(url, (response) => { + if (response.statusCode === 200) { + console.log("Extracting dist..."); + + if (fs.existsSync("./dist")) { + + if (fs.existsSync("./dist-backup")) { + fs.rmSync("./dist-backup", { + recursive: true, + force: true, + }); + } + + fs.renameSync("./dist", "./dist-backup"); + } + + const tarStream = tar.x({ + cwd: "./", + }); + + tarStream.on("close", () => { + if (fs.existsSync("./dist-backup")) { + fs.rmSync("./dist-backup", { + recursive: true, + force: true, + }); + } + console.log("Done"); + process.exit(0); + }); + + tarStream.on("error", () => { + if (fs.existsSync("./dist-backup")) { + fs.renameSync("./dist-backup", "./dist"); + } + console.error("Error from tarStream"); + }); + + response.pipe(tarStream); + } else if (response.statusCode === 302) { + download(response.headers.location); + } else { + console.log("dist not found"); + } + }); +} diff --git a/extra/healthcheck.go b/extra/healthcheck.go new file mode 100644 index 0000000..f79b3e6 --- /dev/null +++ b/extra/healthcheck.go @@ -0,0 +1,90 @@ +/* + * If changed, have to run `npm run build-docker-builder-go`. + * This script should be run after a period of time (180s), because the server may need some time to prepare. + */ +package main + +import ( + "crypto/tls" + "io/ioutil" + "log" + "net/http" + "os" + "runtime" + "strings" + "time" +) + +func main() { + isFreeBSD := runtime.GOOS == "freebsd" + + // Is K8S + uptime-kuma as the container name + // See #2083 + isK8s := strings.HasPrefix(os.Getenv("UPTIME_KUMA_PORT"), "tcp://") + + // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + + client := http.Client{ + Timeout: 28 * time.Second, + } + + sslKey := os.Getenv("UPTIME_KUMA_SSL_KEY") + if len(sslKey) == 0 { + sslKey = os.Getenv("SSL_KEY") + } + + sslCert := os.Getenv("UPTIME_KUMA_SSL_CERT") + if len(sslCert) == 0 { + sslCert = os.Getenv("SSL_CERT") + } + + hostname := os.Getenv("UPTIME_KUMA_HOST") + if len(hostname) == 0 && !isFreeBSD { + hostname = os.Getenv("HOST") + } + if len(hostname) == 0 { + hostname = "127.0.0.1" + } + + port := "" + // UPTIME_KUMA_PORT is override by K8S unexpectedly, + if !isK8s { + port = os.Getenv("UPTIME_KUMA_PORT") + } + if len(port) == 0 { + port = os.Getenv("PORT") + } + if len(port) == 0 { + port = "3001" + } + + protocol := "" + if len(sslKey) != 0 && len(sslCert) != 0 { + protocol = "https" + } else { + protocol = "http" + } + + url := protocol + "://" + hostname + ":" + port + + log.Println("Checking " + url) + resp, err := client.Get(url) + + if err != nil { + log.Fatalln(err) + } + + defer resp.Body.Close() + + _, err = ioutil.ReadAll(resp.Body) + + if err != nil { + log.Fatalln(err) + } + + log.Printf("Health Check OK [Res Code: %d]\n", resp.StatusCode) + +} diff --git a/extra/healthcheck.js b/extra/healthcheck.js new file mode 100644 index 0000000..c9391c4 --- /dev/null +++ b/extra/healthcheck.js @@ -0,0 +1,55 @@ +/* + * ⚠️ ⚠️ ⚠️ ⚠️ Due to the weird issue in Portainer that the healthcheck script is still pointing to this script for unknown reason. + * IT CANNOT BE DROPPED, even though it looks like it is not used. + * See more: https://github.com/louislam/uptime-kuma/issues/2774#issuecomment-1429092359 + * + * ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future. + * This script should be run after a period of time (180s), because the server may need some time to prepare. + */ +const FBSD = /^freebsd/.test(process.platform); + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +let client; + +const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined; +const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined; + +if (sslKey && sslCert) { + client = require("https"); +} else { + client = require("http"); +} + +// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise. +// Dual-stack support for (::) +let hostname = process.env.UPTIME_KUMA_HOST; + +// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD +if (!hostname && !FBSD) { + hostname = process.env.HOST; +} + +const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001); + +let options = { + host: hostname || "127.0.0.1", + port: port, + timeout: 28 * 1000, +}; + +let request = client.request(options, (res) => { + console.log(`Health Check OK [Res Code: ${res.statusCode}]`); + if (res.statusCode === 302) { + process.exit(0); + } else { + process.exit(1); + } +}); + +request.on("error", function (err) { + console.error("Health Check ERROR"); + process.exit(1); +}); + +request.end(); diff --git a/extra/mark-as-nightly.js b/extra/mark-as-nightly.js new file mode 100644 index 0000000..d2cb8cb --- /dev/null +++ b/extra/mark-as-nightly.js @@ -0,0 +1,24 @@ +const pkg = require("../package.json"); +const fs = require("fs"); +const util = require("../src/util"); +const dayjs = require("dayjs"); + +util.polyfill(); + +const oldVersion = pkg.version; +const newVersion = oldVersion + "-nightly-" + dayjs().format("YYYYMMDDHHmmss"); + +console.log("Old Version: " + oldVersion); +console.log("New Version: " + newVersion); + +if (newVersion) { + // Process package.json + pkg.version = newVersion; + pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion); + fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); + + // Process README.md + if (fs.existsSync("README.md")) { + fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion)); + } +} diff --git a/extra/push-examples/.gitignore b/extra/push-examples/.gitignore new file mode 100644 index 0000000..717394d --- /dev/null +++ b/extra/push-examples/.gitignore @@ -0,0 +1,3 @@ +java/Index.class +csharp/index.exe +typescript-fetch/index.js diff --git a/extra/push-examples/bash-curl/index.sh b/extra/push-examples/bash-curl/index.sh new file mode 100644 index 0000000..3031255 --- /dev/null +++ b/extra/push-examples/bash-curl/index.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Filename: index.sh +PUSH_URL="https://example.com/api/push/key?status=up&msg=OK&ping=" +INTERVAL=60 + +while true; do + curl -s -o /dev/null $PUSH_URL + echo "Pushed!" + sleep $INTERVAL +done diff --git a/extra/push-examples/csharp/index.cs b/extra/push-examples/csharp/index.cs new file mode 100644 index 0000000..94eecfb --- /dev/null +++ b/extra/push-examples/csharp/index.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Threading; + +/** + * Compile: C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe index.cs + * Run: index.exe + */ +class Index +{ + const string PushURL = "https://example.com/api/push/key?status=up&msg=OK&ping="; + const int Interval = 60; + + static void Main(string[] args) + { + while (true) + { + WebClient client = new WebClient(); + client.DownloadString(PushURL); + Console.WriteLine("Pushed!"); + Thread.Sleep(Interval * 1000); + } + } +} diff --git a/extra/push-examples/docker/index.sh b/extra/push-examples/docker/index.sh new file mode 100644 index 0000000..1eb43d6 --- /dev/null +++ b/extra/push-examples/docker/index.sh @@ -0,0 +1 @@ +docker run -d --restart=always --name uptime-kuma-push louislam/uptime-kuma:push "https://example.com/api/push/key?status=up&msg=OK&ping=" 60 diff --git a/extra/push-examples/go/index.go b/extra/push-examples/go/index.go new file mode 100644 index 0000000..2e518e7 --- /dev/null +++ b/extra/push-examples/go/index.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "net/http" + "time" +) + +func main() { + const PushURL = "https://example.com/api/push/key?status=up&msg=OK&ping=" + const Interval = 60 + + for { + _, err := http.Get(PushURL) + if err == nil { + fmt.Println("Pushed!") + } + time.Sleep(Interval * time.Second) + } +} diff --git a/extra/push-examples/java/index.java b/extra/push-examples/java/index.java new file mode 100644 index 0000000..5a77342 --- /dev/null +++ b/extra/push-examples/java/index.java @@ -0,0 +1,32 @@ +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Compile: javac index.java + * Run: java Index + */ +class Index { + + public static final String PUSH_URL = "https://example.com/api/push/key?status=up&msg=OK&ping="; + public static final int INTERVAL = 60; + + public static void main(String[] args) { + while (true) { + try { + URL url = new URL(PUSH_URL); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.getResponseCode(); + con.disconnect(); + System.out.println("Pushed!"); + } catch (Exception e) { + e.printStackTrace(); + } + try { + Thread.sleep(INTERVAL * 1000); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/extra/push-examples/javascript-fetch/index.js b/extra/push-examples/javascript-fetch/index.js new file mode 100644 index 0000000..9d21d0d --- /dev/null +++ b/extra/push-examples/javascript-fetch/index.js @@ -0,0 +1,11 @@ +// Supports: Node.js >= 18, Deno, Bun +const pushURL = "https://example.com/api/push/key?status=up&msg=OK&ping="; +const interval = 60; + +const push = async () => { + await fetch(pushURL); + console.log("Pushed!"); +}; + +push(); +setInterval(push, interval * 1000); diff --git a/extra/push-examples/javascript-fetch/package.json b/extra/push-examples/javascript-fetch/package.json new file mode 100644 index 0000000..78aefc2 --- /dev/null +++ b/extra/push-examples/javascript-fetch/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "start": "node index.js" + } +} diff --git a/extra/push-examples/php/index.php b/extra/push-examples/php/index.php new file mode 100644 index 0000000..d08b451 --- /dev/null +++ b/extra/push-examples/php/index.php @@ -0,0 +1,13 @@ +<?php +const PUSH_URL = "https://example.com/api/push/key?status=up&msg=OK&ping="; +const interval = 60; + +while (true) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, PUSH_URL); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_exec($ch); + curl_close($ch); + echo "Pushed!\n"; + sleep(interval); +} diff --git a/extra/push-examples/powershell/index.ps1 b/extra/push-examples/powershell/index.ps1 new file mode 100644 index 0000000..2894e8d --- /dev/null +++ b/extra/push-examples/powershell/index.ps1 @@ -0,0 +1,9 @@ +# Filename: index.ps1 +$pushURL = "https://example.com/api/push/key?status=up&msg=OK&ping=" +$interval = 60 + +while ($true) { + $res = Invoke-WebRequest -Uri $pushURL + Write-Host "Pushed!" + Start-Sleep -Seconds $interval +} diff --git a/extra/push-examples/python/index.py b/extra/push-examples/python/index.py new file mode 100644 index 0000000..c735b29 --- /dev/null +++ b/extra/push-examples/python/index.py @@ -0,0 +1,10 @@ +import urllib.request +import time + +push_url = "https://example.com/api/push/key?status=up&msg=OK&ping=" +interval = 60 + +while True: + urllib.request.urlopen(push_url) + print("Pushed!\n") + time.sleep(interval) diff --git a/extra/push-examples/typescript-fetch/README.md b/extra/push-examples/typescript-fetch/README.md new file mode 100644 index 0000000..c2c96ce --- /dev/null +++ b/extra/push-examples/typescript-fetch/README.md @@ -0,0 +1,19 @@ +# How to run + +Node.js (ts-node) + +```bash +ts-node index.ts +``` + +Deno + +```bash +deno run --allow-net index.ts +``` + +Bun.js + +```bash +bun index.ts +``` diff --git a/extra/push-examples/typescript-fetch/index.ts b/extra/push-examples/typescript-fetch/index.ts new file mode 100644 index 0000000..bb14088 --- /dev/null +++ b/extra/push-examples/typescript-fetch/index.ts @@ -0,0 +1,11 @@ +// Supports: Deno, Bun, Node.js >= 18 (ts-node) +const pushURL : string = "https://example.com/api/push/key?status=up&msg=OK&ping="; +const interval : number = 60; + +const push = async () => { + await fetch(pushURL); + console.log("Pushed!"); +}; + +push(); +setInterval(push, interval * 1000); diff --git a/extra/push-examples/typescript-fetch/package.json b/extra/push-examples/typescript-fetch/package.json new file mode 100644 index 0000000..9d7e974 --- /dev/null +++ b/extra/push-examples/typescript-fetch/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "ts-node": "ts-node index.ts", + "deno": "deno run --allow-net index.ts", + "bun": "bun index.ts" + }, + "devDependencies": { + "@types/node": "^20.6.0", + "ts-node": "^10.9.1", + "tslib": "^2.6.2", + "typescript": "^5.2.2" + } +} diff --git a/extra/rebase-pr.js b/extra/rebase-pr.js new file mode 100644 index 0000000..4921d2e --- /dev/null +++ b/extra/rebase-pr.js @@ -0,0 +1,40 @@ +const { execSync } = require("child_process"); + +/** + * Rebase a PR onto such as 1.23.X or master + * @returns {Promise<void>} + */ +async function main() { + const branch = process.argv[2]; + + // Use gh to get current branch's pr id + let currentBranchPRID = execSync("gh pr view --json number --jq \".number\"").toString().trim(); + console.log("Pr ID: ", currentBranchPRID); + + // Use gh commend to get pr commits + const prCommits = JSON.parse(execSync(`gh pr view ${currentBranchPRID} --json commits`).toString().trim()).commits; + + console.log("Found commits: ", prCommits.length); + + // Sort the commits by authoredDate + prCommits.sort((a, b) => { + return new Date(a.authoredDate) - new Date(b.authoredDate); + }); + + // Get the oldest commit id + const oldestCommitID = prCommits[0].oid; + console.log("Oldest commit id of this pr:", oldestCommitID); + + // Get the latest commit id of the target branch + const latestCommitID = execSync(`git rev-parse origin/${branch}`).toString().trim(); + console.log("Latest commit id of " + branch + ":", latestCommitID); + + // Get the original parent commit id of the oldest commit + const originalParentCommitID = execSync(`git log --pretty=%P -n 1 "${oldestCommitID}"`).toString().trim(); + console.log("Original parent commit id of the oldest commit:", originalParentCommitID); + + // Rebase the pr onto the target branch + execSync(`git rebase --onto ${latestCommitID} ${originalParentCommitID}`); +} + +main(); diff --git a/extra/reformat-changelog.js b/extra/reformat-changelog.js new file mode 100644 index 0000000..80a1b72 --- /dev/null +++ b/extra/reformat-changelog.js @@ -0,0 +1,44 @@ +// Generate on GitHub +const input = ` +* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86 +`; + +const template = ` +### 🆕 New Features + +### 💇♀️ Improvements + +### 🐞 Bug Fixes + +### ⬆️ Security Fixes + +### 🦎 Translation Contributions + +### Others +- Other small changes, code refactoring and comment/doc updates in this repo: +`; + +const lines = input.split("\n").filter((line) => line.trim() !== ""); + +for (const line of lines) { + // Split the last " by " + const usernamePullRequesURL = line.split(" by ").pop(); + + if (!usernamePullRequesURL) { + console.log("Unable to parse", line); + continue; + } + + const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in "); + const pullRequestID = "#" + pullRequestURL.split("/").pop(); + let message = line.split(" by ").shift(); + + if (!message) { + console.log("Unable to parse", line); + continue; + } + + message = message.split("* ").pop(); + console.log("-", pullRequestID, message, `(Thanks ${username})`); +} +console.log(template); diff --git a/extra/release/beta.mjs b/extra/release/beta.mjs new file mode 100644 index 0000000..a2c9809 --- /dev/null +++ b/extra/release/beta.mjs @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { + ver, + buildDist, + buildImage, + checkDocker, + checkTagExists, + checkVersionFormat, + dryRun, + getRepoName, + pressAnyKey, + execSync, uploadArtifacts, +} from "./lib.mjs"; +import semver from "semver"; + +const repoName = getRepoName(); +const version = process.env.RELEASE_BETA_VERSION; +const githubToken = process.env.RELEASE_GITHUB_TOKEN; + +console.log("RELEASE_BETA_VERSION:", version); + +if (!githubToken) { + console.error("GITHUB_TOKEN is required"); + process.exit(1); +} + +// Check if the version is a valid semver +checkVersionFormat(version); + +// Check if the semver identifier is "beta" +const semverIdentifier = semver.prerelease(version); +console.log("Semver identifier:", semverIdentifier); +if (semverIdentifier[0] !== "beta") { + console.error("VERSION should have a semver identifier of 'beta'"); + process.exit(1); +} + +// Check if docker is running +checkDocker(); + +// Check if the tag exists +await checkTagExists(repoName, version); + +// node extra/beta/update-version.js +execSync("node ./extra/beta/update-version.js"); + +// Build frontend dist +buildDist(); + +// Build slim image (rootless) +buildImage(repoName, [ "beta-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image (rootless) +buildImage(repoName, [ "beta-rootless", ver(version, "rootless") ], "rootless"); + +// Build slim image +buildImage(repoName, [ "beta-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image +buildImage(repoName, [ "beta", version ], "release"); + +await pressAnyKey(); + +// npm run upload-artifacts +uploadArtifacts(); diff --git a/extra/release/final.mjs b/extra/release/final.mjs new file mode 100644 index 0000000..d190ac0 --- /dev/null +++ b/extra/release/final.mjs @@ -0,0 +1,57 @@ +import "dotenv/config"; +import { + ver, + buildDist, + buildImage, + checkDocker, + checkTagExists, + checkVersionFormat, + getRepoName, + pressAnyKey, execSync, uploadArtifacts +} from "./lib.mjs"; + +const repoName = getRepoName(); +const version = process.env.RELEASE_VERSION; +const githubToken = process.env.RELEASE_GITHUB_TOKEN; + +console.log("RELEASE_VERSION:", version); + +if (!githubToken) { + console.error("GITHUB_TOKEN is required"); + process.exit(1); +} + +// Check if the version is a valid semver +checkVersionFormat(version); + +// Check if docker is running +checkDocker(); + +// Check if the tag exists +await checkTagExists(repoName, version); + +// node extra/beta/update-version.js +execSync("node extra/update-version.js"); + +// Build frontend dist +buildDist(); + +// Build slim image (rootless) +buildImage(repoName, [ "2-slim-rootless", ver(version, "slim-rootless") ], "rootless", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image (rootless) +buildImage(repoName, [ "2-rootless", ver(version, "rootless") ], "rootless"); + +// Build slim image +buildImage(repoName, [ "next-slim", "2-slim", ver(version, "slim") ], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim"); + +// Build full image +buildImage(repoName, [ "next", "2", version ], "release"); + +await pressAnyKey(); + +// npm run upload-artifacts +uploadArtifacts(); + +// node extra/update-wiki-version.js +execSync("node extra/update-wiki-version.js"); diff --git a/extra/release/lib.mjs b/extra/release/lib.mjs new file mode 100644 index 0000000..d6c294c --- /dev/null +++ b/extra/release/lib.mjs @@ -0,0 +1,191 @@ +import "dotenv/config"; +import * as childProcess from "child_process"; +import semver from "semver"; + +export const dryRun = process.env.RELEASE_DRY_RUN === "1"; + +if (dryRun) { + console.info("Dry run enabled."); +} + +/** + * Check if docker is running + * @returns {void} + */ +export function checkDocker() { + try { + childProcess.execSync("docker ps"); + } catch (error) { + console.error("Docker is not running. Please start docker and try again."); + process.exit(1); + } +} + +/** + * Get Docker Hub repository name + */ +export function getRepoName() { + return process.env.RELEASE_REPO_NAME || "louislam/uptime-kuma"; +} + +/** + * Build frontend dist + * @returns {void} + */ +export function buildDist() { + if (!dryRun) { + childProcess.execSync("npm run build", { stdio: "inherit" }); + } else { + console.info("[DRY RUN] npm run build"); + } +} + +/** + * Build docker image and push to Docker Hub + * @param {string} repoName Docker Hub repository name + * @param {string[]} tags Docker image tags + * @param {string} target Dockerfile's target name + * @param {string} buildArgs Docker build args + * @param {string} dockerfile Path to Dockerfile + * @param {string} platform Build platform + * @returns {void} + */ +export function buildImage(repoName, tags, target, buildArgs = "", dockerfile = "docker/dockerfile", platform = "linux/amd64,linux/arm64,linux/arm/v7") { + let args = [ + "buildx", + "build", + "-f", + dockerfile, + "--platform", + platform, + ]; + + // Add tags + for (let tag of tags) { + args.push("-t", `${repoName}:${tag}`); + } + + args = [ + ...args, + "--target", + target, + ]; + + // Add build args + if (buildArgs) { + args.push("--build-arg", buildArgs); + } + + args = [ + ...args, + ".", + "--push", + ]; + + if (!dryRun) { + childProcess.spawnSync("docker", args, { stdio: "inherit" }); + } else { + console.log(`[DRY RUN] docker ${args.join(" ")}`); + } +} + +/** + * Check if the version already exists on Docker Hub + * TODO: use semver to compare versions if it is greater than the previous? + * @param {string} repoName Docker Hub repository name + * @param {string} version Version to check + * @returns {void} + */ +export async function checkTagExists(repoName, version) { + console.log(`Checking if version ${version} exists on Docker Hub`); + + // Get a list of tags from the Docker Hub repository + let tags = []; + + // It is mainly to check my careless mistake that I forgot to update the release version in .env, so `page_size` is set to 100 is enough, I think. + const response = await fetch(`https://hub.docker.com/v2/repositories/${repoName}/tags/?page_size=100`); + if (response.ok) { + const data = await response.json(); + tags = data.results.map((tag) => tag.name); + } else { + console.error("Failed to get tags from Docker Hub"); + process.exit(1); + } + + // Check if the version already exists + if (tags.includes(version)) { + console.error(`Version ${version} already exists`); + process.exit(1); + } +} + +/** + * Check the version format + * @param {string} version Version to check + * @returns {void} + */ +export function checkVersionFormat(version) { + if (!version) { + console.error("VERSION is required"); + process.exit(1); + } + + // Check the version format, it should be a semver and must be like this: "2.0.0-beta.0" + if (!semver.valid(version)) { + console.error("VERSION is not a valid semver version"); + process.exit(1); + } +} + +/** + * Press any key to continue + * @returns {Promise<void>} + */ +export function pressAnyKey() { + console.log("Git Push and Publish the release note on github, then press any key to continue"); + process.stdin.setRawMode(true); + process.stdin.resume(); + return new Promise(resolve => process.stdin.once("data", data => { + process.stdin.setRawMode(false); + process.stdin.pause(); + resolve(); + })); +} + +/** + * Append version identifier + * @param {string} version Version + * @param {string} identifier Identifier + * @returns {string} Version with identifier + */ +export function ver(version, identifier) { + const obj = semver.parse(version); + + if (obj.prerelease.length === 0) { + obj.prerelease = [ identifier ]; + } else { + obj.prerelease[0] = [ obj.prerelease[0], identifier ].join("-"); + } + return obj.format(); +} + +/** + * Upload artifacts to GitHub + * @returns {void} + */ +export function uploadArtifacts() { + execSync("npm run upload-artifacts"); +} + +/** + * Execute a command + * @param {string} cmd Command to execute + * @returns {void} + */ +export function execSync(cmd) { + if (!dryRun) { + childProcess.execSync(cmd, { stdio: "inherit" }); + } else { + console.info(`[DRY RUN] ${cmd}`); + } +} diff --git a/extra/release/nightly.mjs b/extra/release/nightly.mjs new file mode 100644 index 0000000..c6641ba --- /dev/null +++ b/extra/release/nightly.mjs @@ -0,0 +1,16 @@ +import { buildDist, buildImage, checkDocker, getRepoName } from "./lib.mjs"; + +// Docker Hub repository name +const repoName = getRepoName(); + +// Check if docker is running +checkDocker(); + +// Build frontend dist (it will build on the host machine, TODO: build on a container?) +buildDist(); + +// Build full image (rootless) +buildImage(repoName, [ "nightly2-rootless" ], "nightly-rootless"); + +// Build full image +buildImage(repoName, [ "nightly2" ], "nightly"); diff --git a/extra/remove-2fa.js b/extra/remove-2fa.js new file mode 100644 index 0000000..e6a8b97 --- /dev/null +++ b/extra/remove-2fa.js @@ -0,0 +1,65 @@ +console.log("== Uptime Kuma Remove 2FA Tool =="); +console.log("Loading the database"); + +const Database = require("../server/database"); +const { R } = require("redbean-node"); +const readline = require("readline"); +const TwoFA = require("../server/2fa"); +const args = require("args-parser")(process.argv); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +const main = async () => { + Database.initDataDir(args); + await Database.connect(); + + try { + // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now. + if (!process.env.TEST_BACKEND) { + const user = await R.findOne("user"); + if (! user) { + throw new Error("user not found, have you installed?"); + } + + console.log("Found user: " + user.username); + + let ans = await question("Are you sure want to remove 2FA? [y/N]"); + + if (ans.toLowerCase() === "y") { + await TwoFA.disable2FA(user.id); + console.log("2FA has been removed successfully."); + } + + } + } catch (e) { + console.error("Error: " + e.message); + } + + await Database.close(); + rl.close(); + + console.log("Finished."); +}; + +/** + * Ask question of user + * @param {string} question Question to ask + * @returns {Promise<string>} Users response + */ +function question(question) { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer); + }); + }); +} + +if (!process.env.TEST_BACKEND) { + main(); +} + +module.exports = { + main, +}; diff --git a/extra/remove-empty-lang-keys.js b/extra/remove-empty-lang-keys.js new file mode 100644 index 0000000..6b34fa0 --- /dev/null +++ b/extra/remove-empty-lang-keys.js @@ -0,0 +1,25 @@ +// For #5231 + +const fs = require("fs"); + +let path = "../src/lang"; + +// list directories in the lang directory +let jsonFileList = fs.readdirSync(path); + +for (let jsonFile of jsonFileList) { + if (!jsonFile.endsWith(".json")) { + continue; + } + + let jsonPath = path + "/" + jsonFile; + let langData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); + + for (let key in langData) { + if (langData[key] === "") { + delete langData[key]; + } + } + + fs.writeFileSync(jsonPath, JSON.stringify(langData, null, 4) + "\n"); +} diff --git a/extra/remove-playwright-test-data.js b/extra/remove-playwright-test-data.js new file mode 100644 index 0000000..0628807 --- /dev/null +++ b/extra/remove-playwright-test-data.js @@ -0,0 +1,6 @@ +const fs = require("fs"); + +fs.rmSync("./data/playwright-test", { + recursive: true, + force: true, +}); diff --git a/extra/reset-migrate-aggregate-table-state.js b/extra/reset-migrate-aggregate-table-state.js new file mode 100644 index 0000000..e6c51fb --- /dev/null +++ b/extra/reset-migrate-aggregate-table-state.js @@ -0,0 +1,24 @@ +const { R } = require("redbean-node"); +const Database = require("../server/database"); +const args = require("args-parser")(process.argv); +const { Settings } = require("../server/settings"); + +const main = async () => { + console.log("Connecting the database"); + Database.initDataDir(args); + await Database.connect(false, false, true); + + console.log("Deleting all data from aggregate tables"); + await R.exec("DELETE FROM stat_minutely"); + await R.exec("DELETE FROM stat_hourly"); + await R.exec("DELETE FROM stat_daily"); + + console.log("Resetting the aggregate table state"); + await Settings.set("migrateAggregateTableState", ""); + + await Database.close(); + console.log("Done"); +}; + +main(); + diff --git a/extra/reset-password.js b/extra/reset-password.js new file mode 100644 index 0000000..b87d90f --- /dev/null +++ b/extra/reset-password.js @@ -0,0 +1,141 @@ +console.log("== Uptime Kuma Reset Password Tool =="); + +const Database = require("../server/database"); +const { R } = require("redbean-node"); +const readline = require("readline"); +const { initJWTSecret } = require("../server/util-server"); +const User = require("../server/model/user"); +const { io } = require("socket.io-client"); +const { localWebSocketURL } = require("../server/config"); +const args = require("args-parser")(process.argv); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +const main = async () => { + if ("dry-run" in args) { + console.log("Dry run mode, no changes will be made."); + } + + console.log("Connecting the database"); + + try { + Database.initDataDir(args); + await Database.connect(false, false, true); + // No need to actually reset the password for testing, just make sure no connection problem. It is ok for now. + if (!process.env.TEST_BACKEND) { + const user = await R.findOne("user"); + if (! user) { + throw new Error("user not found, have you installed?"); + } + + console.log("Found user: " + user.username); + + while (true) { + let password; + let confirmPassword; + + // When called with "--new-password" argument for unattended modification (e.g. npm run reset-password -- --new_password=secret) + if ("new-password" in args) { + console.log("Using password from argument"); + console.warn("\x1b[31m%s\x1b[0m", "Warning: the password might be stored, in plain text, in your shell's history"); + password = confirmPassword = args["new-password"] + ""; + } else { + password = await question("New Password: "); + confirmPassword = await question("Confirm New Password: "); + } + + if (password === confirmPassword) { + if (!("dry-run" in args)) { + await User.resetPassword(user.id, password); + + // Reset all sessions by reset jwt secret + await initJWTSecret(); + + // Disconnect all other socket clients of the user + await disconnectAllSocketClients(user.username, password); + } + break; + } else { + console.log("Passwords do not match, please try again."); + } + } + console.log("Password reset successfully."); + + } + } catch (e) { + console.error("Error: " + e.message); + } + + await Database.close(); + rl.close(); + + console.log("Finished."); +}; + +/** + * Ask question of user + * @param {string} question Question to ask + * @returns {Promise<string>} Users response + */ +function question(question) { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer); + }); + }); +} + +/** + * Disconnect all socket clients of the user + * @param {string} username Username + * @param {string} password Password + * @returns {Promise<void>} Promise + */ +function disconnectAllSocketClients(username, password) { + return new Promise((resolve) => { + console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients"); + + // Disconnect all socket connections + const socket = io(localWebSocketURL, { + reconnection: false, + timeout: 5000, + }); + socket.on("connect", () => { + socket.emit("login", { + username, + password, + }, (res) => { + if (res.ok) { + console.log("Logged in."); + socket.emit("disconnectOtherSocketClients"); + } else { + console.warn("Login failed."); + console.warn("Please restart the server to disconnect all sessions."); + } + socket.close(); + }); + }); + + socket.on("connect_error", function () { + // The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup + // Ask the user to restart the server manually + console.warn("Failed to connect to " + localWebSocketURL); + console.warn("Please restart the server to disconnect all sessions manually."); + resolve(); + }); + socket.on("disconnect", () => { + resolve(); + }); + }); +} + +if (!process.env.TEST_BACKEND) { + main(); +} + +module.exports = { + main, +}; diff --git a/extra/simple-dns-server.js b/extra/simple-dns-server.js new file mode 100644 index 0000000..38bf74f --- /dev/null +++ b/extra/simple-dns-server.js @@ -0,0 +1,149 @@ +/* + * Simple DNS Server + * For testing DNS monitoring type, dev only + */ +const dns2 = require("dns2"); + +const { Packet } = dns2; + +const server = dns2.createServer({ + udp: true +}); + +server.on("request", (request, send, rinfo) => { + for (let question of request.questions) { + console.log(question.name, type(question.type), question.class); + + const response = Packet.createResponseFromRequest(request); + + if (question.name === "existing.com") { + + if (question.type === Packet.TYPE.A) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + address: "1.2.3.4" + }); + } else if (question.type === Packet.TYPE.AAAA) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + address: "fe80::::1234:5678:abcd:ef00", + }); + } else if (question.type === Packet.TYPE.CNAME) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + domain: "cname1.existing.com", + }); + } else if (question.type === Packet.TYPE.MX) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + exchange: "mx1.existing.com", + priority: 5 + }); + } else if (question.type === Packet.TYPE.NS) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + ns: "ns1.existing.com", + }); + } else if (question.type === Packet.TYPE.SOA) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + primary: "existing.com", + admin: "admin@existing.com", + serial: 2021082701, + refresh: 300, + retry: 3, + expiration: 10, + minimum: 10, + }); + } else if (question.type === Packet.TYPE.SRV) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + priority: 5, + weight: 5, + port: 8080, + target: "srv1.existing.com", + }); + } else if (question.type === Packet.TYPE.TXT) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + data: "#v=spf1 include:_spf.existing.com ~all", + }); + } else if (question.type === Packet.TYPE.CAA) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + flags: 0, + tag: "issue", + value: "ca.existing.com", + }); + } + + } + + if (question.name === "4.3.2.1.in-addr.arpa") { + if (question.type === Packet.TYPE.PTR) { + response.answers.push({ + name: question.name, + type: question.type, + class: question.class, + ttl: 300, + domain: "ptr1.existing.com", + }); + } + } + + send(response); + } +}); + +server.on("listening", () => { + console.log("Listening"); + console.log(server.addresses()); +}); + +server.on("close", () => { + console.log("server closed"); +}); + +server.listen({ + udp: 5300 +}); + +/** + * Get human readable request type from request code + * @param {number} code Request code to translate + * @returns {string|void} Human readable request type + */ +function type(code) { + for (let name in Packet.TYPE) { + if (Packet.TYPE[name] === code) { + return name; + } + } +} diff --git a/extra/simple-mqtt-server.js b/extra/simple-mqtt-server.js new file mode 100644 index 0000000..66ebfe4 --- /dev/null +++ b/extra/simple-mqtt-server.js @@ -0,0 +1,57 @@ +const { log } = require("../src/util"); + +const mqttUsername = "louis1"; +const mqttPassword = "!@#$LLam"; + +class SimpleMqttServer { + aedes = require("aedes")(); + server = require("net").createServer(this.aedes.handle); + + /** + * @param {number} port Port to listen on + */ + constructor(port) { + this.port = port; + } + + /** + * Start the MQTT server + * @returns {void} + */ + start() { + this.server.listen(this.port, () => { + console.log("server started and listening on port ", this.port); + }); + } +} + +let server1 = new SimpleMqttServer(10000); + +server1.aedes.authenticate = function (client, username, password, callback) { + if (username && password) { + console.log(password.toString("utf-8")); + callback(null, username === mqttUsername && password.toString("utf-8") === mqttPassword); + } else { + callback(null, false); + } +}; + +server1.aedes.on("subscribe", (subscriptions, client) => { + console.log(subscriptions); + + for (let s of subscriptions) { + if (s.topic === "test") { + server1.aedes.publish({ + topic: "test", + payload: Buffer.from("ok"), + }, (error) => { + if (error) { + log.error("mqtt_server", error); + } + }); + } + } + +}); + +server1.start(); diff --git a/extra/sort-contributors.js b/extra/sort-contributors.js new file mode 100644 index 0000000..b60d191 --- /dev/null +++ b/extra/sort-contributors.js @@ -0,0 +1,22 @@ +const fs = require("fs"); + +// Read the file from private/sort-contributors.txt +const file = fs.readFileSync("private/sort-contributors.txt", "utf8"); + +// Convert to an array of lines +let lines = file.split("\n"); + +// Remove empty lines +lines = lines.filter((line) => line !== ""); + +// Remove duplicates +lines = [ ...new Set(lines) ]; + +// Remove @weblate and @UptimeKumaBot +lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam"); + +// Sort the lines +lines = lines.sort(); + +// Output the lines, concat with " " +console.log(lines.join(" ")); diff --git a/extra/update-language-files/.gitignore b/extra/update-language-files/.gitignore new file mode 100644 index 0000000..410c913 --- /dev/null +++ b/extra/update-language-files/.gitignore @@ -0,0 +1,3 @@ +package-lock.json +test.js +languages/ diff --git a/extra/update-language-files/index.js b/extra/update-language-files/index.js new file mode 100644 index 0000000..acb6bd4 --- /dev/null +++ b/extra/update-language-files/index.js @@ -0,0 +1,103 @@ +// Need to use ES6 to read language files + +import fs from "fs"; +import util from "util"; + +/** + * Copy across the required language files + * Creates a local directory (./languages) and copies the required files + * into it. + * @param {string} langCode Code of language to update. A file will be + * created with this code if one does not already exist + * @param {string} baseLang The second base language file to copy. This + * will be ignored if set to "en" as en.js is copied by default + * @returns {void} + */ +function copyFiles(langCode, baseLang) { + if (fs.existsSync("./languages")) { + fs.rmSync("./languages", { + recursive: true, + force: true, + }); + } + fs.mkdirSync("./languages"); + + if (!fs.existsSync(`../../src/languages/${langCode}.js`)) { + fs.closeSync(fs.openSync(`./languages/${langCode}.js`, "a")); + } else { + fs.copyFileSync(`../../src/languages/${langCode}.js`, `./languages/${langCode}.js`); + } + fs.copyFileSync("../../src/languages/en.js", "./languages/en.js"); + if (baseLang !== "en") { + fs.copyFileSync(`../../src/languages/${baseLang}.js`, `./languages/${baseLang}.js`); + } +} + +/** + * Update the specified language file + * @param {string} langCode Language code to update + * @param {string} baseLangCode Second language to copy keys from + * @returns {void} + */ +async function updateLanguage(langCode, baseLangCode) { + const en = (await import("./languages/en.js")).default; + const baseLang = (await import(`./languages/${baseLangCode}.js`)).default; + + let file = langCode + ".js"; + console.log("Processing " + file); + const lang = await import("./languages/" + file); + + let obj; + + if (lang.default) { + obj = lang.default; + } else { + console.log("Empty file"); + obj = { + languageName: "<Your Language name in your language (not in English)>" + }; + } + + // En first + for (const key in en) { + if (! obj[key]) { + obj[key] = en[key]; + } + } + + if (baseLang !== en) { + // Base second + for (const key in baseLang) { + if (! obj[key]) { + obj[key] = key; + } + } + } + + const code = "export default " + util.inspect(obj, { + depth: null, + }); + + fs.writeFileSync(`../../src/languages/${file}`, code); +} + +// Get command line arguments +const baseLangCode = process.env.npm_config_baselang || "en"; +const langCode = process.env.npm_config_language; + +// We need the file to edit +if (langCode == null) { + throw new Error("Argument --language=<code> must be provided"); +} + +console.log("Base Lang: " + baseLangCode); +console.log("Updating: " + langCode); + +copyFiles(langCode, baseLangCode); +await updateLanguage(langCode, baseLangCode); +fs.rmSync("./languages", { + recursive: true, + force: true, +}); + +console.log("Done. Fixing formatting by ESLint..."); diff --git a/extra/update-language-files/package.json b/extra/update-language-files/package.json new file mode 100644 index 0000000..c729517 --- /dev/null +++ b/extra/update-language-files/package.json @@ -0,0 +1,12 @@ +{ + "name": "update-language-files", + "type": "module", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/extra/update-version.js b/extra/update-version.js new file mode 100644 index 0000000..f9aead0 --- /dev/null +++ b/extra/update-version.js @@ -0,0 +1,81 @@ +const pkg = require("../package.json"); +const fs = require("fs"); +const childProcess = require("child_process"); +const util = require("../src/util"); + +util.polyfill(); + +const newVersion = process.env.RELEASE_VERSION; + +console.log("New Version: " + newVersion); + +if (! newVersion) { + console.error("invalid version"); + process.exit(1); +} + +const exists = tagExists(newVersion); + +if (! exists) { + + // Process package.json + pkg.version = newVersion; + + // Replace the version: https://regex101.com/r/hmj2Bc/1 + pkg.scripts.setup = pkg.scripts.setup.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`); + fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); + + // Also update package-lock.json + const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; + childProcess.spawnSync(npm, [ "install" ]); + + commit(newVersion); + tag(newVersion); + +} else { + console.log("version exists"); +} + +/** + * Commit updated files + * @param {string} version Version to update to + * @returns {void} + * @throws Error when committing files + */ +function commit(version) { + let msg = "Update to " + version; + + let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]); + let stdout = res.stdout.toString().trim(); + console.log(stdout); + + if (stdout.includes("no changes added to commit")) { + throw new Error("commit error"); + } +} + +/** + * Create a tag with the specified version + * @param {string} version Tag to create + * @returns {void} + */ +function tag(version) { + let res = childProcess.spawnSync("git", [ "tag", version ]); + console.log(res.stdout.toString().trim()); +} + +/** + * Check if a tag exists for the specified version + * @param {string} version Version to check + * @returns {boolean} Does the tag already exist + * @throws Version is not valid + */ +function tagExists(version) { + if (! version) { + throw new Error("invalid version"); + } + + let res = childProcess.spawnSync("git", [ "tag", "-l", version ]); + + return res.stdout.toString().trim() === version; +} diff --git a/extra/update-wiki-version.js b/extra/update-wiki-version.js new file mode 100644 index 0000000..f6c9612 --- /dev/null +++ b/extra/update-wiki-version.js @@ -0,0 +1,58 @@ +const childProcess = require("child_process"); +const fs = require("fs"); + +const newVersion = process.env.RELEASE_VERSION; + +if (!newVersion) { + console.log("Missing version"); + process.exit(1); +} + +updateWiki(newVersion); + +/** + * Update the wiki with new version number + * @param {string} newVersion Version to update to + * @returns {void} + */ +function updateWiki(newVersion) { + const wikiDir = "./tmp/wiki"; + const howToUpdateFilename = "./tmp/wiki/🆙-How-to-Update.md"; + + safeDelete(wikiDir); + + childProcess.spawnSync("git", [ "clone", "https://github.com/louislam/uptime-kuma.wiki.git", wikiDir ]); + let content = fs.readFileSync(howToUpdateFilename).toString(); + + // Replace the version: https://regex101.com/r/hmj2Bc/1 + content = content.replace(/(git checkout )([^\s]+)/, `$1${newVersion}`); + fs.writeFileSync(howToUpdateFilename, content); + + childProcess.spawnSync("git", [ "add", "-A" ], { + cwd: wikiDir, + }); + + childProcess.spawnSync("git", [ "commit", "-m", `Update to ${newVersion}` ], { + cwd: wikiDir, + }); + + console.log("Pushing to Github"); + childProcess.spawnSync("git", [ "push" ], { + cwd: wikiDir, + }); + + safeDelete(wikiDir); +} + +/** + * Check if a directory exists and then delete it + * @param {string} dir Directory to delete + * @returns {void} + */ +function safeDelete(dir) { + if (fs.existsSync(dir)) { + fs.rm(dir, { + recursive: true, + }); + } +} diff --git a/extra/upload-github-release-asset.sh b/extra/upload-github-release-asset.sh new file mode 100644 index 0000000..206e3cd --- /dev/null +++ b/extra/upload-github-release-asset.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Author: Stefan Buck +# License: MIT +# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447 +# +# +# This script accepts the following parameters: +# +# * owner +# * repo +# * tag +# * filename +# * github_api_token +# +# Script to upload a release asset using the GitHub API v3. +# +# Example: +# +# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip +# + +# Check dependencies. +set -e +xargs=$(which gxargs || which xargs) + +# Validate settings. +[ "$TRACE" ] && set -x + +CONFIG=$@ + +for line in $CONFIG; do + eval "$line" +done + +# Define variables. +GH_API="https://api.github.com" +GH_REPO="$GH_API/repos/$owner/$repo" +GH_TAGS="$GH_REPO/releases/tags/$tag" +AUTH="Authorization: token $github_api_token" +WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie" +CURL_ARGS="-LJO#" + +if [[ "$tag" == 'LATEST' ]]; then + GH_TAGS="$GH_REPO/releases/latest" +fi + +# Validate token. +curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; } + +# Read asset tags. +response=$(curl -sH "$AUTH" $GH_TAGS) + +# Get ID of the asset based on given filename. +eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=') +[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; } + +# Upload asset +echo "Uploading asset... " + +# Construct url +GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)" + +curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET diff --git a/extra/uptime-kuma-push/.gitignore b/extra/uptime-kuma-push/.gitignore new file mode 100644 index 0000000..a007fea --- /dev/null +++ b/extra/uptime-kuma-push/.gitignore @@ -0,0 +1 @@ +build/* diff --git a/extra/uptime-kuma-push/Dockerfile b/extra/uptime-kuma-push/Dockerfile new file mode 100644 index 0000000..9d619d6 --- /dev/null +++ b/extra/uptime-kuma-push/Dockerfile @@ -0,0 +1,18 @@ +FROM node AS build +RUN useradd --create-home kuma +USER kuma +WORKDIR /home/kuma +ARG TARGETPLATFORM +COPY --chown=kuma:kuma ./build/ ./build/ +COPY --chown=kuma:kuma build.js build.js +RUN node build.js $TARGETPLATFORM + +FROM debian:bookworm-slim AS release +RUN useradd --create-home kuma +USER kuma +WORKDIR /home/kuma +COPY --from=build /home/kuma/uptime-kuma-push ./uptime-kuma-push + +ENTRYPOINT ["/home/kuma/uptime-kuma-push"] + + diff --git a/extra/uptime-kuma-push/build.js b/extra/uptime-kuma-push/build.js new file mode 100644 index 0000000..3dc8bf4 --- /dev/null +++ b/extra/uptime-kuma-push/build.js @@ -0,0 +1,48 @@ +const fs = require("fs"); +const platform = process.argv[2]; + +if (!platform) { + console.error("No platform??"); + process.exit(1); +} + +const supportedPlatforms = [ + { + name: "linux/amd64", + bin: "./build/uptime-kuma-push-amd64" + }, + { + name: "linux/arm64", + bin: "./build/uptime-kuma-push-arm64" + }, + { + name: "linux/arm/v7", + bin: "./build/uptime-kuma-push-armv7" + } +]; + +let platformObj = null; + +// Check if the platform is supported +for (let i = 0; i < supportedPlatforms.length; i++) { + if (supportedPlatforms[i].name === platform) { + platformObj = supportedPlatforms[i]; + break; + } +} + +if (platformObj) { + let filename = platformObj.bin; + + if (!fs.existsSync(filename)) { + console.error(`prebuilt: ${filename} is not found, please build it first`); + process.exit(1); + } + + fs.renameSync(filename, "./uptime-kuma-push"); + process.exit(0); +} else { + console.error("Unsupported platform: " + platform); + process.exit(1); +} + diff --git a/extra/uptime-kuma-push/package.json b/extra/uptime-kuma-push/package.json new file mode 100644 index 0000000..f215436 --- /dev/null +++ b/extra/uptime-kuma-push/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "build-docker": "npm run build-all && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:push . --push --target release", + "build-all": "npm run build-win && npm run build-linux-amd64 && npm run build-linux-arm64 && npm run build-linux-armv7 && npm run build-linux-armv6 && npm run build-linux-armv5 && npm run build-linux-riscv64", + "build-win": "cross-env GOOS=windows GOARCH=amd64 go build -x -o ./build/uptime-kuma-push.exe uptime-kuma-push.go", + "build-linux-amd64": "cross-env GOOS=linux GOARCH=amd64 go build -x -o ./build/uptime-kuma-push-amd64 uptime-kuma-push.go", + "build-linux-arm64": "cross-env GOOS=linux GOARCH=arm64 go build -x -o ./build/uptime-kuma-push-arm64 uptime-kuma-push.go", + "build-linux-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./build/uptime-kuma-push-armv7 uptime-kuma-push.go", + "build-linux-armv6": "cross-env GOOS=linux GOARCH=arm GOARM=6 go build -x -o ./build/uptime-kuma-push-armv6 uptime-kuma-push.go", + "build-linux-armv5": "cross-env GOOS=linux GOARCH=arm GOARM=5 go build -x -o ./build/uptime-kuma-push-armv5 uptime-kuma-push.go", + "build-linux-riscv64": "cross-env GOOS=linux GOARCH=riscv64 go build -x -o ./build/uptime-kuma-push-riscv64 uptime-kuma-push.go" + } +} diff --git a/extra/uptime-kuma-push/uptime-kuma-push.go b/extra/uptime-kuma-push/uptime-kuma-push.go new file mode 100644 index 0000000..69cd1f8 --- /dev/null +++ b/extra/uptime-kuma-push/uptime-kuma-push.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "net/http" + os "os" + "time" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "Usage: uptime-kuma-push <url> [<interval>]") + os.Exit(1) + } + + pushURL := os.Args[1] + + var interval time.Duration + + if len(os.Args) >= 3 { + intervalString, err := time.ParseDuration(os.Args[2] + "s") + interval = intervalString + + if err != nil { + fmt.Fprintln(os.Stderr, "Error: Invalid interval", err) + os.Exit(1) + } + + } else { + interval = 60 * time.Second + } + + for { + _, err := http.Get(pushURL) + if err == nil { + fmt.Print("Pushed!") + } else { + fmt.Print("Error: ", err) + } + + fmt.Println(" Sleeping for", interval) + time.Sleep(interval) + } +} |