Merge aee2a0791f295e219c94d966f8fe8c574f9837fa into 0fc6998a31dcfa78bc0dfa8eea2cdcd4ed30bd1a

This commit is contained in:
msaleemCinnova 2025-02-27 16:41:31 +00:00 committed by GitHub
commit 7ffcb32dcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 844 additions and 18 deletions

View File

@ -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;

View File

@ -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;

65
package-lock.json generated
View File

@ -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": {

View File

@ -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": {

View File

@ -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,
};
/**

View File

@ -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");
}

View File

@ -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<string>} - 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;

View File

@ -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();

View File

@ -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<object>} 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<object>} 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

View File

@ -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"
}

View File

@ -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;
}

View File

@ -91,6 +91,7 @@
<option v-if="!$root.info.isContainer" value="tailscale-ping">
Tailscale Ping
</option>
<option value="sip">SIP</option>
</optgroup>
</select>
<i18n-t v-if="monitor.type === 'rabbitmq'" keypath="rabbitmqHelpText" tag="div" class="form-text">
@ -117,7 +118,15 @@
<label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required data-testid="url-input">
</div>
<!--SIP-->
<div v-if="monitor.type === 'sip'" class="my-3">
<label for="sipprotocol" class="form-label">{{ $t("SipProtocol") }}</label>
<select id="sipprotocol" v-model="monitor.sipProtocol" class="form-select" required>
<option value="UDP">UDP</option>
<option value="TCP">TCP</option>
<option value="TLS">TLS</option>
</select>
</div>
<!-- gRPC URL -->
<div v-if="monitor.type === 'grpc-keyword' " class="my-3">
<label for="grpc-url" class="form-label">{{ $t("URL") }}</label>
@ -156,7 +165,21 @@
{{ $t("invertKeywordDescription") }}
</div>
</div>
<!--SIP URL-->
<div v-if="monitor.type === 'sip'" class="my-3">
<label for="sip-url" class="form-label">{{ $t("sipURL") }}</label>
<!-- <input id="sip-url" v-model="monitor.sipURL" type="url" class="form-control" pattern="((https?|ftp):\/\/)?([a-zA-Z0-9.-]
+\.[a-zA-Z]{2,})(:\d{1,5})?\/?|
(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" required> -->
<input id="sip-url" v-model="monitor.sipUrl" type="text" class="form-control" pattern="((https?|ftp):\/\/)?([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})(:\d{1,5})?\/?|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" required>
</div>
<div v-if="monitor.type === 'sip'" class="my-3">
<label for="sipport" class="form-label mt-3">{{ $t("SipPort") }}</label>
<input
v-if="monitor.sipProtocol !== 'SRV'" id="sipport" v-model="monitor.sipPort" type="number"
class="form-control" placeholder="Enter SIP Port"
>
</div>
<!-- Remote Browser -->
<div v-if="monitor.type === 'real-browser'" class="my-3">
<!-- Toggle -->
@ -621,10 +644,10 @@
</div>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
<div v-if="monitor.type === 'http' || monitor.type === 'sip' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
{{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
{{ monitor.type === "redis" || monitor.type === 'sip' ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
</label>
</div>
@ -649,6 +672,18 @@
{{ $t("upsideDownModeDescription") }}
</div>
</div>
<div v-if="monitor.type === 'sip'" class="my-3 form-check">
<input
id="process-503-as-maintenance"
v-model="monitor.sipMaintainence"
class="form-check-input"
type="checkbox"
value=""
/>
<label class="form-check-label" for="process-503-as-maintenance">
{{ $t("process503AsMaintenanceLabel") }}
</label>
</div>
<div v-if="monitor.type === 'gamedig'" class="my-3 form-check">
<input id="gamedig-guess-port" v-model="monitor.gamedigGivenPortOnly" :true-value="false" :false-value="true" class="form-check-input" type="checkbox">
@ -667,7 +702,7 @@
</div>
<!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' ">
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'grpc-keyword' || monitor.type === 'sip'">
<div class="my-3">
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
@ -825,7 +860,91 @@
</div>
</div>
</template>
<!-- SIP Options -->
<template
v-if="monitor.type === 'sip'
"
>
<h2 class="mt-5 mb-2">{{ $t("SIP Options") }}</h2>
<!-- Method -->
<div class="my-3">
<label for="method" class="form-label">{{
$t("Method")
}}</label>
<select id="method" v-model="monitor.sipMethod" class="form-select">
<option value="REGISTER">REGISTER</option>
<option value="OPTIONS">OPTIONS</option>
</select>
</div>
<!-- Encoding -->
<div class="my-3">
<label for="httpBodyEncoding" class="form-label">{{
$t("Body Encoding")
}}</label>
<select id="httpBodyEncoding" v-model="monitor.httpBodyEncoding" class="form-select">
<option value="json">JSON</option>
<option value="xml">XML</option>
</select>
</div>
<!-- Body -->
<div 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 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>
<!-- HTTP Auth -->
<h4 class="mt-5 mb-2">{{ $t("Authentication") }}</h4>
<!-- Method -->
<div class="my-3">
<label for="authmethod" class="form-label">{{
$t("Method")
}}</label>
<select id="authsipmethod" v-model="monitor.sipAuthMethod" class="form-select">
<option :value="null">
{{ $t("None") }}
</option>
<option value="basic">
{{ $t("SIP Basic Auth") }}
</option>
</select>
</div>
<template v-if="monitor.sipAuthMethod === 'basic'">
<div class="my-3">
<label for="basicauth-user" class="form-label">{{ $t("Username") }}</label>
<input
id="basicauth-user" v-model="monitor.sip_basic_auth_user" type="text" class="form-control"
:placeholder="$t('Username')"
/>
</div>
<div class="my-3">
<label for="basicauth-pass" class="form-label">{{ $t("Password") }}</label>
<input
id="basicauth-pass" v-model="monitor.sip_basic_auth_pass" type="password" autocomplete="new-password"
class="form-control" :placeholder="$t('Password')"
/>
</div>
</template>
</template>
<!-- HTTP Options -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' ">
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
@ -1111,7 +1230,13 @@ const monitorDefaults = {
rabbitmqNodes: [],
rabbitmqUsername: "",
rabbitmqPassword: "",
conditions: []
conditions: [],
sipProtocol: "UDP",
sipPort: 5060,
sipUrl: null,
sipMethod: "OPTIONS",
sipMaintainence: false,
sipAuthMethod: null,
};
export default {