Skip to content

Commit 6a2eedd

Browse files
Moved the DebugMonitorDialog into its own component
1 parent c90aacc commit 6a2eedd

File tree

2 files changed

+215
-147
lines changed

2 files changed

+215
-147
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<template>
2+
<div ref="modal" class="modal fade" tabindex="-1">
3+
<div class="modal-dialog modal-dialog-centered">
4+
<div class="modal-content">
5+
<div class="modal-body">
6+
<div v-if="monitor?.type === 'http'">
7+
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
8+
<button
9+
id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0"
10+
type="button" @click.stop="copyToClipboard"
11+
>
12+
<font-awesome-icon icon="copy" />
13+
</button>
14+
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
15+
<template #newiline>
16+
<br>
17+
</template>
18+
<template #firewalls>
19+
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
20+
</template>
21+
<template #dns_resolvers>
22+
<a
23+
href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/"
24+
target="_blank"
25+
>{{ $t('dns resolvers') }}</a>
26+
</template>
27+
<template #docker_networks>
28+
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
29+
</template>
30+
</i18n-t>
31+
<div
32+
v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2"
33+
role="alert"
34+
>
35+
<div role="img" aria-label="Warning:">⚠️</div>
36+
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
37+
<template #curl>
38+
<code>curl</code>
39+
</template>
40+
<template #newline>
41+
<br>
42+
</template>
43+
<template #oauth2_bearer>
44+
<code>--oauth2-bearer TOKEN</code>
45+
</template>
46+
</i18n-t>
47+
</div>
48+
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
49+
<div role="img" aria-label="Warning:">⚠️</div>
50+
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
51+
<template #curl>
52+
<code>curl</code>
53+
</template>
54+
</i18n-t>
55+
</div>
56+
</div>
57+
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @click="confirm">
58+
{{ $t("Confirm") }}
59+
</button>
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
</template>
65+
<script>
66+
import { Modal } from "bootstrap";
67+
import { version } from "../../package.json";
68+
import { useToast } from "vue-toastification";
69+
const toast = useToast();
70+
export default {
71+
name: "DebugMonitor",
72+
props: {
73+
/** Monitor this represents */
74+
monitor: {
75+
type: Object,
76+
default: null,
77+
},
78+
},
79+
data() {
80+
return {
81+
modal: null,
82+
};
83+
},
84+
computed: {
85+
curlCommand() {
86+
if (this.monitor === null) {
87+
return "";
88+
}
89+
let method = this.monitor.method;
90+
if ([ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" ].indexOf(method) === -1) {
91+
// set to a custom value => could lead to injections
92+
method = this.escapeShell(method);
93+
}
94+
const command = [ "curl", "--verbose", "--head", "--request", method, "\\\n" ];
95+
command.push("--user-agent", `'Uptime-Kuma/${version}'`, "\\\n");
96+
if (this.monitor.ignoreTls) {
97+
command.push("--insecure", "\\\n");
98+
}
99+
if (this.monitor.headers) {
100+
try {
101+
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
102+
command.push("--header", `'${this.escapeShellNoQuotes(key)}: ${this.escapeShellNoQuotes(value)}'`, "\\\n");
103+
}
104+
} catch (e) {
105+
command.push("--header", this.escapeShell(this.monitor.headers), "\\\n");
106+
}
107+
}
108+
if (this.monitor.authMethod === "basic") {
109+
command.push("--basic", "--user", `'${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
110+
} else if (this.monitor.authMethod === "mtls") {
111+
command.push("--cacert", this.escapeShell(this.monitor.tlsCa), "\\\n");
112+
command.push("--key", this.escapeShell(this.monitor.tlsKey), "\\\n");
113+
command.push( "--cert", this.escapeShell(this.monitor.tlsCert), "\\\n");
114+
} else if (this.monitor.authMethod === "ntlm") {
115+
let domain = "";
116+
if (this.monitor.authDomain) {
117+
domain = `${this.monitor.authDomain}/`;
118+
}
119+
command.push("--ntlm", "--user", `'${this.escapeShellNoQuotes(domain)}${this.escapeShellNoQuotes(this.monitor.basic_auth_user)}:${this.escapeShellNoQuotes(this.monitor.basic_auth_pass)}'`, "\\\n");
120+
}
121+
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
122+
let json = "";
123+
try {
124+
// trying to parse the supplied data as json to trim whitespace
125+
json = JSON.stringify(JSON.parse(this.monitor.body));
126+
} catch (e) {
127+
json = this.monitor.body;
128+
}
129+
command.push("--header", "'Content-Type: application/json'", "\\\n");
130+
command.push("--data", this.escapeShell(json), "\\\n");
131+
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
132+
command.push("--headers", "'Content-Type: application/xml'", "\\\n");
133+
command.push("--data", this.escapeShell(this.monitor.body), "\\\n");
134+
}
135+
if (this.monitor.maxredirects) {
136+
command.push("--location", "--max-redirs", this.escapeShell(this.monitor.maxredirects), "\\\n");
137+
}
138+
if (this.monitor.timeout) {
139+
command.push("--max-time", this.escapeShell(this.monitor.timeout), "\\\n");
140+
}
141+
if (this.monitor.maxretries) {
142+
command.push("--retry", this.escapeShell(this.monitor.maxretries), "\\\n");
143+
}
144+
command.push("--url", this.escapeShell(this.monitor.url));
145+
return command.join(" ");
146+
},
147+
},
148+
mounted() {
149+
this.modal = new Modal(this.$refs.modal);
150+
},
151+
methods: {
152+
/**
153+
* Show the confirm dialog
154+
* @returns {void}
155+
*/
156+
show() {
157+
this.modal.show();
158+
},
159+
/**
160+
* Dialog confirmed
161+
* @returns {void}
162+
*/
163+
confirm() {
164+
this.modal.hide();
165+
},
166+
/**
167+
* Escape a string for use in a shell
168+
* @param {string|number} s string to escape
169+
* @returns {string} escaped, quoted string
170+
*/
171+
escapeShell(s) {
172+
if (typeof s == "number") {
173+
return s.toString();
174+
}
175+
return "'" + this.escapeShellNoQuotes(s) + "'";
176+
},
177+
/**
178+
* Escape a string for use in a shell
179+
* @param {string} s string to escape
180+
* @returns {string} escaped string
181+
*/
182+
escapeShellNoQuotes(s) {
183+
return s.replace(/(['$`\\])/g, "\\$1");
184+
},
185+
/**
186+
* Copies a value to the clipboard and shows toasts with the success/error
187+
* @returns {void}
188+
*/
189+
async copyToClipboard() {
190+
try {
191+
await navigator.clipboard.writeText(this.curlCommand);
192+
toast.success(this.$t("CopyToClipboardSuccess"));
193+
} catch (err) {
194+
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
195+
}
196+
},
197+
}
198+
};
199+
</script>
200+
<style lang="scss" scoped>
201+
@import "../assets/vars";
202+
203+
textarea {
204+
min-height: 200px;
205+
}
206+
207+
#curl-debug {
208+
font-family: monospace;
209+
overflow: auto;
210+
}
211+
</style>

0 commit comments

Comments
 (0)