/* $Id$ */ /* * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) * Copyright (C) 2003-2008 Benny Prijono * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include struct pj_stun_session { pj_stun_config *cfg; pj_pool_t *pool; pj_grp_lock_t *grp_lock; pj_stun_session_cb cb; void *user_data; pj_bool_t is_destroying; pj_bool_t use_fingerprint; pj_pool_t *rx_pool; #if PJ_LOG_MAX_LEVEL >= 5 char dump_buf[1000]; #endif unsigned log_flag; pj_stun_auth_type auth_type; pj_stun_auth_cred cred; int auth_retry; pj_str_t next_nonce; pj_str_t server_realm; pj_str_t srv_name; pj_stun_tx_data pending_request_list; pj_stun_tx_data cached_response_list; }; #define SNAME(s_) ((s_)->pool->obj_name) #define THIS_FILE "stun_session.c" #if 1 # define TRACE_(expr) PJ_LOG(5,expr) #else # define TRACE_(expr) #endif #define LOG_ERR_(sess,title,rc) PJ_PERROR(3,(sess->pool->obj_name,rc,title)) #define TDATA_POOL_SIZE PJNATH_POOL_LEN_STUN_TDATA #define TDATA_POOL_INC PJNATH_POOL_INC_STUN_TDATA static void stun_tsx_on_complete(pj_stun_client_tsx *tsx, pj_status_t status, const pj_stun_msg *response, const pj_sockaddr_t *src_addr, unsigned src_addr_len); static pj_status_t stun_tsx_on_send_msg(pj_stun_client_tsx *tsx, const void *stun_pkt, pj_size_t pkt_size); static void stun_tsx_on_destroy(pj_stun_client_tsx *tsx); static void stun_sess_on_destroy(void *comp); static void destroy_tdata(pj_stun_tx_data *tdata, pj_bool_t force); static pj_stun_tsx_cb tsx_cb = { &stun_tsx_on_complete, &stun_tsx_on_send_msg, &stun_tsx_on_destroy }; static pj_status_t tsx_add(pj_stun_session *sess, pj_stun_tx_data *tdata) { pj_list_push_front(&sess->pending_request_list, tdata); return PJ_SUCCESS; } static pj_status_t tsx_erase(pj_stun_session *sess, pj_stun_tx_data *tdata) { PJ_UNUSED_ARG(sess); pj_list_erase(tdata); return PJ_SUCCESS; } static pj_stun_tx_data* tsx_lookup(pj_stun_session *sess, const pj_stun_msg *msg) { pj_stun_tx_data *tdata; tdata = sess->pending_request_list.next; while (tdata != &sess->pending_request_list) { pj_assert(sizeof(tdata->msg_key)==sizeof(msg->hdr.tsx_id)); if (tdata->msg_magic == msg->hdr.magic && pj_memcmp(tdata->msg_key, msg->hdr.tsx_id, sizeof(msg->hdr.tsx_id))==0) { return tdata; } tdata = tdata->next; } return NULL; } static pj_status_t create_tdata(pj_stun_session *sess, pj_stun_tx_data **p_tdata) { pj_pool_t *pool; pj_stun_tx_data *tdata; /* Create pool and initialize basic tdata attributes */ pool = pj_pool_create(sess->cfg->pf, "tdata%p", TDATA_POOL_SIZE, TDATA_POOL_INC, NULL); PJ_ASSERT_RETURN(pool, PJ_ENOMEM); tdata = PJ_POOL_ZALLOC_T(pool, pj_stun_tx_data); tdata->pool = pool; tdata->sess = sess; pj_list_init(tdata); *p_tdata = tdata; return PJ_SUCCESS; } static void stun_tsx_on_destroy(pj_stun_client_tsx *tsx) { pj_stun_tx_data *tdata; tdata = (pj_stun_tx_data*) pj_stun_client_tsx_get_data(tsx); pj_stun_client_tsx_stop(tsx); if (tdata) { pj_stun_session *sess = tdata->sess; pj_grp_lock_acquire(sess->grp_lock); tsx_erase(sess, tdata); destroy_tdata(tdata, PJ_TRUE); pj_grp_lock_release(sess->grp_lock); } pj_stun_client_tsx_destroy(tsx); TRACE_((THIS_FILE, "STUN transaction %p destroyed", tsx)); } static void tdata_on_destroy(void *arg) { pj_stun_tx_data *tdata = (pj_stun_tx_data*)arg; pj_pool_safe_release(&tdata->pool); } static void destroy_tdata(pj_stun_tx_data *tdata, pj_bool_t force) { TRACE_((THIS_FILE, "tdata %p destroy request, force=%d, tsx=%p", tdata, force, tdata->client_tsx)); /* STUN session may have been destroyed, except when tdata is cached. */ if (tdata->res_timer.id != PJ_FALSE) { pj_timer_heap_cancel_if_active(tdata->sess->cfg->timer_heap, &tdata->res_timer, PJ_FALSE); } if (force) { pj_list_erase(tdata); if (tdata->client_tsx) { pj_stun_client_tsx_stop(tdata->client_tsx); pj_stun_client_tsx_set_data(tdata->client_tsx, NULL); } if (tdata->grp_lock) { pj_grp_lock_dec_ref(tdata->sess->grp_lock); pj_grp_lock_dec_ref(tdata->grp_lock); } else { tdata_on_destroy(tdata); } } else { if (tdata->client_tsx) { /* "Probably" this is to absorb retransmission */ pj_time_val delay = {0, 300}; pj_stun_client_tsx_schedule_destroy(tdata->client_tsx, &delay); } else { pj_list_erase(tdata); if (tdata->grp_lock) { pj_grp_lock_dec_ref(tdata->sess->grp_lock); pj_grp_lock_dec_ref(tdata->grp_lock); } else { tdata_on_destroy(tdata); } } } } /* * Destroy the transmit data. */ PJ_DEF(void) pj_stun_msg_destroy_tdata( pj_stun_session *sess, pj_stun_tx_data *tdata) { PJ_UNUSED_ARG(sess); destroy_tdata(tdata, PJ_FALSE); } /* Timer callback to be called when it's time to destroy response cache */ static void on_cache_timeout(pj_timer_heap_t *timer_heap, struct pj_timer_entry *entry) { pj_stun_tx_data *tdata; pj_stun_session *sess; PJ_UNUSED_ARG(timer_heap); entry->id = PJ_FALSE; tdata = (pj_stun_tx_data*) entry->user_data; sess = tdata->sess; pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return; } PJ_LOG(5,(SNAME(tdata->sess), "Response cache deleted")); destroy_tdata(tdata, PJ_FALSE); pj_grp_lock_release(sess->grp_lock); } static pj_status_t apply_msg_options(pj_stun_session *sess, pj_pool_t *pool, const pj_stun_req_cred_info *auth_info, pj_stun_msg *msg) { pj_status_t status = 0; pj_str_t realm, username, nonce, auth_key; /* If the agent is sending a request, it SHOULD add a SOFTWARE attribute * to the request. The server SHOULD include a SOFTWARE attribute in all * responses. * * If magic value is not PJ_STUN_MAGIC, only apply the attribute for * responses. */ if (sess->srv_name.slen && pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_SOFTWARE, 0)==NULL && (PJ_STUN_IS_RESPONSE(msg->hdr.type) || (PJ_STUN_IS_REQUEST(msg->hdr.type) && msg->hdr.magic==PJ_STUN_MAGIC))) { pj_stun_msg_add_string_attr(pool, msg, PJ_STUN_ATTR_SOFTWARE, &sess->srv_name); } if (pj_stun_auth_valid_for_msg(msg) && auth_info) { realm = auth_info->realm; username = auth_info->username; nonce = auth_info->nonce; auth_key = auth_info->auth_key; } else { realm.slen = username.slen = nonce.slen = auth_key.slen = 0; } /* Create and add USERNAME attribute if needed */ if (username.slen && PJ_STUN_IS_REQUEST(msg->hdr.type)) { status = pj_stun_msg_add_string_attr(pool, msg, PJ_STUN_ATTR_USERNAME, &username); PJ_ASSERT_RETURN(status==PJ_SUCCESS, status); } /* Add REALM only when long term credential is used */ if (realm.slen && PJ_STUN_IS_REQUEST(msg->hdr.type)) { status = pj_stun_msg_add_string_attr(pool, msg, PJ_STUN_ATTR_REALM, &realm); PJ_ASSERT_RETURN(status==PJ_SUCCESS, status); } /* Add NONCE when desired */ if (nonce.slen && (PJ_STUN_IS_REQUEST(msg->hdr.type) || PJ_STUN_IS_ERROR_RESPONSE(msg->hdr.type))) { status = pj_stun_msg_add_string_attr(pool, msg, PJ_STUN_ATTR_NONCE, &nonce); } /* Add MESSAGE-INTEGRITY attribute */ if (username.slen && auth_key.slen) { status = pj_stun_msg_add_msgint_attr(pool, msg); PJ_ASSERT_RETURN(status==PJ_SUCCESS, status); } /* Add FINGERPRINT attribute if necessary */ if (sess->use_fingerprint) { status = pj_stun_msg_add_uint_attr(pool, msg, PJ_STUN_ATTR_FINGERPRINT, 0); PJ_ASSERT_RETURN(status==PJ_SUCCESS, status); } return PJ_SUCCESS; } static pj_status_t handle_auth_challenge(pj_stun_session *sess, const pj_stun_tx_data *request, const pj_stun_msg *response, const pj_sockaddr_t *src_addr, unsigned src_addr_len, pj_bool_t *notify_user) { const pj_stun_errcode_attr *ea; *notify_user = PJ_TRUE; if (response==NULL) return PJ_SUCCESS; if (sess->auth_type != PJ_STUN_AUTH_LONG_TERM) return PJ_SUCCESS; if (!PJ_STUN_IS_ERROR_RESPONSE(response->hdr.type)) { sess->auth_retry = 0; return PJ_SUCCESS; } ea = (const pj_stun_errcode_attr*) pj_stun_msg_find_attr(response, PJ_STUN_ATTR_ERROR_CODE, 0); if (!ea) { PJ_LOG(4,(SNAME(sess), "Invalid error response: no ERROR-CODE" " attribute")); *notify_user = PJ_FALSE; return PJNATH_EINSTUNMSG; } if (ea->err_code == PJ_STUN_SC_UNAUTHORIZED || ea->err_code == PJ_STUN_SC_STALE_NONCE) { const pj_stun_nonce_attr *anonce; const pj_stun_realm_attr *arealm; pj_stun_tx_data *tdata; unsigned i; pj_status_t status; anonce = (const pj_stun_nonce_attr*) pj_stun_msg_find_attr(response, PJ_STUN_ATTR_NONCE, 0); if (!anonce) { PJ_LOG(4,(SNAME(sess), "Invalid response: missing NONCE")); *notify_user = PJ_FALSE; return PJNATH_EINSTUNMSG; } /* Bail out if we've supplied the correct nonce */ if (pj_strcmp(&anonce->value, &sess->next_nonce)==0) { return PJ_SUCCESS; } /* Bail out if we've tried too many */ if (++sess->auth_retry > 3) { PJ_LOG(4,(SNAME(sess), "Error: authentication failed (too " "many retries)")); return PJ_STATUS_FROM_STUN_CODE(401); } /* Save next_nonce */ pj_strdup(sess->pool, &sess->next_nonce, &anonce->value); /* Copy the realm from the response */ arealm = (pj_stun_realm_attr*) pj_stun_msg_find_attr(response, PJ_STUN_ATTR_REALM, 0); if (arealm) { pj_strdup(sess->pool, &sess->server_realm, &arealm->value); while (sess->server_realm.slen && !sess->server_realm.ptr[sess->server_realm.slen-1]) { --sess->server_realm.slen; } } /* Create new request */ status = pj_stun_session_create_req(sess, request->msg->hdr.type, request->msg->hdr.magic, NULL, &tdata); if (status != PJ_SUCCESS) return status; /* Duplicate all the attributes in the old request, except * USERNAME, REALM, M-I, and NONCE, which will be filled in * later. */ for (i=0; imsg->attr_count; ++i) { const pj_stun_attr_hdr *asrc = request->msg->attr[i]; if (asrc->type == PJ_STUN_ATTR_USERNAME || asrc->type == PJ_STUN_ATTR_REALM || asrc->type == PJ_STUN_ATTR_MESSAGE_INTEGRITY || asrc->type == PJ_STUN_ATTR_NONCE) { continue; } tdata->msg->attr[tdata->msg->attr_count++] = pj_stun_attr_clone(tdata->pool, asrc); } /* Will retry the request with authentication, no need to * notify user. */ *notify_user = PJ_FALSE; PJ_LOG(4,(SNAME(sess), "Retrying request with new authentication")); /* Retry the request */ status = pj_stun_session_send_msg(sess, request->token, PJ_TRUE, request->retransmit, src_addr, src_addr_len, tdata); } else { sess->auth_retry = 0; } return PJ_SUCCESS; } static void stun_tsx_on_complete(pj_stun_client_tsx *tsx, pj_status_t status, const pj_stun_msg *response, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { pj_stun_session *sess; pj_bool_t notify_user = PJ_TRUE; pj_stun_tx_data *tdata; tdata = (pj_stun_tx_data*) pj_stun_client_tsx_get_data(tsx); sess = tdata->sess; /* Lock the session and prevent user from destroying us in the callback */ pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_stun_msg_destroy_tdata(sess, tdata); pj_grp_lock_release(sess->grp_lock); return; } /* Handle authentication challenge */ handle_auth_challenge(sess, tdata, response, src_addr, src_addr_len, ¬ify_user); if (notify_user && sess->cb.on_request_complete) { (*sess->cb.on_request_complete)(sess, status, tdata->token, tdata, response, src_addr, src_addr_len); } /* Destroy the transmit data. This will remove the transaction * from the pending list too. */ if (status == PJNATH_ESTUNTIMEDOUT) destroy_tdata(tdata, PJ_TRUE); else destroy_tdata(tdata, PJ_FALSE); tdata = NULL; pj_grp_lock_release(sess->grp_lock); } static pj_status_t stun_tsx_on_send_msg(pj_stun_client_tsx *tsx, const void *stun_pkt, pj_size_t pkt_size) { pj_stun_tx_data *tdata; pj_stun_session *sess; pj_status_t status; tdata = (pj_stun_tx_data*) pj_stun_client_tsx_get_data(tsx); sess = tdata->sess; /* Lock the session and prevent user from destroying us in the callback */ pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { /* Stray timer */ pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } status = sess->cb.on_send_msg(tdata->sess, tdata->token, stun_pkt, pkt_size, tdata->dst_addr, tdata->addr_len); if (pj_grp_lock_release(sess->grp_lock)) return PJ_EGONE; return status; } /* **************************************************************************/ PJ_DEF(pj_status_t) pj_stun_session_create( pj_stun_config *cfg, const char *name, const pj_stun_session_cb *cb, pj_bool_t fingerprint, pj_grp_lock_t *grp_lock, pj_stun_session **p_sess) { pj_pool_t *pool; pj_stun_session *sess; pj_status_t status; PJ_ASSERT_RETURN(cfg && cb && p_sess, PJ_EINVAL); if (name==NULL) name = "stuse%p"; pool = pj_pool_create(cfg->pf, name, PJNATH_POOL_LEN_STUN_SESS, PJNATH_POOL_INC_STUN_SESS, NULL); PJ_ASSERT_RETURN(pool, PJ_ENOMEM); sess = PJ_POOL_ZALLOC_T(pool, pj_stun_session); sess->cfg = cfg; sess->pool = pool; pj_memcpy(&sess->cb, cb, sizeof(*cb)); sess->use_fingerprint = fingerprint; sess->log_flag = 0xFFFF; if (grp_lock) { sess->grp_lock = grp_lock; } else { status = pj_grp_lock_create(pool, NULL, &sess->grp_lock); if (status != PJ_SUCCESS) { pj_pool_release(pool); return status; } } pj_grp_lock_add_ref(sess->grp_lock); pj_grp_lock_add_handler(sess->grp_lock, pool, sess, &stun_sess_on_destroy); pj_stun_session_set_software_name(sess, &cfg->software_name); sess->rx_pool = pj_pool_create(sess->cfg->pf, name, PJNATH_POOL_LEN_STUN_TDATA, PJNATH_POOL_INC_STUN_TDATA, NULL); pj_list_init(&sess->pending_request_list); pj_list_init(&sess->cached_response_list); *p_sess = sess; return PJ_SUCCESS; } static void stun_sess_on_destroy(void *comp) { pj_stun_session *sess = (pj_stun_session*)comp; while (!pj_list_empty(&sess->pending_request_list)) { pj_stun_tx_data *tdata = sess->pending_request_list.next; destroy_tdata(tdata, PJ_TRUE); } pj_pool_safe_release(&sess->rx_pool); pj_pool_safe_release(&sess->pool); TRACE_((THIS_FILE, "STUN session %p destroyed", sess)); } PJ_DEF(pj_status_t) pj_stun_session_destroy(pj_stun_session *sess) { pj_stun_tx_data *tdata; PJ_ASSERT_RETURN(sess, PJ_EINVAL); TRACE_((SNAME(sess), "STUN session %p destroy request, ref_cnt=%d", sess, pj_grp_lock_get_ref(sess->grp_lock))); pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { /* Prevent from decrementing the ref counter more than once */ pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } sess->is_destroying = PJ_TRUE; /* We need to stop transactions because they are * holding the group lock's reference counter while retransmitting. */ tdata = sess->pending_request_list.next; while (tdata != &sess->pending_request_list) { if (tdata->client_tsx) pj_stun_client_tsx_stop(tdata->client_tsx); tdata = tdata->next; } /* Destroy cached response within session lock protection to avoid * race scenario with on_cache_timeout(). */ while (!pj_list_empty(&sess->cached_response_list)) { pj_stun_tx_data *tmp_tdata = sess->cached_response_list.next; destroy_tdata(tmp_tdata, PJ_TRUE); } pj_grp_lock_dec_ref(sess->grp_lock); pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } PJ_DEF(pj_status_t) pj_stun_session_set_user_data( pj_stun_session *sess, void *user_data) { PJ_ASSERT_RETURN(sess, PJ_EINVAL); pj_grp_lock_acquire(sess->grp_lock); sess->user_data = user_data; pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } PJ_DEF(void*) pj_stun_session_get_user_data(pj_stun_session *sess) { PJ_ASSERT_RETURN(sess, NULL); return sess->user_data; } PJ_DEF(pj_grp_lock_t *) pj_stun_session_get_grp_lock(pj_stun_session *sess) { PJ_ASSERT_RETURN(sess, NULL); return sess->grp_lock; } PJ_DEF(pj_status_t) pj_stun_session_set_software_name(pj_stun_session *sess, const pj_str_t *sw) { PJ_ASSERT_RETURN(sess, PJ_EINVAL); pj_grp_lock_acquire(sess->grp_lock); if (sw && sw->slen) pj_strdup(sess->pool, &sess->srv_name, sw); else sess->srv_name.slen = 0; pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } PJ_DEF(pj_status_t) pj_stun_session_set_credential(pj_stun_session *sess, pj_stun_auth_type auth_type, const pj_stun_auth_cred *cred) { PJ_ASSERT_RETURN(sess, PJ_EINVAL); pj_grp_lock_acquire(sess->grp_lock); sess->auth_type = auth_type; if (cred) { pj_stun_auth_cred_dup(sess->pool, &sess->cred, cred); } else { sess->auth_type = PJ_STUN_AUTH_NONE; pj_bzero(&sess->cred, sizeof(sess->cred)); } pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } PJ_DEF(void) pj_stun_session_set_log( pj_stun_session *sess, unsigned flags) { PJ_ASSERT_ON_FAIL(sess, return); sess->log_flag = flags; } PJ_DEF(pj_bool_t) pj_stun_session_use_fingerprint(pj_stun_session *sess, pj_bool_t use) { pj_bool_t old_use; PJ_ASSERT_RETURN(sess, PJ_FALSE); old_use = sess->use_fingerprint; sess->use_fingerprint = use; return old_use; } static pj_status_t get_auth(pj_stun_session *sess, pj_stun_tx_data *tdata) { if (sess->cred.type == PJ_STUN_AUTH_CRED_STATIC) { //tdata->auth_info.realm = sess->cred.data.static_cred.realm; tdata->auth_info.realm = sess->server_realm; tdata->auth_info.username = sess->cred.data.static_cred.username; tdata->auth_info.nonce = sess->cred.data.static_cred.nonce; pj_stun_create_key(tdata->pool, &tdata->auth_info.auth_key, &tdata->auth_info.realm, &tdata->auth_info.username, sess->cred.data.static_cred.data_type, &sess->cred.data.static_cred.data); } else if (sess->cred.type == PJ_STUN_AUTH_CRED_DYNAMIC) { pj_str_t password; void *user_data = sess->cred.data.dyn_cred.user_data; pj_stun_passwd_type data_type = PJ_STUN_PASSWD_PLAIN; pj_status_t rc; rc = (*sess->cred.data.dyn_cred.get_cred)(tdata->msg, user_data, tdata->pool, &tdata->auth_info.realm, &tdata->auth_info.username, &tdata->auth_info.nonce, &data_type, &password); if (rc != PJ_SUCCESS) return rc; pj_stun_create_key(tdata->pool, &tdata->auth_info.auth_key, &tdata->auth_info.realm, &tdata->auth_info.username, data_type, &password); } else { pj_assert(!"Unknown credential type"); return PJ_EBUG; } return PJ_SUCCESS; } PJ_DEF(pj_status_t) pj_stun_session_create_req(pj_stun_session *sess, int method, pj_uint32_t magic, const pj_uint8_t tsx_id[12], pj_stun_tx_data **p_tdata) { pj_stun_tx_data *tdata = NULL; pj_status_t status; PJ_ASSERT_RETURN(sess && p_tdata, PJ_EINVAL); pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } status = create_tdata(sess, &tdata); if (status != PJ_SUCCESS) goto on_error; /* Create STUN message */ status = pj_stun_msg_create(tdata->pool, method, magic, tsx_id, &tdata->msg); if (status != PJ_SUCCESS) goto on_error; /* copy the request's transaction ID as the transaction key. */ pj_assert(sizeof(tdata->msg_key)==sizeof(tdata->msg->hdr.tsx_id)); tdata->msg_magic = tdata->msg->hdr.magic; pj_memcpy(tdata->msg_key, tdata->msg->hdr.tsx_id, sizeof(tdata->msg->hdr.tsx_id)); /* Get authentication information for the request */ if (sess->auth_type == PJ_STUN_AUTH_NONE) { /* No authentication */ } else if (sess->auth_type == PJ_STUN_AUTH_SHORT_TERM) { /* MUST put authentication in request */ status = get_auth(sess, tdata); if (status != PJ_SUCCESS) goto on_error; } else if (sess->auth_type == PJ_STUN_AUTH_LONG_TERM) { /* Only put authentication information if we've received * response from server. */ if (sess->next_nonce.slen != 0) { status = get_auth(sess, tdata); if (status != PJ_SUCCESS) goto on_error; tdata->auth_info.nonce = sess->next_nonce; tdata->auth_info.realm = sess->server_realm; } } else { pj_assert(!"Invalid authentication type"); status = PJ_EBUG; goto on_error; } *p_tdata = tdata; pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; on_error: if (tdata) pj_pool_safe_release(&tdata->pool); pj_grp_lock_release(sess->grp_lock); return status; } PJ_DEF(pj_status_t) pj_stun_session_create_ind(pj_stun_session *sess, int msg_type, pj_stun_tx_data **p_tdata) { pj_stun_tx_data *tdata = NULL; pj_status_t status; PJ_ASSERT_RETURN(sess && p_tdata, PJ_EINVAL); pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } status = create_tdata(sess, &tdata); if (status != PJ_SUCCESS) { pj_grp_lock_release(sess->grp_lock); return status; } /* Create STUN message */ msg_type |= PJ_STUN_INDICATION_BIT; status = pj_stun_msg_create(tdata->pool, msg_type, PJ_STUN_MAGIC, NULL, &tdata->msg); if (status != PJ_SUCCESS) { pj_pool_safe_release(&tdata->pool); pj_grp_lock_release(sess->grp_lock); return status; } *p_tdata = tdata; pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } /* * Create a STUN response message. */ PJ_DEF(pj_status_t) pj_stun_session_create_res( pj_stun_session *sess, const pj_stun_rx_data *rdata, unsigned err_code, const pj_str_t *err_msg, pj_stun_tx_data **p_tdata) { pj_status_t status; pj_stun_tx_data *tdata = NULL; pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } status = create_tdata(sess, &tdata); if (status != PJ_SUCCESS) { pj_grp_lock_release(sess->grp_lock); return status; } /* Create STUN response message */ status = pj_stun_msg_create_response(tdata->pool, rdata->msg, err_code, err_msg, &tdata->msg); if (status != PJ_SUCCESS) { pj_pool_safe_release(&tdata->pool); pj_grp_lock_release(sess->grp_lock); return status; } /* copy the request's transaction ID as the transaction key. */ pj_assert(sizeof(tdata->msg_key)==sizeof(rdata->msg->hdr.tsx_id)); tdata->msg_magic = rdata->msg->hdr.magic; pj_memcpy(tdata->msg_key, rdata->msg->hdr.tsx_id, sizeof(rdata->msg->hdr.tsx_id)); /* copy the credential found in the request */ pj_stun_req_cred_info_dup(tdata->pool, &tdata->auth_info, &rdata->info); *p_tdata = tdata; pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } /* Print outgoing message to log */ static void dump_tx_msg(pj_stun_session *sess, const pj_stun_msg *msg, unsigned pkt_size, const pj_sockaddr_t *addr) { char dst_name[PJ_INET6_ADDRSTRLEN+10]; if ((PJ_STUN_IS_REQUEST(msg->hdr.type) && (sess->log_flag & PJ_STUN_SESS_LOG_TX_REQ)==0) || (PJ_STUN_IS_RESPONSE(msg->hdr.type) && (sess->log_flag & PJ_STUN_SESS_LOG_TX_RES)==0) || (PJ_STUN_IS_INDICATION(msg->hdr.type) && (sess->log_flag & PJ_STUN_SESS_LOG_TX_IND)==0)) { return; } pj_sockaddr_print(addr, dst_name, sizeof(dst_name), 3); PJ_LOG(5,(SNAME(sess), "TX %d bytes STUN message to %s:\n" "--- begin STUN message ---\n" "%s" "--- end of STUN message ---\n", pkt_size, dst_name, pj_stun_msg_dump(msg, sess->dump_buf, sizeof(sess->dump_buf), NULL))); } PJ_DEF(pj_status_t) pj_stun_session_send_msg( pj_stun_session *sess, void *token, pj_bool_t cache_res, pj_bool_t retransmit, const pj_sockaddr_t *server, unsigned addr_len, pj_stun_tx_data *tdata) { pj_status_t status; PJ_ASSERT_RETURN(sess && addr_len && server && tdata, PJ_EINVAL); /* Lock the session and prevent user from destroying us in the callback */ pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } pj_log_push_indent(); /* Allocate packet */ tdata->max_len = PJ_STUN_MAX_PKT_LEN; tdata->pkt = pj_pool_alloc(tdata->pool, tdata->max_len); tdata->token = token; tdata->retransmit = retransmit; /* Apply options */ status = apply_msg_options(sess, tdata->pool, &tdata->auth_info, tdata->msg); if (status != PJ_SUCCESS) { pj_stun_msg_destroy_tdata(sess, tdata); LOG_ERR_(sess, "Error applying options", status); goto on_return; } /* Encode message */ status = pj_stun_msg_encode(tdata->msg, (pj_uint8_t*)tdata->pkt, tdata->max_len, 0, &tdata->auth_info.auth_key, &tdata->pkt_size); if (status != PJ_SUCCESS) { pj_stun_msg_destroy_tdata(sess, tdata); LOG_ERR_(sess, "STUN encode() error", status); goto on_return; } /* Dump packet */ dump_tx_msg(sess, tdata->msg, (unsigned)tdata->pkt_size, server); /* If this is a STUN request message, then send the request with * a new STUN client transaction. */ if (PJ_STUN_IS_REQUEST(tdata->msg->hdr.type)) { /* Create STUN client transaction */ status = pj_stun_client_tsx_create(sess->cfg, tdata->pool, sess->grp_lock, &tsx_cb, &tdata->client_tsx); PJ_ASSERT_RETURN(status==PJ_SUCCESS, status); pj_stun_client_tsx_set_data(tdata->client_tsx, (void*)tdata); /* Save the remote address */ tdata->addr_len = addr_len; tdata->dst_addr = server; /* Send the request! */ status = pj_stun_client_tsx_send_msg(tdata->client_tsx, retransmit, tdata->pkt, (unsigned)tdata->pkt_size); if (status != PJ_SUCCESS && status != PJ_EPENDING) { pj_stun_msg_destroy_tdata(sess, tdata); LOG_ERR_(sess, "Error sending STUN request", status); goto on_return; } /* Add to pending request list */ tsx_add(sess, tdata); } else { /* Otherwise for non-request message, send directly to transport. */ if (cache_res && (PJ_STUN_IS_SUCCESS_RESPONSE(tdata->msg->hdr.type) || PJ_STUN_IS_ERROR_RESPONSE(tdata->msg->hdr.type))) { /* Requested to keep the response in the cache */ pj_time_val timeout; status = pj_grp_lock_create(tdata->pool, NULL, &tdata->grp_lock); if (status != PJ_SUCCESS) { pj_stun_msg_destroy_tdata(sess, tdata); LOG_ERR_(sess, "Error creating group lock", status); goto on_return; } pj_grp_lock_add_ref(tdata->grp_lock); pj_grp_lock_add_handler(tdata->grp_lock, tdata->pool, tdata, &tdata_on_destroy); /* Also add ref session group lock to make sure that the session * is still valid when cache timeout callback is called. */ pj_grp_lock_add_ref(sess->grp_lock); pj_memset(&tdata->res_timer, 0, sizeof(tdata->res_timer)); pj_timer_entry_init(&tdata->res_timer, PJ_FALSE, tdata, &on_cache_timeout); timeout.sec = sess->cfg->res_cache_msec / 1000; timeout.msec = sess->cfg->res_cache_msec % 1000; status = pj_timer_heap_schedule_w_grp_lock(sess->cfg->timer_heap, &tdata->res_timer, &timeout, PJ_TRUE, tdata->grp_lock); if (status != PJ_SUCCESS) { pj_stun_msg_destroy_tdata(sess, tdata); LOG_ERR_(sess, "Error scheduling response timer", status); goto on_return; } pj_list_push_back(&sess->cached_response_list, tdata); } /* Send to transport directly. */ status = sess->cb.on_send_msg(sess, token, tdata->pkt, tdata->pkt_size, server, addr_len); if (status != PJ_SUCCESS && status != PJ_EPENDING) { pj_stun_msg_destroy_tdata(sess, tdata); LOG_ERR_(sess, "Error sending STUN request", status); goto on_return; } /* Destroy only when response is not cached*/ if (tdata->res_timer.id == 0) { pj_stun_msg_destroy_tdata(sess, tdata); } } on_return: pj_log_pop_indent(); if (pj_grp_lock_release(sess->grp_lock)) return PJ_EGONE; return status; } /* * Create and send STUN response message. */ PJ_DEF(pj_status_t) pj_stun_session_respond( pj_stun_session *sess, const pj_stun_rx_data *rdata, unsigned code, const char *errmsg, void *token, pj_bool_t cache, const pj_sockaddr_t *dst_addr, unsigned addr_len) { pj_status_t status; pj_str_t reason; pj_stun_tx_data *tdata; pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } status = pj_stun_session_create_res(sess, rdata, code, (errmsg?pj_cstr(&reason,errmsg):NULL), &tdata); if (status != PJ_SUCCESS) { pj_grp_lock_release(sess->grp_lock); return status; } status = pj_stun_session_send_msg(sess, token, cache, PJ_FALSE, dst_addr, addr_len, tdata); pj_grp_lock_release(sess->grp_lock); return status; } /* * Cancel outgoing STUN transaction. */ PJ_DEF(pj_status_t) pj_stun_session_cancel_req( pj_stun_session *sess, pj_stun_tx_data *tdata, pj_bool_t notify, pj_status_t notify_status) { PJ_ASSERT_RETURN(sess && tdata, PJ_EINVAL); PJ_ASSERT_RETURN(!notify || notify_status!=PJ_SUCCESS, PJ_EINVAL); PJ_ASSERT_RETURN(PJ_STUN_IS_REQUEST(tdata->msg->hdr.type), PJ_EINVAL); /* Lock the session and prevent user from destroying us in the callback */ pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } if (notify) { (sess->cb.on_request_complete)(sess, notify_status, tdata->token, tdata, NULL, NULL, 0); } /* Just destroy tdata. This will destroy the transaction as well */ pj_stun_msg_destroy_tdata(sess, tdata); pj_grp_lock_release(sess->grp_lock); return PJ_SUCCESS; } /* * Explicitly request retransmission of the request. */ PJ_DEF(pj_status_t) pj_stun_session_retransmit_req(pj_stun_session *sess, pj_stun_tx_data *tdata, pj_bool_t mod_count) { pj_status_t status; PJ_ASSERT_RETURN(sess && tdata, PJ_EINVAL); PJ_ASSERT_RETURN(PJ_STUN_IS_REQUEST(tdata->msg->hdr.type), PJ_EINVAL); /* Lock the session and prevent user from destroying us in the callback */ pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } status = pj_stun_client_tsx_retransmit(tdata->client_tsx, mod_count); pj_grp_lock_release(sess->grp_lock); return status; } /* Send response */ static pj_status_t send_response(pj_stun_session *sess, void *token, pj_pool_t *pool, pj_stun_msg *response, const pj_stun_req_cred_info *auth_info, pj_bool_t retransmission, const pj_sockaddr_t *addr, unsigned addr_len) { pj_uint8_t *out_pkt; pj_size_t out_max_len, out_len; pj_status_t status; /* Apply options */ if (!retransmission) { status = apply_msg_options(sess, pool, auth_info, response); if (status != PJ_SUCCESS) return status; } /* Alloc packet buffer */ out_max_len = PJ_STUN_MAX_PKT_LEN; out_pkt = (pj_uint8_t*) pj_pool_alloc(pool, out_max_len); /* Encode */ status = pj_stun_msg_encode(response, out_pkt, out_max_len, 0, &auth_info->auth_key, &out_len); if (status != PJ_SUCCESS) { LOG_ERR_(sess, "Error encoding message", status); return status; } /* Print log */ dump_tx_msg(sess, response, (unsigned)out_len, addr); /* Send packet */ status = sess->cb.on_send_msg(sess, token, out_pkt, (unsigned)out_len, addr, addr_len); return status; } /* Authenticate incoming message */ static pj_status_t authenticate_req(pj_stun_session *sess, void *token, const pj_uint8_t *pkt, unsigned pkt_len, pj_stun_rx_data *rdata, pj_pool_t *tmp_pool, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { pj_stun_msg *response; pj_status_t status; if (PJ_STUN_IS_ERROR_RESPONSE(rdata->msg->hdr.type) || sess->auth_type == PJ_STUN_AUTH_NONE) { return PJ_SUCCESS; } status = pj_stun_authenticate_request(pkt, pkt_len, rdata->msg, &sess->cred, tmp_pool, &rdata->info, &response); if (status != PJ_SUCCESS && response != NULL) { PJ_PERROR(5,(SNAME(sess), status, "Message authentication failed")); send_response(sess, token, tmp_pool, response, &rdata->info, PJ_FALSE, src_addr, src_addr_len); } return status; } /* Handle incoming response */ static pj_status_t on_incoming_response(pj_stun_session *sess, unsigned options, const pj_uint8_t *pkt, unsigned pkt_len, pj_stun_msg *msg, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { pj_stun_tx_data *tdata; pj_status_t status; /* Lookup pending client transaction */ tdata = tsx_lookup(sess, msg); if (tdata == NULL) { PJ_LOG(5,(SNAME(sess), "Transaction not found, response silently discarded")); return PJ_SUCCESS; } if (sess->auth_type == PJ_STUN_AUTH_NONE) options |= PJ_STUN_NO_AUTHENTICATE; /* Authenticate the message, unless PJ_STUN_NO_AUTHENTICATE * is specified in the option. */ if ((options & PJ_STUN_NO_AUTHENTICATE) == 0 && tdata->auth_info.auth_key.slen != 0 && pj_stun_auth_valid_for_msg(msg)) { status = pj_stun_authenticate_response(pkt, pkt_len, msg, &tdata->auth_info.auth_key); if (status != PJ_SUCCESS) { PJ_PERROR(5,(SNAME(sess), status, "Response authentication failed")); return status; } } /* Pass the response to the transaction. * If the message is accepted, transaction callback will be called, * and this will call the session callback too. */ status = pj_stun_client_tsx_on_rx_msg(tdata->client_tsx, msg, src_addr, src_addr_len); if (status != PJ_SUCCESS) { return status; } return PJ_SUCCESS; } /* For requests, check if we cache the response */ static pj_status_t check_cached_response(pj_stun_session *sess, pj_pool_t *tmp_pool, const pj_stun_msg *msg, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { pj_stun_tx_data *t; /* First lookup response in response cache */ t = sess->cached_response_list.next; while (t != &sess->cached_response_list) { if (t->msg_magic == msg->hdr.magic && t->msg->hdr.type == msg->hdr.type && pj_memcmp(t->msg_key, msg->hdr.tsx_id, sizeof(msg->hdr.tsx_id))==0) { break; } t = t->next; } if (t != &sess->cached_response_list) { /* Found response in the cache */ PJ_LOG(5,(SNAME(sess), "Request retransmission, sending cached response")); send_response(sess, t->token, tmp_pool, t->msg, &t->auth_info, PJ_TRUE, src_addr, src_addr_len); return PJ_SUCCESS; } return PJ_ENOTFOUND; } /* Handle incoming request */ static pj_status_t on_incoming_request(pj_stun_session *sess, unsigned options, void *token, pj_pool_t *tmp_pool, const pj_uint8_t *in_pkt, unsigned in_pkt_len, pj_stun_msg *msg, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { pj_stun_rx_data rdata; pj_status_t status; /* Init rdata */ rdata.msg = msg; pj_bzero(&rdata.info, sizeof(rdata.info)); if (sess->auth_type == PJ_STUN_AUTH_NONE) options |= PJ_STUN_NO_AUTHENTICATE; /* Authenticate the message, unless PJ_STUN_NO_AUTHENTICATE * is specified in the option. */ if ((options & PJ_STUN_NO_AUTHENTICATE) == 0) { status = authenticate_req(sess, token, (const pj_uint8_t*) in_pkt, in_pkt_len,&rdata, tmp_pool, src_addr, src_addr_len); if (status != PJ_SUCCESS) { return status; } } /* Distribute to handler, or respond with Bad Request */ if (sess->cb.on_rx_request) { status = (*sess->cb.on_rx_request)(sess, in_pkt, in_pkt_len, &rdata, token, src_addr, src_addr_len); } else { pj_str_t err_text; pj_stun_msg *response; err_text = pj_str("Callback is not set to handle request"); status = pj_stun_msg_create_response(tmp_pool, msg, PJ_STUN_SC_BAD_REQUEST, &err_text, &response); if (status == PJ_SUCCESS && response) { status = send_response(sess, token, tmp_pool, response, NULL, PJ_FALSE, src_addr, src_addr_len); } } return status; } /* Handle incoming indication */ static pj_status_t on_incoming_indication(pj_stun_session *sess, void *token, pj_pool_t *tmp_pool, const pj_uint8_t *in_pkt, unsigned in_pkt_len, const pj_stun_msg *msg, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { PJ_UNUSED_ARG(tmp_pool); /* Distribute to handler */ if (sess->cb.on_rx_indication) { return (*sess->cb.on_rx_indication)(sess, in_pkt, in_pkt_len, msg, token, src_addr, src_addr_len); } else { return PJ_SUCCESS; } } /* Print outgoing message to log */ static void dump_rx_msg(pj_stun_session *sess, const pj_stun_msg *msg, unsigned pkt_size, const pj_sockaddr_t *addr) { char src_info[PJ_INET6_ADDRSTRLEN+10]; if ((PJ_STUN_IS_REQUEST(msg->hdr.type) && (sess->log_flag & PJ_STUN_SESS_LOG_RX_REQ)==0) || (PJ_STUN_IS_RESPONSE(msg->hdr.type) && (sess->log_flag & PJ_STUN_SESS_LOG_RX_RES)==0) || (PJ_STUN_IS_INDICATION(msg->hdr.type) && (sess->log_flag & PJ_STUN_SESS_LOG_RX_IND)==0)) { return; } pj_sockaddr_print(addr, src_info, sizeof(src_info), 3); PJ_LOG(5,(SNAME(sess), "RX %d bytes STUN message from %s:\n" "--- begin STUN message ---\n" "%s" "--- end of STUN message ---\n", pkt_size, src_info, pj_stun_msg_dump(msg, sess->dump_buf, sizeof(sess->dump_buf), NULL))); } /* Incoming packet */ PJ_DEF(pj_status_t) pj_stun_session_on_rx_pkt(pj_stun_session *sess, const void *packet, pj_size_t pkt_size, unsigned options, void *token, pj_size_t *parsed_len, const pj_sockaddr_t *src_addr, unsigned src_addr_len) { pj_stun_msg *msg, *response; pj_status_t status; PJ_ASSERT_RETURN(sess && packet && pkt_size, PJ_EINVAL); /* Lock the session and prevent user from destroying us in the callback */ pj_grp_lock_acquire(sess->grp_lock); if (sess->is_destroying) { pj_grp_lock_release(sess->grp_lock); return PJ_EINVALIDOP; } pj_log_push_indent(); /* Reset pool */ pj_pool_reset(sess->rx_pool); /* Try to parse the message */ status = pj_stun_msg_decode(sess->rx_pool, (const pj_uint8_t*)packet, pkt_size, options, &msg, parsed_len, &response); if (status != PJ_SUCCESS) { LOG_ERR_(sess, "STUN msg_decode() error", status); if (response) { send_response(sess, token, sess->rx_pool, response, NULL, PJ_FALSE, src_addr, src_addr_len); } goto on_return; } dump_rx_msg(sess, msg, (unsigned)pkt_size, src_addr); /* For requests, check if we have cached response */ status = check_cached_response(sess, sess->rx_pool, msg, src_addr, src_addr_len); if (status == PJ_SUCCESS) { goto on_return; } /* Handle message */ if (PJ_STUN_IS_SUCCESS_RESPONSE(msg->hdr.type) || PJ_STUN_IS_ERROR_RESPONSE(msg->hdr.type)) { status = on_incoming_response(sess, options, (const pj_uint8_t*) packet, (unsigned)pkt_size, msg, src_addr, src_addr_len); } else if (PJ_STUN_IS_REQUEST(msg->hdr.type)) { status = on_incoming_request(sess, options, token, sess->rx_pool, (const pj_uint8_t*) packet, (unsigned)pkt_size, msg, src_addr, src_addr_len); } else if (PJ_STUN_IS_INDICATION(msg->hdr.type)) { status = on_incoming_indication(sess, token, sess->rx_pool, (const pj_uint8_t*) packet, (unsigned)pkt_size, msg, src_addr, src_addr_len); } else { pj_assert(!"Unexpected!"); status = PJ_EBUG; } on_return: pj_log_pop_indent(); if (pj_grp_lock_release(sess->grp_lock)) return PJ_EGONE; return status; }