Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugins/janus_sip.c: MESSAGE Authentication and Deliver Status Report #2786

Merged
merged 8 commits into from
Nov 22, 2021
6 changes: 4 additions & 2 deletions html/siptest.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,12 @@ <h3>Demo details</h3>
<span class="input-group-addon"><i class="fa fa-phone fa-fw"></i></span>
<input class="form-control" type="text" placeholder="SIP URI to call (e.g., sip:1000@example.com)" autocomplete="off" id="peer" onkeypress="return checkEnter(this, event);" />
</div>
<button class="btn btn-success margin-bottom-sm" autocomplete="off" id="call">Call</button> <input autocomplete="off" id="dovideo" type="checkbox" />Use Video
<input class="form-control" type="text" placeholder="any text message" autocomplete="off" id="message" ></input>
<button class="btn btn-success margin-bottom-sm" autocomplete="off" id="call">Call</button> <input autocomplete="off" id="dovideo" type="checkbox" />Use Video<br />
<button class="btn btn-success margin-bottom-sm" autocomplete="off" id="sendmessage" onclick="return doMessage();" >Message</button>
</div>
</div>
<div/>
</div>
<div id="videos" class="hide">
<div class="col-md-6">
<div class="panel panel-default">
Expand Down
56 changes: 54 additions & 2 deletions html/siptest.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,16 @@ $(document).ready(function() {
$('#call').removeAttr('disabled').html('Call')
.removeClass("btn-danger").addClass("btn-success")
.unbind('click').click(doCall);
} else if(event === 'messagedelivery') {
// message delivery status
let reason = result["reason"];
let code = result["code"];
let callid = msg['call_id'];
if (code == 200) {
toastr.success(`${callid} Delivery Status: ${code} ${reason}`);
} else {
toastr.error(`${callid} Delivery Status: ${code} ${reason}`);
}
}
}
},
Expand Down Expand Up @@ -961,10 +971,12 @@ function addHelper(helperCreated) {
' <span class="input-group-addon"><i class="fa fa-phone fa-fw"></i></span>' +
' <input disabled class="form-control" type="text" placeholder="SIP URI to call (e.g., sip:1000@example.com)" autocomplete="off" id="peer' + helperId + '" onkeypress="return checkEnter(this, event, ' + helperId + ');"></input>' +
' </div>' +
' <button disabled class="btn btn-success margin-bottom-sm" autocomplete="off" id="call' + helperId + '">Call</button> <input autocomplete="off" id="dovideo' + helperId + '" type="checkbox">Use Video</input>' +
' <input class="form-control" type="text" placeholder="any text message" autocomplete="off" id="message' + helperId + '" ></input>' +
' <button disabled class="btn btn-success margin-bottom-sm" autocomplete="off" id="call' + helperId + '">Call</button> <input autocomplete="off" id="dovideo' + helperId + '" type="checkbox">Use Video</input><br />' +
' <button class="btn btn-success margin-bottom-sm" autocomplete="off" id="sendmessage' + helperId + '" onclick="return doMessage('+helperId+');">Message</button>' +
' </div>' +
' </div>' +
' <div/>' +
' </div>' +
' <div id="videos' + helperId + '" class="hide">' +
' <div class="col-md-6">' +
' <div class="panel panel-default">' +
Expand Down Expand Up @@ -1313,6 +1325,16 @@ function addHelper(helperCreated) {
$('#call' + helperId).removeAttr('disabled').html('Call')
.removeClass("btn-danger").addClass("btn-success")
.unbind('click').click(doCall);
} else if(event === 'messagedelivery') {
// message delivery status
let reason = result["reason"];
let code = result["code"];
let callid = msg['call_id'];
if (code == 200) {
toastr.success(`${callid}/${helperId} Delivery Status: ${code} ${reason}`);
} else {
toastr.error(`${callid}/${helperId} Delivery Status: ${code} ${reason}`);
}
}
}
},
Expand Down Expand Up @@ -1519,3 +1541,33 @@ function removeHelper(helperId) {
$('#sipcall'+helperId).remove();
}
}
/**
* send message function
*
* @param {number} suffix helper id
*/
function doMessage(suffix) {
if (suffix === undefined) {
suffix = '';
}
$('#peer' + suffix).attr('disabled', true);
$('#message' + suffix).attr('disabled', true);
$('#sendmessage' + suffix).attr('disabled', true);
let to = $('#peer' + suffix).val();
let body = $('#message' + suffix).val();

if(to === "" || to.indexOf("sip:") != 0 || to.indexOf("@") < 0) {
bootbox.alert('Please insert a valid SIP address (e.g., sip:pluto@example.com)');
} else {
let handle = sipcall;
if(suffix && helpers[suffix] && helpers[suffix]) {
handle = helpers[suffix].sipcall;
}
var msg = { request: "message", uri: to, content: body };
handle.send({ message: msg });
}
$('#peer' + suffix).removeAttr('disabled').val("");
$('#message' + suffix).removeAttr('disabled').val("");
$('#sendmessage' + suffix).removeAttr('disabled');
return;
}
161 changes: 153 additions & 8 deletions plugins/janus_sip.c
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@
\verbatim
{
"request" : "message",
"call_id" : "<user-defined value of Call-ID SIP header used to send the message; optional>",
"content_type" : "<content type; optional>"
"content" : "<text to send>",
"uri" : "<SIP URI of the peer; optional; if set, the message will be sent out of dialog>",
Expand All @@ -435,6 +436,21 @@
"headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
}
}
\endverbatim
*
* After delivery a \c messagedelivery event will be sent back with the SIP server response.
* Used to track the delivery status of the message.
*
\verbatim
{
"sip" : "event",
"call_id" : "<value of SIP Call-ID header for related message>",
"result" : {
"event" : "messagedelivery",
"code" : "<SIP error code>",
"reason" : "<SIP error reason>",
}
}
\endverbatim
*
* SIP INFO works pretty much the same way, except that you use an \c info
Expand Down Expand Up @@ -1049,6 +1065,7 @@ typedef struct janus_sip_session {
static GHashTable *sessions;
static GHashTable *identities;
static GHashTable *callids;
static GHashTable *messageids;
static GHashTable *masters;
static GHashTable *transfers;
static janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER;
Expand Down Expand Up @@ -1076,6 +1093,11 @@ static void janus_sip_session_destroy(janus_sip_session *session) {
janus_refcount_decrease(&session->ref);
}

static void janus_sip_message_key_destroy(char *key) {
if (key)
g_free(key);
}

static void janus_sip_session_free(const janus_refcount *session_ref) {
janus_sip_session *session = janus_refcount_containerof(session_ref, janus_sip_session, ref);
/* Remove the reference to the core plugin session */
Expand Down Expand Up @@ -1936,6 +1958,7 @@ int janus_sip_init(janus_callbacks *callback, const char *config_path) {
sessions = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_sip_session_destroy);
identities = g_hash_table_new(g_str_hash, g_str_equal);
callids = g_hash_table_new(g_str_hash, g_str_equal);
messageids = g_hash_table_new_full(NULL, NULL, (GDestroyNotify)janus_sip_message_key_destroy, (GDestroyNotify)janus_sip_session_destroy);
masters = g_hash_table_new(NULL, NULL);
transfers = g_hash_table_new_full(NULL, NULL, NULL, (GDestroyNotify)janus_sip_transfer_destroy);
messages = g_async_queue_new_full((GDestroyNotify) janus_sip_message_free);
Expand Down Expand Up @@ -1972,11 +1995,13 @@ void janus_sip_destroy(void) {
janus_mutex_lock(&sessions_mutex);
g_hash_table_destroy(sessions);
g_hash_table_destroy(callids);
g_hash_table_destroy(messageids);
g_hash_table_destroy(identities);
g_hash_table_destroy(masters);
g_hash_table_destroy(transfers);
sessions = NULL;
callids = NULL;
messageids = NULL;
identities = NULL;
masters = NULL;
transfers = NULL;
Expand Down Expand Up @@ -4637,37 +4662,76 @@ static void *janus_sip_handler(void *data) {
char custom_headers[2048];
janus_sip_parse_custom_headers(root, (char *)&custom_headers, sizeof(custom_headers));

char *message_callid = NULL;
if(in_dialog_message) {
/* Take Call-ID, later used to report delivery status */
message_callid = g_strdup(session->callid) ;
nua_message(session->stack->s_nh_i,
SIPTAG_CONTENT_TYPE_STR(content_type),
SIPTAG_PAYLOAD_STR(msg_content),
TAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),
TAG_END());
} else {
janus_mutex_lock(&session->stack->smutex);
if(session->stack->s_nh_m == NULL) {
if (session->stack->s_nua == NULL) {
/* Get appropriate handle */
nua_handle_t *nh = NULL;
if(!session->helper) {
janus_mutex_lock(&session->stack->smutex);
if(session->stack->s_nua == NULL) {
janus_mutex_unlock(&session->stack->smutex);
JANUS_LOG(LOG_ERR, "NUA destroyed while sending message?\n");
error_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;
g_snprintf(error_cause, 512, "Invalid NUA");
goto error;
}
session->stack->s_nh_m = nua_handle(session->stack->s_nua, session, TAG_END());
nh = nua_handle(session->stack->s_nua, session, TAG_END());
janus_mutex_unlock(&session->stack->smutex);
} else {
/* This is a helper, we need to use the master's SIP stack */
if(session->master == NULL || session->master->stack == NULL) {
error_code = JANUS_SIP_ERROR_HELPER_ERROR;
g_snprintf(error_cause, 512, "Invalid master SIP stack");
goto error;
}
janus_mutex_lock(&session->master->stack->smutex);
if(session->master->stack->s_nua == NULL) {
janus_mutex_unlock(&session->master->stack->smutex);
JANUS_LOG(LOG_ERR, "NUA destroyed while sending message?\n");
error_code = JANUS_SIP_ERROR_LIBSOFIA_ERROR;
g_snprintf(error_cause, 512, "Invalid NUA");
goto error;
}
nh = nua_handle(session->master->stack->s_nua, session, TAG_END());
janus_mutex_unlock(&session->master->stack->smutex);
}
janus_mutex_unlock(&session->stack->smutex);
nua_message(session->stack->s_nh_m,
json_t *request_callid = json_object_get(root, "call_id");
/* Use call-id from the request, if it exists */
if(request_callid) {
message_callid = g_strdup(json_string_value(request_callid));
} else {
/* If call-id does not exist in request, create a random one */
message_callid = g_malloc0(24);
janus_sip_random_string(24, message_callid);
}
nua_message(nh,
SIPTAG_TO_STR(uri_text),
SIPTAG_CONTENT_TYPE_STR(content_type),
SIPTAG_PAYLOAD_STR(msg_content),
NUTAG_PROXY(session->helper && session->master ?
session->master->account.outbound_proxy : session->account.outbound_proxy),
TAG_IF(strlen(custom_headers) > 0, SIPTAG_HEADER_STR(custom_headers)),
SIPTAG_CALL_ID_STR(message_callid),
TAG_END());
}
/* Notify the operation */
/* Notify the application */
result = json_object();
json_object_set_new(result, "event", json_string("messagesent"));
json_object_set_new(result, "call_id", json_string(message_callid));
/* Store message id and session */
janus_mutex_lock(&sessions_mutex);
janus_refcount_increase(&session->ref);
g_hash_table_insert(messageids, g_strdup(message_callid), session);
janus_mutex_unlock(&sessions_mutex);
g_free(message_callid);
} else if(!strcasecmp(request_text, "dtmf_info")) {
/* Send DMTF tones using SIP INFO
* (https://tools.ietf.org/html/draft-kaplan-dispatch-info-dtmf-package-00)
Expand Down Expand Up @@ -5419,7 +5483,88 @@ void janus_sip_sofia_callback(nua_event_t event, int status, char const *phrase,
break;
case nua_r_message:
JANUS_LOG(LOG_VERB, "[%s][%s]: %d %s\n", session->account.username, nua_event_name(event), status, phrase ? phrase : "??");
/* FIXME Should we notify the user, in case the SIP MESSAGE returned an error? */
/* Handle authetntication for SIP MESSAGE - eg. SippySoft Softswitch requires 401 authentication even if SIP user is registerered */
if(status == 401 || status == 407) {
const char *scheme = NULL;
const char *realm = NULL;
if(status == 401) {
/* Get scheme/realm from 401 error */
sip_www_authenticate_t const* www_auth = sip->sip_www_authenticate;
scheme = www_auth->au_scheme;
realm = msg_params_find(www_auth->au_params, "realm=");
} else {
/* Get scheme/realm from 407 error, proxy-auth */
sip_proxy_authenticate_t const* proxy_auth = sip->sip_proxy_authenticate;
scheme = proxy_auth->au_scheme;
realm = msg_params_find(proxy_auth->au_params, "realm=");
}
char authuser[100], secret[100];
memset(authuser, 0, sizeof(authuser));
memset(secret, 0, sizeof(secret));
if(session->helper) {
/* This is an helper session, we'll need the credentials from the master */
if(session->master == NULL) {
JANUS_LOG(LOG_WARN, "No master session for this helper, authentication will fail...\n");
} else {
session = session->master;
}
}
if(session->account.authuser && strchr(session->account.authuser, ':')) {
/* The authuser contains a colon: wrap it in quotes */
g_snprintf(authuser, sizeof(authuser), "\"%s\"", session->account.authuser);
} else {
g_snprintf(authuser, sizeof(authuser), "%s", session->account.authuser);
}
if(session->account.secret && strchr(session->account.secret, ':')) {
/* The secret contains a colon: wrap it in quotes */
g_snprintf(secret, sizeof(secret), "\"%s\"", session->account.secret);
} else {
g_snprintf(secret, sizeof(secret), "%s", session->account.secret);
}
char auth[256];
memset(auth, 0, sizeof(auth));
g_snprintf(auth, sizeof(auth), "%s%s:%s:%s:%s%s",
session->account.secret_type == janus_sip_secret_type_hashed ? "HA1+" : "",
scheme,
realm,
authuser,
session->account.secret_type == janus_sip_secret_type_hashed ? "HA1+" : "",
secret);
JANUS_LOG(LOG_VERB, "\t%s\n", auth);
/* Authenticate */
nua_authenticate(nh,
NUTAG_AUTH(auth),
TAG_END());
Comment on lines +5488 to +5537
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that this bloc of code repeated the third time. Could we move it to a separate function?

I am implementing publish command and will repeat it a fourth time. For sure I could do it in my PR, just curious is it the right way of refactoring, or is there some other sense in repeating the same code several times?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a function or could be before the main switch(event) in janus_sip_sofia_callback. Auth and ProxyAuth are not used outside of this function, IMHO it is better to split code into functions only if used elsewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do re-use the same auth code for different SIP responses, true. Anyway, this isn't something I'd do here: this is a change I'd do separately in another PR, devoted just to that, so I can look into this once we merge this effort.

} else {
char *messageid = g_strdup(sip->sip_call_id->i_id);
/* Find session associated with the message */
janus_mutex_lock(&sessions_mutex);
janus_sip_session *message_session = g_hash_table_lookup(messageids, messageid);
if (!message_session) {
message_session = session;
JANUS_LOG(LOG_VERB, "Message (%s) not associated with any session, event will be reported to master\n", messageid);
}
janus_mutex_unlock(&sessions_mutex);
/* MESSAGE response, notify the application */
json_t *result = json_object();
/* SIP code and reason */
json_object_set_new(result, "event", json_string("messagedelivery"));
json_object_set_new(result, "code", json_integer(status));
json_object_set_new(result, "reason", json_string(phrase));
/* Build the delivery receipt */
json_t *dr = json_object();
json_object_set_new(dr, "sip", json_string("event"));
json_object_set_new(dr, "result", result);
json_object_set_new(dr, "call_id", json_string(messageid));
/* Report delivery */
int ret = gateway->push_event(message_session->handle, &janus_sip_plugin, message_session->transaction, dr, NULL);
JANUS_LOG(LOG_VERB, " >> Pushing event to peer: %d (%s)\n", ret, janus_get_api_error(ret));
json_decref(dr);
janus_mutex_lock(&sessions_mutex);
g_hash_table_remove(messageids, messageid);
janus_mutex_unlock(&sessions_mutex);
g_free(messageid);
}
break;
case nua_r_refer: {
JANUS_LOG(LOG_VERB, "[%s][%s]: %d %s\n", session->account.username, nua_event_name(event), status, phrase ? phrase : "??");
Expand Down