diff options
author | David M. Lee <dlee@digium.com> | 2013-01-07 14:24:28 -0600 |
---|---|---|
committer | David M. Lee <dlee@digium.com> | 2013-01-07 14:24:28 -0600 |
commit | f3ab456a17af1c89a6e3be4d20c5944853df1cb0 (patch) | |
tree | d00e1a332cd038a6d906a1ea0ac91e1a4458e617 /pjnath/src/pjnath/turn_session.c |
Import pjproject-2.0.1
Diffstat (limited to 'pjnath/src/pjnath/turn_session.c')
-rw-r--r-- | pjnath/src/pjnath/turn_session.c | 2040 |
1 files changed, 2040 insertions, 0 deletions
diff --git a/pjnath/src/pjnath/turn_session.c b/pjnath/src/pjnath/turn_session.c new file mode 100644 index 0000000..cbe8f5c --- /dev/null +++ b/pjnath/src/pjnath/turn_session.c @@ -0,0 +1,2040 @@ +/* $Id: turn_session.c 3844 2011-10-24 15:03:43Z bennylp $ */ +/* + * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) + * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org> + * + * 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 <pjnath/turn_session.h> +#include <pjnath/errno.h> +#include <pjlib-util/srv_resolver.h> +#include <pj/addr_resolv.h> +#include <pj/assert.h> +#include <pj/errno.h> +#include <pj/hash.h> +#include <pj/lock.h> +#include <pj/log.h> +#include <pj/os.h> +#include <pj/pool.h> +#include <pj/rand.h> +#include <pj/sock.h> + +#define PJ_TURN_CHANNEL_MIN 0x4000 +#define PJ_TURN_CHANNEL_MAX 0x7FFF /* inclusive */ +#define PJ_TURN_CHANNEL_HTABLE_SIZE 8 +#define PJ_TURN_PERM_HTABLE_SIZE 8 + +static const char *state_names[] = +{ + "Null", + "Resolving", + "Resolved", + "Allocating", + "Ready", + "Deallocating", + "Deallocated", + "Destroying" +}; + +enum timer_id_t +{ + TIMER_NONE, + TIMER_KEEP_ALIVE, + TIMER_DESTROY +}; + +/* This structure describes a channel binding. A channel binding is index by + * the channel number or IP address and port number of the peer. + */ +struct ch_t +{ + /* The channel number */ + pj_uint16_t num; + + /* PJ_TRUE if we've received successful response to ChannelBind request + * for this channel. + */ + pj_bool_t bound; + + /* The peer IP address and port */ + pj_sockaddr addr; + + /* The channel binding expiration */ + pj_time_val expiry; +}; + + +/* This structure describes a permission. A permission is identified by the + * IP address only. + */ +struct perm_t +{ + /* Cache of hash value to speed-up lookup */ + pj_uint32_t hval; + + /* The permission IP address. The port number MUST be zero */ + pj_sockaddr addr; + + /* Number of peers that uses this permission. */ + unsigned peer_cnt; + + /* Automatically renew this permission once it expires? */ + pj_bool_t renew; + + /* The permission expiration */ + pj_time_val expiry; + + /* Arbitrary/random pointer value (token) to map this perm with the + * request to create it. It is used to invalidate this perm when the + * request fails. + */ + void *req_token; +}; + + +/* The TURN client session structure */ +struct pj_turn_session +{ + pj_pool_t *pool; + const char *obj_name; + pj_turn_session_cb cb; + void *user_data; + pj_stun_config stun_cfg; + + pj_lock_t *lock; + int busy; + + pj_turn_state_t state; + pj_status_t last_status; + pj_bool_t pending_destroy; + pj_bool_t destroy_notified; + + pj_stun_session *stun; + + unsigned lifetime; + int ka_interval; + pj_time_val expiry; + + pj_timer_heap_t *timer_heap; + pj_timer_entry timer; + + pj_dns_srv_async_query *dns_async; + pj_uint16_t default_port; + + pj_uint16_t af; + pj_turn_tp_type conn_type; + pj_uint16_t srv_addr_cnt; + pj_sockaddr *srv_addr_list; + pj_sockaddr *srv_addr; + + pj_bool_t pending_alloc; + pj_turn_alloc_param alloc_param; + + pj_sockaddr mapped_addr; + pj_sockaddr relay_addr; + + pj_hash_table_t *ch_table; + pj_hash_table_t *perm_table; + + pj_uint32_t send_ind_tsx_id[3]; + /* tx_pkt must be 16bit aligned */ + pj_uint8_t tx_pkt[PJ_TURN_MAX_PKT_LEN]; + + pj_uint16_t next_ch; +}; + + +/* + * Prototypes. + */ +static void sess_shutdown(pj_turn_session *sess, + pj_status_t status); +static void do_destroy(pj_turn_session *sess); +static void send_refresh(pj_turn_session *sess, int lifetime); +static pj_status_t stun_on_send_msg(pj_stun_session *sess, + void *token, + const void *pkt, + pj_size_t pkt_size, + const pj_sockaddr_t *dst_addr, + unsigned addr_len); +static void stun_on_request_complete(pj_stun_session *sess, + pj_status_t status, + void *token, + pj_stun_tx_data *tdata, + const pj_stun_msg *response, + const pj_sockaddr_t *src_addr, + unsigned src_addr_len); +static pj_status_t stun_on_rx_indication(pj_stun_session *sess, + const pj_uint8_t *pkt, + unsigned pkt_len, + const pj_stun_msg *msg, + void *token, + const pj_sockaddr_t *src_addr, + unsigned src_addr_len); +static void dns_srv_resolver_cb(void *user_data, + pj_status_t status, + const pj_dns_srv_record *rec); +static struct ch_t *lookup_ch_by_addr(pj_turn_session *sess, + const pj_sockaddr_t *addr, + unsigned addr_len, + pj_bool_t update, + pj_bool_t bind_channel); +static struct ch_t *lookup_ch_by_chnum(pj_turn_session *sess, + pj_uint16_t chnum); +static struct perm_t *lookup_perm(pj_turn_session *sess, + const pj_sockaddr_t *addr, + unsigned addr_len, + pj_bool_t update); +static void invalidate_perm(pj_turn_session *sess, + struct perm_t *perm); +static void on_timer_event(pj_timer_heap_t *th, pj_timer_entry *e); + + +/* + * Create default pj_turn_alloc_param. + */ +PJ_DEF(void) pj_turn_alloc_param_default(pj_turn_alloc_param *prm) +{ + pj_bzero(prm, sizeof(*prm)); +} + +/* + * Duplicate pj_turn_alloc_param. + */ +PJ_DEF(void) pj_turn_alloc_param_copy( pj_pool_t *pool, + pj_turn_alloc_param *dst, + const pj_turn_alloc_param *src) +{ + PJ_UNUSED_ARG(pool); + pj_memcpy(dst, src, sizeof(*dst)); +} + +/* + * Get TURN state name. + */ +PJ_DEF(const char*) pj_turn_state_name(pj_turn_state_t state) +{ + return state_names[state]; +} + +/* + * Create TURN client session. + */ +PJ_DEF(pj_status_t) pj_turn_session_create( const pj_stun_config *cfg, + const char *name, + int af, + pj_turn_tp_type conn_type, + const pj_turn_session_cb *cb, + unsigned options, + void *user_data, + pj_turn_session **p_sess) +{ + pj_pool_t *pool; + pj_turn_session *sess; + pj_stun_session_cb stun_cb; + pj_lock_t *null_lock; + pj_status_t status; + + PJ_ASSERT_RETURN(cfg && cfg->pf && cb && p_sess, PJ_EINVAL); + PJ_ASSERT_RETURN(cb->on_send_pkt, PJ_EINVAL); + + PJ_UNUSED_ARG(options); + + if (name == NULL) + name = "turn%p"; + + /* Allocate and create TURN session */ + pool = pj_pool_create(cfg->pf, name, PJNATH_POOL_LEN_TURN_SESS, + PJNATH_POOL_INC_TURN_SESS, NULL); + sess = PJ_POOL_ZALLOC_T(pool, pj_turn_session); + sess->pool = pool; + sess->obj_name = pool->obj_name; + sess->timer_heap = cfg->timer_heap; + sess->af = (pj_uint16_t)af; + sess->conn_type = conn_type; + sess->ka_interval = PJ_TURN_KEEP_ALIVE_SEC; + sess->user_data = user_data; + sess->next_ch = PJ_TURN_CHANNEL_MIN; + + /* Copy STUN session */ + pj_memcpy(&sess->stun_cfg, cfg, sizeof(pj_stun_config)); + + /* Copy callback */ + pj_memcpy(&sess->cb, cb, sizeof(*cb)); + + /* Peer hash table */ + sess->ch_table = pj_hash_create(pool, PJ_TURN_CHANNEL_HTABLE_SIZE); + + /* Permission hash table */ + sess->perm_table = pj_hash_create(pool, PJ_TURN_PERM_HTABLE_SIZE); + + /* Session lock */ + status = pj_lock_create_recursive_mutex(pool, sess->obj_name, + &sess->lock); + if (status != PJ_SUCCESS) { + do_destroy(sess); + return status; + } + + /* Timer */ + pj_timer_entry_init(&sess->timer, TIMER_NONE, sess, &on_timer_event); + + /* Create STUN session */ + pj_bzero(&stun_cb, sizeof(stun_cb)); + stun_cb.on_send_msg = &stun_on_send_msg; + stun_cb.on_request_complete = &stun_on_request_complete; + stun_cb.on_rx_indication = &stun_on_rx_indication; + status = pj_stun_session_create(&sess->stun_cfg, sess->obj_name, &stun_cb, + PJ_FALSE, &sess->stun); + if (status != PJ_SUCCESS) { + do_destroy(sess); + return status; + } + + /* Attach ourself to STUN session */ + pj_stun_session_set_user_data(sess->stun, sess); + + /* Replace mutex in STUN session with a NULL mutex, since access to + * STUN session is serialized. + */ + status = pj_lock_create_null_mutex(pool, name, &null_lock); + if (status != PJ_SUCCESS) { + do_destroy(sess); + return status; + } + pj_stun_session_set_lock(sess->stun, null_lock, PJ_TRUE); + + /* Done */ + + PJ_LOG(4,(sess->obj_name, "TURN client session created")); + + *p_sess = sess; + return PJ_SUCCESS; +} + + +/* Destroy */ +static void do_destroy(pj_turn_session *sess) +{ + /* Lock session */ + if (sess->lock) { + pj_lock_acquire(sess->lock); + } + + /* Cancel pending timer, if any */ + if (sess->timer.id != TIMER_NONE) { + pj_timer_heap_cancel(sess->timer_heap, &sess->timer); + sess->timer.id = TIMER_NONE; + } + + /* Destroy STUN session */ + if (sess->stun) { + pj_stun_session_destroy(sess->stun); + sess->stun = NULL; + } + + /* Destroy lock */ + if (sess->lock) { + pj_lock_release(sess->lock); + pj_lock_destroy(sess->lock); + sess->lock = NULL; + } + + /* Destroy pool */ + if (sess->pool) { + pj_pool_t *pool = sess->pool; + + PJ_LOG(4,(sess->obj_name, "TURN client session destroyed")); + + sess->pool = NULL; + pj_pool_release(pool); + } +} + + +/* Set session state */ +static void set_state(pj_turn_session *sess, enum pj_turn_state_t state) +{ + pj_turn_state_t old_state = sess->state; + + if (state==sess->state) + return; + + PJ_LOG(4,(sess->obj_name, "State changed %s --> %s", + state_names[old_state], state_names[state])); + sess->state = state; + + if (sess->cb.on_state) { + (*sess->cb.on_state)(sess, old_state, state); + } +} + +/* + * Notify application and shutdown the TURN session. + */ +static void sess_shutdown(pj_turn_session *sess, + pj_status_t status) +{ + pj_bool_t can_destroy = PJ_TRUE; + + PJ_LOG(4,(sess->obj_name, "Request to shutdown in state %s, cause:%d", + state_names[sess->state], status)); + + if (sess->last_status == PJ_SUCCESS && status != PJ_SUCCESS) + sess->last_status = status; + + switch (sess->state) { + case PJ_TURN_STATE_NULL: + break; + case PJ_TURN_STATE_RESOLVING: + if (sess->dns_async != NULL) { + pj_dns_srv_cancel_query(sess->dns_async, PJ_FALSE); + sess->dns_async = NULL; + } + break; + case PJ_TURN_STATE_RESOLVED: + break; + case PJ_TURN_STATE_ALLOCATING: + /* We need to wait until allocation complete */ + sess->pending_destroy = PJ_TRUE; + can_destroy = PJ_FALSE; + break; + case PJ_TURN_STATE_READY: + /* Send REFRESH with LIFETIME=0 */ + can_destroy = PJ_FALSE; + send_refresh(sess, 0); + break; + case PJ_TURN_STATE_DEALLOCATING: + can_destroy = PJ_FALSE; + /* This may recursively call this function again with + * state==PJ_TURN_STATE_DEALLOCATED. + */ + send_refresh(sess, 0); + break; + case PJ_TURN_STATE_DEALLOCATED: + case PJ_TURN_STATE_DESTROYING: + break; + } + + if (can_destroy) { + /* Schedule destroy */ + pj_time_val delay = {0, 0}; + + set_state(sess, PJ_TURN_STATE_DESTROYING); + + if (sess->timer.id != TIMER_NONE) { + pj_timer_heap_cancel(sess->timer_heap, &sess->timer); + sess->timer.id = TIMER_NONE; + } + + sess->timer.id = TIMER_DESTROY; + pj_timer_heap_schedule(sess->timer_heap, &sess->timer, &delay); + } +} + + +/* + * Public API to destroy TURN client session. + */ +PJ_DEF(pj_status_t) pj_turn_session_shutdown(pj_turn_session *sess) +{ + PJ_ASSERT_RETURN(sess, PJ_EINVAL); + + pj_lock_acquire(sess->lock); + + sess_shutdown(sess, PJ_SUCCESS); + + pj_lock_release(sess->lock); + + return PJ_SUCCESS; +} + + +/** + * Forcefully destroy the TURN session. + */ +PJ_DEF(pj_status_t) pj_turn_session_destroy( pj_turn_session *sess, + pj_status_t last_err) +{ + PJ_ASSERT_RETURN(sess, PJ_EINVAL); + + if (last_err != PJ_SUCCESS && sess->last_status == PJ_SUCCESS) + sess->last_status = last_err; + set_state(sess, PJ_TURN_STATE_DEALLOCATED); + sess_shutdown(sess, PJ_SUCCESS); + return PJ_SUCCESS; +} + + +/* + * Get TURN session info. + */ +PJ_DEF(pj_status_t) pj_turn_session_get_info( pj_turn_session *sess, + pj_turn_session_info *info) +{ + pj_time_val now; + + PJ_ASSERT_RETURN(sess && info, PJ_EINVAL); + + pj_gettimeofday(&now); + + info->state = sess->state; + info->conn_type = sess->conn_type; + info->lifetime = sess->expiry.sec - now.sec; + info->last_status = sess->last_status; + + if (sess->srv_addr) + pj_memcpy(&info->server, sess->srv_addr, sizeof(info->server)); + else + pj_bzero(&info->server, sizeof(info->server)); + + pj_memcpy(&info->mapped_addr, &sess->mapped_addr, + sizeof(sess->mapped_addr)); + pj_memcpy(&info->relay_addr, &sess->relay_addr, + sizeof(sess->relay_addr)); + + return PJ_SUCCESS; +} + + +/* + * Re-assign user data. + */ +PJ_DEF(pj_status_t) pj_turn_session_set_user_data( pj_turn_session *sess, + void *user_data) +{ + sess->user_data = user_data; + return PJ_SUCCESS; +} + + +/** + * Retrieve user data. + */ +PJ_DEF(void*) pj_turn_session_get_user_data(pj_turn_session *sess) +{ + return sess->user_data; +} + + +/* + * Configure message logging. By default all flags are enabled. + * + * @param sess The TURN client session. + * @param flags Bitmask combination of #pj_stun_sess_msg_log_flag + */ +PJ_DEF(void) pj_turn_session_set_log( pj_turn_session *sess, + unsigned flags) +{ + pj_stun_session_set_log(sess->stun, flags); +} + + +/* + * Set software name + */ +PJ_DEF(pj_status_t) pj_turn_session_set_software_name( pj_turn_session *sess, + const pj_str_t *sw) +{ + pj_status_t status; + + pj_lock_acquire(sess->lock); + status = pj_stun_session_set_software_name(sess->stun, sw); + pj_lock_release(sess->lock); + + return status; +} + + +/** + * Set the server or domain name of the server. + */ +PJ_DEF(pj_status_t) pj_turn_session_set_server( pj_turn_session *sess, + const pj_str_t *domain, + int default_port, + pj_dns_resolver *resolver) +{ + pj_sockaddr tmp_addr; + pj_bool_t is_ip_addr; + pj_status_t status; + + PJ_ASSERT_RETURN(sess && domain, PJ_EINVAL); + PJ_ASSERT_RETURN(sess->state == PJ_TURN_STATE_NULL, PJ_EINVALIDOP); + + pj_lock_acquire(sess->lock); + + /* See if "domain" contains just IP address */ + tmp_addr.addr.sa_family = sess->af; + status = pj_inet_pton(sess->af, domain, + pj_sockaddr_get_addr(&tmp_addr)); + is_ip_addr = (status == PJ_SUCCESS); + + if (!is_ip_addr && resolver) { + /* Resolve with DNS SRV resolution, and fallback to DNS A resolution + * if default_port is specified. + */ + unsigned opt = 0; + pj_str_t res_name; + + switch (sess->conn_type) { + case PJ_TURN_TP_UDP: + res_name = pj_str("_turn._udp."); + break; + case PJ_TURN_TP_TCP: + res_name = pj_str("_turn._tcp."); + break; + case PJ_TURN_TP_TLS: + res_name = pj_str("_turns._tcp."); + break; + default: + status = PJNATH_ETURNINTP; + goto on_return; + } + + /* Fallback to DNS A only if default port is specified */ + if (default_port>0 && default_port<65536) { + opt = PJ_DNS_SRV_FALLBACK_A; + sess->default_port = (pj_uint16_t)default_port; + } + + PJ_LOG(5,(sess->obj_name, "Resolving %.*s%.*s with DNS SRV", + (int)res_name.slen, res_name.ptr, + (int)domain->slen, domain->ptr)); + set_state(sess, PJ_TURN_STATE_RESOLVING); + + /* User may have destroyed us in the callback */ + if (sess->state != PJ_TURN_STATE_RESOLVING) { + status = PJ_ECANCELLED; + goto on_return; + } + + status = pj_dns_srv_resolve(domain, &res_name, default_port, + sess->pool, resolver, opt, sess, + &dns_srv_resolver_cb, &sess->dns_async); + if (status != PJ_SUCCESS) { + set_state(sess, PJ_TURN_STATE_NULL); + goto on_return; + } + + } else { + /* Resolver is not specified, resolve with standard gethostbyname(). + * The default_port MUST be specified in this case. + */ + pj_addrinfo *ai; + unsigned i, cnt; + + /* Default port must be specified */ + PJ_ASSERT_RETURN(default_port>0 && default_port<65536, PJ_EINVAL); + sess->default_port = (pj_uint16_t)default_port; + + cnt = PJ_TURN_MAX_DNS_SRV_CNT; + ai = (pj_addrinfo*) + pj_pool_calloc(sess->pool, cnt, sizeof(pj_addrinfo)); + + PJ_LOG(5,(sess->obj_name, "Resolving %.*s with DNS A", + (int)domain->slen, domain->ptr)); + set_state(sess, PJ_TURN_STATE_RESOLVING); + + /* User may have destroyed us in the callback */ + if (sess->state != PJ_TURN_STATE_RESOLVING) { + status = PJ_ECANCELLED; + goto on_return; + } + + status = pj_getaddrinfo(sess->af, domain, &cnt, ai); + if (status != PJ_SUCCESS) + goto on_return; + + sess->srv_addr_cnt = (pj_uint16_t)cnt; + sess->srv_addr_list = (pj_sockaddr*) + pj_pool_calloc(sess->pool, cnt, + sizeof(pj_sockaddr)); + for (i=0; i<cnt; ++i) { + pj_sockaddr *addr = &sess->srv_addr_list[i]; + pj_memcpy(addr, &ai[i].ai_addr, sizeof(pj_sockaddr)); + addr->addr.sa_family = sess->af; + addr->ipv4.sin_port = pj_htons(sess->default_port); + } + + sess->srv_addr = &sess->srv_addr_list[0]; + set_state(sess, PJ_TURN_STATE_RESOLVED); + } + +on_return: + pj_lock_release(sess->lock); + return status; +} + + +/** + * Set credential to be used by the session. + */ +PJ_DEF(pj_status_t) pj_turn_session_set_credential(pj_turn_session *sess, + const pj_stun_auth_cred *cred) +{ + PJ_ASSERT_RETURN(sess && cred, PJ_EINVAL); + PJ_ASSERT_RETURN(sess->stun, PJ_EINVALIDOP); + + pj_lock_acquire(sess->lock); + + pj_stun_session_set_credential(sess->stun, PJ_STUN_AUTH_LONG_TERM, cred); + + pj_lock_release(sess->lock); + + return PJ_SUCCESS; +} + + +/** + * Create TURN allocation. + */ +PJ_DEF(pj_status_t) pj_turn_session_alloc(pj_turn_session *sess, + const pj_turn_alloc_param *param) +{ + pj_stun_tx_data *tdata; + pj_bool_t retransmit; + pj_status_t status; + + PJ_ASSERT_RETURN(sess, PJ_EINVAL); + PJ_ASSERT_RETURN(sess->state>PJ_TURN_STATE_NULL && + sess->state<=PJ_TURN_STATE_RESOLVED, + PJ_EINVALIDOP); + + pj_lock_acquire(sess->lock); + + if (param && param != &sess->alloc_param) + pj_turn_alloc_param_copy(sess->pool, &sess->alloc_param, param); + + if (sess->state < PJ_TURN_STATE_RESOLVED) { + sess->pending_alloc = PJ_TRUE; + + PJ_LOG(4,(sess->obj_name, "Pending ALLOCATE in state %s", + state_names[sess->state])); + + pj_lock_release(sess->lock); + return PJ_SUCCESS; + + } + + /* Ready to allocate */ + pj_assert(sess->state == PJ_TURN_STATE_RESOLVED); + + /* Create a bare request */ + status = pj_stun_session_create_req(sess->stun, PJ_STUN_ALLOCATE_REQUEST, + PJ_STUN_MAGIC, NULL, &tdata); + if (status != PJ_SUCCESS) { + pj_lock_release(sess->lock); + return status; + } + + /* MUST include REQUESTED-TRANSPORT attribute */ + pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_REQ_TRANSPORT, + PJ_STUN_SET_RT_PROTO(PJ_TURN_TP_UDP)); + + /* Include BANDWIDTH if requested */ + if (sess->alloc_param.bandwidth > 0) { + pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_BANDWIDTH, + sess->alloc_param.bandwidth); + } + + /* Include LIFETIME if requested */ + if (sess->alloc_param.lifetime > 0) { + pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_LIFETIME, + sess->alloc_param.lifetime); + } + + /* Server address must be set */ + pj_assert(sess->srv_addr != NULL); + + /* Send request */ + set_state(sess, PJ_TURN_STATE_ALLOCATING); + retransmit = (sess->conn_type == PJ_TURN_TP_UDP); + status = pj_stun_session_send_msg(sess->stun, NULL, PJ_FALSE, + retransmit, sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr), + tdata); + if (status != PJ_SUCCESS) { + /* Set state back to RESOLVED. We don't want to destroy session now, + * let the application do it if it wants to. + */ + set_state(sess, PJ_TURN_STATE_RESOLVED); + } + + pj_lock_release(sess->lock); + return status; +} + + +/* + * Install or renew permissions + */ +PJ_DEF(pj_status_t) pj_turn_session_set_perm( pj_turn_session *sess, + unsigned addr_cnt, + const pj_sockaddr addr[], + unsigned options) +{ + pj_stun_tx_data *tdata; + pj_hash_iterator_t it_buf, *it; + void *req_token; + unsigned i, attr_added=0; + pj_status_t status; + + PJ_ASSERT_RETURN(sess && addr_cnt && addr, PJ_EINVAL); + + pj_lock_acquire(sess->lock); + + /* Create a bare CreatePermission request */ + status = pj_stun_session_create_req(sess->stun, + PJ_STUN_CREATE_PERM_REQUEST, + PJ_STUN_MAGIC, NULL, &tdata); + if (status != PJ_SUCCESS) { + pj_lock_release(sess->lock); + return status; + } + + /* Create request token to map the request to the perm structures + * which the request belongs. + */ + req_token = (void*)(long)pj_rand(); + + /* Process the addresses */ + for (i=0; i<addr_cnt; ++i) { + struct perm_t *perm; + + /* Lookup the perm structure and create if it doesn't exist */ + perm = lookup_perm(sess, &addr[i], pj_sockaddr_get_len(&addr[i]), + PJ_TRUE); + perm->renew = (options & 0x01); + + /* Only add to the request if the request doesn't contain this + * address yet. + */ + if (perm->req_token != req_token) { + perm->req_token = req_token; + + /* Add XOR-PEER-ADDRESS */ + status = pj_stun_msg_add_sockaddr_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_XOR_PEER_ADDR, + PJ_TRUE, + &addr[i], + sizeof(addr[i])); + if (status != PJ_SUCCESS) + goto on_error; + + ++attr_added; + } + } + + pj_assert(attr_added != 0); + + /* Send the request */ + status = pj_stun_session_send_msg(sess->stun, req_token, PJ_FALSE, + (sess->conn_type==PJ_TURN_TP_UDP), + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr), + tdata); + if (status != PJ_SUCCESS) { + /* tdata is already destroyed */ + tdata = NULL; + goto on_error; + } + + pj_lock_release(sess->lock); + return PJ_SUCCESS; + +on_error: + /* destroy tdata */ + if (tdata) { + pj_stun_msg_destroy_tdata(sess->stun, tdata); + } + /* invalidate perm structures associated with this request */ + it = pj_hash_first(sess->perm_table, &it_buf); + while (it) { + struct perm_t *perm = (struct perm_t*) + pj_hash_this(sess->perm_table, it); + it = pj_hash_next(sess->perm_table, it); + if (perm->req_token == req_token) + invalidate_perm(sess, perm); + } + pj_lock_release(sess->lock); + return status; +} + +/* + * Send REFRESH + */ +static void send_refresh(pj_turn_session *sess, int lifetime) +{ + pj_stun_tx_data *tdata; + pj_status_t status; + + PJ_ASSERT_ON_FAIL(sess->state==PJ_TURN_STATE_READY, return); + + /* Create a bare REFRESH request */ + status = pj_stun_session_create_req(sess->stun, PJ_STUN_REFRESH_REQUEST, + PJ_STUN_MAGIC, NULL, &tdata); + if (status != PJ_SUCCESS) + goto on_error; + + /* Add LIFETIME */ + if (lifetime >= 0) { + pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_LIFETIME, lifetime); + } + + /* Send request */ + if (lifetime == 0) { + set_state(sess, PJ_TURN_STATE_DEALLOCATING); + } + + status = pj_stun_session_send_msg(sess->stun, NULL, PJ_FALSE, + (sess->conn_type==PJ_TURN_TP_UDP), + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr), + tdata); + if (status != PJ_SUCCESS) + goto on_error; + + return; + +on_error: + if (lifetime == 0) { + set_state(sess, PJ_TURN_STATE_DEALLOCATED); + sess_shutdown(sess, status); + } +} + + +/** + * Relay data to the specified peer through the session. + */ +PJ_DEF(pj_status_t) pj_turn_session_sendto( pj_turn_session *sess, + const pj_uint8_t *pkt, + unsigned pkt_len, + const pj_sockaddr_t *addr, + unsigned addr_len) +{ + struct ch_t *ch; + struct perm_t *perm; + pj_status_t status; + + PJ_ASSERT_RETURN(sess && pkt && pkt_len && addr && addr_len, + PJ_EINVAL); + + /* Return error if we're not ready */ + if (sess->state != PJ_TURN_STATE_READY) { + return PJ_EIGNORED; + } + + /* Lock session now */ + pj_lock_acquire(sess->lock); + + /* Lookup permission first */ + perm = lookup_perm(sess, addr, pj_sockaddr_get_len(addr), PJ_FALSE); + if (perm == NULL) { + /* Permission doesn't exist, install it first */ + char ipstr[PJ_INET6_ADDRSTRLEN+2]; + + PJ_LOG(4,(sess->obj_name, + "sendto(): IP %s has no permission, requesting it first..", + pj_sockaddr_print(addr, ipstr, sizeof(ipstr), 2))); + + status = pj_turn_session_set_perm(sess, 1, (const pj_sockaddr*)addr, + 0); + if (status != PJ_SUCCESS) { + pj_lock_release(sess->lock); + return status; + } + } + + /* See if the peer is bound to a channel number */ + ch = lookup_ch_by_addr(sess, addr, pj_sockaddr_get_len(addr), + PJ_FALSE, PJ_FALSE); + if (ch && ch->num != PJ_TURN_INVALID_CHANNEL && ch->bound) { + unsigned total_len; + + /* Peer is assigned a channel number, we can use ChannelData */ + pj_turn_channel_data *cd = (pj_turn_channel_data*)sess->tx_pkt; + + pj_assert(sizeof(*cd)==4); + + /* Calculate total length, including paddings */ + total_len = (pkt_len + sizeof(*cd) + 3) & (~3); + if (total_len > sizeof(sess->tx_pkt)) { + status = PJ_ETOOBIG; + goto on_return; + } + + cd->ch_number = pj_htons((pj_uint16_t)ch->num); + cd->length = pj_htons((pj_uint16_t)pkt_len); + pj_memcpy(cd+1, pkt, pkt_len); + + pj_assert(sess->srv_addr != NULL); + + status = sess->cb.on_send_pkt(sess, sess->tx_pkt, total_len, + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr)); + + } else { + /* Use Send Indication. */ + pj_stun_sockaddr_attr peer_attr; + pj_stun_binary_attr data_attr; + pj_stun_msg send_ind; + pj_size_t send_ind_len; + + /* Increment counter */ + ++sess->send_ind_tsx_id[2]; + + /* Create blank SEND-INDICATION */ + status = pj_stun_msg_init(&send_ind, PJ_STUN_SEND_INDICATION, + PJ_STUN_MAGIC, + (const pj_uint8_t*)sess->send_ind_tsx_id); + if (status != PJ_SUCCESS) + goto on_return; + + /* Add XOR-PEER-ADDRESS */ + pj_stun_sockaddr_attr_init(&peer_attr, PJ_STUN_ATTR_XOR_PEER_ADDR, + PJ_TRUE, addr, addr_len); + pj_stun_msg_add_attr(&send_ind, (pj_stun_attr_hdr*)&peer_attr); + + /* Add DATA attribute */ + pj_stun_binary_attr_init(&data_attr, NULL, PJ_STUN_ATTR_DATA, NULL, 0); + data_attr.data = (pj_uint8_t*)pkt; + data_attr.length = pkt_len; + pj_stun_msg_add_attr(&send_ind, (pj_stun_attr_hdr*)&data_attr); + + /* Encode the message */ + status = pj_stun_msg_encode(&send_ind, sess->tx_pkt, + sizeof(sess->tx_pkt), 0, + NULL, &send_ind_len); + if (status != PJ_SUCCESS) + goto on_return; + + /* Send the Send Indication */ + status = sess->cb.on_send_pkt(sess, sess->tx_pkt, send_ind_len, + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr)); + } + +on_return: + pj_lock_release(sess->lock); + return status; +} + + +/** + * Bind a peer address to a channel number. + */ +PJ_DEF(pj_status_t) pj_turn_session_bind_channel(pj_turn_session *sess, + const pj_sockaddr_t *peer_adr, + unsigned addr_len) +{ + struct ch_t *ch; + pj_stun_tx_data *tdata; + pj_uint16_t ch_num; + pj_status_t status; + + PJ_ASSERT_RETURN(sess && peer_adr && addr_len, PJ_EINVAL); + PJ_ASSERT_RETURN(sess->state == PJ_TURN_STATE_READY, PJ_EINVALIDOP); + + pj_lock_acquire(sess->lock); + + /* Create blank ChannelBind request */ + status = pj_stun_session_create_req(sess->stun, + PJ_STUN_CHANNEL_BIND_REQUEST, + PJ_STUN_MAGIC, NULL, &tdata); + if (status != PJ_SUCCESS) + goto on_return; + + /* Lookup if this peer has already been assigned a number */ + ch = lookup_ch_by_addr(sess, peer_adr, pj_sockaddr_get_len(peer_adr), + PJ_TRUE, PJ_FALSE); + pj_assert(ch); + + if (ch->num != PJ_TURN_INVALID_CHANNEL) { + /* Channel is already bound. This is a refresh request. */ + ch_num = ch->num; + } else { + PJ_ASSERT_ON_FAIL(sess->next_ch <= PJ_TURN_CHANNEL_MAX, + {status=PJ_ETOOMANY; goto on_return;}); + ch->num = ch_num = sess->next_ch++; + } + + /* Add CHANNEL-NUMBER attribute */ + pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_CHANNEL_NUMBER, + PJ_STUN_SET_CH_NB(ch_num)); + + /* Add XOR-PEER-ADDRESS attribute */ + pj_stun_msg_add_sockaddr_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_XOR_PEER_ADDR, PJ_TRUE, + peer_adr, addr_len); + + /* Send the request, associate peer data structure with tdata + * for future reference when we receive the ChannelBind response. + */ + status = pj_stun_session_send_msg(sess->stun, ch, PJ_FALSE, + (sess->conn_type==PJ_TURN_TP_UDP), + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr), + tdata); + +on_return: + pj_lock_release(sess->lock); + return status; +} + + +/** + * Notify TURN client session upon receiving a packet from server. + * The packet maybe a STUN packet or ChannelData packet. + */ +PJ_DEF(pj_status_t) pj_turn_session_on_rx_pkt(pj_turn_session *sess, + void *pkt, + pj_size_t pkt_len, + pj_size_t *parsed_len) +{ + pj_bool_t is_stun; + pj_status_t status; + pj_bool_t is_datagram; + + /* Packet could be ChannelData or STUN message (response or + * indication). + */ + + /* Start locking the session */ + pj_lock_acquire(sess->lock); + + is_datagram = (sess->conn_type==PJ_TURN_TP_UDP); + + /* Quickly check if this is STUN message */ + is_stun = ((((pj_uint8_t*)pkt)[0] & 0xC0) == 0); + + if (is_stun) { + /* This looks like STUN, give it to the STUN session */ + unsigned options; + + options = PJ_STUN_CHECK_PACKET | PJ_STUN_NO_FINGERPRINT_CHECK; + if (is_datagram) + options |= PJ_STUN_IS_DATAGRAM; + status=pj_stun_session_on_rx_pkt(sess->stun, pkt, pkt_len, + options, NULL, parsed_len, + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr)); + + } else { + /* This must be ChannelData. */ + pj_turn_channel_data cd; + struct ch_t *ch; + + if (pkt_len < 4) { + if (parsed_len) *parsed_len = 0; + return PJ_ETOOSMALL; + } + + /* Decode ChannelData packet */ + pj_memcpy(&cd, pkt, sizeof(pj_turn_channel_data)); + cd.ch_number = pj_ntohs(cd.ch_number); + cd.length = pj_ntohs(cd.length); + + /* Check that size is sane */ + if (pkt_len < cd.length+sizeof(cd)) { + if (parsed_len) { + if (is_datagram) { + /* Discard the datagram */ + *parsed_len = pkt_len; + } else { + /* Insufficient fragment */ + *parsed_len = 0; + } + } + status = PJ_ETOOSMALL; + goto on_return; + } else { + if (parsed_len) { + /* Apply padding too */ + *parsed_len = ((cd.length + 3) & (~3)) + sizeof(cd); + } + } + + /* Lookup channel */ + ch = lookup_ch_by_chnum(sess, cd.ch_number); + if (!ch || !ch->bound) { + status = PJ_ENOTFOUND; + goto on_return; + } + + /* Notify application */ + if (sess->cb.on_rx_data) { + (*sess->cb.on_rx_data)(sess, ((pj_uint8_t*)pkt)+sizeof(cd), + cd.length, &ch->addr, + pj_sockaddr_get_len(&ch->addr)); + } + + status = PJ_SUCCESS; + } + +on_return: + pj_lock_release(sess->lock); + return status; +} + + +/* + * This is a callback from STUN session to send outgoing packet. + */ +static pj_status_t stun_on_send_msg(pj_stun_session *stun, + void *token, + const void *pkt, + pj_size_t pkt_size, + const pj_sockaddr_t *dst_addr, + unsigned addr_len) +{ + pj_turn_session *sess; + + PJ_UNUSED_ARG(token); + + sess = (pj_turn_session*) pj_stun_session_get_user_data(stun); + return (*sess->cb.on_send_pkt)(sess, (const pj_uint8_t*)pkt, pkt_size, + dst_addr, addr_len); +} + + +/* + * Handle failed ALLOCATE or REFRESH request. This may switch to alternate + * server if we have one. + */ +static void on_session_fail( pj_turn_session *sess, + enum pj_stun_method_e method, + pj_status_t status, + const pj_str_t *reason) +{ + sess->last_status = status; + + do { + pj_str_t reason1; + char err_msg[PJ_ERR_MSG_SIZE]; + + if (reason == NULL) { + pj_strerror(status, err_msg, sizeof(err_msg)); + reason1 = pj_str(err_msg); + reason = &reason1; + } + + PJ_LOG(4,(sess->obj_name, "%s error: %.*s", + pj_stun_get_method_name(method), + (int)reason->slen, reason->ptr)); + + /* If this is ALLOCATE response and we don't have more server + * addresses to try, notify application and destroy the TURN + * session. + */ + if (method==PJ_STUN_ALLOCATE_METHOD && + sess->srv_addr == &sess->srv_addr_list[sess->srv_addr_cnt-1]) + { + + set_state(sess, PJ_TURN_STATE_DEALLOCATED); + sess_shutdown(sess, status); + return; + } + + /* Otherwise if this is not ALLOCATE response, notify application + * that session has been TERMINATED. + */ + if (method!=PJ_STUN_ALLOCATE_METHOD) { + set_state(sess, PJ_TURN_STATE_DEALLOCATED); + sess_shutdown(sess, status); + return; + } + + /* Try next server */ + ++sess->srv_addr; + reason = NULL; + + PJ_LOG(4,(sess->obj_name, "Trying next server")); + set_state(sess, PJ_TURN_STATE_RESOLVED); + + } while (0); +} + + +/* + * Handle successful response to ALLOCATE or REFRESH request. + */ +static void on_allocate_success(pj_turn_session *sess, + enum pj_stun_method_e method, + const pj_stun_msg *msg) +{ + const pj_stun_lifetime_attr *lf_attr; + const pj_stun_xor_relayed_addr_attr *raddr_attr; + const pj_stun_sockaddr_attr *mapped_attr; + pj_str_t s; + pj_time_val timeout; + + /* Must have LIFETIME attribute */ + lf_attr = (const pj_stun_lifetime_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_LIFETIME, 0); + if (lf_attr == NULL) { + on_session_fail(sess, method, PJNATH_EINSTUNMSG, + pj_cstr(&s, "Error: Missing LIFETIME attribute")); + return; + } + + /* If LIFETIME is zero, this is a deallocation */ + if (lf_attr->value == 0) { + set_state(sess, PJ_TURN_STATE_DEALLOCATED); + sess_shutdown(sess, PJ_SUCCESS); + return; + } + + /* Update lifetime and keep-alive interval */ + sess->lifetime = lf_attr->value; + pj_gettimeofday(&sess->expiry); + + if (sess->lifetime < PJ_TURN_KEEP_ALIVE_SEC) { + if (sess->lifetime <= 2) { + on_session_fail(sess, method, PJ_ETOOSMALL, + pj_cstr(&s, "Error: LIFETIME too small")); + return; + } + sess->ka_interval = sess->lifetime - 2; + sess->expiry.sec += (sess->ka_interval-1); + } else { + int timeout; + + sess->ka_interval = PJ_TURN_KEEP_ALIVE_SEC; + + timeout = sess->lifetime - PJ_TURN_REFRESH_SEC_BEFORE; + if (timeout < sess->ka_interval) + timeout = sess->ka_interval - 1; + + sess->expiry.sec += timeout; + } + + /* Check that relayed transport address contains correct + * address family. + */ + raddr_attr = (const pj_stun_xor_relayed_addr_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_XOR_RELAYED_ADDR, 0); + if (raddr_attr == NULL && method==PJ_STUN_ALLOCATE_METHOD) { + on_session_fail(sess, method, PJNATH_EINSTUNMSG, + pj_cstr(&s, "Error: Received ALLOCATE without " + "RELAY-ADDRESS attribute")); + return; + } + if (raddr_attr && raddr_attr->sockaddr.addr.sa_family != sess->af) { + on_session_fail(sess, method, PJNATH_EINSTUNMSG, + pj_cstr(&s, "Error: RELAY-ADDRESS with non IPv4" + " address family is not supported " + "for now")); + return; + } + if (raddr_attr && !pj_sockaddr_has_addr(&raddr_attr->sockaddr)) { + on_session_fail(sess, method, PJNATH_EINSTUNMSG, + pj_cstr(&s, "Error: Invalid IP address in " + "RELAY-ADDRESS attribute")); + return; + } + + /* Save relayed address */ + if (raddr_attr) { + /* If we already have relay address, check if the relay address + * in the response matches our relay address. + */ + if (pj_sockaddr_has_addr(&sess->relay_addr)) { + if (pj_sockaddr_cmp(&sess->relay_addr, &raddr_attr->sockaddr)) { + on_session_fail(sess, method, PJNATH_EINSTUNMSG, + pj_cstr(&s, "Error: different RELAY-ADDRESS is" + "returned by server")); + return; + } + } else { + /* Otherwise save the relayed address */ + pj_memcpy(&sess->relay_addr, &raddr_attr->sockaddr, + sizeof(pj_sockaddr)); + } + } + + /* Get mapped address */ + mapped_attr = (const pj_stun_sockaddr_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_XOR_MAPPED_ADDR, 0); + if (mapped_attr) { + pj_memcpy(&sess->mapped_addr, &mapped_attr->sockaddr, + sizeof(mapped_attr->sockaddr)); + } + + /* Success */ + + /* Cancel existing keep-alive timer, if any */ + pj_assert(sess->timer.id != TIMER_DESTROY); + + if (sess->timer.id != TIMER_NONE) { + pj_timer_heap_cancel(sess->timer_heap, &sess->timer); + sess->timer.id = TIMER_NONE; + } + + /* Start keep-alive timer once allocation succeeds */ + timeout.sec = sess->ka_interval; + timeout.msec = 0; + + sess->timer.id = TIMER_KEEP_ALIVE; + pj_timer_heap_schedule(sess->timer_heap, &sess->timer, &timeout); + + set_state(sess, PJ_TURN_STATE_READY); +} + +/* + * Notification from STUN session on request completion. + */ +static void stun_on_request_complete(pj_stun_session *stun, + pj_status_t status, + void *token, + pj_stun_tx_data *tdata, + const pj_stun_msg *response, + const pj_sockaddr_t *src_addr, + unsigned src_addr_len) +{ + pj_turn_session *sess; + enum pj_stun_method_e method = (enum pj_stun_method_e) + PJ_STUN_GET_METHOD(tdata->msg->hdr.type); + + PJ_UNUSED_ARG(src_addr); + PJ_UNUSED_ARG(src_addr_len); + + sess = (pj_turn_session*)pj_stun_session_get_user_data(stun); + + if (method == PJ_STUN_ALLOCATE_METHOD) { + + /* Destroy if we have pending destroy request */ + if (sess->pending_destroy) { + if (status == PJ_SUCCESS) + sess->state = PJ_TURN_STATE_READY; + else + sess->state = PJ_TURN_STATE_DEALLOCATED; + sess_shutdown(sess, PJ_SUCCESS); + return; + } + + /* Handle ALLOCATE response */ + if (status==PJ_SUCCESS && + PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type)) + { + + /* Successful Allocate response */ + on_allocate_success(sess, method, response); + + } else { + /* Failed Allocate request */ + const pj_str_t *err_msg = NULL; + + if (status == PJ_SUCCESS) { + const pj_stun_errcode_attr *err_attr; + err_attr = (const pj_stun_errcode_attr*) + pj_stun_msg_find_attr(response, + PJ_STUN_ATTR_ERROR_CODE, 0); + if (err_attr) { + status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code); + err_msg = &err_attr->reason; + } else { + status = PJNATH_EINSTUNMSG; + } + } + + on_session_fail(sess, method, status, err_msg); + } + + } else if (method == PJ_STUN_REFRESH_METHOD) { + /* Handle Refresh response */ + if (status==PJ_SUCCESS && + PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type)) + { + /* Success, schedule next refresh. */ + on_allocate_success(sess, method, response); + + } else { + /* Failed Refresh request */ + const pj_str_t *err_msg = NULL; + + pj_assert(status != PJ_SUCCESS); + + if (response) { + const pj_stun_errcode_attr *err_attr; + err_attr = (const pj_stun_errcode_attr*) + pj_stun_msg_find_attr(response, + PJ_STUN_ATTR_ERROR_CODE, 0); + if (err_attr) { + status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code); + err_msg = &err_attr->reason; + } + } + + /* Notify and destroy */ + on_session_fail(sess, method, status, err_msg); + } + + } else if (method == PJ_STUN_CHANNEL_BIND_METHOD) { + /* Handle ChannelBind response */ + if (status==PJ_SUCCESS && + PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type)) + { + /* Successful ChannelBind response */ + struct ch_t *ch = (struct ch_t*)token; + + pj_assert(ch->num != PJ_TURN_INVALID_CHANNEL); + ch->bound = PJ_TRUE; + + /* Update hash table */ + lookup_ch_by_addr(sess, &ch->addr, + pj_sockaddr_get_len(&ch->addr), + PJ_TRUE, PJ_TRUE); + + } else { + /* Failed ChannelBind response */ + pj_str_t reason = {"", 0}; + int err_code = 0; + char errbuf[PJ_ERR_MSG_SIZE]; + + pj_assert(status != PJ_SUCCESS); + + if (response) { + const pj_stun_errcode_attr *err_attr; + err_attr = (const pj_stun_errcode_attr*) + pj_stun_msg_find_attr(response, + PJ_STUN_ATTR_ERROR_CODE, 0); + if (err_attr) { + err_code = err_attr->err_code; + status = PJ_STATUS_FROM_STUN_CODE(err_attr->err_code); + reason = err_attr->reason; + } + } else { + err_code = status; + reason = pj_strerror(status, errbuf, sizeof(errbuf)); + } + + PJ_LOG(1,(sess->obj_name, "ChannelBind failed: %d/%.*s", + err_code, (int)reason.slen, reason.ptr)); + + if (err_code == PJ_STUN_SC_ALLOCATION_MISMATCH) { + /* Allocation mismatch means allocation no longer exists */ + on_session_fail(sess, PJ_STUN_CHANNEL_BIND_METHOD, + status, &reason); + return; + } + } + + } else if (method == PJ_STUN_CREATE_PERM_METHOD) { + /* Handle CreatePermission response */ + if (status==PJ_SUCCESS && + PJ_STUN_IS_SUCCESS_RESPONSE(response->hdr.type)) + { + /* No special handling when the request is successful. */ + } else { + /* Iterate the permission table and invalidate all permissions + * that are related to this request. + */ + pj_hash_iterator_t it_buf, *it; + char ipstr[PJ_INET6_ADDRSTRLEN+10]; + int err_code; + char errbuf[PJ_ERR_MSG_SIZE]; + pj_str_t reason; + + pj_assert(status != PJ_SUCCESS); + + if (response) { + const pj_stun_errcode_attr *eattr; + + eattr = (const pj_stun_errcode_attr*) + pj_stun_msg_find_attr(response, + PJ_STUN_ATTR_ERROR_CODE, 0); + if (eattr) { + err_code = eattr->err_code; + reason = eattr->reason; + } else { + err_code = -1; + reason = pj_str("?"); + } + } else { + err_code = status; + reason = pj_strerror(status, errbuf, sizeof(errbuf)); + } + + it = pj_hash_first(sess->perm_table, &it_buf); + while (it) { + struct perm_t *perm = (struct perm_t*) + pj_hash_this(sess->perm_table, it); + it = pj_hash_next(sess->perm_table, it); + + if (perm->req_token == token) { + PJ_LOG(1,(sess->obj_name, + "CreatePermission failed for IP %s: %d/%.*s", + pj_sockaddr_print(&perm->addr, ipstr, + sizeof(ipstr), 2), + err_code, (int)reason.slen, reason.ptr)); + + invalidate_perm(sess, perm); + } + } + + if (err_code == PJ_STUN_SC_ALLOCATION_MISMATCH) { + /* Allocation mismatch means allocation no longer exists */ + on_session_fail(sess, PJ_STUN_CREATE_PERM_METHOD, + status, &reason); + return; + } + } + + } else { + PJ_LOG(4,(sess->obj_name, "Unexpected STUN %s response", + pj_stun_get_method_name(response->hdr.type))); + } +} + + +/* + * Notification from STUN session on incoming STUN Indication + * message. + */ +static pj_status_t stun_on_rx_indication(pj_stun_session *stun, + const pj_uint8_t *pkt, + unsigned pkt_len, + const pj_stun_msg *msg, + void *token, + const pj_sockaddr_t *src_addr, + unsigned src_addr_len) +{ + pj_turn_session *sess; + pj_stun_xor_peer_addr_attr *peer_attr; + pj_stun_icmp_attr *icmp; + pj_stun_data_attr *data_attr; + + PJ_UNUSED_ARG(token); + PJ_UNUSED_ARG(pkt); + PJ_UNUSED_ARG(pkt_len); + PJ_UNUSED_ARG(src_addr); + PJ_UNUSED_ARG(src_addr_len); + + sess = (pj_turn_session*)pj_stun_session_get_user_data(stun); + + /* Expecting Data Indication only */ + if (msg->hdr.type != PJ_STUN_DATA_INDICATION) { + PJ_LOG(4,(sess->obj_name, "Unexpected STUN %s indication", + pj_stun_get_method_name(msg->hdr.type))); + return PJ_EINVALIDOP; + } + + /* Check if there is ICMP attribute in the message */ + icmp = (pj_stun_icmp_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_ICMP, 0); + if (icmp != NULL) { + /* This is a forwarded ICMP packet. Ignore it for now */ + return PJ_SUCCESS; + } + + /* Get XOR-PEER-ADDRESS attribute */ + peer_attr = (pj_stun_xor_peer_addr_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_XOR_PEER_ADDR, 0); + + /* Get DATA attribute */ + data_attr = (pj_stun_data_attr*) + pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_DATA, 0); + + /* Must have both XOR-PEER-ADDRESS and DATA attributes */ + if (!peer_attr || !data_attr) { + PJ_LOG(4,(sess->obj_name, + "Received Data indication with missing attributes")); + return PJ_EINVALIDOP; + } + + /* Notify application */ + if (sess->cb.on_rx_data) { + (*sess->cb.on_rx_data)(sess, data_attr->data, data_attr->length, + &peer_attr->sockaddr, + pj_sockaddr_get_len(&peer_attr->sockaddr)); + } + + return PJ_SUCCESS; +} + + +/* + * Notification on completion of DNS SRV resolution. + */ +static void dns_srv_resolver_cb(void *user_data, + pj_status_t status, + const pj_dns_srv_record *rec) +{ + pj_turn_session *sess = (pj_turn_session*) user_data; + unsigned i, cnt, tot_cnt; + + /* Clear async resolver */ + sess->dns_async = NULL; + + /* Check failure */ + if (status != PJ_SUCCESS) { + sess_shutdown(sess, status); + return; + } + + /* Calculate total number of server entries in the response */ + tot_cnt = 0; + for (i=0; i<rec->count; ++i) { + tot_cnt += rec->entry[i].server.addr_count; + } + + if (tot_cnt > PJ_TURN_MAX_DNS_SRV_CNT) + tot_cnt = PJ_TURN_MAX_DNS_SRV_CNT; + + /* Allocate server entries */ + sess->srv_addr_list = (pj_sockaddr*) + pj_pool_calloc(sess->pool, tot_cnt, + sizeof(pj_sockaddr)); + + /* Copy results to server entries */ + for (i=0, cnt=0; i<rec->count && cnt<PJ_TURN_MAX_DNS_SRV_CNT; ++i) { + unsigned j; + + for (j=0; j<rec->entry[i].server.addr_count && + cnt<PJ_TURN_MAX_DNS_SRV_CNT; ++j) + { + pj_sockaddr_in *addr = &sess->srv_addr_list[cnt].ipv4; + + addr->sin_family = sess->af; + addr->sin_port = pj_htons(rec->entry[i].port); + addr->sin_addr.s_addr = rec->entry[i].server.addr[j].s_addr; + + ++cnt; + } + } + sess->srv_addr_cnt = (pj_uint16_t)cnt; + + /* Set current server */ + sess->srv_addr = &sess->srv_addr_list[0]; + + /* Set state to PJ_TURN_STATE_RESOLVED */ + set_state(sess, PJ_TURN_STATE_RESOLVED); + + /* Run pending allocation */ + if (sess->pending_alloc) { + pj_turn_session_alloc(sess, NULL); + } +} + + +/* + * Lookup peer descriptor from its address. + */ +static struct ch_t *lookup_ch_by_addr(pj_turn_session *sess, + const pj_sockaddr_t *addr, + unsigned addr_len, + pj_bool_t update, + pj_bool_t bind_channel) +{ + pj_uint32_t hval = 0; + struct ch_t *ch; + + ch = (struct ch_t*) + pj_hash_get(sess->ch_table, addr, addr_len, &hval); + if (ch == NULL && update) { + ch = PJ_POOL_ZALLOC_T(sess->pool, struct ch_t); + ch->num = PJ_TURN_INVALID_CHANNEL; + pj_memcpy(&ch->addr, addr, addr_len); + + /* Register by peer address */ + pj_hash_set(sess->pool, sess->ch_table, &ch->addr, addr_len, + hval, ch); + } + + if (ch && update) { + pj_gettimeofday(&ch->expiry); + ch->expiry.sec += PJ_TURN_PERM_TIMEOUT - sess->ka_interval - 1; + + if (bind_channel) { + pj_uint32_t hval = 0; + /* Register by channel number */ + pj_assert(ch->num != PJ_TURN_INVALID_CHANNEL && ch->bound); + + if (pj_hash_get(sess->ch_table, &ch->num, + sizeof(ch->num), &hval)==0) { + pj_hash_set(sess->pool, sess->ch_table, &ch->num, + sizeof(ch->num), hval, ch); + } + } + } + + /* Also create/update permission for this destination. Ideally we + * should update this when we receive the successful response, + * but that would cause duplicate CreatePermission to be sent + * during refreshing. + */ + if (ch && update) { + lookup_perm(sess, &ch->addr, pj_sockaddr_get_len(&ch->addr), PJ_TRUE); + } + + return ch; +} + + +/* + * Lookup channel descriptor from its channel number. + */ +static struct ch_t *lookup_ch_by_chnum(pj_turn_session *sess, + pj_uint16_t chnum) +{ + return (struct ch_t*) pj_hash_get(sess->ch_table, &chnum, + sizeof(chnum), NULL); +} + + +/* + * Lookup permission and optionally create if it doesn't exist. + */ +static struct perm_t *lookup_perm(pj_turn_session *sess, + const pj_sockaddr_t *addr, + unsigned addr_len, + pj_bool_t update) +{ + pj_uint32_t hval = 0; + pj_sockaddr perm_addr; + struct perm_t *perm; + + /* make sure port number if zero */ + if (pj_sockaddr_get_port(addr) != 0) { + pj_memcpy(&perm_addr, addr, addr_len); + pj_sockaddr_set_port(&perm_addr, 0); + addr = &perm_addr; + } + + /* lookup and create if it doesn't exist and wanted */ + perm = (struct perm_t*) + pj_hash_get(sess->perm_table, addr, addr_len, &hval); + if (perm == NULL && update) { + perm = PJ_POOL_ZALLOC_T(sess->pool, struct perm_t); + pj_memcpy(&perm->addr, addr, addr_len); + perm->hval = hval; + + pj_hash_set(sess->pool, sess->perm_table, &perm->addr, addr_len, + perm->hval, perm); + } + + if (perm && update) { + pj_gettimeofday(&perm->expiry); + perm->expiry.sec += PJ_TURN_PERM_TIMEOUT - sess->ka_interval - 1; + + } + + return perm; +} + +/* + * Delete permission + */ +static void invalidate_perm(pj_turn_session *sess, + struct perm_t *perm) +{ + pj_hash_set(NULL, sess->perm_table, &perm->addr, + pj_sockaddr_get_len(&perm->addr), perm->hval, NULL); +} + +/* + * Scan permission's hash table to refresh the permission. + */ +static unsigned refresh_permissions(pj_turn_session *sess, + const pj_time_val *now) +{ + pj_stun_tx_data *tdata = NULL; + unsigned count = 0; + void *req_token = NULL; + pj_hash_iterator_t *it, itbuf; + pj_status_t status; + + it = pj_hash_first(sess->perm_table, &itbuf); + while (it) { + struct perm_t *perm = (struct perm_t*) + pj_hash_this(sess->perm_table, it); + + it = pj_hash_next(sess->perm_table, it); + + if (perm->expiry.sec-1 <= now->sec) { + if (perm->renew) { + /* Renew this permission */ + if (tdata == NULL) { + /* Create a bare CreatePermission request */ + status = pj_stun_session_create_req( + sess->stun, + PJ_STUN_CREATE_PERM_REQUEST, + PJ_STUN_MAGIC, NULL, &tdata); + if (status != PJ_SUCCESS) { + PJ_LOG(1,(sess->obj_name, + "Error creating CreatePermission request: %d", + status)); + return 0; + } + + /* Create request token to map the request to the perm + * structures which the request belongs. + */ + req_token = (void*)(long)pj_rand(); + } + + status = pj_stun_msg_add_sockaddr_attr( + tdata->pool, + tdata->msg, + PJ_STUN_ATTR_XOR_PEER_ADDR, + PJ_TRUE, + &perm->addr, + sizeof(perm->addr)); + if (status != PJ_SUCCESS) { + pj_stun_msg_destroy_tdata(sess->stun, tdata); + return 0; + } + + perm->expiry = *now; + perm->expiry.sec += PJ_TURN_PERM_TIMEOUT-sess->ka_interval-1; + perm->req_token = req_token; + ++count; + + } else { + /* This permission has expired and app doesn't want + * us to renew, so delete it from the hash table. + */ + invalidate_perm(sess, perm); + } + } + } + + if (tdata) { + status = pj_stun_session_send_msg(sess->stun, req_token, PJ_FALSE, + (sess->conn_type==PJ_TURN_TP_UDP), + sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr), + tdata); + if (status != PJ_SUCCESS) { + PJ_LOG(1,(sess->obj_name, + "Error sending CreatePermission request: %d", + status)); + count = 0; + } + + } + + return count; +} + +/* + * Timer event. + */ +static void on_timer_event(pj_timer_heap_t *th, pj_timer_entry *e) +{ + pj_turn_session *sess = (pj_turn_session*)e->user_data; + enum timer_id_t eid; + + PJ_UNUSED_ARG(th); + + pj_lock_acquire(sess->lock); + + eid = (enum timer_id_t) e->id; + e->id = TIMER_NONE; + + if (eid == TIMER_KEEP_ALIVE) { + pj_time_val now; + pj_hash_iterator_t itbuf, *it; + pj_bool_t resched = PJ_TRUE; + pj_bool_t pkt_sent = PJ_FALSE; + + pj_gettimeofday(&now); + + /* Refresh allocation if it's time to do so */ + if (PJ_TIME_VAL_LTE(sess->expiry, now)) { + int lifetime = sess->alloc_param.lifetime; + + if (lifetime == 0) + lifetime = -1; + + send_refresh(sess, lifetime); + resched = PJ_FALSE; + pkt_sent = PJ_TRUE; + } + + /* Scan hash table to refresh bound channels */ + it = pj_hash_first(sess->ch_table, &itbuf); + while (it) { + struct ch_t *ch = (struct ch_t*) + pj_hash_this(sess->ch_table, it); + if (ch->bound && PJ_TIME_VAL_LTE(ch->expiry, now)) { + + /* Send ChannelBind to refresh channel binding and + * permission. + */ + pj_turn_session_bind_channel(sess, &ch->addr, + pj_sockaddr_get_len(&ch->addr)); + pkt_sent = PJ_TRUE; + } + + it = pj_hash_next(sess->ch_table, it); + } + + /* Scan permission table to refresh permissions */ + if (refresh_permissions(sess, &now)) + pkt_sent = PJ_TRUE; + + /* If no packet is sent, send a blank Send indication to + * refresh local NAT. + */ + if (!pkt_sent && sess->alloc_param.ka_interval > 0) { + pj_stun_tx_data *tdata; + pj_status_t rc; + + /* Create blank SEND-INDICATION */ + rc = pj_stun_session_create_ind(sess->stun, + PJ_STUN_SEND_INDICATION, &tdata); + if (rc == PJ_SUCCESS) { + /* Add DATA attribute with zero length */ + pj_stun_msg_add_binary_attr(tdata->pool, tdata->msg, + PJ_STUN_ATTR_DATA, NULL, 0); + + /* Send the indication */ + pj_stun_session_send_msg(sess->stun, NULL, PJ_FALSE, + PJ_FALSE, sess->srv_addr, + pj_sockaddr_get_len(sess->srv_addr), + tdata); + } + } + + /* Reshcedule timer */ + if (resched) { + pj_time_val delay; + + delay.sec = sess->ka_interval; + delay.msec = 0; + + sess->timer.id = TIMER_KEEP_ALIVE; + pj_timer_heap_schedule(sess->timer_heap, &sess->timer, &delay); + } + + pj_lock_release(sess->lock); + + } else if (eid == TIMER_DESTROY) { + /* Time to destroy */ + pj_lock_release(sess->lock); + do_destroy(sess); + } else { + pj_assert(!"Unknown timer event"); + pj_lock_release(sess->lock); + } +} + |