diff --git a/.dockerignore b/.dockerignore index 47e82a105..00ee58159 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,7 +33,7 @@ tsconfig.json /ecosystem.config.js /extra/healthcheck.exe /extra/healthcheck - +extra/exe-builder ### .gitignore content (commented rules are duplicated) @@ -48,6 +48,4 @@ dist-ssr #!/data/.gitkeep #.vscode - - ### End of .gitignore content diff --git a/.gitignore b/.gitignore index 06dca04b4..9ed1e282b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ cypress/screenshots /extra/healthcheck.exe /extra/healthcheck /extra/healthcheck-armv7 + +extra/exe-builder/bin +extra/exe-builder/obj diff --git a/.stylelintrc b/.stylelintrc index e590c98ca..00ddcaaef 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -10,5 +10,6 @@ "color-function-notation": "legacy", "shorthand-property-no-redundant-values": null, "color-hex-length": null, + "declaration-block-no-redundant-longhand-properties": null } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09c94e713..4c6a5587b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -235,6 +235,7 @@ https://github.com/louislam/uptime-kuma/issues?q=sort%3Aupdated-desc 1. Draft a release note 2. Make sure the repo is cleared +3. If the healthcheck is updated, remember to re-compile it: `npm run build-docker-builder-go` 3. `npm run release-final with env vars: `VERSION` and `GITHUB_TOKEN` 4. Wait until the `Press any key to continue` 5. `git push` diff --git a/db/patch-add-description-monitor.sql b/db/patch-add-description-monitor.sql new file mode 100644 index 000000000..da1aa55bc --- /dev/null +++ b/db/patch-add-description-monitor.sql @@ -0,0 +1,7 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD description TEXT default null; + +COMMIT; diff --git a/db/patch-api-key-table.sql b/db/patch-api-key-table.sql new file mode 100644 index 000000000..fc3a405bf --- /dev/null +++ b/db/patch-api-key-table.sql @@ -0,0 +1,13 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; +CREATE TABLE [api_key] ( + [id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + [key] VARCHAR(255) NOT NULL, + [name] VARCHAR(255) NOT NULL, + [user_id] INTEGER NOT NULL, + [created_date] DATETIME DEFAULT (DATETIME('now')) NOT NULL, + [active] BOOLEAN DEFAULT 1 NOT NULL, + [expires] DATETIME DEFAULT NULL, + CONSTRAINT FK_user FOREIGN KEY ([user_id]) REFERENCES [user]([id]) ON DELETE CASCADE ON UPDATE CASCADE +); +COMMIT; diff --git a/db/patch-http-body-encoding.sql b/db/patch-http-body-encoding.sql new file mode 100644 index 000000000..322c8b893 --- /dev/null +++ b/db/patch-http-body-encoding.sql @@ -0,0 +1,12 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE monitor ADD http_body_encoding VARCHAR(25); + +COMMIT; + +BEGIN TRANSACTION; + +UPDATE monitor SET http_body_encoding = 'json' WHERE (type = 'http' or type = 'keyword') AND http_body_encoding IS NULL; + +COMMIT; diff --git a/docker/alpine-base.dockerfile b/docker/alpine-base.dockerfile index 276d6e450..7aa0e8dcc 100644 --- a/docker/alpine-base.dockerfile +++ b/docker/alpine-base.dockerfile @@ -4,5 +4,5 @@ WORKDIR /app # Install apprise, iputils for non-root ping, setpriv RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \ - pip3 --no-cache-dir install apprise==1.2.1 && \ + pip3 --no-cache-dir install apprise==1.3.0 && \ rm -rf /root/.cache diff --git a/docker/debian-base.dockerfile b/docker/debian-base.dockerfile index 026189c47..e7c51ded4 100644 --- a/docker/debian-base.dockerfile +++ b/docker/debian-base.dockerfile @@ -11,7 +11,7 @@ WORKDIR /app RUN apt update && \ apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ sqlite3 iputils-ping util-linux dumb-init git && \ - pip3 --no-cache-dir install apprise==1.2.1 && \ + pip3 --no-cache-dir install apprise==1.3.0 && \ rm -rf /var/lib/apt/lists/* && \ apt --yes autoremove diff --git a/extra/beta/update-version.js b/extra/beta/update-version.js index 7abac5efe..3dafbe8d4 100644 --- a/extra/beta/update-version.js +++ b/extra/beta/update-version.js @@ -22,7 +22,8 @@ if (! exists) { fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); // Also update package-lock.json - childProcess.spawnSync("npm", [ "install" ]); + const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; + childProcess.spawnSync(npm, [ "install" ]); commit(version); tag(version); diff --git a/extra/download-dist.js b/extra/download-dist.js index b04beec7a..a854ca8b2 100644 --- a/extra/download-dist.js +++ b/extra/download-dist.js @@ -47,6 +47,7 @@ function download(url) { }); } console.log("Done"); + process.exit(0); }); tarStream.on("error", () => { diff --git a/extra/exe-builder/.gitignore b/extra/exe-builder/.gitignore new file mode 100644 index 000000000..d52874b6c --- /dev/null +++ b/extra/exe-builder/.gitignore @@ -0,0 +1 @@ +packages/ diff --git a/extra/exe-builder/App.config b/extra/exe-builder/App.config new file mode 100644 index 000000000..97eb34aff --- /dev/null +++ b/extra/exe-builder/App.config @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extra/exe-builder/DownloadForm.Designer.cs b/extra/exe-builder/DownloadForm.Designer.cs new file mode 100644 index 000000000..26a474e9c --- /dev/null +++ b/extra/exe-builder/DownloadForm.Designer.cs @@ -0,0 +1,84 @@ +using System.ComponentModel; + +namespace UptimeKuma { + partial class DownloadForm { + /// + /// Required designer variable. + /// + private IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) { + if (disposing && (components != null)) { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadForm)); + this.progressBar = new System.Windows.Forms.ProgressBar(); + this.label = new System.Windows.Forms.Label(); + this.labelData = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // progressBar + // + this.progressBar.Location = new System.Drawing.Point(12, 12); + this.progressBar.Name = "progressBar"; + this.progressBar.Size = new System.Drawing.Size(472, 41); + this.progressBar.TabIndex = 0; + // + // label + // + this.label.Location = new System.Drawing.Point(12, 59); + this.label.Name = "label"; + this.label.Size = new System.Drawing.Size(472, 23); + this.label.TabIndex = 1; + this.label.Text = "Preparing..."; + // + // labelData + // + this.labelData.Location = new System.Drawing.Point(12, 82); + this.labelData.Name = "labelData"; + this.labelData.Size = new System.Drawing.Size(472, 23); + this.labelData.TabIndex = 2; + // + // DownloadForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(496, 117); + this.Controls.Add(this.labelData); + this.Controls.Add(this.label); + this.Controls.Add(this.progressBar); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MaximizeBox = false; + this.Name = "DownloadForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Uptime Kuma"; + this.Load += new System.EventHandler(this.DownloadForm_Load); + this.ResumeLayout(false); + } + + private System.Windows.Forms.Label labelData; + + private System.Windows.Forms.Label label; + + private System.Windows.Forms.ProgressBar progressBar; + + #endregion + } +} + diff --git a/extra/exe-builder/DownloadForm.cs b/extra/exe-builder/DownloadForm.cs new file mode 100644 index 000000000..28a57c527 --- /dev/null +++ b/extra/exe-builder/DownloadForm.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Newtonsoft.Json; + +namespace UptimeKuma { + public partial class DownloadForm : Form { + private readonly Queue downloadQueue = new(); + private readonly WebClient webClient = new(); + private DownloadItem currentDownloadItem; + + public DownloadForm() { + InitializeComponent(); + } + + private void DownloadForm_Load(object sender, EventArgs e) { + webClient.DownloadProgressChanged += DownloadProgressChanged; + webClient.DownloadFileCompleted += DownloadFileCompleted; + + label.Text = "Reading latest version..."; + + // Read json from https://uptime.kuma.pet/version + var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version"); + var versionObj = JsonConvert.DeserializeObject(versionJson); + + var nodeVersion = versionObj.nodejs; + var uptimeKumaVersion = versionObj.latest; + var hasUpdateFile = File.Exists("update"); + + if (!Directory.Exists("node")) { + downloadQueue.Enqueue(new DownloadItem { + URL = $"https://nodejs.org/dist/v{nodeVersion}/node-v{nodeVersion}-win-x64.zip", + Filename = "node.zip", + TargetFolder = "node" + }); + } + + if (!Directory.Exists("core") || hasUpdateFile) { + + // It is update, rename the core folder to core.old + if (Directory.Exists("core")) { + // Remove the old core.old folder + if (Directory.Exists("core.old")) { + Directory.Delete("core.old", true); + } + + Directory.Move("core", "core.old"); + } + + downloadQueue.Enqueue(new DownloadItem { + URL = $"https://github.com/louislam/uptime-kuma/archive/refs/tags/{uptimeKumaVersion}.zip", + Filename = "core.zip", + TargetFolder = "core" + }); + + File.WriteAllText("version.json", versionJson); + + // Delete the update file + if (hasUpdateFile) { + File.Delete("update"); + } + } + + DownloadNextFile(); + } + + void DownloadNextFile() { + if (downloadQueue.Count > 0) { + var item = downloadQueue.Dequeue(); + + currentDownloadItem = item; + + // Download if the zip file is not existing + if (!File.Exists(item.Filename)) { + label.Text = item.URL; + webClient.DownloadFileAsync(new Uri(item.URL), item.Filename); + } else { + progressBar.Value = 100; + label.Text = "Use local " + item.Filename; + DownloadFileCompleted(null, null); + } + } else { + npmSetup(); + } + } + + void npmSetup() { + labelData.Text = ""; + + var npm = "..\\node\\npm.cmd"; + var cmd = $"{npm} ci --production & {npm} run download-dist & exit"; + + var startInfo = new ProcessStartInfo { + FileName = "cmd.exe", + Arguments = $"/k \"{cmd}\"", + RedirectStandardOutput = false, + RedirectStandardError = false, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = false, + WorkingDirectory = "core" + }; + + var process = new Process(); + process.StartInfo = startInfo; + process.EnableRaisingEvents = true; + process.Exited += (_, e) => { + progressBar.Value = 100; + + if (process.ExitCode == 0) { + Task.Delay(2000).ContinueWith(_ => { + Application.Restart(); + }); + label.Text = "Done"; + } else { + label.Text = "Failed, exit code: " + process.ExitCode; + } + + }; + process.Start(); + label.Text = "Installing dependencies and download dist files"; + progressBar.Value = 50; + process.WaitForExit(); + } + + void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) { + progressBar.Value = e.ProgressPercentage; + var total = e.TotalBytesToReceive / 1024; + var current = e.BytesReceived / 1024; + + if (total > 0) { + labelData.Text = $"{current}KB/{total}KB"; + } + } + + void DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) { + Extract(currentDownloadItem); + DownloadNextFile(); + } + + void Extract(DownloadItem item) { + if (Directory.Exists(item.TargetFolder)) { + var dir = new DirectoryInfo(item.TargetFolder); + dir.Delete(true); + } + + if (Directory.Exists("temp")) { + var dir = new DirectoryInfo("temp"); + dir.Delete(true); + } + + labelData.Text = $"Extracting {item.Filename}..."; + + ZipFile.ExtractToDirectory(item.Filename, "temp"); + + string[] dirList; + + // Move to the correct level + dirList = Directory.GetDirectories("temp"); + + + + if (dirList.Length > 0) { + var dir = dirList[0]; + + // As sometime ExtractToDirectory is still locking the directory, loop until ok + while (true) { + try { + Directory.Move(dir, item.TargetFolder); + break; + } catch (Exception exception) { + Thread.Sleep(1000); + } + } + + } else { + MessageBox.Show("Unexcepted Error: Cannot move extracted files, folder not found."); + } + + labelData.Text = $"Extracted"; + + if (Directory.Exists("temp")) { + var dir = new DirectoryInfo("temp"); + dir.Delete(true); + } + + File.Delete(item.Filename); + } + } + + public class DownloadItem { + public string URL { get; set; } + public string Filename { get; set; } + public string TargetFolder { get; set; } + } +} + diff --git a/extra/exe-builder/DownloadForm.resx b/extra/exe-builder/DownloadForm.resx new file mode 100644 index 000000000..e87e0c0d4 --- /dev/null +++ b/extra/exe-builder/DownloadForm.resx @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA + AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA////BPT09Bfu7u4e8fHxJPPz8yv19fUy9fX1M/Pz8yvx8fEk9vb2HPPz8xXMzMwFAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP// + /wHv7+8f7u7uPPPz81Tx8fFs8fHxgPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGB8fHxcfHx8V3x8fFI9PT0MOvr6w0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADy8vIU8fHxS/Dw8Hbx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fFr9PT0R/Dw8CIAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA8vLyFPHx8Vnx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fFs9fX1Mb+/vwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAICAgALy8vI88fHxfvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy8nby8vI8gICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAzMzMBfHx8Vrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyYf///wwAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8vLyYPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8W/z8/MWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+9R8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLw8PB26urqDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLy8ijx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu7w7Ifj79ud2u7PtNLrw83P677dzeu85c3r + u+rM67rwzOu68c7rverQ68Dj0uvD3NbuyM3b7c+64u7apujv5ZPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxXgAAAAEAAAAAAAAAAAAAAAAAAAAA4+PjCfDw + 8Hfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLd7tSmzeu92MbqsvvG6bH/xumy/8fq + s//H6rP/yOq0/8jqtf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//Q7MDx1u7Kz9/t + 163s8OuJ8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgu/v7y8AAAAAAAAAAAAA + AAAAAAAA7u7uPfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC5PDdl8jqtuTE6a7/xOmv/8Xp + sP/G6bH/xumx/8bpsv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zr + u//N67v/zey8/87svf/P67742e3Mx+jv5ZLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw + 8HWAgIACAAAAAAAAAACqqqoD8vLyc/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLf7degxOiu+cPo + rf/D6a7/xOmu/8Xpr//F6bD/xumx/8bpsf/G6bL/x+qz/8fqs//I6rT/yOq1/8nqtv/J6rb/yuu3/8rr + uP/L67j/y+u5/8zruv/M67v/zeu7/83svP/O7L3/zuy9/87svfzc7tK28fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fEkAAAAAAAAAADz8/Mq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgunv + 5o3D6a/0wuis/8Lorf/D6K3/xOmu/8Tprv/F6a//xemw/8bpsf/G6bH/xumy/8fqs//H6rP/yOq0/8jq + tf/J6rb/yeq2/8rrt//K67j/y+u4/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/87svf/O7L3/3e/TtPHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAADy8vJM8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgszqutDB6Kv/weir/8LorP/D6K3/w+it/8Tprv/E6a7/xemv/8XpsP/G6bH/xumx/8bp + sv/H6rP/x+qz/8jqtP/I6rX/yeq2/8nqtv/K67f/yuu4/8vruP/L67n/zOu6/8zru//N67v/zey8/87s + vf/O7L3/zuy++u3w6Yzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJ1AAAAAAAAAADx8fFr8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC6O/kjsDoqvzA6Kr/weir/8Loq//C6Kz/w+it/8Porf/E6a7/xOmu/8Xp + r//F6bD/xumx/8bpsf/G6bL/x+qz/8fqtP/I6rT/yOq1/8nqtv/J6rb/yuu3/8rruP/L67n/y+u5/8zr + uv/M67v/zeu7/83svP/O7L3/zuy9/93u07Xx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC////Bv// + /wfx8fGB8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1ezJsr/nqf/A56n/weiq/8Hoq//C6Kv/wuis/8Po + rf/D6K3/xOmu/8Pprv+856T/uOed/7bmmv+05Zf/teWZ/7jnnf+86KP/wOio/8fqs//J6rb/yeq2/8rr + t//K67j/y+u5/8vruf/M67r/zOu7/83ru//N7Lz/zuy9/9buyNLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8vLyE/Ly8hPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCy+q6zr/nqP/A56n/wOep/8Ho + qv/B6Kv/wuir/8LorP+u5Y//neF2/5bgav+V4Gr/luBr/5fhbP+Y4W7/meFv/5rhcf+b4nL/nOJ0/53i + dv+j5H//reaM/7nnnf/E6q//y+y4/8vruf/L67n/zOu6/8zru//N67v/zey8/9Lsxd/x8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC7+/vIPb29hzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/n + qP+/56j/wOep/8Dnqf/B6Kr/weir/7nmn/+R32T/kt9l/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nh + b/+a4XH/m+Jy/5zidP+d4nX/nuN3/5/jeP+f4nn/weqq/8rruP/L67n/y+u5/8zruv/M67v/zeu7/9Ls + w+Lx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwI/Hx8SXx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGCxeix5L/nqP+/56j/v+eo/8Dnqf/A56n/weiq/7Pllv+Q3mP/kd9k/5LfZf+T32f/lOBo/5Xg + av+W4Gv/l+Ft/5jhbv+Z4W//muFx/5vicv+c4nT/neJ1/57jd/+f43j/xOmu/8rrt//K67j/y+u5/8vr + uf/M67r/zOu7/9Tsxtfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC9PT0GO/v7yDx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGCx+m037/nqP+/56j/v+eo/7/nqP/A56n/wOip/7TmmP+P3mH/kN5j/5Hf + ZP+S32b/k99n/5TgaP+V4Gr/luBr/5fhbf+Y4W7/meFw/5rhcf+b4nL/nOJ0/53idf+h5Hz/yuu2/8nq + t//K67f/yuu4/8vruf/L67n/zOu6/9ftysrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7e3tDvT0 + 9Bfx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCyOq117/nqP+/56j/v+eo/7/nqP+/56j/wOep/7vn + of+O3mD/j95h/5DeY/+R32T/kt9m/5PfZ/+U4Gj/leBq/5bga/+X4W3/mOFu/5nhcP+a4nH/m+Jy/5zi + dP+r5Yr/yOq1/8nqtv/J6rf/yuu3/8rruP/L67n/y+u5/9zu1LHx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLz8/OA////A+7u7g/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCz+q+xb/nqP+/56j/v+eo/7/n + qP+/56j/v+eo/8Dnqf+S4Gb/jt5g/4/eYf+Q3mP/kd9k/5LfZv+T32f/lOBo/5Xgav+W4Gv/l+Ft/5jh + bv+Z4XD/muJx/5vic/+4553/yOq0/8jqtf/J6rb/yeq3/8rrt//K67j/y+u5/+bw4Zfx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fFrAAAAAP///wHz8/N88fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC1+zMrr/n + qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+f4Xn/jd5f/47eYP+P3mH/kN5j/5HfZP+S32b/k99n/5Tg + af+V4Gr/luBr/5fhbf+Y4W7/meFw/5vic//F6rD/x+q0/8jqtP/I6rX/yeq2/8nqt//K67f/zOu88u/x + 74Px8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLv7+9QAAAAAAAAAADw8PBm8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC5e7gk7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//jN1d/43eX/+O3mD/j95h/5De + Y/+R32T/kt9m/5PfZ/+U4Gn/leBq/5bga/+X4W3/mOFu/6rliP/G6rL/x+qz/8fqtP/I6rT/yOq1/8nq + tv/J6rf/1OzGy/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YL19fUzAAAAAAAAAADy8vJO8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgsPoru2/56j/v+eo/7/nqP+/56j/v+eo/7/nqP++6Kf/j95i/4zd + Xf+N3l//jt5g/4/eYv+Q3mP/kd9k/5LfZv+T32f/lOBp/5Xgav+W4Gz/l+Ft/7voov/G6bL/xuqy/8fq + s//H6rT/yOq1/8jqtf/J6rb/4e/Zo/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PARAAAAAAAA + AADu7u4u8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgszpvMm/56j/v+eo/7/nqP+/56j/v+eo/7/n + qP+/56j/q+SL/4vdXP+M3V3/jd5f/47eYP+P3mL/kN9j/5HfZP+S32b/k99n/5Tgaf+V4Gr/qOOH/8Xp + sP/G6bH/xumy/8bqsv/H6rP/x+q0/8jqtf/K67jy8PHwhPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8WoAAAAAAAAAAAAAAADo6OgL8fHxgfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguDv2J2/56j/v+eo/7/n + qP+/56j/v+eo/7/nqP+/56j/v+eo/6Xjgv+L3Vz/jN1d/43eX/+O3mD/j95i/5DfY/+R32T/kt9m/5Pf + Z/+k44D/xOmu/8XpsP/F6bD/xumx/8bpsv/G6rL/x+qz/8fqtP/W7cnB8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvPz80AAAAAAAAAAAAAAAAAAAAAA8PDwZ/Hx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLD6K/rv+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+u5I//kt5n/4zdXf+N3l//jt5g/4/e + Yv+Q32P/luFs/67kj//D6K3/xOmu/8Tpr//F6bD/xemw/8bpsf/G6bL/xuqy/8fqtP7o7+WR8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xYAAAAAAAAAAAAAAAAAAAAA8vLyPPHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLV7ci0v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOio/7Xl + mv+u5I7/rOSM/67kj/+35pz/wumr/8Lorf/D6K3/w+it/8Tprv/E6a//xemw/8XpsP/G6bH/xumy/9Ds + wNPx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyZQAAAAAAAAAAAAAAAAAAAAAAAAAA////DPHx + 8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGCx+m03L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n + qP+/56j/v+eo/7/nqP+/56j/wOep/8Doqv/B6Kr/weir/8LorP/C6K3/w+it/8Porv/E6a7/xOmv/8Xp + sP/F6bD/yOq18uvw6Yvx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC7+/vMQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAPHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6O/ij8LorPG/56j/v+eo/7/n + qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weiq/8Hoq//C6Kz/wuit/8Po + rf/D6K7/xOmu/8Tpr//F6bH74u/anvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLw8PB6////BQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPz8yrx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxguHu + 2pnB56v2v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/wOiq/8Ho + q//B6Kv/wuis/8Lorf/D6K3/w+mu/8Tprv3b7dKq8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fFJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHy8vJf8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLi7tyXwumt8L/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/n + qP+/56j/wOep/8Doqv/B6Kv/weir/8LorP/C6K3/xOiv+d7u1aTx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvLy8nb///8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADv7+8Q8/Pze/Hx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC6/Dpiszqu82/56j/v+eo/7/nqP+/56j/v+eo/7/n + qP+/56j/v+eo/7/nqP+/56j/v+eo/8Dnqf/A6Kr/weir/8Hoq//H6bTj5e7elfHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8yoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA9fX1MvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLe7tShx+mz3r/n + qP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/v+eo/7/nqP/A56n/xumy5drtz6rv8e+D8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8vLyTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAPHx8Unx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgubv45DU68e2y+q6z8XoseTD6a7uweir9MPpru7F6bHly+q50tLsxLrl796U8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLy8vJh////AwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHx8fFZ8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8Wzf398IAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8D8/PzVfHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8PDwZujo + 6AsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA////AfHx8Ujx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fFa////BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/Mp8vLydvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8/PzfPHx8TcAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CvLy8lDz8/N/8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvPz84Hx8fFa8PDwEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADw8PAR8vLyTvHx8X3x8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fF/8/PzVvT09BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wXz8/Mq8/PzU/Hx8XDx8fGB8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLy8vJz8fHxWO/v7y////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8G7e3tHfLy + 8ifu7u4u8PDwNPT09C/y8vIo7+/vH+Pj4wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP//gAf//wAA//gAAD//AAD/wAAAB/8AAP+A + AAAB/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA8AAAAAAPAADwAAAAAAcAAOAAAAAABwAA4AAAAAAD + AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAACAAAAAAAEAAIAA + AAAAAQAAgAAAAAABAACAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAABwAAwAAAAAAH + AADgAAAAAAcAAOAAAAAADwAA4AAAAAAPAADwAAAAAB8AAPAAAAAAHwAA+AAAAAA/AAD8AAAAAD8AAPwA + AAAAfwAA/gAAAAD/AAD/AAAAAf8AAP+AAAAD/wAA/8AAAAf/AAD/8AAAH/8AAP/8AAA//wAA//8AAf// + AAD//+AP//8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAgICAAu/v7xD09PQX7u7uHvDw8CP29vYb8vLyFOrq6gwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICA + gALy8vIm7+/vT/Pz82fz8/N98fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvDw8Hrw8PBm7+/vUPT0 + 9C3o6OgLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOPj + 4wnz8/NC8vLydPHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YHy8vJj8/PzKoCAgAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADx8fEl8vLydfHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxcfHx8SUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA9PT0LfHx8YDx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8/PzgPLy8j0AAAABAAAAAAAA + AAAAAAAAAAAAAO3t7Rzx8fGA8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLr8OmM5O7emeTv + 3Z7h79mj5fDem+nv45Tu8u6H8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvLy + 8joAAAAAAAAAAAAAAAD///8E8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC7vDshtns0K7N67zayeq288fq + s//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/P7L7w0+zF29vv0Lrn8OKX8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8/PzfvPz8xUAAAAAAAAAAPX19TLx8fGC8fHxgvHx8YLx8fGC8fHxgt3u1KXF6rHzxOmv/8Xp + sP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruf/M67v/zey8/87svf/S7MPj4u7Zp/Hx + 8YLx8fGC8fHxgvHx8YLx8fGC8/PzVQAAAAAAAAAA8fHxavHx8YLx8fGC8fHxgvHx8YLf7defwuis/cPo + rf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vruv/M67v/zey8/87s + vf/N67z/3e7SufHx8YLx8fGC8fHxgvHx8YLz8/N8////Bf///w3x8fGC8fHxgvHx8YLx8fGC8fHxgsXp + sOnB6Kv/wuis/8Porf/E6a7/xOmv/8XpsP/G6bH/xumy/8fqs//I6rT/yOq1/8nqtv/K67f/y+u4/8vr + uv/M67v/zey8/87svf/O67z96/Hoj/Hx8YLx8fGC8fHxgvHx8YLy8vIm8/PzK/Hx8YLx8fGC8fHxgvHx + 8YLg79icwOep/8Hoqv/B6Kv/wuis/8Porf/E6a7/wuit/73opP+76KL/u+eh/77opv/D6a3/yeu1/8nq + tv/K67f/y+u5/8zruv/M67v/zey8/87svf/d7tSz8fHxgvHx8YLx8fGC8fHxgvHx8Tby8vI68fHxgvHx + 8YLx8fGC8fHxgtTrxre/56j/wOep/8Hoqv/B6Kv/uOad/53idv+V4Gn/leBq/5fhbP+Y4W//muFx/5vi + c/+e4Xb/puWD/7PmlP/D6a3/y+u5/8zruv/M67v/zey8/9rtzsHx8fGC8fHxgvHx8YLx8fGC8/PzQfPz + 80Lx8fGC8fHxgvHx8YLx8fGC0OvAwr/nqP+/56j/wOep/8Hoqv+o44b/kd9k/5LfZv+U4Gj/leBq/5fh + bf+Y4W//muFx/5vic/+d4nX/n+N3/7fnm//K67j/y+u5/8zruv/M67v/2u3QvPHx8YLx8fGC8fHxgvHx + 8YLy8vI98/PzP/Hx8YLx8fGC8fHxgvHx8YLQ6sK/v+eo/7/nqP+/56j/wOep/6jjhv+P3mL/kd9k/5Lf + Zv+U4Gj/leBr/5fhbf+Y4W//muFx/5zic/+d4nX/v+mm/8nqt//K67j/y+u5/8zruv/f79au8fHxgvHx + 8YLx8fGC8fHxgvX19TLx8fE38fHxgvHx8YLx8fGC8fHxgtTrybO/56j/v+eo/7/nqP+/56j/sOSS/47e + YP+P3mL/kd9k/5LfZv+U4Gj/leBr/5fhbf+Z4W//muJx/5/jd//H6bP/yeq2/8nqt//K67j/y+u5/+nv + 45Tx8fGC8fHxgvHx8YLx8fGC7+/vIPHx8SXx8fGC8fHxgvHx8YLx8fGC4e/Zm7/nqP+/56j/v+eo/7/n + qP+956X/jt5h/47eYP+P3mL/kd9k/5LfZv+U4Gn/luBr/5fhbf+Z4W//q+aK/8fqs//I6rT/yeq2/8nq + t//N7Lvw8fHxgvHx8YLx8fGC8fHxgvPz84D///8G6+vrDfHx8YLx8fGC8fHxgvHx8YLv8e+Dweis87/n + qP+/56j/v+eo/7/nqP+d4XX/jN1e/47eYP+P3mL/kd9k/5PfZ/+U4Gn/luBr/5fhbf+86KP/xuqy/8fq + s//I6rX/yeq2/9Tsx8nx8fGC8fHxgvHx8YLx8fGC8PDwaAAAAAAAAAAA8fHxbPHx8YLx8fGC8fHxgvHx + 8YLM6rrMv+eo/7/nqP+/56j/v+eo/7blmv+N3V//jN1e/47eYP+Q3mL/kd9k/5PfZ/+U4Gn/qeSH/8Xp + sP/G6bH/xuqy/8fqs//I6rX/5fDem/Hx8YLx8fGC8fHxgvHx8YLz8/M/AAAAAAAAAADz8/NB8fHxgvHx + 8YLx8fGC8fHxgt3s06O/56j/v+eo/7/nqP+/56j/v+eo/7Xmmf+U32n/jN1e/47eYP+Q3mL/k99o/6zk + i//D6a7/xemv/8XpsP/G6bH/xuqy/8vqu+jx8fGC8fHxgvHx8YLx8fGC8fHxgvPz8xUAAAAAAAAAAPT0 + 9Bfx8fGC8fHxgvHx8YLx8fGC8fHvg8Tpsee/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+35pz/suWV/7Xm + mf/A6Kj/wuit/8Porf/E6a7/xemv/8XpsP/G6bH/3e3UqvHx8YLx8fGC8fHxgvHx8YLw8PBmAAAAAAAA + AAAAAAAAAAAAAPHx8W7x8fGC8fHxgvHx8YLx8fGC4u7cmMHnqvm/56j/v+eo/7/nqP+/56j/v+eo/7/n + qP+/56j/wOep/8Hoqv/C6Kz/wuit/8Porf/E6a7/xemv/9Hrwszx8fGC8fHxgvHx8YLx8fGC8fHxgvX1 + 9TEAAAAAAAAAAAAAAAAAAAAA7u7uO/Hx8YLx8fGC8fHxgvHx8YLx8fGC3e7SpMHoqfq/56j/v+eo/7/n + qP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz/wuit/8Porf/O67zV8PHwhPHx8YLx8fGC8fHxgvHx + 8YLy8vJ2////BQAAAAAAAAAAAAAAAAAAAACqqqoD8PDwafHx8YLx8fGC8fHxgvHx8YLx8fGC4O/YnMTo + ruy/56j/v+eo/7/nqP+/56j/v+eo/7/nqP+/56j/wOip/8Hoq//C6Kz90uvEwe/x74Px8fGC8fHxgvHx + 8YLx8fGC8fHxgvPz8ykAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz8/MW8fHxfPHx8YLx8fGC8fHxgvHx + 8YLx8fGC8PLuhdXtyLXF6bHlv+eo/7/nqP+/56j/v+eo/7/nqP/B6Kv0zeq8zOXv4JTx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLy8vJNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADy8vIm8fHxgPHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLs8OmJ4e/Zm93u06Pf7def5+/hkvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxXf///wIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADy8vIo8/PzffHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8VnMzMwFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD29vYb8fHxbvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz83/v7+9BgICAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMzMwF8/PzQPLy8nnx8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgvPz84Hx8fFc9PT0GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////B/X19TLx8fFc8PDwevHx + 8YLx8fGC8fHxgvHx8YLx8fGC8fHxgPHx8Wv09PRE9PT0FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA7+/vEPb29hvw8PAj7+/vH/T09Be/v78EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////8B///wAA//wAAD/wAAAP4AAAB+AA + AAfAAAADwAAAA4AAAAGAAAABgAAAAYAAAAGAAAABgAAAAYAAAAGAAAADwAAAA8AAAAPAAAAH4AAAB+AA + AA/wAAAP+AAAH/gAAD/+AAB//wAB///AA///+B////////////8oAAAAEAAAACAAAAABACAAAAAAAAAE + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////CfDw8BH///8GAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAAu7u7i7x8fFe8PDwevHx8YLx8fGC8fHxgvDw + 8Hvx8fFs7+/vT/Dw8CMAAAABAAAAAAAAAAAAAAAA5ubmCvLy8l/x8fGC8fHxgvHx8YLx8fGC8fHxgvHx + 8YLx8fGC8fHxgvHx8YLx8fGC8/PzZu7u7g8AAAAAAAAAAPHx8V3x8fGC8fHxgunv5o7Z7c200+vFytTs + xc7W7cnH2+7QueLu2qbu8OyH8fHxgvHx8YLx8fFu////BfHx8STx8fGC8fHxgtrtzq3D6a/8xemw/8bp + sv/I6rT/yeq2/8vruP/M67v/z+u++Nzu0bjx8fGC8fHxgu/v7zDx8fFI8fHxguzw6ojC56z3wuis/8Tp + rv/E6q3/weiq/8fqsv/J6rb/y+u5/8zru//N67z/6/HpjfHx8YLy8vJN8fHxXPHx8YLg79icv+eo/8Ho + qv+k4n//lOBo/5fhbf+a4XH/n+J5/7Pmlv/L67n/zOu7/+Xw353x8fGC8fHxXvHx8Vrx8fGC4O3Zm7/n + qP+/56j/nuF3/5HfZP+U4Gj/l+Ft/5ricf+x5pL/yeq3/8vruf/r8emN8fHxgu/v70/x8fFK8fHxguzw + 6ojA6Kn8v+eo/6njiP+O3mD/kd9k/5Tgaf+X4W3/vuim/8jqtP/N67zr8fHxgvHx8YLy8vI68/PzK/Hx + 8YLx8fGCx+m03L/nqP++6Kb/meBw/47eYP+S32X/q+SL/8XpsP/G6rL/1+zLvvHx8YLz8/OB8PDwEdXV + 1Qbx8fF98fHxgt/t1Z/A56j9v+eo/7/nqP+656H/vuim/8Lorf/E6a7/yOq18Ovw6Yvx8fGC8vLyYwAA + AAAAAAAA8fHxR/Hx8YLx8fGC2O3NrMDnqfq/56j/v+eo/7/nqP/B6Kv/xumy7OTu3Zfx8fGC8/PzgfLy + 8icAAAAAAAAAAP///wPz8/Nm8fHxgvHx8YLo7+SO0+zFuczquszM6bzJ1+zMru7w7Ibx8fGC8fHxgvHx + 8UcAAAAAAAAAAAAAAAAAAAAA4+PjCfHx8Vzx8fGC8fHxgvHx8YLx8fGC8fHxgvHx8YLx8fGC8fHxgfPz + 80D///8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8/PzK/Ly8mDz8/N+8fHxgvHx8YLy8vJ68vLyUezs + 7BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAevr6w3j4+MJAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA4AcAAMADAACAAQAAgAEAAIABAACAAQAAgAEAAIAB + AADAAwAAwAMAAOAHAADwDwAA/n8AAP//AAA= + + + \ No newline at end of file diff --git a/extra/exe-builder/FodyWeavers.xml b/extra/exe-builder/FodyWeavers.xml new file mode 100644 index 000000000..f1dea8fce --- /dev/null +++ b/extra/exe-builder/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/extra/exe-builder/FodyWeavers.xsd b/extra/exe-builder/FodyWeavers.xsd new file mode 100644 index 000000000..ff119f713 --- /dev/null +++ b/extra/exe-builder/FodyWeavers.xsd @@ -0,0 +1,141 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/extra/exe-builder/Program.cs b/extra/exe-builder/Program.cs new file mode 100644 index 000000000..6004f6d4b --- /dev/null +++ b/extra/exe-builder/Program.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Win32; +using Newtonsoft.Json; +using UptimeKuma.Properties; + +namespace UptimeKuma { + static class Program { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) { + var cwd = Path.GetDirectoryName(Application.ExecutablePath); + + if (cwd != null) { + Environment.CurrentDirectory = cwd; + } + + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new UptimeKumaApplicationContext()); + } + } + + public class UptimeKumaApplicationContext : ApplicationContext + { + private static Mutex mutex = null; + + const string appName = "Uptime Kuma"; + + private NotifyIcon trayIcon; + private Process process; + + private MenuItem statusMenuItem; + private MenuItem runWhenStarts; + private MenuItem openMenuItem; + + private RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true); + + + public UptimeKumaApplicationContext() { + + // Single instance only + bool createdNew; + mutex = new Mutex(true, appName, out createdNew); + if (!createdNew) { + return; + } + + var startingText = "Starting server..."; + trayIcon = new NotifyIcon(); + trayIcon.Text = startingText; + + runWhenStarts = new MenuItem("Run when system starts", RunWhenStarts); + runWhenStarts.Checked = registryKey.GetValue(appName) != null; + + statusMenuItem = new MenuItem(startingText); + statusMenuItem.Enabled = false; + + openMenuItem = new MenuItem("Open", Open); + openMenuItem.Enabled = false; + + trayIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location); + trayIcon.ContextMenu = new ContextMenu(new MenuItem[] { + statusMenuItem, + openMenuItem, + //new("Debug Console", DebugConsole), + runWhenStarts, + new("Check for Update...", CheckForUpdate), + new("Visit GitHub...", VisitGitHub), + new("About", About), + new("Exit", Exit), + }); + + trayIcon.MouseDoubleClick += new MouseEventHandler(Open); + trayIcon.Visible = true; + + var hasUpdateFile = File.Exists("update"); + + if (!hasUpdateFile && Directory.Exists("core") && Directory.Exists("node") && Directory.Exists("core/node_modules") && Directory.Exists("core/dist")) { + // Go go go + StartProcess(); + } else { + DownloadFiles(); + } + } + + void DownloadFiles() { + var form = new DownloadForm(); + form.Closed += Exit; + form.Show(); + } + + private void RunWhenStarts(object sender, EventArgs e) { + if (registryKey == null) { + MessageBox.Show("Error: Unable to set startup registry key."); + return; + } + + if (runWhenStarts.Checked) { + registryKey.DeleteValue(appName, false); + runWhenStarts.Checked = false; + } else { + registryKey.SetValue(appName, Application.ExecutablePath); + runWhenStarts.Checked = true; + } + } + + void StartProcess() { + var startInfo = new ProcessStartInfo { + FileName = "node/node.exe", + Arguments = "server/server.js --data-dir=\"../data/\"", + RedirectStandardOutput = false, + RedirectStandardError = false, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = "core" + }; + + process = new Process(); + process.StartInfo = startInfo; + process.EnableRaisingEvents = true; + process.Exited += ProcessExited; + + try { + process.Start(); + //Open(null, null); + + // Async task to check if the server is ready + Task.Run(() => { + var runningText = "Server is running"; + using TcpClient tcpClient = new TcpClient(); + while (true) { + try { + tcpClient.Connect("127.0.0.1", 3001); + statusMenuItem.Text = runningText; + openMenuItem.Enabled = true; + trayIcon.Text = runningText; + break; + } catch (Exception) { + System.Threading.Thread.Sleep(2000); + } + } + }); + + } catch (Exception e) { + MessageBox.Show("Startup failed: " + e.Message, "Uptime Kuma Error"); + } + } + + void StopProcess() { + process?.Kill(); + } + + void Open(object sender, EventArgs e) { + Process.Start("http://localhost:3001"); + } + + void DebugConsole(object sender, EventArgs e) { + + } + + void CheckForUpdate(object sender, EventArgs e) { + var needUpdate = false; + + // Check version.json exists + if (File.Exists("version.json")) { + // Load version.json and compare with the latest version from GitHub + var currentVersionObj = JsonConvert.DeserializeObject(File.ReadAllText("version.json")); + + var versionJson = new WebClient().DownloadString("https://uptime.kuma.pet/version"); + var latestVersionObj = JsonConvert.DeserializeObject(versionJson); + + // Compare version, if the latest version is newer, then update + if (new System.Version(latestVersionObj.latest).CompareTo(new System.Version(currentVersionObj.latest)) > 0) { + var result = MessageBox.Show("A new version is available. Do you want to update?", "Update", MessageBoxButtons.YesNo); + if (result == DialogResult.Yes) { + // Create a empty file `update`, so the app will download the core files again at startup + File.Create("update").Close(); + + trayIcon.Visible = false; + process?.Kill(); + + // Restart the app, it will download the core files again at startup + Application.Restart(); + } + } else { + MessageBox.Show("You are using the latest version."); + } + } + + + } + + void VisitGitHub(object sender, EventArgs e) + { + Process.Start("https://github.com/louislam/uptime-kuma"); + } + + void About(object sender, EventArgs e) + { + MessageBox.Show("Uptime Kuma Windows Runtime v1.0.0" + Environment.NewLine + "© 2023 Louis Lam", "Info"); + } + + void Exit(object sender, EventArgs e) + { + // Hide tray icon, otherwise it will remain shown until user mouses over it + trayIcon.Visible = false; + process?.Kill(); + Application.Exit(); + } + + void ProcessExited(object sender, EventArgs e) { + + if (process.ExitCode != 0) { + var line = ""; + while (!process.StandardOutput.EndOfStream) + { + line += process.StandardOutput.ReadLine(); + } + + MessageBox.Show("Uptime Kuma exited unexpectedly. Exit code: " + process.ExitCode + " " + line); + } + + trayIcon.Visible = false; + Application.Exit(); + } + + } +} + diff --git a/extra/exe-builder/Properties/AssemblyInfo.cs b/extra/exe-builder/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..59b36bb81 --- /dev/null +++ b/extra/exe-builder/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Uptime Kuma")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Uptime Kuma")] +[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/extra/exe-builder/Properties/Resources.Designer.cs b/extra/exe-builder/Properties/Resources.Designer.cs new file mode 100644 index 000000000..8c8e559c5 --- /dev/null +++ b/extra/exe-builder/Properties/Resources.Designer.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace UptimeKuma.Properties { + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", + "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", + "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState + .Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if ((resourceMan == null)) { + global::System.Resources.ResourceManager temp = + new global::System.Resources.ResourceManager("UptimeKuma.Properties.Resources", + typeof(Resources).Assembly); + resourceMan = temp; + } + + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState + .Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } + set { resourceCulture = value; } + } + } +} \ No newline at end of file diff --git a/extra/exe-builder/Properties/Resources.resx b/extra/exe-builder/Properties/Resources.resx new file mode 100644 index 000000000..ffecec851 --- /dev/null +++ b/extra/exe-builder/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/extra/exe-builder/Properties/Settings.Designer.cs b/extra/exe-builder/Properties/Settings.Designer.cs new file mode 100644 index 000000000..6c63b395b --- /dev/null +++ b/extra/exe-builder/Properties/Settings.Designer.cs @@ -0,0 +1,23 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace UptimeKuma.Properties { + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute( + "Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + private static Settings defaultInstance = + ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { return defaultInstance; } + } + } +} \ No newline at end of file diff --git a/extra/exe-builder/Properties/Settings.settings b/extra/exe-builder/Properties/Settings.settings new file mode 100644 index 000000000..abf36c5d3 --- /dev/null +++ b/extra/exe-builder/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/extra/exe-builder/UptimeKuma.csproj b/extra/exe-builder/UptimeKuma.csproj new file mode 100644 index 000000000..ecd6a46b6 --- /dev/null +++ b/extra/exe-builder/UptimeKuma.csproj @@ -0,0 +1,212 @@ + + + + + + Debug + AnyCPU + {2DB53988-1D93-4AC0-90C4-96ADEAAC5C04} + WinExe + UptimeKuma + uptime-kuma + v4.7.2 + 512 + true + true + ..\..\public\favicon.ico + 9 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + app.manifest + + + COPY "$(SolutionDir)bin\Debug\uptime-kuma.exe" "%UserProfile%\Desktop\uptime-kuma-win64\" + + + + packages\Costura.Fody.5.7.0\lib\netstandard1.0\Costura.dll + + + packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + + + + packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll + + + + packages\System.AppContext.4.3.0\lib\net463\System.AppContext.dll + + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + packages\System.Console.4.3.1\lib\net46\System.Console.dll + + + + packages\System.Diagnostics.DiagnosticSource.7.0.1\lib\net462\System.Diagnostics.DiagnosticSource.dll + + + packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll + + + packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll + + + packages\System.IO.4.3.0\lib\net462\System.IO.dll + + + packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + + + + packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + + + packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + + + packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + + + packages\System.Linq.4.3.0\lib\net463\System.Linq.dll + + + packages\System.Linq.Expressions.4.3.0\lib\net463\System.Linq.Expressions.dll + + + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll + + + packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll + + + packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll + + + packages\System.Runtime.InteropServices.4.3.0\lib\net463\System.Runtime.InteropServices.dll + + + packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + + + packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll + + + packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + + + packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + + + packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll + + + packages\System.Text.RegularExpressions.4.3.1\lib\net463\System.Text.RegularExpressions.dll + + + + + + + + + + + packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll + + + + + Form + + + DownloadForm.cs + + + + + + DownloadForm.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + favicon.ico + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/extra/exe-builder/UptimeKuma.sln b/extra/exe-builder/UptimeKuma.sln new file mode 100644 index 000000000..201d7e234 --- /dev/null +++ b/extra/exe-builder/UptimeKuma.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UptimeKuma", "UptimeKuma.csproj", "{2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DB53988-1D93-4AC0-90C4-96ADEAAC5C04}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/extra/exe-builder/UptimeKuma.sln.DotSettings.user b/extra/exe-builder/UptimeKuma.sln.DotSettings.user new file mode 100644 index 000000000..b4ca9dadf --- /dev/null +++ b/extra/exe-builder/UptimeKuma.sln.DotSettings.user @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/extra/exe-builder/Version.cs b/extra/exe-builder/Version.cs new file mode 100644 index 000000000..896c7a244 --- /dev/null +++ b/extra/exe-builder/Version.cs @@ -0,0 +1,9 @@ +namespace UptimeKuma { + public class Version { + public string latest { get; set; } + public string slow { get; set; } + public string beta { get; set; } + public string nodejs { get; set; } + public string exe { get; set; } + } +} diff --git a/extra/exe-builder/app.manifest b/extra/exe-builder/app.manifest new file mode 100644 index 000000000..4a48528fc --- /dev/null +++ b/extra/exe-builder/app.manifest @@ -0,0 +1,28 @@ + + + + + true + PerMonitorV2 + + + + + + + + + + + diff --git a/extra/exe-builder/packages.config b/extra/exe-builder/packages.config new file mode 100644 index 000000000..aca26d670 --- /dev/null +++ b/extra/exe-builder/packages.config @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extra/sort-contributors.js b/extra/sort-contributors.js new file mode 100644 index 000000000..418bc233d --- /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"); + +// Sort the lines +lines = lines.sort(); + +// Output the lines, concat with " " +console.log(lines.join(" ")); diff --git a/extra/update-version.js b/extra/update-version.js index 246e1c1c4..8d78f17db 100644 --- a/extra/update-version.js +++ b/extra/update-version.js @@ -26,7 +26,8 @@ if (! exists) { fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); // Also update package-lock.json - childProcess.spawnSync("npm", [ "install" ]); + const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm"; + childProcess.spawnSync(npm, [ "install" ]); commit(newVersion); tag(newVersion); diff --git a/package-lock.json b/package-lock.json index baf9f4f28..d568f5d3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "uptime-kuma", - "version": "1.20.0-beta.0", + "version": "1.21.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "1.20.0-beta.0", + "version": "1.21.0-beta.0", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.7.3", - "@louislam/ping": "~0.4.2-mod.1", + "@louislam/ping": "~0.4.2-mod.2", "@louislam/sqlite3": "15.1.2", "args-parser": "~1.3.0", "axios": "~0.27.0", @@ -41,10 +41,11 @@ "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", "limiter": "~2.1.0", - "mongodb": "~4.13.0", + "mongodb": "~4.14.0", "mqtt": "~4.3.7", "mssql": "~8.1.4", "mysql2": "~2.3.3", + "nanoid": "^3.3.4", "node-cloudflared-tunnel": "~1.0.9", "node-radius-client": "~1.0.0", "nodemailer": "~6.6.5", @@ -55,7 +56,7 @@ "prom-client": "~13.2.0", "prometheus-api-metrics": "~3.2.1", "protobufjs": "~7.1.1", - "qs": "~6.10.0", + "qs": "~6.10.4", "redbean-node": "~0.2.0", "redis": "~4.5.1", "socket.io": "~4.5.3", @@ -4212,9 +4213,9 @@ "integrity": "sha512-retLUN4TwCJ0QJDi9OCJwYVaXAz93NeOkEtEQL98M2bykBOxmURlP0YlfsuE46kItOOVZIWRYC3KsSLhQ1R2Qw==" }, "node_modules/@louislam/ping": { - "version": "0.4.2-mod.1", - "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.2-mod.1.tgz", - "integrity": "sha512-KkRDo8qcF9kzzR0Hh8Iqz+XNnzKOdobUquP7UyBYrjxAB1jNT3qO0gvAZeDUknF28LXBPSzkiVlf1NG+tb/iyQ==", + "version": "0.4.2-mod.2", + "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.2-mod.2.tgz", + "integrity": "sha512-4krrRGohYdhQOD+Mt0Q8e1Z05DEKntZ7TgiY1jYaqWrMz0H2XJyRh+mLPOUVPL5zSymiHsZiK2ZACXtp/d9Wxg==", "dependencies": { "command-exists": "~1.2.9", "q": "1.x", @@ -14008,9 +14009,9 @@ } }, "node_modules/mongodb": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz", - "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.14.0.tgz", + "integrity": "sha512-coGKkWXIBczZPr284tYKFLg+KbGPPLlSbdgfKAb6QqCFt5bo5VFZ50O3FFzsw4rnkqjwT6D8Qcoo9nshYKM7Mg==", "dependencies": { "bson": "^4.7.0", "mongodb-connection-string-url": "^2.5.4", @@ -14247,7 +14248,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -22231,9 +22231,9 @@ "integrity": "sha512-retLUN4TwCJ0QJDi9OCJwYVaXAz93NeOkEtEQL98M2bykBOxmURlP0YlfsuE46kItOOVZIWRYC3KsSLhQ1R2Qw==" }, "@louislam/ping": { - "version": "0.4.2-mod.1", - "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.2-mod.1.tgz", - "integrity": "sha512-KkRDo8qcF9kzzR0Hh8Iqz+XNnzKOdobUquP7UyBYrjxAB1jNT3qO0gvAZeDUknF28LXBPSzkiVlf1NG+tb/iyQ==", + "version": "0.4.2-mod.2", + "resolved": "https://registry.npmjs.org/@louislam/ping/-/ping-0.4.2-mod.2.tgz", + "integrity": "sha512-4krrRGohYdhQOD+Mt0Q8e1Z05DEKntZ7TgiY1jYaqWrMz0H2XJyRh+mLPOUVPL5zSymiHsZiK2ZACXtp/d9Wxg==", "requires": { "command-exists": "~1.2.9", "q": "1.x", @@ -29626,9 +29626,9 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "mongodb": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz", - "integrity": "sha512-+taZ/bV8d1pYuHL4U+gSwkhmDrwkWbH1l4aah4YpmpscMwgFBkufIKxgP/G7m87/NUuQzc2Z75ZTI7ZOyqZLbw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.14.0.tgz", + "integrity": "sha512-coGKkWXIBczZPr284tYKFLg+KbGPPLlSbdgfKAb6QqCFt5bo5VFZ50O3FFzsw4rnkqjwT6D8Qcoo9nshYKM7Mg==", "requires": { "@aws-sdk/credential-providers": "^3.186.0", "bson": "^4.7.0", @@ -29825,8 +29825,7 @@ "nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, "native-duplexpair": { "version": "1.0.0", diff --git a/package.json b/package.json index 5d4652d65..5b2bda4a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uptime-kuma", - "version": "1.20.0", + "version": "1.21.0-beta.0", "license": "MIT", "repository": { "type": "git", @@ -39,7 +39,7 @@ "build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain", - "setup": "git checkout 1.20.0 && npm ci --production && npm run download-dist", + "setup": "git checkout 1.20.2 && npm ci --production && npm run download-dist", "download-dist": "node extra/download-dist.js", "mark-as-nightly": "node extra/mark-as-nightly.js", "reset-password": "node extra/reset-password.js", @@ -64,11 +64,12 @@ "cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js", "cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"", "build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go", - "depoly-demo-server": "node extra/deploy-demo-server.js" + "depoly-demo-server": "node extra/deploy-demo-server.js", + "sort-contributors": "node extra/sort-contributors.js" }, "dependencies": { "@grpc/grpc-js": "~1.7.3", - "@louislam/ping": "~0.4.2-mod.1", + "@louislam/ping": "~0.4.2-mod.2", "@louislam/sqlite3": "15.1.2", "args-parser": "~1.3.0", "axios": "~0.27.0", @@ -99,10 +100,11 @@ "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", "limiter": "~2.1.0", - "mongodb": "~4.13.0", + "mongodb": "~4.14.0", "mqtt": "~4.3.7", "mssql": "~8.1.4", "mysql2": "~2.3.3", + "nanoid": "^3.3.4", "node-cloudflared-tunnel": "~1.0.9", "node-radius-client": "~1.0.0", "nodemailer": "~6.6.5", diff --git a/server/auth.js b/server/auth.js index fd19b0e44..c42a74c40 100644 --- a/server/auth.js +++ b/server/auth.js @@ -2,7 +2,9 @@ const basicAuth = require("express-basic-auth"); const passwordHash = require("./password-hash"); const { R } = require("redbean-node"); const { setting } = require("./util-server"); -const { loginRateLimiter } = require("./rate-limiter"); +const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter"); +const { Settings } = require("./settings"); +const dayjs = require("dayjs"); /** * Login to web app @@ -34,8 +36,36 @@ exports.login = async function (username, password) { }; /** - * Callback for myAuthorizer - * @callback myAuthorizerCB + * Validate a provided API key + * @param {string} key API key to verify + */ +async function verifyAPIKey(key) { + if (typeof key !== "string") { + return false; + } + + // uk prefix + key ID is before _ + let index = key.substring(2, key.indexOf("_")); + let clear = key.substring(key.indexOf("_") + 1, key.length); + + let hash = await R.findOne("api_key", " id=? ", [ index ]); + + if (hash === null) { + return false; + } + + let current = dayjs(); + let expiry = dayjs(hash.expires); + if (expiry.diff(current) < 0 || !hash.active) { + return false; + } + + return hash && passwordHash.verify(clear, hash.key); +} + +/** + * Callback for basic auth authorizers + * @callback authCallback * @param {any} err Any error encountered * @param {boolean} authorized Is the client authorized? */ @@ -44,9 +74,31 @@ exports.login = async function (username, password) { * Custom authorizer for express-basic-auth * @param {string} username * @param {string} password - * @param {myAuthorizerCB} callback + * @param {authCallback} callback + */ +function apiAuthorizer(username, password, callback) { + // API Rate Limit + apiRateLimiter.pass(null, 0).then((pass) => { + if (pass) { + verifyAPIKey(password).then((valid) => { + callback(null, valid); + // Only allow a set number of api requests per minute + // (currently set to 60) + apiRateLimiter.removeTokens(1); + }); + } else { + callback(null, false); + } + }); +} + +/** + * Custom authorizer for express-basic-auth + * @param {string} username + * @param {string} password + * @param {authCallback} callback */ -function myAuthorizer(username, password, callback) { +function userAuthorizer(username, password, callback) { // Login Rate Limit loginRateLimiter.pass(null, 0).then((pass) => { if (pass) { @@ -71,7 +123,7 @@ function myAuthorizer(username, password, callback) { */ exports.basicAuth = async function (req, res, next) { const middleware = basicAuth({ - authorizer: myAuthorizer, + authorizer: userAuthorizer, authorizeAsync: true, challenge: true, }); @@ -84,3 +136,32 @@ exports.basicAuth = async function (req, res, next) { next(); } }; + +/** + * Use use API Key if API keys enabled, else use basic auth + * @param {express.Request} req Express request object + * @param {express.Response} res Express response object + * @param {express.NextFunction} next + */ +exports.apiAuth = async function (req, res, next) { + if (!await Settings.get("disableAuth")) { + let usingAPIKeys = await Settings.get("apiKeysEnabled"); + let middleware; + if (usingAPIKeys) { + middleware = basicAuth({ + authorizer: apiAuthorizer, + authorizeAsync: true, + challenge: true, + }); + } else { + middleware = basicAuth({ + authorizer: userAuthorizer, + authorizeAsync: true, + challenge: true, + }); + } + middleware(req, res, next); + } else { + next(); + } +}; diff --git a/server/client.js b/server/client.js index ef96c7f44..3efbe8fdc 100644 --- a/server/client.js +++ b/server/client.js @@ -113,6 +113,31 @@ async function sendProxyList(socket) { return list; } +/** + * Emit API key list to client + * @param {Socket} socket Socket.io socket instance + * @returns {Promise} + */ +async function sendAPIKeyList(socket) { + const timeLogger = new TimeLogger(); + + let result = []; + const list = await R.find( + "api_key", + "user_id=?", + [ socket.userID ], + ); + + for (let bean of list) { + result.push(bean.toPublicJSON()); + } + + io.to(socket.userID).emit("apiKeyList", result); + timeLogger.print("Sent API Key List"); + + return list; +} + /** * Emits the version information to the client. * @param {Socket} socket Socket.io socket instance @@ -157,6 +182,7 @@ module.exports = { sendImportantHeartbeatList, sendHeartbeatList, sendProxyList, + sendAPIKeyList, sendInfo, sendDockerHostList }; diff --git a/server/database.js b/server/database.js index 449f16d55..5a83e1fbf 100644 --- a/server/database.js +++ b/server/database.js @@ -70,6 +70,9 @@ class Database { "patch-maintenance-table2.sql": true, "patch-add-gamedig-monitor.sql": true, "patch-add-google-analytics-status-page-tag.sql": true, + "patch-http-body-encoding.sql": true, + "patch-add-description-monitor.sql": true, + "patch-api-key-table.sql": true, }; /** diff --git a/server/model/api_key.js b/server/model/api_key.js new file mode 100644 index 000000000..1b27a60f6 --- /dev/null +++ b/server/model/api_key.js @@ -0,0 +1,76 @@ +const { BeanModel } = require("redbean-node/dist/bean-model"); +const { R } = require("redbean-node"); +const dayjs = require("dayjs"); + +class APIKey extends BeanModel { + /** + * Get the current status of this API key + * @returns {string} active, inactive or expired + */ + getStatus() { + let current = dayjs(); + let expiry = dayjs(this.expires); + if (expiry.diff(current) < 0) { + return "expired"; + } + + return this.active ? "active" : "inactive"; + } + + /** + * Returns an object that ready to parse to JSON + * @returns {Object} + */ + toJSON() { + return { + id: this.id, + key: this.key, + name: this.name, + userID: this.user_id, + createdDate: this.created_date, + active: this.active, + expires: this.expires, + status: this.getStatus(), + }; + } + + /** + * Returns an object that ready to parse to JSON with sensitive fields + * removed + * @returns {Object} + */ + toPublicJSON() { + return { + id: this.id, + name: this.name, + userID: this.user_id, + createdDate: this.created_date, + active: this.active, + expires: this.expires, + status: this.getStatus(), + }; + } + + /** + * Create a new API Key and store it in the database + * @param {Object} key Object sent by client + * @param {int} userID ID of socket user + * @returns {Promise} + */ + static async save(key, userID) { + let bean; + bean = R.dispense("api_key"); + + bean.key = key.key; + bean.name = key.name; + bean.user_id = userID; + bean.active = key.active; + bean.expires = key.expires; + + await R.store(bean); + + return bean; + } +} + +module.exports = APIKey; diff --git a/server/model/monitor.js b/server/model/monitor.js index 4cbb56e1a..312ac732b 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -72,6 +72,7 @@ class Monitor extends BeanModel { let data = { id: this.id, name: this.name, + description: this.description, url: this.url, method: this.method, hostname: this.hostname, @@ -111,6 +112,7 @@ class Monitor extends BeanModel { radiusCalledStationId: this.radiusCalledStationId, radiusCallingStationId: this.radiusCallingStationId, game: this.game, + httpBodyEncoding: this.httpBodyEncoding }; if (includeSensitiveData) { @@ -143,7 +145,7 @@ class Monitor extends BeanModel { * @returns {Promise[]>} */ async getTags() { - return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]); + return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]); } /** @@ -203,7 +205,7 @@ class Monitor extends BeanModel { let previousBeat = null; let retries = 0; - let prometheus = new Prometheus(this); + this.prometheus = new Prometheus(this); const beat = async () => { @@ -272,17 +274,34 @@ class Monitor extends BeanModel { log.debug("monitor", `[${this.name}] Prepare Options for axios`); + let contentType = null; + let bodyValue = null; + + if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) { + if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") { + try { + bodyValue = JSON.parse(this.body); + contentType = "application/json"; + } catch (e) { + throw new Error("Your JSON body is invalid. " + e.message); + } + } else if (this.httpBodyEncoding === "xml") { + bodyValue = this.body; + contentType = "text/xml; charset=utf-8"; + } + } + // Axios Options const options = { url: this.url, method: (this.method || "get").toLowerCase(), - ...(this.body ? { data: JSON.parse(this.body) } : {}), timeout: this.interval * 1000 * 0.8, headers: { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "User-Agent": "Uptime-Kuma/" + version, - ...(this.headers ? JSON.parse(this.headers) : {}), + ...(contentType ? { "Content-Type": contentType } : {}), ...(basicAuthHeader), + ...(this.headers ? JSON.parse(this.headers) : {}) }, maxRedirects: this.maxredirects, validateStatus: (status) => { @@ -290,6 +309,10 @@ class Monitor extends BeanModel { }, }; + if (bodyValue) { + options.data = bodyValue; + } + if (this.proxy_id) { const proxy = await R.load("proxy", this.proxy_id); @@ -755,7 +778,7 @@ class Monitor extends BeanModel { await R.store(bean); log.debug("monitor", `[${this.name}] prometheus.update`); - prometheus.update(bean, tlsInfo); + this.prometheus?.update(bean, tlsInfo); previousBeat = bean; @@ -840,15 +863,15 @@ class Monitor extends BeanModel { clearTimeout(this.heartbeatInterval); this.isStop = true; - this.prometheus().remove(); + this.prometheus?.remove(); } /** - * Get a new prometheus instance - * @returns {Prometheus} + * Get prometheus instance + * @returns {Prometheus|undefined} */ - prometheus() { - return new Prometheus(this); + getPrometheus() { + return this.prometheus; } /** diff --git a/server/notification-providers/lunasea.js b/server/notification-providers/lunasea.js index 2985425ef..4d7136f75 100644 --- a/server/notification-providers/lunasea.js +++ b/server/notification-providers/lunasea.js @@ -8,7 +8,12 @@ class LunaSea extends NotificationProvider { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { let okMsg = "Sent Successfully."; - let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice; + let lunaseaurl = ""; + if (notification.lunaseaTarget === "user") { + lunaseaurl = "https://notify.lunasea.app/v1/custom/user/" + notification.lunaseaUserID; + } else { + lunaseaurl = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice; + } try { if (heartbeatJSON == null) { @@ -16,7 +21,7 @@ class LunaSea extends NotificationProvider { "title": "Uptime Kuma Alert", "body": msg, }; - await axios.post(lunaseadevice, testdata); + await axios.post(lunaseaurl, testdata); return okMsg; } @@ -25,7 +30,7 @@ class LunaSea extends NotificationProvider { "title": "UptimeKuma Alert: " + monitorJSON["name"], "body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], }; - await axios.post(lunaseadevice, downdata); + await axios.post(lunaseaurl, downdata); return okMsg; } @@ -34,7 +39,7 @@ class LunaSea extends NotificationProvider { "title": "UptimeKuma Alert: " + monitorJSON["name"], "body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], }; - await axios.post(lunaseadevice, updata); + await axios.post(lunaseaurl, updata); return okMsg; } diff --git a/server/notification-providers/pagertree.js b/server/notification-providers/pagertree.js new file mode 100644 index 000000000..8a0c4e368 --- /dev/null +++ b/server/notification-providers/pagertree.js @@ -0,0 +1,91 @@ +const NotificationProvider = require("./notification-provider"); +const axios = require("axios"); +const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util"); +const { setting } = require("../util-server"); +let successMessage = "Sent Successfully."; + +class PagerTree extends NotificationProvider { + name = "PagerTree"; + + /** + * @inheritdoc + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + try { + if (heartbeatJSON == null) { + // general messages + return this.postNotification(notification, msg, monitorJSON, heartbeatJSON); + } + + if (heartbeatJSON.status === UP && notification.pagertreeAutoResolve === "resolve") { + return this.postNotification(notification, null, monitorJSON, heartbeatJSON, notification.pagertreeAutoResolve); + } + + if (heartbeatJSON.status === DOWN) { + const title = `Uptime Kuma Monitor "${monitorJSON.name}" is DOWN`; + return this.postNotification(notification, title, monitorJSON, heartbeatJSON); + } + } catch (error) { + this.throwGeneralAxiosError(error); + } + } + + /** + * Check if result is successful, result code should be in range 2xx + * @param {Object} result Axios response object + * @throws {Error} The status code is not in range 2xx + */ + checkResult(result) { + if (result.status == null) { + throw new Error("PagerTree notification failed with invalid response!"); + } + if (result.status < 200 || result.status >= 300) { + throw new Error("PagerTree notification failed with status code " + result.status); + } + } + + /** + * Send the message + * @param {BeanModel} notification Message title + * @param {string} title Message title + * @param {Object} monitorJSON Monitor details (For Up/Down only) + * @param {?string} eventAction Action event for PagerTree (create, resolve) + * @returns {string} + */ + async postNotification(notification, title, monitorJSON, heartbeatJSON, eventAction = "create") { + + if (eventAction == null) { + return "No action required"; + } + + const options = { + method: "POST", + url: notification.pagertreeIntegrationUrl, + headers: { "Content-Type": "application/json" }, + data: { + event_type: eventAction, + id: heartbeatJSON?.monitorID || "uptime-kuma", + title: title, + urgency: notification.pagertreeUrgency, + heartbeat: heartbeatJSON, + monitor: monitorJSON + } + }; + + const baseURL = await setting("primaryBaseURL"); + if (baseURL && monitorJSON) { + options.client = "Uptime Kuma"; + options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id); + } + + let result = await axios.request(options); + this.checkResult(result); + if (result.statusText != null) { + return "PagerTree notification succeed: " + result.statusText; + } + + return successMessage; + } +} + +module.exports = PagerTree; diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index 5a5d40cb3..da89f0f7a 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -42,7 +42,7 @@ class Slack extends NotificationProvider { const time = heartbeatJSON["time"]; const textMsg = "Uptime Kuma Alert"; let data = { - "text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg, + "text": `${textMsg}\n${msg}`, "channel": notification.slackchannel, "username": notification.slackusername, "icon_emoji": notification.slackiconemo, diff --git a/server/notification-providers/telegram.js b/server/notification-providers/telegram.js index 2b0576224..3c490655d 100644 --- a/server/notification-providers/telegram.js +++ b/server/notification-providers/telegram.js @@ -9,11 +9,18 @@ class Telegram extends NotificationProvider { let okMsg = "Sent Successfully."; try { + let params = { + chat_id: notification.telegramChatID, + text: msg, + disable_notification: notification.telegramSendSilently ?? false, + protect_content: notification.telegramProtectContent ?? false, + }; + if (notification.telegramMessageThreadID) { + params.message_thread_id = notification.telegramMessageThreadID; + } + await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, { - params: { - chat_id: notification.telegramChatID, - text: msg, - }, + params: params, }); return okMsg; diff --git a/server/notification.js b/server/notification.js index fd3491238..1897f5cc0 100644 --- a/server/notification.js +++ b/server/notification.js @@ -24,6 +24,7 @@ const Ntfy = require("./notification-providers/ntfy"); const Octopush = require("./notification-providers/octopush"); const OneBot = require("./notification-providers/onebot"); const PagerDuty = require("./notification-providers/pagerduty"); +const PagerTree = require("./notification-providers/pagertree"); const PromoSMS = require("./notification-providers/promosms"); const Pushbullet = require("./notification-providers/pushbullet"); const PushDeer = require("./notification-providers/pushdeer"); @@ -83,6 +84,7 @@ class Notification { new Octopush(), new OneBot(), new PagerDuty(), + new PagerTree(), new PromoSMS(), new Pushbullet(), new PushDeer(), diff --git a/server/rate-limiter.js b/server/rate-limiter.js index 6f185beb9..ec77f1a4e 100644 --- a/server/rate-limiter.js +++ b/server/rate-limiter.js @@ -54,6 +54,13 @@ const loginRateLimiter = new KumaRateLimiter({ errorMessage: "Too frequently, try again later." }); +const apiRateLimiter = new KumaRateLimiter({ + tokensPerInterval: 60, + interval: "minute", + fireImmediately: true, + errorMessage: "Too frequently, try again later." +}); + const twoFaRateLimiter = new KumaRateLimiter({ tokensPerInterval: 30, interval: "minute", @@ -63,5 +70,6 @@ const twoFaRateLimiter = new KumaRateLimiter({ module.exports = { loginRateLimiter, + apiRateLimiter, twoFaRateLimiter, }; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index 665163aee..a36159cae 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -1,5 +1,5 @@ let express = require("express"); -const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, send403 } = require("../util-server"); +const { allowDevAllOrigin, allowAllOrigin, percentageToColor, filterAndJoin, sendHttpError } = require("../util-server"); const { R } = require("redbean-node"); const apicache = require("../modules/apicache"); const Monitor = require("../model/monitor"); @@ -7,6 +7,7 @@ const dayjs = require("dayjs"); const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util"); const StatusPage = require("../model/status_page"); const { UptimeKumaServer } = require("../uptime-kuma-server"); +const { UptimeCacheList } = require("../uptime-cache-list"); const { makeBadge } = require("badge-maker"); const { badgeConstants } = require("../config"); @@ -86,6 +87,7 @@ router.get("/api/push/:pushToken", async (request, response) => { await R.store(bean); io.to(monitor.user_id).emit("heartbeat", bean.toJSON()); + UptimeCacheList.clearCache(monitor.id); Monitor.sendStats(io, monitor.id, monitor.user_id); response.json({ @@ -175,7 +177,7 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response response.type("image/svg+xml"); response.send(svg); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -242,7 +244,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques response.type("image/svg+xml"); response.send(svg); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -303,7 +305,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request, response.type("image/svg+xml"); response.send(svg); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -373,7 +375,7 @@ router.get("/api/badge/:id/avg-response/:duration?", cache("5 minutes"), async ( response.type("image/svg+xml"); response.send(svg); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -464,7 +466,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon response.type("image/svg+xml"); response.send(svg); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -536,7 +538,7 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon response.type("image/svg+xml"); response.send(svg); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index de075db8d..28cf5f4c9 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -2,7 +2,7 @@ let express = require("express"); const apicache = require("../modules/apicache"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const StatusPage = require("../model/status_page"); -const { allowDevAllOrigin, send403 } = require("../util-server"); +const { allowDevAllOrigin, sendHttpError } = require("../util-server"); const { R } = require("redbean-node"); const Monitor = require("../model/monitor"); @@ -44,10 +44,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons let statusPageData = await StatusPage.getStatusPageData(statusPage); if (!statusPageData) { - response.statusCode = 404; - response.json({ - msg: "Not Found" - }); + sendHttpError(response, "Not Found"); return; } @@ -55,7 +52,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons response.json(statusPageData); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -103,7 +100,7 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques }); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); @@ -119,10 +116,7 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async ]); if (!statusPage) { - response.statusCode = 404; - response.json({ - msg: "Not Found" - }); + sendHttpError(response, "Not Found"); return; } @@ -141,7 +135,7 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async }); } catch (error) { - send403(response, error.message); + sendHttpError(response, error.message); } }); diff --git a/server/server.js b/server/server.js index 1073f3bef..b7308a5ab 100644 --- a/server/server.js +++ b/server/server.js @@ -87,7 +87,7 @@ log.debug("server", "Importing Background Jobs"); const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs"); const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter"); -const { basicAuth } = require("./auth"); +const { apiAuth } = require("./auth"); const { login } = require("./auth"); const passwordHash = require("./password-hash"); @@ -129,7 +129,7 @@ if (config.demoMode) { } // Must be after io instantiation -const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client"); +const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client"); const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler"); const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); const TwoFA = require("./2fa"); @@ -138,6 +138,7 @@ const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudfl const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler"); const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler"); const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler"); +const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handler"); const { generalSocketHandler } = require("./socket-handlers/general-socket-handler"); const { Settings } = require("./settings"); const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); @@ -229,7 +230,7 @@ let needSetup = false; // Prometheus API metrics /metrics // With Basic Auth using the first user's username/password - app.get("/metrics", basicAuth, prometheusAPIMetrics()); + app.get("/metrics", apiAuth, prometheusAPIMetrics()); app.use("/", expressStaticGzip("dist", { enableBrotli: true, @@ -677,10 +678,8 @@ let needSetup = false; throw new Error("Permission denied."); } - // Reset Prometheus labels - server.monitorList[monitor.id]?.prometheus()?.remove(); - bean.name = monitor.name; + bean.description = monitor.description; bean.type = monitor.type; bean.url = monitor.url; bean.method = monitor.method; @@ -729,6 +728,7 @@ let needSetup = false; bean.radiusCalledStationId = monitor.radiusCalledStationId; bean.radiusCallingStationId = monitor.radiusCallingStationId; bean.radiusSecret = monitor.radiusSecret; + bean.httpBodyEncoding = monitor.httpBodyEncoding; bean.validate(); @@ -1320,6 +1320,7 @@ let needSetup = false; let monitor = { // Define the new variable from earlier here name: monitorListData[i].name, + description: monitorListData[i].description, type: monitorListData[i].type, url: monitorListData[i].url, method: monitorListData[i].method || "GET", @@ -1503,6 +1504,7 @@ let needSetup = false; proxySocketHandler(socket); dockerSocketHandler(socket); maintenanceSocketHandler(socket); + apiKeySocketHandler(socket); generalSocketHandler(socket, server); pluginsHandler(socket, server); @@ -1611,6 +1613,7 @@ async function afterLogin(socket, user) { sendNotificationList(socket); sendProxyList(socket); sendDockerHostList(socket); + sendAPIKeyList(socket); await sleep(500); diff --git a/server/socket-handlers/api-key-socket-handler.js b/server/socket-handlers/api-key-socket-handler.js new file mode 100644 index 000000000..69b0b60de --- /dev/null +++ b/server/socket-handlers/api-key-socket-handler.js @@ -0,0 +1,150 @@ +const { checkLogin } = require("../util-server"); +const { log } = require("../../src/util"); +const { R } = require("redbean-node"); +const { nanoid } = require("nanoid"); +const passwordHash = require("../password-hash"); +const apicache = require("../modules/apicache"); +const APIKey = require("../model/api_key"); +const { Settings } = require("../settings"); +const { sendAPIKeyList } = require("../client"); + +/** + * Handlers for Maintenance + * @param {Socket} socket Socket.io instance + */ +module.exports.apiKeySocketHandler = (socket) => { + // Add a new api key + socket.on("addAPIKey", async (key, callback) => { + try { + checkLogin(socket); + + let clearKey = nanoid(40); + let hashedKey = passwordHash.generate(clearKey); + key["key"] = hashedKey; + let bean = await APIKey.save(key, socket.userID); + + log.debug("apikeys", "Added API Key"); + log.debug("apikeys", key); + + // Append key ID and prefix to start of key seperated by _, used to get + // correct hash when validating key. + let formattedKey = "uk" + bean.id + "_" + clearKey; + await sendAPIKeyList(socket); + + // Enable API auth if the user creates a key, otherwise only basic + // auth will be used for API. + await Settings.set("apiKeysEnabled", true); + + callback({ + ok: true, + msg: "Added Successfully.", + key: formattedKey, + keyID: bean.id, + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("getAPIKeyList", async (callback) => { + try { + checkLogin(socket); + await sendAPIKeyList(socket); + callback({ + ok: true, + }); + } catch (e) { + console.error(e); + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("deleteAPIKey", async (keyID, callback) => { + try { + checkLogin(socket); + + log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`); + + await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [ + keyID, + socket.userID, + ]); + + apicache.clear(); + + callback({ + ok: true, + msg: "Deleted Successfully.", + }); + + await sendAPIKeyList(socket); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("disableAPIKey", async (keyID, callback) => { + try { + checkLogin(socket); + + log.debug("apikeys", `Disabled Key: ${keyID} User ID: ${socket.userID}`); + + await R.exec("UPDATE api_key SET active = 0 WHERE id = ? ", [ + keyID, + ]); + + apicache.clear(); + + callback({ + ok: true, + msg: "Disabled Successfully.", + }); + + await sendAPIKeyList(socket); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("enableAPIKey", async (keyID, callback) => { + try { + checkLogin(socket); + + log.debug("apikeys", `Enabled Key: ${keyID} User ID: ${socket.userID}`); + + await R.exec("UPDATE api_key SET active = 1 WHERE id = ? ", [ + keyID, + ]); + + apicache.clear(); + + callback({ + ok: true, + msg: "Enabled Successfully", + }); + + await sendAPIKeyList(socket); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); +}; diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js index 11b47a5bf..bb4a38086 100644 --- a/server/socket-handlers/general-socket-handler.js +++ b/server/socket-handlers/general-socket-handler.js @@ -9,10 +9,10 @@ let gameList = null; /** * Get a game list via GameDig - * @returns {any[]} + * @returns {Object[]} list of games supported by GameDig */ function getGameList() { - if (!gameList) { + if (gameList == null) { gameList = gameResolver._readGames().games.sort((a, b) => { if ( a.pretty < b.pretty ) { return -1; @@ -22,9 +22,8 @@ function getGameList() { } return 0; }); - } else { - return gameList; } + return gameList; } module.exports.generalSocketHandler = (socket, server) => { diff --git a/server/util-server.js b/server/util-server.js index edce28901..2cf81f6aa 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -87,7 +87,10 @@ exports.ping = async (hostname, size = 56) => { return await exports.pingAsync(hostname, false, size); } catch (e) { // If the host cannot be resolved, try again with ipv6 - if (e.message.includes("service not known")) { + console.debug("ping", "IPv6 error message: " + e.message); + + // As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what. + if (!e.message) { return await exports.pingAsync(hostname, true, size); } else { throw e; @@ -292,14 +295,23 @@ exports.postgresQuery = function (connectionString, query) { client.end(); } else { // Connected here - client.query(query, (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); + try { + // No query provided by user, use SELECT 1 + if (!query || (typeof query === "string" && query.trim() === "")) { + query = "SELECT 1"; } - client.end(); - }); + + client.query(query, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + client.end(); + }); + } catch (e) { + reject(e); + } } }); @@ -730,15 +742,27 @@ exports.filterAndJoin = (parts, connector = "") => { }; /** - * Send a 403 response + * Send an Error response * @param {Object} res Express response object * @param {string} [msg=""] Message to send */ -module.exports.send403 = (res, msg = "") => { - res.status(403).json({ - "status": "fail", - "msg": msg, - }); +module.exports.sendHttpError = (res, msg = "") => { + if (msg.includes("SQLITE_BUSY") || msg.includes("SQLITE_LOCKED")) { + res.status(503).json({ + "status": "fail", + "msg": msg, + }); + } else if (msg.toLowerCase().includes("not found")) { + res.status(404).json({ + "status": "fail", + "msg": msg, + }); + } else { + res.status(403).json({ + "status": "fail", + "msg": msg, + }); + } }; function timeObjectConvertTimezone(obj, timezone, timeObjectToUTC = true) { diff --git a/src/components/APIKeyDialog.vue b/src/components/APIKeyDialog.vue new file mode 100644 index 000000000..745efd4ab --- /dev/null +++ b/src/components/APIKeyDialog.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/src/components/Confirm.vue b/src/components/Confirm.vue index 1a1addc6e..4bc2217cb 100644 --- a/src/components/Confirm.vue +++ b/src/components/Confirm.vue @@ -4,7 +4,7 @@ @@ -44,8 +44,13 @@ export default { type: String, default: "No", }, + /** Title to show on modal. Defaults to translated version of "Config" */ + title: { + type: String, + default: null, + } }, - emits: [ "yes" ], + emits: [ "yes", "no" ], data: () => ({ modal: null, }), @@ -63,6 +68,12 @@ export default { yes() { this.$emit("yes"); }, + /** + * @emits string "no" Notify the parent when No is pressed + */ + no() { + this.$emit("no"); + } }, }; diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue index 115660a5e..d64b43c18 100644 --- a/src/components/MonitorList.vue +++ b/src/components/MonitorList.vue @@ -19,7 +19,7 @@ {{ $t("No Monitors, please") }} {{ $t("add one") }} - +
diff --git a/src/components/NotificationDialog.vue b/src/components/NotificationDialog.vue index 0ca95c222..c3851b568 100644 --- a/src/components/NotificationDialog.vue +++ b/src/components/NotificationDialog.vue @@ -13,7 +13,10 @@
@@ -67,7 +70,7 @@ - diff --git a/src/components/notifications/PagerTree.vue b/src/components/notifications/PagerTree.vue new file mode 100644 index 000000000..0121f65ef --- /dev/null +++ b/src/components/notifications/PagerTree.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/notifications/Telegram.vue b/src/components/notifications/Telegram.vue index 723bd1be6..a7e46fded 100644 --- a/src/components/notifications/Telegram.vue +++ b/src/components/notifications/Telegram.vue @@ -28,6 +28,30 @@ {{ telegramGetUpdatesURL("masked") }}

