feat(http-requests): add support for methods, body and headers for http

This commit is contained in:
Bert Verhelst 2021-10-02 16:48:27 +02:00
parent c93f42794f
commit 3f0b85e5a8
8 changed files with 144 additions and 7 deletions

View file

@ -0,0 +1,13 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD method TEXT default 'GET' not null;
ALTER TABLE monitor
ADD body TEXT default null;
ALTER TABLE monitor
ADD headers TEXT default null;
COMMIT;

View file

@ -49,6 +49,7 @@ class Database {
"patch-incident-table.sql": true, "patch-incident-table.sql": true,
"patch-group-table.sql": true, "patch-group-table.sql": true,
"patch-monitor-push_token.sql": true, "patch-monitor-push_token.sql": true,
"patch-http-monitor-method-body-and-headers.sql": true,
} }
/** /**

View file

@ -53,6 +53,9 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
url: this.url, url: this.url,
method: this.method,
body: this.body,
headers: this.headers,
hostname: this.hostname, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries, maxretries: this.maxretries,
@ -95,6 +98,31 @@ class Monitor extends BeanModel {
return JSON.parse(this.accepted_statuscodes_json); return JSON.parse(this.accepted_statuscodes_json);
} }
/**
* Convert header string into an object:
* eg:
*
* Authorization: Basic <credentials>
* Content-Type: application/json
*
* into
*
* {
* "Authorization": "Basic <credentials>",
* "Content-Type": "application/json"
* }
**/
getParsedHeaders() {
if (!this.headers || !this.headers.includes(":")) {
return {};
}
return Object.fromEntries(this.headers.split("\n").map(header => {
const trimmedHeader = header.trim();
const firstColonIndex = trimmedHeader.indexOf(":");
return [trimmedHeader.slice(0, firstColonIndex), trimmedHeader.slice(firstColonIndex + 1) || ""];
}).filter(arr => !!arr[0] && !!arr[1]));
}
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
let retries = 0; let retries = 0;
@ -136,11 +164,15 @@ class Monitor extends BeanModel {
// Do not do any queries/high loading things before the "bean.ping" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
let res = await axios.get(this.url, { const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
...(this.body ? { data: JSON.parse(this.body) } : {}),
timeout: this.interval * 1000 * 0.8, timeout: this.interval * 1000 * 0.8,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
...this.getParsedHeaders(),
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
@ -150,7 +182,8 @@ class Monitor extends BeanModel {
validateStatus: (status) => { validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes()); return checkStatusCode(status, this.getAcceptedStatuscodes());
}, },
}); };
let res = await axios.request(options);
bean.msg = `${res.status} - ${res.statusText}`; bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;

View file

@ -505,6 +505,9 @@ exports.entryPage = "dashboard";
bean.name = monitor.name; bean.name = monitor.name;
bean.type = monitor.type; bean.type = monitor.type;
bean.url = monitor.url; bean.url = monitor.url;
bean.method = monitor.method;
bean.body = monitor.body;
bean.headers = monitor.headers;
bean.interval = monitor.interval; bean.interval = monitor.interval;
bean.retryInterval = monitor.retryInterval; bean.retryInterval = monitor.retryInterval;
bean.hostname = monitor.hostname; bean.hostname = monitor.hostname;
@ -1028,6 +1031,9 @@ exports.entryPage = "dashboard";
name: monitorListData[i].name, name: monitorListData[i].name,
type: monitorListData[i].type, type: monitorListData[i].type,
url: monitorListData[i].url, url: monitorListData[i].url,
method: monitorListData[i].method || "GET",
body: monitorListData[i].body,
headers: monitorListData[i].headers,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
hostname: monitorListData[i].hostname, hostname: monitorListData[i].hostname,

View file

