mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-04-02 06:22:20 +00:00
Sip module code added
This commit is contained in:
parent
3daf71c632
commit
78d46662b9
11 changed files with 789 additions and 19 deletions
67
db/old_migrations/patch-add-sip-fields.sql
Normal file
67
db/old_migrations/patch-add-sip-fields.sql
Normal 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;
|
11
db/old_migrations/patch-sip-auth.sql
Normal file
11
db/old_migrations/patch-sip-auth.sql
Normal 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
69
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
64
server/notification-providers/sip.js
Normal file
64
server/notification-providers/sip.js
Normal 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;
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue