Compare commits

...

3 commits

Author SHA1 Message Date
Frank Elsinga
15a26cf48b
Merge ff197839b3 into c01494ec33 2024-10-20 13:57:15 +00:00
Frank Elsinga
ff197839b3 Moved the DebugMonitorDialog into its own component 2024-10-09 04:53:35 +02:00
Frank Elsinga
c90aacc62c made sure that the debug output for editing the monitor is shell-escaped 2024-10-09 03:45:07 +02:00
2 changed files with 206 additions and 115 deletions

View file

@ -0,0 +1,201 @@
<template>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<div v-if="monitor?.type === 'http'">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button
id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0"
type="button" @click.stop="copyToClipboard"
>
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a
href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/"
target="_blank"
>{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div
v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2"
role="alert"
>
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import { version } from "../../package.json";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
name: "DebugMonitor",
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
},
data() {
return {
modal: null,
};
},
computed: {
curlCommand() {
if (this.monitor === null) {
return "";
}
let method = this.monitor.method;
if ([ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" ].indexOf(method) === -1) {
// set to a custom value => could lead to injections
method = this.escapeShell(method);
}
const command = [ "curl", "--verbose", "--head", "--request", method, "\\\n" ];
command.push("--user-agent", `'Uptime-Kuma/${version}'`, "\\\n");
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${this.escapeShellNoQuotes(key)}: ${this.escapeShellNoQuotes(value)}'`, "\\\n");
}
} catch (e) {
command.push("--header", this.escapeShell(this.monitor.headers), "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--basic", "--user", `'${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
} else if (this.monitor.authMethod === "mtls") {
command.push("--cacert", this.escapeShell(this.monitor.tlsCa), "\\\n");
command.push("--key", this.escapeShell(this.monitor.tlsKey), "\\\n");
command.push( "--cert", this.escapeShell(this.monitor.tlsCert), "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
let domain = "";
if (this.monitor.authDomain) {
domain = `${this.monitor.authDomain}/`;
}
command.push("--ntlm", "--user", `'${this.escapeShellNoQuotes(domain)}${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n");
command.push("--data", this.escapeShell(json), "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n");
command.push("--data", this.escapeShell(this.monitor.body), "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.escapeShell(this.monitor.maxredirects), "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.escapeShell(this.monitor.timeout), "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.escapeShell(this.monitor.maxretries), "\\\n");
}
command.push("--url", this.escapeShell(this.monitor.url));
return command.join(" ");
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
/**
* Show the dialog
* @returns {void}
*/
show() {
this.modal.show();
},
/**
* Escape a string for use in a shell
* @param {string|number} s string to escape
* @returns {string} escaped, quoted string
*/
escapeShell(s) {
if (typeof s == "number") {
return s.toString();
}
return "'" + this.escapeShellNoQuotes(s) + "'";
},
/**
* Escape a string for use in a shell
* @param {string} s string to escape
* @returns {string} escaped string
*/
escapeShellNoQuotes(s) {
return s.replace(/(['"$`\\])/g, "\\$1");
},
/**
* Copies a value to the clipboard and shows toasts with the success/error
* @returns {void}
*/
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
}
};
</script>
<style lang="scss" scoped>
@import "../assets/vars";
textarea {
min-height: 200px;
}
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style>

View file

@ -1042,7 +1042,7 @@
class="btn btn-outline-primary" class="btn btn-outline-primary"
type="button" type="button"
:disabled="processing" :disabled="processing"
@click.stop="modal.show()" @click="$refs.debugMonitorDialog.show()"
> >
{{ $t("Debug") }} {{ $t("Debug") }}
</button> </button>
@ -1057,58 +1057,10 @@
<RemoteBrowserDialog ref="remoteBrowserDialog" /> <RemoteBrowserDialog ref="remoteBrowserDialog" />
</div> </div>
</transition> </transition>
<div ref="modal" class="modal fade" tabindex="-1"> <DebugMonitorDialog ref="debugMonitorDialog" :monitor="monitor" />
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0" type="button" @click.stop="copyToClipboard">
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/" target="_blank">{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import ActionSelect from "../components/ActionSelect.vue"; import ActionSelect from "../components/ActionSelect.vue";
@ -1123,8 +1075,7 @@ import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } fro
import { hostNameRegexPattern } from "../util-frontend"; import { hostNameRegexPattern } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue"; import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue"; import EditMonitorConditions from "../components/EditMonitorConditions.vue";
import { version } from "../../package.json"; import DebugMonitorDialog from "../components/DebugMonitorDialog.vue";
const userAgent = `'Uptime-Kuma/${version}'`;
const toast = useToast(); const toast = useToast();
@ -1177,6 +1128,7 @@ const monitorDefaults = {
export default { export default {
components: { components: {
DebugMonitorDialog,
HiddenInput, HiddenInput,
ActionSelect, ActionSelect,
ProxyDialog, ProxyDialog,
@ -1219,58 +1171,9 @@ export default {
}, },
computed: { computed: {
curlCommand() {
const command = [ "curl", "--verbose", "--head", "--request", this.monitor.method, "\\\n", "--user-agent", userAgent, "\\\n" ];
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
// trying to parse the supplied data as json to trim whitespace
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${key}: ${value}'`, "\\\n");
}
} catch (e) {
command.push("--header", `'${this.monitor.headers}'`, "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--user", `${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}`, "--basic", "\\\n");
} else if (this.monitor.authmethod === "mtls") {
command.push("--cacert", `'${this.monitor.tlsCa}'`, "\\\n", "--key", `'${this.monitor.tlsKey}'`, "\\\n", "--cert", `'${this.monitor.tlsCert}'`, "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
command.push("--user", `'${this.monitor.authDomain ? `${this.monitor.authDomain}/` : ""}${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}'`, "--ntlm", "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n", "--data", `'${json}'`, "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n", "--data", `'${this.monitor.body}'`, "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.monitor.maxredirects, "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.monitor.timeout, "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.monitor.maxretries, "\\\n");
}
command.push("--url", this.monitor.url);
return command.join(" ");
},
ipRegex() { ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300) // Allow to test with simple dns server with port (127.0.0.1:5300)
if (! isDev) { if (!isDev) {
return this.ipRegexPattern; return this.ipRegexPattern;
} }
return null; return null;
@ -1623,7 +1526,6 @@ message HealthCheckResponse {
}, },
}, },
mounted() { mounted() {
this.modal = new Modal(this.$refs.modal);
this.init(); this.init();
let acceptedStatusCodeOptions = [ let acceptedStatusCodeOptions = [
@ -1664,14 +1566,6 @@ message HealthCheckResponse {
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions; this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
}, },
methods: { methods: {
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
/** /**
* Initialize the edit monitor form * Initialize the edit monitor form
* @returns {void} * @returns {void}
@ -1975,8 +1869,4 @@ message HealthCheckResponse {
min-height: 200px; min-height: 200px;
} }
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style> </style>