@ -14,6 +14,10 @@ h2 {
font-size: 26px; font-size: 26px;
} }
textarea.form-control {
border-radius: 19px;
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
} }
@ -413,4 +417,4 @@ h2 {
// Localization // Localization
@import "localization.scss"; @import "localization.scss";

View file

@ -21,7 +21,7 @@
} }
.multiselect__tag { .multiselect__tag {
border-radius: 50rem; border-radius: $border-radius;
margin-bottom: 0; margin-bottom: 0;
padding: 6px 26px 6px 10px; padding: 6px 26px 6px 10px;
background: $primary !important; background: $primary !important;

View file

@ -186,7 +186,7 @@ export default {
.beat { .beat {
display: inline-block; display: inline-block;
background-color: $primary; background-color: $primary;
border-radius: 50rem; border-radius: $border-radius;
&.empty { &.empty {
background-color: aliceblue; background-color: aliceblue;

View file

@ -44,6 +44,46 @@
<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>
<!-- Method -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="method" class="form-label">{{ $t("Method") }}</label>
<select id="method" v-model="monitor.method" class="form-select">
<option value="GET">
GET
</option>
<option value="POST">
POST
</option>
<option value="PUT">
PUT
</option>
<option value="PATCH">
PATCH
</option>
<option value="DELETE">
DELETE
</option>
<option value="HEAD">
HEAD
</option>
<option value="OPTIONS">
OPTIONS
</option>
</select>
</div>
<!-- Body -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="body" class="form-label">{{ $t("Body") }}</label>
<textarea id="body" v-model="monitor.body" class="form-control" :placeholder="bodyPlaceholder"></textarea>
</div>
<!-- Headers -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<label for="headers" class="form-label">{{ $t("Headers") }}</label>
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
</div>
<!-- Push URL --> <!-- Push URL -->
<div v-if="monitor.type === 'push' " class="my-3"> <div v-if="monitor.type === 'push' " class="my-3">
<label for="push-url" class="form-label">{{ $t("Push URL") }}</label> <label for="push-url" class="form-label">{{ $t("Push URL") }}</label>
@ -285,6 +325,18 @@ export default {
pushURL() { pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping="; return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
},
bodyPlaceholder() {
return `{
"id": 124357,
"username": "admin",
"password": "myAdminPassword"
}`;
},
headersPlaceholder() {
return "Authorization: Bearer abc123\nContent-Type: application/json";
} }
}, },
@ -295,7 +347,7 @@ export default {
}, },
"monitor.interval"(value, oldValue) { "monitor.interval"(value, oldValue) {
// Link interval and retryInerval if they are the same value. // Link interval and retryInterval if they are the same value.
if (this.monitor.retryInterval === oldValue) { if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value; this.monitor.retryInterval = value;
} }
@ -349,6 +401,7 @@ export default {
type: "http", type: "http",
name: "", name: "",
url: "https://", url: "https://",
method: "GET",
interval: 60, interval: 60,
retryInterval: this.interval, retryInterval: this.interval,
maxretries: 0, maxretries: 0,
@ -383,9 +436,32 @@ export default {
}, },
isInputValid() {
if (this.monitor.body) {
try {
JSON.parse(this.monitor.body);
} catch (err) {
toast.error(this.$t("The request body is not valid json: ") + err.message);
return false;
}
}
if (this.monitor.headers) {
if (!/^([^:]+:.*)([\s]+[^:]+:.*)+$/g.test(this.monitor.headers)) {
toast.error(this.$t("Headers do not have a valid format: \"key: value<new line>key: value<new line>...\""));
return false;
}
}
return true;
},
async submit() { async submit() {
this.processing = true; this.processing = true;
if (!this.isInputValid()) {
this.processing = false;
return;
}
if (this.isAdd) { if (this.isAdd) {
this.$root.add(this.monitor, async (res) => { this.$root.add(this.monitor, async (res) => {
@ -422,8 +498,12 @@ export default {
}; };
</script> </script>
<style scoped> <style lang="scss" scoped>
.shadow-box { .shadow-box {
padding: 20px; padding: 20px;
} }
textarea {
min-height: 200px;
}
</style> </style>