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 000000000..25959e771 --- /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 000000000..0d3651f88 --- /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 586b0d6a4..1aba7d704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": "~8.0.5", @@ -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": { @@ -1212,6 +1214,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.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -5270,6 +5280,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.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -5686,6 +5705,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", @@ -10173,6 +10197,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", @@ -15036,6 +15069,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", @@ -16721,12 +16773,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 81cae5736..213e4b1e0 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": "~8.0.5", @@ -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 0e6a7405d..f6c23a8fa 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 3ad8cfafc..67d711e7e 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, }; } @@ -319,6 +484,14 @@ class Monitor extends BeanModel { 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,6 +1047,62 @@ class Monitor extends BeanModel { bean.status = UP; bean.ping = dayjs().valueOf() - startTime; + } else if (this.type === "sip") { + try { + console.log("Ping Result:", this.sipMethod); + let sipResponse; + let sipMessage; + let startTime = dayjs().valueOf(); + let totalResponseTime; + 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", totalResponseTime); + 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); + 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 000000000..86c1e2687 --- /dev/null +++ b/server/notification-providers/sip.js @@ -0,0 +1,93 @@ +const NotificationProvider = require("./notification-provider"); + +class SIP extends NotificationProvider { + name = "sip"; + + /** + * Sends a SIP notification message. + * @param {object} notification - SIP notification configuration. + * @param {string} msg - The message content. + * @param {object | null} monitorJSON - The monitor data (if available). + * @param {object | null} heartbeatJSON - The heartbeat data (if available). + * @returns {Promise} - Confirmation message if successful. + * @throws {Error} - If sending the SIP message fails. + */ + async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { + let monitorName = monitorJSON ? monitorJSON["name"] : "Unknown Monitor"; + let body = ""; + + if (heartbeatJSON) { + body += `\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`; + } + + try { + console.log("Sending SIP message:", { notification, + msg, + monitorName, + body }); + return "SIP Message Sent Successfully."; + } catch (error) { + console.error("Error sending SIP message:", error); + throw new Error("Failed to send SIP message."); + } + } + + /** + * Generates a subject line based on the message content. + * @param {string} message - The incoming status message. + * @param {string} monitorName - The name of the monitored service. + * @returns {string} - The formatted subject line. + */ + updateSubject(message, monitorName) { + if (!message) { + return "Default Subject"; + } + + 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"; + } + + /** + * Sends a SIP message using the provided notification configuration. + * @param {object} notification - SIP notification settings. + * @param {string} sipMessage - The message content to send. + * @returns {void} + */ + 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 ec5ad49f6..3669e9632 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 5ebc62ac5..2a47fa45d 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -31,6 +31,8 @@ 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"); const isWindows = process.platform === /^win/.test(process.platform); /** @@ -259,7 +261,235 @@ exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, sa }); }); }; +/** + * Sends a SIP REGISTER request + * @param {string} sipServer The SIP server to register with + * @param {number} sipPort The port of the SIP server + * @param {string} transport The transport protocol to use (e.g., 'udp' or 'tcp') + * @param {string} username The username for registration + * @param {string} userPassword The userPassword for registration + * @param {string} version The version of the SIP health monitor + * @returns {Promise} The response from the SIP REGISTER request + */ +exports.sipRegisterRequest = function (sipServer, sipPort, transport, username, userPassword = undefined, 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, + userPassword, + 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 ensure proper resource management + /** + * Clears the timeout and destroys the SIP server instance. + * This function is called to prevent memory leaks and ensure that no lingering processes are left running. + * @returns {void} This function does not return any value. + */ + 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, userPassword = undefined, proxyAuthenticateHeader) { + const digestChallenge = { + realm: proxyAuthenticateHeader.realm.replace(/"/g, ""), + nonce: proxyAuthenticateHeader.nonce.replace(/"/g, ""), + }; + // Construct Digest authentication header manually + const ha1 = crypto.createHash("sha256").update(`${username}:${digestChallenge.realm}:${passwordHash.generate(userPassword)}`).digest("hex"); + const ha2 = crypto.createHash("sha256").update(`${request.method}:${request.uri}`).digest("hex"); + const response = crypto.createHash("sha256").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 {number} sipPort The port of the SIP server + * @param {string} transport The transport protocol to use (e.g., 'udp' or 'tcp') + * @param {string} username The username for authentication (optional) + * @param {string} password The password for authentication (optional) + * @param {string} version The version of the SIP Health Monitor + * @returns {Promise} The response from the SIP OPTIONS request + */ +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) => { + + let timeoutID; + // Cleanup function to ensure proper resource management + /** + * Clears the timeout and destroys the SIP server instance. + * This function is called to prevent memory leaks and ensure that no lingering processes are left running. + * @returns {void} This function does not return any value. + */ + 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 e215f1031..dc5fa9817 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/modules/dayjs/plugin/timezone/index.d.ts b/src/modules/dayjs/plugin/timezone/index.d.ts index 8d9035905..e9e5a48f8 100644 --- a/src/modules/dayjs/plugin/timezone/index.d.ts +++ b/src/modules/dayjs/plugin/timezone/index.d.ts @@ -1,12 +1,12 @@ -import { PluginFunc, ConfigType } from 'dayjs/esm' +import { PluginFunc, ConfigType } from "dayjs/esm"; -declare const plugin: PluginFunc +declare const plugin: PluginFunc; export = plugin -declare module 'dayjs/esm' { +declare module "dayjs/esm" { interface Dayjs { tz(timezone?: string, keepLocalTime?: boolean): Dayjs - offsetName(type?: 'short' | 'long'): string | undefined + offsetName(type?: "short" | "long"): string | undefined } interface DayjsTimezone { @@ -16,5 +16,5 @@ declare module 'dayjs/esm' { setDefault(timezone?: string): void } - const tz: DayjsTimezone + const tz: DayjsTimezone; } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index a83f91cab..c83887d1a 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -91,6 +91,7 @@ + @@ -117,7 +118,15 @@ - + +
+ + +
@@ -156,7 +165,21 @@ {{ $t("invertKeywordDescription") }}
- + +
+ + + +
+
+ + +
@@ -621,10 +644,10 @@
-
+
@@ -649,6 +672,18 @@ {{ $t("upsideDownModeDescription") }}
+
+ + +
@@ -667,7 +702,7 @@
-