Merge pull request #173 from chakflying/redirects&status

Feat: Implement Max.Redirects & Accepted Status Codes
This commit is contained in:
Louis Lam 2021-08-08 21:19:20 +08:00 committed by GitHub
commit 44391117ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 268 additions and 22 deletions

74
db/patch6.sql Normal file
View File

@ -0,0 +1,74 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
create table monitor_dg_tmp (
id INTEGER not null primary key autoincrement,
name VARCHAR(150),
active BOOLEAN default 1 not null,
user_id INTEGER references user on update cascade on delete
set
null,
interval INTEGER default 20 not null,
url TEXT,
type VARCHAR(20),
weight INTEGER default 2000,
hostname VARCHAR(255),
port INTEGER,
created_date DATETIME default (DATETIME('now')) not null,
keyword VARCHAR(255),
maxretries INTEGER NOT NULL DEFAULT 0,
ignore_tls BOOLEAN default 0 not null,
upside_down BOOLEAN default 0 not null,
maxredirects INTEGER default 10 not null,
accepted_statuscodes_json TEXT default '["200-299"]' not null
);
insert into
monitor_dg_tmp(
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
created_date,
keyword,
maxretries,
ignore_tls,
upside_down
)
select
id,
name,
active,
user_id,
interval,
url,
type,
weight,
hostname,
port,
created_date,
keyword,
maxretries,
ignore_tls,
upside_down
from
monitor;
drop table monitor;
alter table
monitor_dg_tmp rename to monitor;
create index user_id on monitor (user_id);
COMMIT;
PRAGMA foreign_keys = on;

5
package-lock.json generated
View File

@ -6859,6 +6859,11 @@
} }
} }
}, },
"vue-multiselect": {
"version": "3.0.0-alpha.2",
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.0.0-alpha.2.tgz",
"integrity": "sha512-Xp9fGJECns45v+v8jXbCIsAkCybYkEg0lNwr7Z6HDUSMyx2TEIK2giipPE+qXiShEc1Ipn+ZtttH2iq9hwXP4Q=="
},
"vue-router": { "vue-router": {
"version": "4.0.10", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.10.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.10.tgz",

View File

@ -52,6 +52,7 @@
"v-pagination-3": "^0.1.6", "v-pagination-3": "^0.1.6",
"vue": "^3.1.5", "vue": "^3.1.5",
"vue-confirm-dialog": "^1.0.2", "vue-confirm-dialog": "^1.0.2",
"vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "^4.0.10", "vue-router": "^4.0.10",
"vue-toastification": "^2.0.0-rc.1" "vue-toastification": "^2.0.0-rc.1"
}, },

View File

@ -8,7 +8,7 @@ class Database {
static templatePath = "./db/kuma.db" static templatePath = "./db/kuma.db"
static path = "./data/kuma.db"; static path = "./data/kuma.db";
static latestVersion = 5; static latestVersion = 6;
static noReject = true; static noReject = true;
static connect() { static connect() {

View File

@ -7,7 +7,7 @@ dayjs.extend(timezone)
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util"); const { debug, UP, DOWN, PENDING, flipStatus } = require("../../src/util");
const { tcping, ping, checkCertificate } = require("../util-server"); const { tcping, ping, checkCertificate, checkStatusCode } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model"); const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification") const { Notification } = require("../notification")
@ -45,6 +45,8 @@ class Monitor extends BeanModel {
keyword: this.keyword, keyword: this.keyword,
ignoreTls: this.getIgnoreTls(), ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(), upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
notificationIDList, notificationIDList,
}; };
} }
@ -65,6 +67,10 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown); return Boolean(this.upsideDown);
} }
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
let retries = 0; let retries = 0;
@ -111,6 +117,10 @@ class Monitor extends BeanModel {
maxCachedSessions: 0, maxCachedSessions: 0,
rejectUnauthorized: ! this.getIgnoreTls(), rejectUnauthorized: ! this.getIgnoreTls(),
}), }),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
}); });
bean.msg = `${res.status} - ${res.statusText}` bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;

View File

@ -1,4 +1,5 @@
console.log("Welcome to Uptime Kuma") console.log("Welcome to Uptime Kuma");
console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug } = require("../src/util"); const { sleep, debug } = require("../src/util");
@ -230,6 +231,9 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList; let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList; delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
bean.import(monitor) bean.import(monitor)
bean.user_id = socket.userID bean.user_id = socket.userID
await R.store(bean) await R.store(bean)
@ -274,6 +278,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.keyword = monitor.keyword; bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls; bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown; bean.upsideDown = monitor.upsideDown;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
await R.store(bean) await R.store(bean)

View File

