From 3daf71c6326e8631d012a3edd471066575c699a7 Mon Sep 17 00:00:00 2001 From: maryamsaleem Date: Tue, 26 Nov 2024 13:59:13 +0500 Subject: [PATCH 01/11] Sip Options and register ping From 78d46662b9748299ec600a4f5e9ddb74b904e994 Mon Sep 17 00:00:00 2001 From: maryamsaleem Date: Thu, 6 Feb 2025 14:30:09 +0500 Subject: [PATCH 02/11] Sip module code added --- db/old_migrations/patch-add-sip-fields.sql | 67 ++++++ db/old_migrations/patch-sip-auth.sql | 11 + package-lock.json | 69 +++++- package.json | 2 + server/database.js | 2 + server/model/monitor.js | 238 ++++++++++++++++++++- server/notification-providers/sip.js | 64 ++++++ server/server.js | 7 + server/util-server.js | 210 ++++++++++++++++++ src/lang/en.json | 7 +- src/pages/EditMonitor.vue | 131 +++++++++++- 11 files changed, 789 insertions(+), 19 deletions(-) create mode 100644 db/old_migrations/patch-add-sip-fields.sql create mode 100644 db/old_migrations/patch-sip-auth.sql create mode 100644 server/notification-providers/sip.js diff --git a/db/old_migrations/patch-add-sip-fields.sql b/db/old_migrations/patch-add-sip-fields.sql new file mode 100644 index 0000000000..25959e7711 --- /dev/null +++ b/db/old_migrations/patch-add-sip-fields.sql @@ -0,0 +1,67 @@ +BEGIN TRANSACTION; + + + +ALTER TABLE monitor + + ADD sip_auth_method VARCHAR(10) default null; + +COMMIT; + + + + + +BEGIN TRANSACTION; + + + +ALTER TABLE monitor + + ADD sip_protocol VARCHAR(10); + +COMMIT; + + + +BEGIN TRANSACTION; + + + +ALTER TABLE monitor + + ADD sip_port INT; + + + +ALTER TABLE monitor + + ADD sip_url VARCHAR(255); + + + +COMMIT; + +BEGIN TRANSACTION; + + + +ALTER TABLE monitor + + ADD sip_maintainence BOOLEAN; + + + +COMMIT; + +BEGIN TRANSACTION; + + + +ALTER TABLE monitor + + ADD COLUMN sip_method VARCHAR(250) NULL; + + + +COMMIT; \ No newline at end of file diff --git a/db/old_migrations/patch-sip-auth.sql b/db/old_migrations/patch-sip-auth.sql new file mode 100644 index 0000000000..0d3651f883 --- /dev/null +++ b/db/old_migrations/patch-sip-auth.sql @@ -0,0 +1,11 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD sip_basic_auth_user TEXT default null; + +ALTER TABLE monitor + ADD sip_basic_auth_pass TEXT default null; + + +COMMIT; diff --git a/package-lock.json b/package-lock.json index 100cdcd882..39d8e8988f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "2.0.0-dev", + "version": "2.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "2.0.0-dev", + "version": "2.0.0-beta.0", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.8.22", @@ -75,6 +75,7 @@ "redbean-node": "~0.3.0", "redis": "~4.5.1", "semver": "~7.5.4", + "sip": "^0.0.6", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", "socks-proxy-agent": "6.1.1", @@ -82,6 +83,7 @@ "tcp-ping": "~0.1.1", "thirty-two": "~1.0.2", "tough-cookie": "~4.1.3", + "uuid": "^11.0.5", "ws": "^8.13.0" }, "devDependencies": { @@ -1238,6 +1240,14 @@ "node": ">=16" } }, + "node_modules/@azure/msal-node/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", @@ -5200,6 +5210,15 @@ "node": ">=10" } }, + "node_modules/aedes/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -5620,6 +5639,11 @@ "dev": true, "license": "MIT" }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "node_modules/async-lock": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", @@ -10039,6 +10063,15 @@ "uuid-parse": "^1.1.0" } }, + "node_modules/hyperid/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -14588,6 +14621,25 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/sip": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/sip/-/sip-0.0.6.tgz", + "integrity": "sha512-t+FYic4EQ25GTsIRWFVvsq+GmVkoZhrcoghANlnN6CsWMHGcfjPDYMD+nTBNrHR/WnRykF4nqx4i+gahAnW5NA==", + "dependencies": { + "ws": "^6.1.0" + }, + "engines": { + "node": ">=0.2.2" + } + }, + "node_modules/sip/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/sirv": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", @@ -16205,12 +16257,15 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/uuid-parse": { diff --git a/package.json b/package.json index 4f8eef138f..5d21195f32 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "redbean-node": "~0.3.0", "redis": "~4.5.1", "semver": "~7.5.4", + "sip": "^0.0.6", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", "socks-proxy-agent": "6.1.1", @@ -140,6 +141,7 @@ "tcp-ping": "~0.1.1", "thirty-two": "~1.0.2", "tough-cookie": "~4.1.3", + "uuid": "^11.0.5", "ws": "^8.13.0" }, "devDependencies": { diff --git a/server/database.js b/server/database.js index 3b7646de8c..2ef1a27788 100644 --- a/server/database.js +++ b/server/database.js @@ -112,6 +112,8 @@ class Database { "patch-fix-kafka-producer-booleans.sql": true, "patch-timeout.sql": true, "patch-monitor-tls-info-add-fk.sql": true, // The last file so far converted to a knex migration file + "patch-add-sip-fields.sql": true, + "patch-sip-auth.sql": true, }; /** diff --git a/server/model/monitor.js b/server/model/monitor.js index 3ad8cfafc1..fed3278dec 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -5,7 +5,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI SQL_DATETIME_FORMAT, evaluateJsonQuery } = require("../../src/util"); const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, - redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal + redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, sipRegisterRequest,sipOptionRequest } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); @@ -34,6 +34,163 @@ const rootCertificates = rootCertificatesFingerprints(); * 2 = PENDING * 3 = MAINTENANCE */ + +const sipStatusCodes = [ + { status: 100, + msg: "Trying" }, + { status: 180, + msg: "Ringing" }, + { status: 181, + msg: "Call Being Forwarded" }, + { status: 182, + msg: "Queued" }, + { status: 183, + msg: "Session Progress" }, + { status: 199, + msg: "Early Dialog Terminated" }, + { status: 200, + msg: "OK" }, + { status: 202, + msg: "Accepted" }, + { status: 204, + msg: "No Notification" }, + { status: 300, + msg: "Multiple Choices" }, + { status: 301, + msg: "Moved Permanently" }, + { status: 302, + msg: "Moved Temporarily" }, + { status: 305, + msg: "Use Proxy" }, + { status: 380, + msg: "Alternate Service" }, + { status: 400, + msg: "Bad Request" }, + { status: 401, + msg: "Unauthorized" }, + { status: 402, + msg: "Payment Required" }, + { status: 403, + msg: "Forbidden" }, + { status: 404, + msg: "Not Found" }, + { status: 405, + msg: "Method Not Allowed" }, + { status: 406, + msg: "Not Acceptable" }, + { status: 407, + msg: "Proxy Authentication Required" }, + { status: 408, + msg: "Request Timeout" }, + { status: 409, + msg: "Conflict" }, + { status: 410, + msg: "Gone" }, + { status: 411, + msg: "Length Required" }, + { status: 412, + msg: "Conditional Request Failed" }, + { status: 413, + msg: "Request Entity Too Large" }, + { status: 414, + msg: "Request-URI Too Long" }, + { status: 415, + msg: "Unsupported Media Type" }, + { status: 416, + msg: "Unsupported URI Scheme" }, + { status: 417, + msg: "Unknown Resource-Priority" }, + { status: 420, + msg: "Bad Extension" }, + { status: 421, + msg: "Extension Required" }, + { status: 422, + msg: "Session Interval Too Small" }, + { status: 423, + msg: "Interval Too Brief" }, + { status: 424, + msg: "Bad Location Information" }, + { status: 425, + msg: "Bad Alert Message" }, + { status: 428, + msg: "Use Identity Header" }, + { status: 429, + msg: "Provide Referrer Identity" }, + { status: 430, + msg: "Flow Failed" }, + { status: 433, + msg: "Anonymity Disallowed" }, + { status: 436, + msg: "Bad Identity-Info" }, + { status: 437, + msg: "Unsupported Certificate" }, + { status: 438, + msg: "Invalid Identity Header" }, + { status: 439, + msg: "First Hop Lacks Outbound Support" }, + { status: 440, + msg: "Max-Breadth Exceeded" }, + { status: 469, + msg: "Bad Info Package" }, + { status: 470, + msg: "Consent Needed" }, + { status: 480, + msg: "Temporarily Unavailable" }, + { status: 481, + msg: "Call/Transaction Does Not Exist" }, + { status: 482, + msg: "Loop Detected" }, + { status: 483, + msg: "Too Many Hops" }, + { status: 484, + msg: "Address Incomplete" }, + { status: 485, + msg: "Ambiguous" }, + { status: 486, + msg: "Busy Here" }, + { status: 487, + msg: "Request Terminated" }, + { status: 488, + msg: "Not Acceptable Here" }, + { status: 489, + msg: "Bad Event" }, + { status: 491, + msg: "Request Pending" }, + { status: 493, + msg: "Undecipherable" }, + { status: 494, + msg: "Security Agreement Required" }, + { status: 500, + msg: "Internal Server Error" }, + { status: 501, + msg: "Not Implemented" }, + { status: 502, + msg: "Bad Gateway" }, + { status: 503, + msg: "Service Unavailable" }, + { status: 504, + msg: "Server Time-out" }, + { status: 505, + msg: "Version Not Supported" }, + { status: 513, + msg: "Message Too Large" }, + { status: 555, + msg: "Push Notification Service Not Supported" }, + { status: 580, + msg: "Precondition Failure" }, + { status: 600, + msg: "Busy Everywhere" }, + { status: 603, + msg: "Decline" }, + { status: 604, + msg: "Does Not Exist Anywhere" }, + { status: 606, + msg: "Not Acceptable" }, + { status: 607, + msg: "Unwanted" }, + { status: 608, + msg: "Rejected" }, +]; class Monitor extends BeanModel { /** @@ -155,6 +312,12 @@ class Monitor extends BeanModel { snmpVersion: this.snmpVersion, rabbitmqNodes: JSON.parse(this.rabbitmqNodes), conditions: JSON.parse(this.conditions), + sipUrl: this.sipUrl, + sipPort: this.sipPort, + sipProtocol: this.sipProtocol, + sipMethod: this.sipMethod, + sipMaintainence: this.isSipMaintainence(), + sipAuthMethod: this.sipAuthMethod, }; if (includeSensitiveData) { @@ -186,6 +349,8 @@ class Monitor extends BeanModel { kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), rabbitmqUsername: this.rabbitmqUsername, rabbitmqPassword: this.rabbitmqPassword, + sip_basic_auth_user: this.sip_basic_auth_user, + sip_basic_auth_pass: this.sip_basic_auth_pass, }; } @@ -318,7 +483,13 @@ class Monitor extends BeanModel { getKafkaProducerAllowAutoTopicCreation() { return Boolean(this.kafkaProducerAllowAutoTopicCreation); } - + /** + * Parse to boolean + * @returns {boolean} Sip Allow Maintainenece Option + */ + isSipMaintainence() { + return Boolean(this.sipMaintainence); + } /** * Start monitor * @param {Server} io Socket server instance @@ -874,7 +1045,68 @@ class Monitor extends BeanModel { bean.status = UP; bean.ping = dayjs().valueOf() - startTime; - } else { + } else if (this.type === "sip") { + try { + console.log("Ping Result:", this.sipMethod); + let sipResponse; + let sipMessage; + let startTime = dayjs().valueOf(); + let totalResponseTime; + let requestCount; + if (this.sipMethod !== "OPTIONS") { + sipResponse = await sipRegisterRequest(this.sipUrl, this.sipPort, this.sipProtocol, this.sip_basic_auth_user, this.sip_basic_auth_pass, version); + let sipResponseTime = dayjs().valueOf() - startTime; + totalResponseTime += sipResponseTime; + console.log("sipResponse", sipResponse); + console.log("this.sipMaintainence", this.sipMaintainence); + const matchingStatus = sipStatusCodes.find(code => code.status === sipResponse?.status); + if (matchingStatus) { + sipMessage = `${sipResponse?.status}-${matchingStatus.msg}`; + // Assuming UP and DOWN are previously defined constants or variables + bean.status = sipResponse?.status === 200 ? UP : DOWN; + console.log("sipResponse?.status", sipResponse?.status); + // Additional check for 503 status within matchingStatus + if (sipResponse?.status === 503 && this.sipMaintainence == 1) { + sipMessage = "Monitor under maintenance"; + bean.status = MAINTENANCE; + } + } else { + sipMessage = ` ${sipResponse?.status}-Not Ok`; + bean.status = DOWN; + } + + } else if (this.sipMethod === "OPTIONS") { + sipResponse = await sipOptionRequest(this.sipUrl, this.sipPort, this.sipProtocol, this.sip_basic_auth_user, this.sip_basic_auth_pass, version); + let sipOptionsResponseTime = dayjs().valueOf() - startTime; + totalResponseTime = sipOptionsResponseTime; + requestCount++; + console.log("=====resposne status", sipResponse?.status); + console.log("this.sipMaintainence", this.sipMaintainence); + const matchingStatus = sipStatusCodes.find(code => code.status === sipResponse?.status); + if (matchingStatus) { + sipMessage = `${sipResponse?.status}-${matchingStatus.msg}`; + // Assuming UP and DOWN are previously defined constants or variables + bean.status = sipResponse?.status === 200 ? UP : DOWN; + + // Additional check for 503 status within matchingStatus + if (sipResponse?.status === 503 && this.sipMaintainence == 1) { + sipMessage = "Monitor under maintenance"; + bean.status = MAINTENANCE; + } + } else { + sipMessage = ` ${sipResponse?.status}-Not Ok`; + bean.status = DOWN; + } + } + bean.ping = dayjs().valueOf() - startTime; + bean.msg = sipMessage; + // bean.msg = `${sipResponse?.status} - ${sipResponse?.reason}` + } catch (error) { + bean.msg = `Error: ${error.message}`; + bean.status = DOWN; + } + } + else { throw new Error("Unknown Monitor Type"); } diff --git a/server/notification-providers/sip.js b/server/notification-providers/sip.js new file mode 100644 index 0000000000..19c8959ce7 --- /dev/null +++ b/server/notification-providers/sip.js @@ -0,0 +1,64 @@ +const NotificationProvider = require("./notification-provider"); + +class SIP extends NotificationProvider { + name = "sip"; + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let monitorName = monitorJSON ? monitorJSON["name"] : "Unknown Monitor"; + let subject = this.updateSubject(msg, monitorName); + let body; + if (heartbeatJSON) { + body += `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`; + } + try { + return "SIP Message Sent Successfully."; + } catch (error) { + console.error("Error sending SIP message:", error); + throw new Error("Failed to send SIP message."); + } + } + + updateSubject(message, monitorName) { + if (!message) return "Default Subject"; // Handle null/undefined message + + message = message.toLowerCase(); // Normalize input + + if (/\bdown\b/i.test(message) || message.includes("offline")) { + return `🚨 ❌ Service Impacted...`; + } + if (/\bup\b/i.test(message) || message.includes("online")) { + return `🚨 ✅ Service Restored...`; + } + if (message.includes("maintenance")) { + if (message.includes("begin")) { + return `🚧 🔧 ❌ Maintenance Start...`; + } + if (/\bend\b/i.test(message)) { + return `🚧 🔧 ✅ Maintenance Complete...`; + } + if (message.includes("scheduled")) { + return `🚧 🪟 📆 Maintenance Window Scheduled...`; + } + if (message.includes("window begin")) { + return `🚧 🪟 🛑 Maintenance Window Start...`; + } + if (message.includes("window end")) { + return `🚧 🪟 ✅ Maintenance Window Complete...`; + } + } + if (message.includes("started on node")) { + return `📈 🔬 ✅ Monitoring Start...`; + } + if (message.includes("started")) { + return `📈 🔬 ✅ ${monitorName}`; + } + + return "Default Subject"; + } + + async sendSIPMessage(notification, sipMessage) { + console.log("Sending SIP message with config:", notification); + console.log("Message:", sipMessage); + } +} + +module.exports = SIP; diff --git a/server/server.js b/server/server.js index ec5ad49f68..3669e9632e 100644 --- a/server/server.js +++ b/server/server.js @@ -874,6 +874,13 @@ let needSetup = false; bean.rabbitmqUsername = monitor.rabbitmqUsername; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); + bean.sipUrl = monitor.sipUrl; + bean.sipPort = monitor.sipPort; + bean.sip_basic_auth_user = monitor.sip_basic_auth_user; + bean.sip_basic_auth_pass = monitor.sip_basic_auth_pass; + bean.sipMaintainence = monitor.sipMaintainence; + bean.sipMethod = monitor.sipMethod; + bean.sipAuthMethod = monitor.sipAuthMethod; bean.validate(); diff --git a/server/util-server.js b/server/util-server.js index 5ebc62ac56..9101d3e303 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -31,6 +31,10 @@ const dayjs = require("dayjs"); // eslint-disable-next-line no-unused-vars const { Kafka, SASLOptions } = require("kafkajs"); const crypto = require("crypto"); +let sip = require("sip"); +const uuid = require("uuid"); +let sharedSipServer; +const SERVER_PORT = 25060; const isWindows = process.platform === /^win/.test(process.platform); /** @@ -259,7 +263,213 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa }); }); }; +/** + * Sends a SIP REGISTER request + * @param {string} sipServer The SIP server to register with + * @param {string} transport The transport protocol to use (e.g., 'udp' or 'tcp') + * @returns {Promise} + */ +exports.sipRegisterRequest = function (sipServer, sipPort, transport, username, password, version) { + + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + try { + const registerRequest = { + method: 'REGISTER', + uri: `sip:${sipServer}:${sipPort}`, + headers: { + to: { uri: `sip:${sipServer}:${sipPort}` }, + from: { uri: `sip:${username}` }, + 'call-id': uuid.v4(), + cseq: { method: 'REGISTER', seq: 1 }, + 'content-length': 0, + contact: { uri: `sip:${username}` }, + "User-Agent": "SIP Health Monitor " + version, + "Expires": 60, + }, + transport: transport, + }; + const registrationResponse = await exports.sipRegister(registerRequest); + console.log("registrationResponse", registrationResponse); + if (registrationResponse.status === 407 && registrationResponse.headers["proxy-authenticate"]) { + const proxyAuthenticateHeader = registrationResponse.headers["proxy-authenticate"][0]; + const authorizedRegisterRequest = exports.constructAuthorizedRequest( + registerRequest, + username, + password, + proxyAuthenticateHeader + ); + + const secondResponse = await exports.sipRegister(authorizedRegisterRequest); + resolve(secondResponse); + } else { + resolve(registrationResponse); + } + } catch (error) { + console.error("Error:", error.message); + reject(error); + } + }); +}; + +exports.sipRegister = function (registerRequest) { + const server = sip.create({ + logger: "console", + port: 25060, + }); + console.log("SIP server created:", server); + return new Promise((resolve, reject) => { + const timeout = 5000; // Timeout duration in milliseconds + let timeoutID; + // Cleanup function to clear the timeout and destroy the server + function cleanup() { + if (timeoutID) clearTimeout(timeoutID); + if (server && server.destroy) { + server.destroy(); + console.log("SIP server destroyed."); + } + } + // Set a timeout to handle request expiry + timeoutID = setTimeout(() => { + console.error("SIP Register request timed out."); + reject(new Error("SIP Register request timed out.")); + cleanup(); + }, timeout); + + try { + // Send the SIP register request + server.send(registerRequest, (response) => { + console.log("Received SIP register response:", response); + if (response) { + resolve(response); // Resolve the promise with the response + cleanup(); // Cleanup after resolving or rejecting + } else { + reject(new Error("Empty SIP response received.")); + cleanup(); // Cleanup after resolving or rejecting + + } + }); + } catch (error) { + console.error("Error sending SIP register request:", error.message); + reject(new Error("Error sending SIP register request: " + error.message)); + cleanup(); + } + }); +}; +exports.constructAuthorizedRequest = function (request, username, password, proxyAuthenticateHeader) { + const digestChallenge = { + realm: proxyAuthenticateHeader.realm.replace(/"/g, ""), + nonce: proxyAuthenticateHeader.nonce.replace(/"/g, ""), + }; + // Construct Digest authentication header manually + const ha1 = crypto.createHash("md5").update(`${username}:${digestChallenge.realm}:${password}`).digest("hex"); + const ha2 = crypto.createHash("md5").update(`${request.method}:${request.uri}`).digest("hex"); + const response = crypto.createHash("md5").update(`${ha1}:${digestChallenge.nonce}:${ha2}`).digest("hex"); + const authorizationHeader = `Digest username="${username}", realm="${digestChallenge.realm}", nonce="${digestChallenge.nonce}", uri="${request.uri}", response="${response}"`; + const authorizedRequest = { + ...request, + headers: { + ...request.headers, + "Proxy-Authorization": authorizationHeader, + }, + }; + return authorizedRequest; +}; +/** + * Sends a SIP OPTIONS request + * @param {string} sipServer The SIP server to send OPTIONS to + * @param {string} transport The transport protocol to use (e.g., 'udp' or 'tcp') + * @returns {Promise} + */ +exports.sipOptionRequest = function (sipServer, sipPort, transport, username, password, version) { + const publicIP = process.env.PUBLIC_IP; + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + try { + const optionsRequest = { + method: 'OPTIONS', + uri: `sip:${sipServer}:${sipPort}`,//hostname + headers: { + to: { uri: `sip:${sipServer}:${sipPort}` },//hostname + from: { uri: `sip:${publicIP}` },//live ip || primary url + 'call-id': 1234, + cseq: { method: 'OPTIONS', seq: 1 }, + 'content-length': 0, + contact: [ { uri: `sip:${publicIP}` }], + "User-Agent": "SIP Health Monitor" + version, + + }, + transport: transport, + }; + let optionResponse + if(!username) { + console.log("will only send ok") + const optionResponse = await exports.sipOption(optionsRequest); + console.log("optionResponse", optionResponse); + resolve(optionResponse) + } + else { + optionResponse = await exports.sipRegister(optionsRequest); + console.log("optionResponse", optionResponse); + if (optionResponse.status === 407 && optionResponse.headers["proxy-authenticate"]) { + const proxyAuthenticateHeader = optionResponse.headers["proxy-authenticate"][0]; + const authorizedOptionRequest = exports.constructAuthorizedRequest( + optionsRequest, + username, + password, + proxyAuthenticateHeader + ); + + const secondResponse = await exports.sipOption(authorizedOptionRequest); + resolve(secondResponse); + } + } + + } catch (error) { + console.error("Error:", error.message); + reject(error); + } + }); +}; +exports.sipOption = function (optionsRequest) { + const server = sip.create({ + logger: "console", + port: 5060, + }); + console.log("SIP server created:", server); + return new Promise((resolve, reject) => { + const timeout = 5000; // Timeout duration in milliseconds + let timeoutID; + // Cleanup function to clear the timeout and destroy the server + function cleanup() { + if (timeoutID) clearTimeout(timeoutID); + if (server) { + server.destroy(); + console.log("SIP server destroyed."); + } + } + + + try { + // Send the SIP options request + server.send(optionsRequest, (response) => { + console.log("Received SIP options response:", response); + if (response) { + resolve(response); // Resolve the promise with the response + cleanup(); // Perform cleanup + } else { + reject(new Error("Empty SIP response received.")); + cleanup(); + } + }); + } catch (error) { + console.error("Error sending SIP options request:", error.message); + reject(new Error("Error sending SIP options request: " + error.message)); + cleanup(); + } + }); +}; /** * Use NTLM Auth for a http request. * @param {object} options The http request options diff --git a/src/lang/en.json b/src/lang/en.json index e215f1031f..7d8a98f9f8 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1051,5 +1051,10 @@ "RabbitMQ Password": "RabbitMQ Password", "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.", "SendGrid API Key": "SendGrid API Key", - "Separate multiple email addresses with commas": "Separate multiple email addresses with commas" + "Separate multiple email addresses with commas": "Separate multiple email addresses with commas", + "SipProtocol": "Sip Protocol", + "SipPort": "SIP Port", + "process503AsMaintenanceLabel": "Process 503 As Maintenance Label", + "sipURL": "Hostname (IP/Domain)", + "ignoreTLSErrorForSIP":"Ignore TLS/SSL error for SIP websites" } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index a83f91cabb..745969a56c 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -91,6 +91,7 @@ + @@ -117,7 +118,15 @@ - + +
+ + +
@@ -156,7 +165,19 @@ {{ $t("invertKeywordDescription") }}
- + +
+ + + +
+
+ + +
@@ -616,15 +637,15 @@ +
-
+
@@ -649,7 +670,19 @@ {{ $t("upsideDownModeDescription") }}
- +
+ + +
+
-