Аутентификация Node.js Asterisk SIP-заголовки UDP

Я пытаюсь узнать больше о SIP и пытаюсь реализовать небольшие части протокола в node.js с помощью UDP.

пока у меня это

const dgram = require('dgram');
const crypto = require('crypto');

const asteriskIP = 'ASTERISK_IP';
const asteriskPort = 6111;
const clientIP = '192.168.1.2';
const clientPort = 6111;
const username = 'USERNAME';
const password = 'PASSWORD';

// Create a UDP socket
const socket = dgram.createSocket('udp4');

// Generate a random branch identifier for the Via header
const generateBranch = () => {
  const branchId = Math.floor(Math.random() * 100000000);
  return `z9hG4bK${branchId}`;
};

// Generate Digest response
function generateDigestResponse(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;
}

// SIP REGISTER request
const generateRegisterRequest = (branch, withAuth = false, realm = '', nonce = '') => {
  let request = `REGISTER sip:${asteriskIP}:${asteriskPort} SIP/2.0\r\n` +
    `Via: SIP/2.0/UDP ${clientIP}:${clientPort};branch=${branch}\r\n` +
    `From: <sip:${username}@${asteriskIP}>;tag=${branch}\r\n` +
    `To: <sip:${username}@${asteriskIP}>\r\n` +
    `Call-ID: ${branch}@${clientIP}\r\n` +
    `CSeq: 1 REGISTER\r\n` +
    `Contact: <sip:${username}@${clientIP}:${clientPort}>\r\n` +
    'Max-Forwards: 70\r\n' +
    'Expires: 3600\r\n' +
    'User-Agent: Node.js SIP Library\r\n';

  if (withAuth && realm && nonce) {
    const digestResponse = generateDigestResponse(username, password, realm, nonce, 'REGISTER', `sip:${asteriskIP}:${asteriskPort}`);
    request += 'Authorization: Digest ' +
      `username = "${username}", realm = "${realm}", ` +
      `nonce = "${nonce}", uri = "sip:${asteriskIP}:${asteriskPort}", ` +
      `response = "${digestResponse}"\r\n`;
  }

  request += 'Content-Length: 0\r\n\r\n';

  return request;
};

// Send the REGISTER request
const sendRegisterRequest = (request) => {
  socket.send(request, 0, request.length, asteriskPort, asteriskIP, (error) => {
    if (error) {
      console.error('Error sending UDP packet:', error);
    } else {
      console.info('REGISTER request sent successfully.');
    }
  });
};

let realm = '';
let nonce = '';

// Listen for incoming responses
socket.on('message', (message) => {
  const response = message.toString();
  console.info('Received response:', response);

  if (response.startsWith('SIP/2.0 200 OK')) {
    console.info('Registration successful.');
    // Do further processing or initiate calls here
  } else if (response.startsWith('SIP/2.0 401 Unauthorized')) {
    const authenticateHeader = response.match(/WWW-Authenticate:.*realm = "([^"]+)".*nonce = "([^"]+)"/i);
    if (authenticateHeader) {
      realm = authenticateHeader[1];
      nonce = authenticateHeader[2];
      console.info('Received realm:', realm);
      console.info('Received nonce:', nonce);

      // Generate Digest response and proceed with registration
      const branch = generateBranch();
      const registerRequestWithAuth = generateRegisterRequest(branch, true, realm, nonce);

      console.info('Sending REGISTER request with authentication:');
      console.info(registerRequestWithAuth);
      // Send the REGISTER request with authentication
      sendRegisterRequest(registerRequestWithAuth+ '\r\n');
    }
  }
});

// Bind the socket to the client's port and IP
socket.bind(clientPort, clientIP, () => {
  console.info('Socket bound successfully.');

  // Generate branch identifier for the initial REGISTER request
  const branch = generateBranch();
  const registerRequest = generateRegisterRequest(branch);

  // Send the initial REGISTER request
  sendRegisterRequest(registerRequest);
});

Я включил отладку SIP в астериске 18 и вот что я вижу.

SIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK2049260;received=72.172.213.173;rport=6111
From: <sip:[email protected]>;tag=z9hG4bK2049260
To: <sip:[email protected]>;tag=as12883ca4
Call-ID: [email protected]
CSeq: 1 REGISTER
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
WWW-Authenticate: Digest algorithm=MD5, realm = "asterisk", nonce = "5b4af6d3"
Content-Length: 0