@ -9,7 +9,7 @@ exports.tcping = function (hostname, port) {
address: hostname, address: hostname,
port: port, port: port,
attempts: 1, attempts: 1,
}, function(err, data) { }, function (err, data) {
if (err) { if (err) {
reject(err); reject(err);
@ -28,7 +28,7 @@ exports.ping = function (hostname) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ping = new Ping(hostname); const ping = new Ping(hostname);
ping.send(function(err, ms) { ping.send(function (err, ms) {
if (err) { if (err) {
reject(err) reject(err)
} else if (ms === null) { } else if (ms === null) {
@ -58,7 +58,7 @@ exports.setSetting = async function (key, value) {
let bean = await R.findOne("setting", " `key` = ? ", [ let bean = await R.findOne("setting", " `key` = ? ", [
key, key,
]) ])
if (! bean) { if (!bean) {
bean = R.dispense("setting") bean = R.dispense("setting")
bean.key = key; bean.key = key;
} }
@ -158,3 +158,32 @@ exports.checkCertificate = function (res) {
fingerprint, fingerprint,
}; };
} }
// Check if the provided status code is within the accepted ranges
// Param: status - the status code to check
// Param: accepted_codes - an array of accepted status codes
// Return: true if the status code is within the accepted ranges, false otherwise
// Will throw an error if the provided status code is not a valid range string or code string
exports.checkStatusCode = function (status, accepted_codes) {
if (accepted_codes == null || accepted_codes.length === 0) {
return false;
}
for (const code_range of accepted_codes) {
const code_range_split = code_range.split("-").map(string => parseInt(string));
if (code_range_split.length === 1) {
if (status === code_range_split[0]) {
return true;
}
} else if (code_range_split.length === 2) {
if (status >= code_range_split[0] && status <= code_range_split[1]) {
return true;
}
} else {
throw new Error("Invalid status code range");
}
}
return false;
}

View File

@ -5,6 +5,15 @@
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji; font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,noto sans,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol,noto color emoji;
} }
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background: #CCC;
border-radius: 20px;
}
.modal { .modal {
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
} }
@ -26,7 +35,7 @@
.shadow-box { .shadow-box {
overflow: hidden; //overflow: hidden; // Forget why add this, but multiple select hide by this
box-shadow: 0 15px 70px rgba(0, 0, 0, .1); box-shadow: 0 15px 70px rgba(0, 0, 0, .1);
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
@ -62,6 +71,10 @@
background-color: #090C10; background-color: #090C10;
color: $dark-font-color; color: $dark-font-color;
&::-webkit-scrollbar-thumb {
background: $dark-border-color;
}
.shadow-box { .shadow-box {
background-color: $dark-bg; background-color: $dark-bg;
} }
@ -132,4 +145,28 @@
border-color: $dark-border-color; border-color: $dark-border-color;
color: $dark-font-color; color: $dark-font-color;
} }
// Multiselect
.multiselect__tags {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect__input, .multiselect__single {
background-color: $dark-bg2;
color: $dark-font-color;
}
.multiselect__content-wrapper {
background-color: $dark-bg2;
border-color: $dark-border-color;
}
.multiselect--above .multiselect__content-wrapper {
border-color: $dark-border-color;
}
.multiselect__option--selected {
background-color: $dark-bg;
}
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<h1 class="mb-3"> <h1 class="my-3">
{{ pageName }} {{ pageName }}
</h1> </h1>
<form @submit.prevent="submit"> <form @submit.prevent="submit">
@ -8,7 +8,7 @@
<div class="col-md-6"> <div class="col-md-6">
<h2>General</h2> <h2>General</h2>
<div class="mb-3"> <div class="my-3">
<label for="type" class="form-label">Monitor Type</label> <label for="type" class="form-label">Monitor Type</label>
<select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example"> <select id="type" v-model="monitor.type" class="form-select" aria-label="Default select example">
<option value="http"> <option value="http">
@ -26,17 +26,17 @@
</select> </select>
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="name" class="form-label">Friendly Name</label> <label for="name" class="form-label">Friendly Name</label>
<input id="name" v-model="monitor.name" type="text" class="form-control" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="url" class="form-label">URL</label> <label for="url" class="form-label">URL</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
<div v-if="monitor.type === 'keyword' " class="mb-3"> <div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">Keyword</label> <label for="keyword" class="form-label">Keyword</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required> <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text"> <div class="form-text">
@ -44,22 +44,22 @@
</div> </div>
</div> </div>
<div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="mb-3"> <div v-if="monitor.type === 'port' || monitor.type === 'ping' " class="my-3">
<label for="hostname" class="form-label">Hostname</label> <label for="hostname" class="form-label">Hostname</label>
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required> <input id="hostname" v-model="monitor.hostname" type="text" class="form-control" required>
</div> </div>
<div v-if="monitor.type === 'port' " class="mb-3"> <div v-if="monitor.type === 'port' " class="my-3">
<label for="port" class="form-label">Port</label> <label for="port" class="form-label">Port</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1"> <input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> <label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
</div> </div>
<div class="mb-3"> <div class="my-3">
<label for="maxRetries" class="form-label">Retries</label> <label for="maxRetries" class="form-label">Retries</label>
<input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1"> <input id="maxRetries" v-model="monitor.maxretries" type="number" class="form-control" required min="0" step="1">
<div class="form-text"> <div class="form-text">
@ -67,16 +67,16 @@
</div> </div>
</div> </div>
<h2>Advanced</h2> <h2 class="my-3">Advanced</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="mb-3 form-check"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value=""> <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls"> <label class="form-check-label" for="ignore-tls">
Ignore TLS/SSL error for HTTPS websites Ignore TLS/SSL error for HTTPS websites
</label> </label>
</div> </div>
<div class="mb-3 form-check"> <div class="my-3 form-check">
<input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox"> <input id="upside-down" v-model="monitor.upsideDown" class="form-check-input" type="checkbox">
<label class="form-check-label" for="upside-down"> <label class="form-check-label" for="upside-down">
Upside Down Mode Upside Down Mode
@ -86,6 +86,36 @@
</div> </div>
</div> </div>
<div class="my-3">
<label for="maxRedirects" class="form-label">Max. Redirects</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
Maximum number of redirects to follow. Set to 0 to disable redirects.
</div>
</div>
<div class="my-3">
<label for="acceptedStatusCodes" class="form-label">Accepted Status Codes</label>
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitor.accepted_statuscodes"
:options="acceptedStatusCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
placeholder="Pick Accepted Status Codes..."
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
<div class="form-text">
Select status codes which are considered as a successful response.
</div>
</div>
<div> <div>
<button class="btn btn-primary" type="submit" :disabled="processing"> <button class="btn btn-primary" type="submit" :disabled="processing">
Save Save
@ -101,7 +131,7 @@
Not available, please setup. Not available, please setup.
</p> </p>
<div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch mb-3"> <div v-for="notification in $root.notificationList" :key="notification.id" class="form-check form-switch my-3">
<input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox"> <input :id=" 'notification' + notification.id" v-model="monitor.notificationIDList[notification.id]" class="form-check-input" type="checkbox">
<label class="form-check-label" :for=" 'notification' + notification.id"> <label class="form-check-label" :for=" 'notification' + notification.id">
@ -124,20 +154,25 @@
<script> <script>
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect"
const toast = useToast() const toast = useToast()
export default { export default {
components: { components: {
NotificationDialog, NotificationDialog,
VueMultiselect,
}, },
data() { data() {
return { return {
processing: false, processing: false,
monitor: { monitor: {
notificationIDList: {}, notificationIDList: {},
}, },
acceptedStatusCodeOptions: [],
} }
}, },
computed: { computed: {
pageName() { pageName() {
return (this.isAdd) ? "Add New Monitor" : "Edit" return (this.isAdd) ? "Add New Monitor" : "Edit"
@ -156,6 +191,20 @@ export default {
}, },
mounted() { mounted() {
this.init(); this.init();
let acceptedStatusCodeOptions = [
"100-199",
"200-299",
"300-399",
"400-499",
"500-599",
];
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
}, },
methods: { methods: {
init() { init() {
@ -170,6 +219,8 @@ export default {
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
upsideDown: false, upsideDown: false,
maxredirects: 10,
accepted_statuscodes: ["200-299"],
} }
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
@ -209,6 +260,40 @@ export default {
} }
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss">
@import "../assets/vars.scss";
.multiselect__tags {
border-radius: 1.5rem;
border: 1px solid #ced4da;
}
.multiselect--active .multiselect__tags {
border-radius: 1rem;
}
.multiselect__option--highlight {
background: $primary !important;
}
.multiselect__option--highlight::after {
background: $primary !important;
}
.multiselect__tag {
border-radius: 50rem;
background: $primary !important;
}
.dark {
.multiselect__tag {
color: $dark-font-color2;
}
}
</style>
<style scoped> <style scoped>
.shadow-box { .shadow-box {
padding: 20px; padding: 20px;

View File

@ -29,7 +29,7 @@ function ucfirst(str) {
exports.ucfirst = ucfirst; exports.ucfirst = ucfirst;
function debug(msg) { function debug(msg) {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.debug(msg); console.log(msg);
} }
} }
exports.debug = debug; exports.debug = debug;

View File

@ -39,6 +39,6 @@ export function ucfirst(str) {
export function debug(msg) { export function debug(msg) {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.debug(msg); console.log(msg);
} }
} }