+ + + +

{{ $t("telegramMessageThreadIDDescription") }}

+ +
+ + +
+ +
+ {{ $t("telegramSendSilentlyDescription") }} +
+
+ +
+
+ + +
+ +
+ {{ $t("telegramProtectContentDescription") }} +
diff --git a/src/components/notifications/index.js b/src/components/notifications/index.js index 3c8b26210..ed9dde0f1 100644 --- a/src/components/notifications/index.js +++ b/src/components/notifications/index.js @@ -22,6 +22,7 @@ import Ntfy from "./Ntfy.vue"; import Octopush from "./Octopush.vue"; import OneBot from "./OneBot.vue"; import PagerDuty from "./PagerDuty.vue"; +import PagerTree from "./PagerTree.vue"; import PromoSMS from "./PromoSMS.vue"; import Pushbullet from "./Pushbullet.vue"; import PushDeer from "./PushDeer.vue"; @@ -76,6 +77,7 @@ const NotificationFormList = { "octopush": Octopush, "OneBot": OneBot, "PagerDuty": PagerDuty, + "PagerTree": PagerTree, "promosms": PromoSMS, "pushbullet": Pushbullet, "PushByTechulus": TechulusPush, diff --git a/src/components/settings/APIKeys.vue b/src/components/settings/APIKeys.vue new file mode 100644 index 000000000..757789937 --- /dev/null +++ b/src/components/settings/APIKeys.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/components/settings/Tags.vue b/src/components/settings/Tags.vue index 347a6ef29..71ad9b7bd 100644 --- a/src/components/settings/Tags.vue +++ b/src/components/settings/Tags.vue @@ -1,5 +1,9 @@ + +
+ + +
+
@@ -503,6 +509,15 @@
+ +
+ + +
+
@@ -606,10 +621,10 @@
+ -
- -
+
+
@@ -673,13 +688,23 @@ export default { }, pageName() { - return this.$t((this.isAdd) ? "Add New Monitor" : "Edit"); + let name = "Add New Monitor"; + if (this.isClone) { + name = "Clone Monitor"; + } else if (this.isEdit) { + name = "Edit"; + } + return this.$t(name); }, isAdd() { return this.$route.path === "/add"; }, + isClone() { + return this.$route.path.startsWith("/clone"); + }, + isEdit() { return this.$route.path.startsWith("/edit"); }, @@ -723,6 +748,15 @@ message HealthCheckResponse { ` ]); }, bodyPlaceholder() { + if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") { + return this.$t("Example:", [ ` + + + + Kuma + +` ]); + } return this.$t("Example:", [ ` { "key": "value" @@ -872,6 +906,7 @@ message HealthCheckResponse { mqttTopic: "", mqttSuccessMessage: "", authMethod: null, + httpBodyEncoding: "json" }; if (this.$root.proxyList && !this.monitor.proxyId) { @@ -887,11 +922,32 @@ message HealthCheckResponse { this.monitor.notificationIDList[this.$root.notificationList[i].id] = true; } } - } else if (this.isEdit) { + } else if (this.isEdit || this.isClone) { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { if (res.ok) { this.monitor = res.monitor; + if (this.isClone) { + /* + * Cloning a monitor will include properties that can not be posted to backend + * as they are not valid columns in the SQLite table. + */ + this.monitor.id = undefined; // Remove id when cloning as we want a new id + this.monitor.includeSensitiveData = undefined; + this.monitor.maintenance = undefined; + this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]); + this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => { + return { + id: monitorTag.tag_id, + name: monitorTag.name, + color: monitorTag.color, + value: monitorTag.value, + new: true, + }; + }); + this.monitor.tags = undefined; + } + // Handling for monitors that are created before 1.7.0 if (this.monitor.retryInterval === 0) { this.monitor.retryInterval = this.monitor.interval; @@ -909,7 +965,7 @@ message HealthCheckResponse { * @returns {boolean} Is the form input valid? */ isInputValid() { - if (this.monitor.body) { + if (this.monitor.body && (!this.monitor.httpBodyEncoding || this.monitor.httpBodyEncoding === "json")) { try { JSON.parse(this.monitor.body); } catch (err) { @@ -933,6 +989,7 @@ message HealthCheckResponse { * @returns {void} */ async submit() { + this.processing = true; if (!this.isInputValid()) { @@ -940,11 +997,15 @@ message HealthCheckResponse { return; } - // Beautify the JSON format - if (this.monitor.body) { + // Beautify the JSON format (only if httpBodyEncoding is not set or === json) + if (this.monitor.body && (!this.monitor.httpBodyEncoding || this.monitor.httpBodyEncoding === "json")) { this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4); } + if (this.monitor.type && this.monitor.type !== "http" && this.monitor.type !== "keyword") { + this.monitor.httpBodyEncoding = null; + } + if (this.monitor.headers) { this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4); } @@ -957,7 +1018,7 @@ message HealthCheckResponse { this.monitor.url = this.monitor.url.trim(); } - if (this.isAdd) { + if (this.isAdd || this.isClone) { this.$root.add(this.monitor, async (res) => { if (res.ok) { @@ -1012,11 +1073,33 @@ message HealthCheckResponse { diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index b034a5411..d3c153df1 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -107,6 +107,9 @@ export default { security: { title: this.$t("Security"), }, + "api-keys": { + title: this.$t("API Keys") + }, proxies: { title: this.$t("Proxies"), }, diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index dcee15e7b..edf32561b 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -20,6 +20,9 @@
+
+ {{ $t("markdownSupported") }} +
@@ -258,7 +261,9 @@ {{ $t("Description") }}: - + + +
@@ -497,11 +502,27 @@ export default { }, incidentHTML() { - return DOMPurify.sanitize(marked(this.incident.content)); + if (this.incident.content != null) { + return DOMPurify.sanitize(marked(this.incident.content)); + } else { + return ""; + } + }, + + descriptionHTML() { + if (this.config.description != null) { + return DOMPurify.sanitize(marked(this.config.description)); + } else { + return ""; + } }, footerHTML() { - return DOMPurify.sanitize(marked(this.config.footerText)); + if (this.config.footerText != null) { + return DOMPurify.sanitize(marked(this.config.footerText)); + } else { + return ""; + } }, }, watch: { diff --git a/src/router.js b/src/router.js index 35647511f..4d5293b8e 100644 --- a/src/router.js +++ b/src/router.js @@ -18,6 +18,7 @@ import NotFound from "./pages/NotFound.vue"; import DockerHosts from "./components/settings/Docker.vue"; import MaintenanceDetails from "./pages/MaintenanceDetails.vue"; import ManageMaintenance from "./pages/ManageMaintenance.vue"; +import APIKeys from "./components/settings/APIKeys.vue"; import Plugins from "./components/settings/Plugins.vue"; // Settings - Sub Pages @@ -67,6 +68,10 @@ const routes = [ }, ], }, + { + path: "/clone/:id", + component: EditMonitor, + }, { path: "/add", component: EditMonitor, @@ -113,6 +118,10 @@ const routes = [ path: "security", component: Security, }, + { + path: "api-keys", + component: APIKeys, + }, { path: "proxies", component: Proxies,