REGISTER sip:ASTERISK_IP:6111 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK99247361
From: <sip:Tim@ASTERISK_IP>;tag=z9hG4bK99247361
To: <sip:Tim@ASTERISK_IP>
Call-ID: [email protected]
CSeq: 1 REGISTER
Contact: <sip:[email protected]:6111>
Max-Forwards: 70
Expires: 3600
User-Agent: Node.js SIP Library
Authorization: Digest username = "Tim", realm = "asterisk", nonce = "5b4af6d3", uri = "sip:ASTERISK_IP:6111", response = "2e0cbc55739537d46ff0c0ff862ae28a"
Content-Length: 0

SIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK99247361;received=72.172.213.173;rport=6111
From: <sip:Tim@ASTERISK_IP>;tag=z9hG4bK99247361
To: <sip:Tim@ASTERISK_IP>;tag=as75c4d7b6
Call-ID: [email protected]
CSeq: 1 REGISTER
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
WWW-Authenticate: Digest algorithm=MD5, realm = "asterisk", nonce = "1fd88a71"
Content-Length: 0

Но я вообще не получаю 200 OK от звездочки. Мне интересно, неправильно ли я вычисляю одноразовый номер. Я знаю, что код немного запутан, но сначала это просто быстрый тест. Я пропустил какие-либо заголовки? астериску нужно что-то еще? Будем очень признательны за любую помощь или толчок в правильном направлении. Спасибо.

Я обновил свой код. и что возвращает звездочка.

Nik Hendricks 24.05.2023 07:36

Это кажется довольно скудным. Мой сервер Asterisk 16.29 возвращает заголовок WWW-Authenticate, который выглядит так: pastebin.com/5uxutaZB

miken32 24.05.2023 22:52

Вы говорите, что используете Asterisk 14, но ответ сервера, который вы показываете, относится к Asterisk 18, с кучей шума, добавленного в строку версии. Кроме того, RFC 3261 говорит, что «серверы ДОЛЖНЫ всегда отправлять параметр «qop» в WWW-Authenticate», и маловероятно, что Asterisk пропустит эту деталь.

miken32 24.05.2023 23:08

@ miken32 ах, да, я не уверен, что заставило меня подумать, что это 14, но я подтвердил, что это версия 18. Что касается этого параметра qop в www-authenticate, я не вижу его в своих трассировках wireshark с помощью софтфона Zoiper, который работает отлично. Что может быть причиной этого?

Nik Hendricks 24.05.2023 23:59

Вы случайно не используете старый драйвер канала chan_sip? Я нашел старую трассировку пакетов из системы Asterisk 11, и в ней отсутствуют эти дополнительные значения. Вам определенно следует использовать chan_pjsip на этом этапе, chan_sip устарел уже много лет.

miken32 25.05.2023 00:12

Из того, что я вижу в RFC, ваши расчеты верны. Однако много кода добавляет дополнительные значения для получения окончательного хэша. Например, смотрите здесь , здесь или здесь. Однако я не могу найти никакой официальной документации, которая определяет это.

miken32 25.05.2023 00:13

@ miken32 Большое спасибо за всю вашу помощь. Да, я использую chan_sip Я собираюсь обновиться в ближайшее время, я просто хотел обеспечить поддержку некоторых старых телефонов voip, с которыми я экспериментирую. Я проверю ссылки, которые вы мне дали, спасибо.

Nik Hendricks 25.05.2023 00:36

Думаю, я просто запутался. Я полагал, что для каждого запроса существует универсальная структура. Действительно ли моя проблема заключается только в значениях, которые я отправляю для одноразового номера, и в том, как они отформатированы. или есть еще переменные, которые вступают в игру? Я полагал, что, поскольку это SIP, все будет одинаково, за исключением значений аутентификации.

Nik Hendricks 25.05.2023 00:42

Я бы посоветовал попробовать скопировать эти другие примеры кода, сделав окончательный хеш как ${ha1}:${nonce}:${cnonce}:auth:${ha2}, где cnonce — случайное значение, которое вы генерируете. Обязательно верните cnonce = "whatever" и qop=auth (без кавычек) в ответе.

miken32 25.05.2023 01:12

qop здесь не используется (и не является обязательным), и ваш дайджест-ответ ${ha1}:${nonce}:${ha2} кажется правильным. Возможно, ваше имя пользователя не Tim, а tim в нижнем регистре. Стоит попробовать. (или даже воссоздайте другого пользователя со строчными буквами, чтобы убедиться, что ha1 также рассчитывается со строчными буквами на звездочке)

