Я пытаюсь создать свою собственную базовую реализацию SIP.
В настоящее время я просто пытаюсь вызвать на другой телефон для первоначального тестирования, пока я работаю над структурой своей программы. Как видно из кода, после успешной регистрации (благодаря этому ответу) я отправляю INVITE запрос (как показано выше). Однако я не получаю ответа 180 RINGING, чего, согласно RFC, мне и следует ожидать. Я пытался использовать как добавочный номер, так и имя пользователя SIP, но безрезультатно. Действительно ли мне нужен SDP, чтобы звонить другому добавочному номеру? Может ли проблема заключаться не в приведенном выше SIP-сообщении, а, возможно, в другом месте моей реализации?
Вот полный фрагмент кода для справки:
const dgram = require("dgram");
const crypto = require("crypto");
const asteriskDOMAIN = "";
const asteriskIP = "";
const asteriskPort = "";
const clientIP = "";
const clientPort = "";
const username = "";
const password = "";
let callId;
const generateBranch = () => {
const branchId = Math.floor(Math.random() * 10000000000000);
return `z9hG4bK${branchId}X2`;
};
const generateCallid = () => {
const branchId = Math.floor(Math.random() * 10000000000000);
return `${branchId}`;
};
const Parser = {
parse: (message) => {
const lines = message.split('\r\n');
const firstLine = lines.shift();
const isResponse = firstLine.startsWith('SIP');
if (isResponse) {
// Parse SIP response
const [protocol, statusCode, statusText] = firstLine.split(' ');
const headers = {};
let index = 0;
// Parse headers
while (index < lines.length && lines[index] !== '') {
const line = lines[index];
const colonIndex = line.indexOf(':');
if (colonIndex !== -1) {
const headerName = line.substr(0, colonIndex).trim();
const headerValue = line.substr(colonIndex + 1).trim();
if (headers[headerName]) {
// If header name already exists, convert it to an array
if (Array.isArray(headers[headerName])) {
headers[headerName].push(headerValue);
} else {
headers[headerName] = [headers[headerName], headerValue];
}
} else {
headers[headerName] = headerValue;
}
}
index++;
}
// Parse message body if it exists
const body = lines.slice(index + 1).join('\r\n');
return {
isResponse: true,
protocol,
statusCode: parseInt(statusCode),
statusText,
headers,
body,
};
} else {
// Parse SIP request
const [method, requestUri, protocol] = firstLine.split(' ');
const headers = {};
let index = 0;
// Parse headers
while (index < lines.length && lines[index] !== '') {
const line = lines[index];
const colonIndex = line.indexOf(':');
if (colonIndex !== -1) {
const headerName = line.substr(0, colonIndex).trim();
const headerValue = line.substr(colonIndex + 1).trim();
if (headers[headerName]) {
// If header name already exists, convert it to an array
if (Array.isArray(headers[headerName])) {
headers[headerName].push(headerValue);
} else {
headers[headerName] = [headers[headerName], headerValue];
}
} else {
headers[headerName] = headerValue;
}
}
index++;
}
// Parse message body if it exists
const body = lines.slice(index + 1).join('\r\n');
return {
isResponse: false,
method,
requestUri,
protocol,
headers,
body,
};
}
},
getResponseType: (message) => {
var response = message.split("\r\n")[0];
if (response.split(" ")[0].includes("SIP/2.0")){
return response.split(" ")[1];
}else{
return response.split(" ")[0];
}
return response;
}
}
class Builder{
constructor(context){
this.context = context;
return this;
}
register(props){
if (props.realm && props.nonce && props.realm != "" && props.nonce != ""){
return {
'Via': `SIP/2.0/UDP ${clientIP}:${clientPort};branch=${generateBranch()}`,
'From': `<sip:${this.context.username}@${this.context.ip}>;tag=${generateBranch()}`,
'To': `<sip:${this.context.username}@${this.context.ip}>`,
'Call-ID': `${this.context.callId}@${clientIP}`,
'CSeq': `${this.context.cseq_count['REGISTER']} REGISTER`,
'Contact': `<sip:${this.context.username}@${clientIP}:${clientPort}>`,
'Max-Forwards': '70',
'Expires': '3600',
'User-Agent': 'Node.js SIP Library',
'Content-Length': '0',
'Authorization': `Digest username = "${this.context.username}", realm = "${props.realm}", nonce = "${props.nonce}", uri = "sip:${this.context.ip}:${this.context.port}", response = "${this.DigestResponse(this.context.username, this.context.password, this.context.realm, this.context.nonce, "REGISTER", `sip:${this.context.ip}:${this.context.port}`)}"`
}
}else{
return {
'Via': `SIP/2.0/UDP ${clientIP}:${clientPort};branch=${generateBranch()}`,
'From': `<sip:${this.context.username}@${this.context.ip}>;tag=${generateBranch()}`,
'To': `<sip:${this.context.username}@${this.context.ip}>`,
'Call-ID': `${this.context.callId}@${clientIP}`,
'CSeq': `${this.context.cseq_count['REGISTER']} REGISTER`,
'Contact': `<sip:${this.context.username}@${clientIP}:${clientPort}>`,
'Max-Forwards': '70',
'Expires': '3600',
'User-Agent': 'Node.js SIP Library',
'Content-Length': '0'
}
}
}
invite(props) {
if (props.realm && props.nonce && props.realm != "" && props.nonce != ""){
return {
'Via': `SIP/2.0/UDP ${clientIP}:${clientPort};branch=${generateBranch()}`,
'From': `<sip:${this.context.username}@${this.context.ip}>;tag=${generateBranch()}`,
'To': `<sip:${props.extension}@${this.context.ip}>`,
'Call-ID': `${this.context.callId}@${clientIP}`,
'CSeq': `${this.context.cseq_count['INVITE']} INVITE`,
'Contact': `<sip:${this.context.username}@${clientIP}:${clientPort}>`,
'Max-Forwards': '70',
'Expires': '3600',
'User-Agent': 'Node.js SIP Library',
'Content-Length': '0',
'Authorization': `Digest username = "${this.context.username}", realm = "${props.realm}", nonce = "${props.nonce}", uri = "sip:${this.context.ip}:${this.context.port}", response = "${this.DigestResponse(this.context.username, this.context.password, this.context.realm, this.context.nonce, "INVITE", `sip:${this.context.ip}:${this.context.port}`)}"`
};
}else{
return {
'Via': `SIP/2.0/UDP ${clientIP}:${clientPort};branch=${generateBranch()}`,
'From': `<sip:${this.context.username}@${this.context.ip}>;tag=${generateBranch()}`,
'To': `<sip:${props.extension}@${this.context.ip}>`,
'Call-ID': `${this.context.callId}@${clientIP}`,
'CSeq': `${this.context.cseq_count['INVITE']} INVITE`,
'Contact': `<sip:${this.context.username}@${clientIP}:${clientPort}>`,
'Max-Forwards': '70',
'Expires': '3600',
'User-Agent': 'Node.js SIP Library',
'Content-Length': '0',
};
}
}
ack(){
}
BuildResponse(type, props){
var map = {
"REGISTER": this.register(props),
"INVITE": this.invite(props),
"ACK": this.ack(props),
}
return this.JsonToSip(type, map[type]);
}
JsonToSip(type, props){
var request = `${type} sip:${asteriskDOMAIN}:${asteriskPort} SIP/2.0\r\n`;
for(let prop in props){
request += `${prop}: ${props[prop]}\r\n`;
}
request += `\r\n`;
return request;
}
DigestResponse(username, password, realm, nonce, method, uri) {
const ha1 = crypto.createHash("md5")
.update(`${username}:${realm}:${password}`)
.digest("hex");
const ha2 = crypto.createHash("md5")
.update(`${method}:${uri}`)
.digest("hex");
const response = crypto.createHash("md5")
.update(`${ha1}:${nonce}:${ha2}`)
.digest("hex");
return response;
}
}
class SIP{
constructor(ip, port, username, password){
this.ip = ip;
this.port = port;
this.username = username;
this.password = password;
this.Generator = new Builder(this);
this.Socket = dgram.createSocket("udp4");
this.callId = generateCallid();
this.Events = [];
this.cseq_count = {REGISTER: 1, INVITE: 1, ACK: 1}
return this;
}
send(message){
return new Promise(resolve => {
this.Socket.send(message, 0, message.length, this.port, this.ip, (error) => {
if (error){
resolve({context: this, 'error': error})
} else {
resolve({context: this, 'success':'success'});
}
})
})
}
registerEvent(event, callback){
this.Events.push({event: event, callback: callback});
}
listen(){
this.Socket.on("message", (message) => {
var response = message.toString();
var type = Parser.getResponseType(response);
if (this.Events.length > 0){
this.Events.forEach(event => {
if (event.event == type){
event.callback(response);
}
})
}
})
}
on(event, callback){
this.Events.push({event: event, callback: callback});
}
start(){
return new Promise(resolve => {
this.listen();
var test = this.Generator.BuildResponse("REGISTER", {})
this.send(test).then(response => {
if (!response.error){
this.on("401", (res) => {
var cseq = Parser.parse(res).headers.CSeq;
console.info(cseq);
if (cseq == "1 REGISTER"){
const authenticateHeader = res.match(/WWW-Authenticate:.*realm = "([^"]+)".*nonce = "([^"]+)"/i);
if (authenticateHeader) {
this.realm = authenticateHeader[1];
this.nonce = authenticateHeader[2];
const registerRequestWithAuth = this.Generator.BuildResponse("REGISTER", {realm: this.realm, nonce: this.nonce});
this.send(registerRequestWithAuth).then(res => {
})
}
}else if (cseq == "1 INVITE"){
const authenticateHeader = res.match(/WWW-Authenticate:.*realm = "([^"]+)".*nonce = "([^"]+)"/i);
if (authenticateHeader) {
this.realm = authenticateHeader[1];
this.nonce = authenticateHeader[2];
const registerRequestWithAuth = this.Generator.BuildResponse("INVITE", {extension:"420", realm: this.realm, nonce: this.nonce});
this.send(registerRequestWithAuth).then(res => {
})
}
}
})
this.on("200", (res) => {
//console.info(Parser.parse(res));
var cseq = Parser.parse(res).headers.CSeq
if (cseq.includes("REGISTER")){
console.info("REGISTERED")
}else if (cseq.includes("INVITE")){
console.info("INVITED")
}
this.cseq_count[cseq.split(" ")[1]] = this.cseq_count[cseq.split(" ")[1]] + 1;
resolve({context: this, 'success':'success'})
})
this.on("INVITE", (res) => {
//console.info(res);
})
this.on("NOTIFY", (res) => {
//console.info(res);
this.send('SIP/2.0 200 OK\r\n\r\n')
})
} else {
resolve({context: this, 'error': res.error})
}
})
})
}
}
new SIP(asteriskIP, asteriskPort, username, password).start().then(res => {
var invite_request = res.context.Generator.BuildResponse("INVITE", {extension: "420"});
res.context.send(invite_request).then(res => {
})
});
ОБНОВЛЯТЬ
Изучив захват SIP Wireshark, я заметил, что получаю 401 Unauthorized SIP-сообщение после отправки вышеуказанного INVITE запроса, хотя я успешно зарегистрировался. Это происходит только после отправки запроса INVITE. Нужно ли мне также включать заголовок аутентификации в мой запрос?
Требует ли Asterisk чего-то другого? Кроме того, я наблюдаю постоянную повторную передачу одного и того же NOTIFY SIP-сообщения, даже после отправки 200 OK. Хотя это может быть неуместно, я подумал, что было бы полезно упомянуть об этом.
Вот загрузка моего захвата Wireshark без отправки сообщения INVITE. Вот захват с INVITE SIP-сообщением. Я не беспокоюсь о том, что люди используют его для входа в мою АТС, так как она будет перемещена на другой сервер, получит новый IP и полностью перенастроится в ближайшее время, так что получайте удовольствие.
Обновление 2
После некоторых предложений теперь я отправляю заголовок Authorization только после того, как получу 401 Unauthorized. Я обнаружил, что значение cseq можно использовать для дифференциации ответов 401. После изменения этого я получаю новый ответ, 482 Loop Detected
Обновление 3
Я заметил ошибку в своем INVITE sip-сообщении. После изменения Request-URI на sip:[email protected]:6111 я все еще не получаю никаких изменений.
Вот как выглядит мое второе сообщение INVITE после добавления заголовка Authentication
INVITE sip:[email protected]:6111 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK8777308828561X2
From: <sip:[email protected]>;tag=z9hG4bK1286583240470X2
To: <sip:[email protected]>
Call-ID: [email protected]
CSeq: 1 INVITE
Contact: <sip:[email protected]:6111>
Max-Forwards: 70
User-Agent: Node.js SIP Library
Content-Length: 0
Authorization: Digest username = "Rob", realm = "asterisk", nonce = "1e5f4517", uri = "sip:[email protected]:6111", response = "85d378163c5e059ac3c9ee293d5e69d3"
Я получаю этот ответ в ответ
SIP/2.0 482 (Loop Detected)
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK8777308828561X2;received=72.172.213.173;rport=41390
From: <sip:[email protected]>;tag=z9hG4bK1286583240470X2
To: <sip:[email protected]>;tag=as3e8a4d9d
Call-ID: [email protected]
CSeq: 1 INVITE
Server: Asterisk PBX 18.14.0~dfsg+~cs6.12.40431414-1
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, SUBSCRIBE, NOTIFY, INFO, PUBLISH, MESSAGE
Supported: replaces
Content-Length: 0
Ваш первый INVITE обычно не будет иметь заголовка авторизации. Затем вы получаете новый 401 или 407. Затем вы отправляете второй INVITE с заголовком авторизации. (Тот же идентификатор вызова и увеличение cseq, как и для РЕГИСТРА). После этого можно ожидать 180 звон!
Круто спасибо за помощь. Есть ли способ, которым я могу понять нашу контекстуализацию, для чего предназначен ответ 401? В настоящее время 401 запускает часть авторизации REGISTER. Есть ли заголовок или значение, отправленное обратно, которое могло бы помочь мне увидеть, что 401 является ответом на приглашение, которое я отправил? После некоторого поиска, для этого ли предназначен параметр cseq?
После некоторых изменений я получаю новый ответ. Я получаю 482 Loop Detected Я не уверен, что это шаг вперед, но, похоже, это так. Кто-нибудь знает, что может быть причиной этого?
Чтобы сопоставить 401 с запросом, вы в основном сравниваете ветку Via. 482 Обнаружена петля означает, что сервер пересылает INVITE самому себе, что, вероятно, означает, что вы используете неправильный домен запроса-uri (первая строка INVITE): сервер не распознает себя.
Итак, что бы я использовал в запросе uri вместо IP-адреса серверов АТС? Буду ли я использовать IP-адрес расширения вместо этого? предоставляет ли SIP механизм для просмотра зарегистрированных клиентов, если это так?
Итак, вы говорите, что я могу сопоставить запрос с параметром branch заголовка VIA? Но в настоящее время я регенерирую его с каждым запросом. Должен ли я создавать новый только для каждого диалога? Думаю, я бы сохранил это в массиве, а предыдущий запрос соответствовал 401-му. Если это так. Наверное, я неправильно понимаю, для чего на самом деле используется CSEQ.
ветвь используется для сопоставления ответа с запросом (новый для каждой транзакции). Call-id и увеличивающаяся cseq используются для того, чтобы рассматривать 2 INVITE как часть одного и того же диалога. Кроме того, вам необходимо учитывать параметры тега To tag и From tag, чтобы эти ПРИГЛАШЕНИЯ рассматривались как часть одного и того же диалога SIP!
@AymericM Спасибо за вашу помощь. Если вы ответите здесь, я могу пометить его как отвеченный. Хотя ваши знания проницательны, я все еще не понимаю, какой request-uri мне следует использовать для запроса на приглашение, если я не должен использовать свой URI АТС Asterisk? чтобы избежать обнаружения петли 482. Я чувствую, что очень близок к тому, чтобы заставить это работать благодаря вашей помощи.
Итак, URI запроса, который я сейчас использую для приглашений, sip:${extension}@${asterisk_ip}:${port}, но я все еще получаю 482 Loop Detected Я действительно не уверен, что я делаю неправильно



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Как вы и предложили, давайте попробуем составить ответ со всеми правилами, которые вы должны соблюдать для INVITE.
При обмене SIP-сообщениями возникает несколько тем. Среди них давайте обсудим эти конкретные элементы:
Для любого нового запроса требуется новая ветвь Via. Он должен быть случайным и всегда начинается с волшебного файла cookie z9hG4bK. Он всегда будет копироваться в SIP-ответе на этот запрос. Только повторные передачи SIP-сообщений будут иметь то же самое.
Первоначальный REGISTER и начальный INVITE, используя очень простую реализацию Digest, будут отправлены без каких-либо заголовков авторизации или прокси-авторизации.
После 401 или 407 требуется аутентификация. Новый REGISTER (или INVITE) будет отправлен с заголовком Authorization или Proxy-Authorization.
Когда вы запускаете приложение, вы создаете Call-ID для начального REGISTER. Любой новый REGISTER для аутентификации или обновления регистрации будет использовать тот же Call-ID и будет иметь увеличенный CSeq. Это упорядочивает все последовательные транзакции и помогает серверу следить за потоком. Это обязательная операция.
Когда вы начинаете новый вызов, вы создаете новый Call-ID для первоначального INVITE. Любое SIP-сообщение в том же диалоговом окне SIP (тот же вызов) должно повторно использовать один и тот же Call-ID и будет иметь увеличенный CSeq. Опять же, это помогает заказывать на удаленной стороне и является обязательным.
Тег From помогает сопоставить все запросы в одном и том же диалоговом окне SIP (один и тот же вызов). Он остается неизменным в пределах вызова и определяется инициатором вызова.
Тег To помогает сопоставить все запросы в одном и том же диалоговом окне SIP (один и тот же вызов). Он остается неизменным в пределах вызова и определяется/добавляется получателем вызова. Это известно только тогда, когда удаленный пользователь сначала отправляет ответ 1xx (например, 180 Ringing) или ответ 2xx (например, 200 Ok).
Когда сервер получает (и принимает) запрос, ему необходимо решить, будет ли он переадресован другому оператору или будет обрабатываться самостоятельно. Чтобы было ясно, пользователи Verizon не находятся в базе данных AT&T, поэтому, если звонок от AT&T к Verizon поступает на AT&T, его необходимо перенаправить в Verizon.
То же самое происходит в Request-URI с доменом SIP URI. Если он не распознается, используется DNS, и запрос перенаправляется. Однако, если запрос с использованием DNS снова получен на том же сервере, мы можем обнаружить петлю и отклонить ее с ошибкой 482 Loop Detected.
Вы можете попытаться удалить порт из своего Request-URI или исправить конфигурацию звездочки, чтобы правильно направить ваш запрос. Не знаю достаточно о звездочке, чтобы дать лучший намек.
ПРИМЕЧАНИЕ. Возможно, в будущих вопросах о stackoverflow будет лучше указать более конкретные вопросы. Периметр был слишком велик в этом вопросе. Спасибо.
Большое спасибо еще раз, AymericM. Моя проблема заключалась во многих вещах. Но ваше объяснение помогло мне лучше понять это. Проблема, вызвавшая это, оказалась в неправильном подсчете значения cseq. Спасибо!
Привет! Неясно, касается ли ваш вопрос исходящего звонка, входящего звонка или того и другого. И это немного сбивает с толку, потому что у вас много вопросов! Можно переписать и указать свой точный вопрос. Ответить будет проще!