Sip module code added

This commit is contained in:
maryamsaleem 2025-02-06 14:30:09 +05:00
parent 3daf71c632
commit 78d46662b9
11 changed files with 789 additions and 19 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;

69
package-lock.json generated
View file

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

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

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

View file

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

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

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

@ -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" class="form-select" required v-model="monitor.sipProtocol">
<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,19 @@
{{ $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" type="number" class="form-control"
v-model="monitor.sipPort" placeholder="Enter SIP Port">
</div>
<!-- Remote Browser -->
<div v-if="monitor.type === 'real-browser'" class="my-3">
<!-- Toggle -->
@ -616,15 +637,15 @@
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
<label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }}
</label>
</label>
<div class="form-text">
</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,7 +670,19 @@
{{ $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">
<label class="form-check-label" for="gamedig-guess-port">
@ -667,7 +700,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 +858,83 @@
</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 +1220,13 @@ const monitorDefaults = {
rabbitmqNodes: [],
rabbitmqUsername: "",
rabbitmqPassword: "",
conditions: []
conditions: [],
sipProtocol: "UDP",
sipPort: 5060,
sipUrl:null,
sipMethod: "OPTIONS",
sipMaintainence:false,
sipAuthMethod: null,
};
export default {