AymericM 26.05.2023 00:20

Спасибо всем за помощь. Я очень стараюсь, но даже после использования других пользователей. Четырехкратная проверка учетных данных. регистрация со следующим кодом не удалась. Со счетами я знаю работу. Мне сказали посмотреть на справочный материал, и я это сделал. Но мне также сказали, что мои расчеты верны. Я действительно в недоумении, что здесь делать. Я хотел бы присудить награду кому-нибудь, хотя. На самом деле, я был бы буквально готов заплатить за помощь в том, чтобы начать. Видя, что SIP должен быть протоколом, и все стандартно, я решил, что мою проблему будет легко найти.

Nik Hendricks 27.05.2023 06:19

На моем сервере основной проблемой были неправильные заголовки CSEQ и CALL-ID. Бьюсь об заклад, то же самое произошло на вашем астериске. Чтобы избежать повторения атаки, одноразовый номер можно использовать только в том случае, если вы следуете рекомендациям протокола: Call-ID должен быть таким же, а CSEQ должен быть увеличен. Была также крошечная проблема \r\n. Новый код, который я предложил, определенно протестирован и работает! Я благодарен, что вы приняли мой ответ.

AymericM 30.05.2023 17:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
13
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Авторизация - это заголовок, и все заголовки должны появляться до появления \r\n\r\n (пустая строка)

Обычно авторизацию помещают непосредственно перед заголовком Content-Length. Только с \r\n до и после.

После \r\n\r\n у вас будет тело (если длина содержимого указывает один) или новое SIP-сообщение.

ОБНОВЛЯТЬ: Вот rfc3261 Раздел 20.7, чтобы было понятно, что авторизация — это SIP-заголовок:

20.7 Авторизация

Поле заголовка Authorization содержит учетные данные для аутентификации. UA. Раздел 22.2 описывает использование Разрешения. поле заголовка, а раздел 22.4 описывает синтаксис и семантику при использовании HTTP-аутентификации.

Ваш второй РЕГИСТР должен быть таким:

REGISTER sip:ASTERISKIP:ASTERISKPORT SIP/2.0
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK30130523
From: <sip:USERNAME@ASTERISKIP>;tag=z9hG4bK30130523
To: <sip:USERNAME@ASTERISKIP>
Call-ID: [email protected]
CSeq: 1 REGISTER
Contact: <sip:[email protected]:6111>
Max-Forwards: 70
Expires: 3600
User-Agent: Node.js SIP Library
Authorization: Digest username = "Tim", realm = "asterisk", nonce = "120ef084", uri = "sip:ASTERISKIP:ASTERISKPORT", response = "e6a25e09e1bb2099436cf10526d955e0"
Content-Length: 0

ОБНОВЛЕНИЕ 2:

Вам действительно следует определить имя пользователя в нижнем регистре как на вашем сервере (когда вы создаете пароль), так и на клиенте, когда вы настраиваете имя пользователя. Довольно часто возникают проблемы с регистром в программном обеспечении HTTP или SIP. Имя пользователя обычно нечувствительно к регистру, поэтому использование Tim приведет к ошибке, если звездочка считает его строчным.

Чтобы проверить ваш ответ md5, проще всего вычислить строку с помощью инструмента md5sum:

$> echo -n "Tim:asterisk:secret" | md5sum
d86544eb768e7936519f727599d51fbb

$> echo -n "REGISTER:sip:ASTERISK_IP:6111" | md5sum
0a1fc837871b52dfe5c7d9a009079265

$> echo -n "d86544eb768e7936519f727599d51fbb:5b4af6d3:0a1fc837871b52dfe5c7d9a009079265" | md5sum
504804156ca15812da67ea5439a9ea00

Затем проверьте, является ли это результатом вашего кода! Я не могу этого сделать, потому что ты спрятал свои настоящие ценности.

Вы можете попробовать с «Тим» или «Тим», чтобы увидеть разницу.

ОБНОВЛЕНИЕ 3:

Я проверил ваш код и исправил несколько вещей:

  • Я создаю заголовок Call-Id, который хранится поверх нового REGISTER.
  • Я использую возрастающую CSeq для каждого нового РЕГИСТРА.
  • Небольшая модификация для использования «домена» вместо «IP» (для моей собственной тестовой цели)
  • Я также удалил лишний \r\n в конце второго РЕГИСТРА

Если этого не сделать, сервер может определить это как атаку. Мой собственный сервер (kamailio) отклонял запрос с информацией об истечении срока действия.

Этот код работает для пользователя sip.antisip.com и «stackoverflow» с паролем «WuZkA6T@sQ8W». Я очень скоро удалю этого пользователя из своей службы. Но вы можете убедиться, что они работают.

const dgram = require("dgram");
const crypto = require("crypto");

const asteriskDOMAIN = "sip.antisip.com";
const asteriskIP = "94.23.17.185";
const asteriskPort = 5060;
const clientIP = "192.168.1.9";
const clientPort = 6111;
const username = "stackoverflow";
const password = "WuZkA6T@sQ8W";
let callId;
let cseq = 1;

// Create a UDP socket
const socket = dgram.createSocket("udp4");

// Generate a random branch identifier for the Via header
const generateBranch = () => {
  const branchId = Math.floor(Math.random() * 10000000000000);
  return `z9hG4bK${branchId}X2`;
};

const generateCallid = () => {
  const branchId = Math.floor(Math.random() * 10000000000000);
  return `${branchId}`;
};

// Generate Digest response
function generateDigestResponse(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;
}

// SIP REGISTER request
const generateRegisterRequest = (branch, withAuth = false, realm = "", nonce = "") => {
  let request = `REGISTER sip:${asteriskDOMAIN}:${asteriskPort} SIP/2.0\r\n`
    + `Via: SIP/2.0/UDP ${clientIP}:${clientPort};branch=${branch}\r\n`
    + `From: <sip:${username}@${asteriskDOMAIN}>;tag=${branch}\r\n`
    + `To: <sip:${username}@${asteriskDOMAIN}>\r\n`
    + `Call-ID: ${callId}@${clientIP}\r\n`
    + `CSeq: ${cseq} REGISTER\r\n`
    + `Contact: <sip:${username}@${clientIP}:${clientPort}>\r\n`
    + "Max-Forwards: 70\r\n"
    + "Expires: 3600\r\n"
    + "User-Agent: Node.js SIP Library\r\n";

  cseq += 1;

  if (withAuth && realm && nonce) {
    const digestResponse = generateDigestResponse(username, password, realm, nonce, "REGISTER", `sip:${asteriskDOMAIN}:${asteriskPort}`);
    request += "Authorization: Digest "
      + `username = "${username}", realm = "${realm}", `
      + `nonce = "${nonce}", uri = "sip:${asteriskDOMAIN}:${asteriskPort}", `
      + `response = "${digestResponse}"\r\n`;
  }

  request += "Content-Length: 0\r\n\r\n";

  return request;
};

// Send the REGISTER request
const sendRegisterRequest = (request) => {
  socket.send(request, 0, request.length, asteriskPort, asteriskIP, (error) => {
    if (error) {
      console.error("Error sending UDP packet:", error);
    } else {
      console.info("REGISTER request sent successfully.");
    }
  });
};

let realm = "";
let nonce = "";

// Listen for incoming responses
socket.on("message", (message) => {
  const response = message.toString();
  console.info("Received response:", response);

  if (response.startsWith("SIP/2.0 200 OK")) {
    console.info("Registration successful.");
    // Do further processing or initiate calls here
  } else if (response.startsWith("SIP/2.0 401 Unauthorized")) {
    const authenticateHeader = response.match(/WWW-Authenticate:.*realm = "([^"]+)".*nonce = "([^"]+)"/i);
    if (authenticateHeader) {
      realm = authenticateHeader[1];
      nonce = authenticateHeader[2];
      console.info("Received realm:", realm);
      console.info("Received nonce:", nonce);

      // Generate Digest response and proceed with registration
      const branch = generateBranch();
      const registerRequestWithAuth = generateRegisterRequest(branch, true, realm, nonce);

      console.info("Sending REGISTER request with authentication:");
      console.info(registerRequestWithAuth);
      // Send the REGISTER request with authentication
      sendRegisterRequest(`${registerRequestWithAuth}`);
    }
  }
});

// Bind the socket to the client's port and IP
socket.bind(clientPort, clientIP, () => {
  console.info("Socket bound successfully.");

  // Generate branch identifier for the initial REGISTER request
  const branch = generateBranch();
  callId = generateCallid();
  const registerRequest = generateRegisterRequest(branch);

  // Send the initial REGISTER request
  sendRegisterRequest(registerRequest);
});

Другие вопросы по теме