/* linphone Copyright (C) 2012 Belledonne Communications SARL 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 "upnp.h" #include "private.h" #include "lpconfig.h" #include #define UPNP_STRINGIFY(x) #x #define UPNP_TOSTRING(x) UPNP_STRINGIFY(x) #define UPNP_ADD_MAX_RETRY 4 #define UPNP_REMOVE_MAX_RETRY 4 #define UPNP_SECTION_NAME "uPnP" #define UPNP_CORE_READY_CHECK 1 #define UPNP_CORE_RETRY_DELAY 10 #define UPNP_CALL_RETRY_DELAY 3 #define UPNP_UUID_LEN 128 #define UPNP_UUID_LEN_STR UPNP_TOSTRING(UPNP_UUID_LEN) /* * uPnP Definitions */ typedef struct _UpnpPortBinding { ms_mutex_t mutex; LinphoneUpnpState state; upnp_igd_ip_protocol protocol; char *device_id; char local_addr[LINPHONE_IPADDR_SIZE]; int local_port; char external_addr[LINPHONE_IPADDR_SIZE]; int external_port; int retry; int ref; bool_t to_remove; bool_t to_add; time_t last_update; } UpnpPortBinding; typedef struct _UpnpStream { UpnpPortBinding *rtp; UpnpPortBinding *rtcp; LinphoneUpnpState state; } UpnpStream; struct _UpnpSession { LinphoneCall *call; UpnpStream *audio; UpnpStream *video; LinphoneUpnpState state; }; struct _UpnpContext { LinphoneCore *lc; upnp_igd_context *upnp_igd_ctxt; UpnpPortBinding *sip_tcp; UpnpPortBinding *sip_tls; UpnpPortBinding *sip_udp; LinphoneUpnpState state; MSList *removing_configs; MSList *adding_configs; MSList *pending_bindings; ms_mutex_t mutex; ms_cond_t empty_cond; time_t last_ready_check; LinphoneUpnpState last_ready_state; }; bool_t linphone_core_upnp_hook(void *data); void linphone_upnp_update(UpnpContext *ctx); bool_t linphone_upnp_is_blacklisted(UpnpContext *ctx); UpnpPortBinding *linphone_upnp_port_binding_new(); UpnpPortBinding *linphone_upnp_port_binding_new_with_parameters(upnp_igd_ip_protocol protocol, int local_port, int external_port); UpnpPortBinding *linphone_upnp_port_binding_new_or_collect(MSList *list, upnp_igd_ip_protocol protocol, int local_port, int external_port); UpnpPortBinding *linphone_upnp_port_binding_copy(const UpnpPortBinding *port); void linphone_upnp_port_binding_set_device_id(UpnpPortBinding *port, const char * device_id); bool_t linphone_upnp_port_binding_equal(const UpnpPortBinding *port1, const UpnpPortBinding *port2); UpnpPortBinding *linphone_upnp_port_binding_equivalent_in_list(MSList *list, const UpnpPortBinding *port); UpnpPortBinding *linphone_upnp_port_binding_retain(UpnpPortBinding *port); void linphone_upnp_update_port_binding(UpnpContext *lupnp, UpnpPortBinding **port_mapping, upnp_igd_ip_protocol protocol, int port, int retry_delay); void linphone_upnp_port_binding_log(int level, const char *msg, const UpnpPortBinding *port); void linphone_upnp_port_binding_release(UpnpPortBinding *port); void linphone_upnp_update_config(UpnpContext *lupnp); void linphone_upnp_update_proxy(UpnpContext *lupnp, bool_t force); // Configuration MSList *linphone_upnp_config_list_port_bindings(struct _LpConfig *lpc, const char *device_id); void linphone_upnp_config_add_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port); void linphone_upnp_config_remove_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port); // uPnP int linphone_upnp_context_send_remove_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry); int linphone_upnp_context_send_add_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry); static int linphone_upnp_strncmpi(const char *str1, const char *str2, int len) { int i = 0; char char1, char2; while(i < len) { char1 = toupper(*str1); char2 = toupper(*str2); if(char1 == '\0' || char1 != char2) { return char1 - char2; } str1++; str2++; i++; } return 0; } static int linphone_upnp_str_min(const char *str1, const char *str2) { int len1 = strlen(str1); int len2 = strlen(str2); if(len1 > len2) { return len2; } return len1; } char * linphone_upnp_format_device_id(const char *device_id) { char *ret = NULL; char *tmp; char tchar; bool_t copy; if(device_id == NULL) { return ret; } ret = ms_new(char, UPNP_UUID_LEN + 1); tmp = ret; if(linphone_upnp_strncmpi(device_id, "uuid:", linphone_upnp_str_min(device_id, "uuid:")) == 0) { device_id += strlen("uuid:"); } while(*device_id != '\0' && tmp - ret < UPNP_UUID_LEN) { copy = FALSE; tchar = *device_id; if(tchar >= '0' && tchar <= '9') copy = TRUE; if(!copy && tchar >= 'A' && tchar <= 'Z') copy = TRUE; if(!copy && tchar >= 'a' && tchar <= 'z') copy = TRUE; if(copy) { *tmp = *device_id; tmp++; } device_id++; } *tmp = '\0'; return ret; } /** * uPnP Callbacks */ /* Convert uPnP IGD logs to ortp logs */ void linphone_upnp_igd_print(void *cookie, upnp_igd_print_level level, const char *fmt, va_list list) { int ortp_level = ORTP_DEBUG; switch(level) { case UPNP_IGD_MESSAGE: ortp_level = ORTP_MESSAGE; break; case UPNP_IGD_WARNING: ortp_level = ORTP_DEBUG; // Too verbose otherwise break; case UPNP_IGD_ERROR: ortp_level = ORTP_DEBUG; // Too verbose otherwise break; default: break; } ortp_logv(ortp_level, fmt, list); } void linphone_upnp_igd_callback(void *cookie, upnp_igd_event event, void *arg) { UpnpContext *lupnp = (UpnpContext *)cookie; upnp_igd_port_mapping *mapping = NULL; UpnpPortBinding *port_mapping = NULL; const char *ip_address = NULL; const char *connection_status = NULL; bool_t nat_enabled = FALSE; bool_t blacklisted = FALSE; LinphoneUpnpState old_state; if(lupnp == NULL || lupnp->upnp_igd_ctxt == NULL) { ms_error("uPnP IGD: Invalid context in callback"); return; } ms_mutex_lock(&lupnp->mutex); old_state = lupnp->state; switch(event) { case UPNP_IGD_DEVICE_ADDED: case UPNP_IGD_DEVICE_REMOVED: case UPNP_IGD_EXTERNAL_IPADDRESS_CHANGED: case UPNP_IGD_NAT_ENABLED_CHANGED: case UPNP_IGD_CONNECTION_STATUS_CHANGED: ip_address = upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt); connection_status = upnp_igd_get_connection_status(lupnp->upnp_igd_ctxt); nat_enabled = upnp_igd_get_nat_enabled(lupnp->upnp_igd_ctxt); blacklisted = linphone_upnp_is_blacklisted(lupnp); if(ip_address == NULL || connection_status == NULL) { ms_message("uPnP IGD: Pending"); lupnp->state = LinphoneUpnpStatePending; } else if(strcasecmp(connection_status, "Connected") || !nat_enabled) { ms_message("uPnP IGD: Not Available"); lupnp->state = LinphoneUpnpStateNotAvailable; } else if(blacklisted) { ms_message("uPnP IGD: Router is blacklisted"); lupnp->state = LinphoneUpnpStateBlacklisted; } else { ms_message("uPnP IGD: Connected"); lupnp->state = LinphoneUpnpStateOk; if(old_state != LinphoneUpnpStateOk) { linphone_upnp_update(lupnp); } } break; case UPNP_IGD_PORT_MAPPING_ADD_SUCCESS: mapping = (upnp_igd_port_mapping *) arg; port_mapping = (UpnpPortBinding*) mapping->cookie; port_mapping->external_port = mapping->remote_port; port_mapping->state = LinphoneUpnpStateOk; linphone_upnp_port_binding_log(ORTP_MESSAGE, "Added port binding", port_mapping); linphone_upnp_config_add_port_binding(lupnp, port_mapping); break; case UPNP_IGD_PORT_MAPPING_ADD_FAILURE: mapping = (upnp_igd_port_mapping *) arg; port_mapping = (UpnpPortBinding*) mapping->cookie; port_mapping->external_port = -1; //Force random external port if(linphone_upnp_context_send_add_port_binding(lupnp, port_mapping, TRUE) != 0) { linphone_upnp_port_binding_log(ORTP_ERROR, "Can't add port binding", port_mapping); } break; case UPNP_IGD_PORT_MAPPING_REMOVE_SUCCESS: mapping = (upnp_igd_port_mapping *) arg; port_mapping = (UpnpPortBinding*) mapping->cookie; port_mapping->state = LinphoneUpnpStateIdle; linphone_upnp_port_binding_log(ORTP_MESSAGE, "Removed port binding", port_mapping); linphone_upnp_config_remove_port_binding(lupnp, port_mapping); break; case UPNP_IGD_PORT_MAPPING_REMOVE_FAILURE: mapping = (upnp_igd_port_mapping *) arg; port_mapping = (UpnpPortBinding*) mapping->cookie; if(linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, TRUE) != 0) { linphone_upnp_port_binding_log(ORTP_ERROR, "Can't remove port binding", port_mapping); linphone_upnp_config_remove_port_binding(lupnp, port_mapping); } break; default: break; } if(port_mapping != NULL) { /* * Execute delayed actions */ if(port_mapping->to_remove) { if(port_mapping->state == LinphoneUpnpStateOk) { port_mapping->to_remove = FALSE; linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, FALSE); } else if(port_mapping->state == LinphoneUpnpStateKo) { port_mapping->to_remove = FALSE; } } if(port_mapping->to_add) { if(port_mapping->state == LinphoneUpnpStateIdle || port_mapping->state == LinphoneUpnpStateKo) { port_mapping->to_add = FALSE; linphone_upnp_context_send_add_port_binding(lupnp, port_mapping, FALSE); } } lupnp->pending_bindings = ms_list_remove(lupnp->pending_bindings, port_mapping); linphone_upnp_port_binding_release(port_mapping); } /* * If there is no pending binding emit a signal */ if(lupnp->pending_bindings == NULL) { ms_cond_signal(&lupnp->empty_cond); } ms_mutex_unlock(&lupnp->mutex); } /** * uPnP Context */ UpnpContext* linphone_upnp_context_new(LinphoneCore *lc) { UpnpContext *lupnp = (UpnpContext *)ms_new0(UpnpContext,1); char address[LINPHONE_IPADDR_SIZE]; const char*upnp_binding_address=address; if (linphone_core_get_local_ip_for(lc->sip_conf.ipv6_enabled ? AF_INET6 : AF_INET,NULL,address)) { ms_warning("Linphone core [%p] cannot guess local address for upnp, let's choice the lib",lc); upnp_binding_address=NULL; } ms_mutex_init(&lupnp->mutex, NULL); ms_cond_init(&lupnp->empty_cond, NULL); lupnp->last_ready_check = 0; lupnp->last_ready_state = LinphoneUpnpStateIdle; lupnp->lc = lc; lupnp->pending_bindings = NULL; lupnp->adding_configs = NULL; lupnp->removing_configs = NULL; lupnp->state = LinphoneUpnpStateIdle; ms_message("uPnP IGD: New %p for core %p bound to %s", lupnp, lc,upnp_binding_address); // Init ports lupnp->sip_udp = NULL; lupnp->sip_tcp = NULL; lupnp->sip_tls = NULL; linphone_core_add_iterate_hook(lc, linphone_core_upnp_hook, lupnp); lupnp->upnp_igd_ctxt = NULL; lupnp->upnp_igd_ctxt = upnp_igd_create(linphone_upnp_igd_callback, linphone_upnp_igd_print, address, lupnp); if(lupnp->upnp_igd_ctxt == NULL) { lupnp->state = LinphoneUpnpStateKo; ms_error("Can't create uPnP IGD context"); return NULL; } lupnp->state = LinphoneUpnpStatePending; upnp_igd_start(lupnp->upnp_igd_ctxt); return lupnp; } void linphone_upnp_context_destroy(UpnpContext *lupnp) { linphone_core_remove_iterate_hook(lupnp->lc, linphone_core_upnp_hook, lupnp); ms_mutex_lock(&lupnp->mutex); if(lupnp->lc->network_reachable) { /* Send port binding removes */ if(lupnp->sip_udp != NULL) { linphone_upnp_context_send_remove_port_binding(lupnp, lupnp->sip_udp, TRUE); } if(lupnp->sip_tcp != NULL) { linphone_upnp_context_send_remove_port_binding(lupnp, lupnp->sip_tcp, TRUE); } if(lupnp->sip_tls != NULL) { linphone_upnp_context_send_remove_port_binding(lupnp, lupnp->sip_tls, TRUE); } } /* Wait all pending bindings are done */ if(lupnp->pending_bindings != NULL) { ms_message("uPnP IGD: Wait all pending port bindings ..."); ms_cond_wait(&lupnp->empty_cond, &lupnp->mutex); } ms_mutex_unlock(&lupnp->mutex); if(lupnp->upnp_igd_ctxt != NULL) { upnp_igd_destroy(lupnp->upnp_igd_ctxt); lupnp->upnp_igd_ctxt = NULL; } /* No more multi threading here */ /* Run one more time configuration update and proxy */ linphone_upnp_update_config(lupnp); linphone_upnp_update_proxy(lupnp, TRUE); /* Release port bindings */ if(lupnp->sip_udp != NULL) { linphone_upnp_port_binding_release(lupnp->sip_udp); lupnp->sip_udp = NULL; } if(lupnp->sip_tcp != NULL) { linphone_upnp_port_binding_release(lupnp->sip_tcp); lupnp->sip_tcp = NULL; } if(lupnp->sip_tls != NULL) { linphone_upnp_port_binding_release(lupnp->sip_tls); lupnp->sip_tcp = NULL; } /* Release lists */ ms_list_for_each(lupnp->adding_configs,(void (*)(void*))linphone_upnp_port_binding_release); lupnp->adding_configs = ms_list_free(lupnp->adding_configs); ms_list_for_each(lupnp->removing_configs,(void (*)(void*))linphone_upnp_port_binding_release); lupnp->removing_configs = ms_list_free(lupnp->removing_configs); ms_list_for_each(lupnp->pending_bindings,(void (*)(void*))linphone_upnp_port_binding_release); lupnp->pending_bindings = ms_list_free(lupnp->pending_bindings); ms_mutex_destroy(&lupnp->mutex); ms_cond_destroy(&lupnp->empty_cond); ms_message("uPnP IGD: destroy %p", lupnp); ms_free(lupnp); } LinphoneUpnpState linphone_upnp_context_get_state(UpnpContext *lupnp) { LinphoneUpnpState state = LinphoneUpnpStateKo; if(lupnp != NULL) { ms_mutex_lock(&lupnp->mutex); state = lupnp->state; ms_mutex_unlock(&lupnp->mutex); } return state; } bool_t _linphone_upnp_context_is_ready_for_register(UpnpContext *lupnp) { bool_t ready = TRUE; // 1 Check global uPnP state ready = (lupnp->state == LinphoneUpnpStateOk); // 2 Check external ip address if(ready) { if (upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt) == NULL) { ready = FALSE; } } // 3 Check sip ports bindings if(ready) { if(lupnp->sip_udp != NULL) { if(lupnp->sip_udp->state != LinphoneUpnpStateOk) { ready = FALSE; } } else if(lupnp->sip_tcp != NULL) { if(lupnp->sip_tcp->state != LinphoneUpnpStateOk) { ready = FALSE; } } else if(lupnp->sip_tls != NULL) { if(lupnp->sip_tls->state != LinphoneUpnpStateOk) { ready = FALSE; } } else { ready = FALSE; } } return ready; } bool_t linphone_upnp_context_is_ready_for_register(UpnpContext *lupnp) { bool_t ready = FALSE; if(lupnp != NULL) { ms_mutex_lock(&lupnp->mutex); ready = _linphone_upnp_context_is_ready_for_register(lupnp); ms_mutex_unlock(&lupnp->mutex); } return ready; } int linphone_upnp_context_get_external_port(UpnpContext *lupnp) { int port = -1; if(lupnp != NULL) { ms_mutex_lock(&lupnp->mutex); if(lupnp->sip_udp != NULL) { if(lupnp->sip_udp->state == LinphoneUpnpStateOk) { port = lupnp->sip_udp->external_port; } } else if(lupnp->sip_tcp != NULL) { if(lupnp->sip_tcp->state == LinphoneUpnpStateOk) { port = lupnp->sip_tcp->external_port; } } else if(lupnp->sip_tls != NULL) { if(lupnp->sip_tls->state == LinphoneUpnpStateOk) { port = lupnp->sip_tls->external_port; } } ms_mutex_unlock(&lupnp->mutex); } return port; } bool_t linphone_upnp_is_blacklisted(UpnpContext *lupnp) { const char * device_model_name = upnp_igd_get_device_model_name(lupnp->upnp_igd_ctxt); const char * device_model_number = upnp_igd_get_device_model_number(lupnp->upnp_igd_ctxt); const char * blacklist = lp_config_get_string(lupnp->lc->config, "net", "upnp_blacklist", NULL); bool_t blacklisted = FALSE; char *str; char *pch; char *model_name; char *model_number; // Sanity checks if(device_model_name == NULL || device_model_number == NULL || blacklist == NULL) { return FALSE; } // Find in the list str = strdup(blacklist); pch = strtok(str, ";"); while (pch != NULL && !blacklisted) { // Extract model name & number model_name = pch; model_number = strstr(pch, ","); if(model_number != NULL) { *(model_number++) = '\0'; } // Compare with current device if(strcmp(model_name, device_model_name) == 0) { if(model_number == NULL || strcmp(model_number, device_model_number) == 0) { blacklisted = TRUE; } } pch = strtok(NULL, ";"); } free(str); return blacklisted; } void linphone_upnp_refresh(UpnpContext * lupnp) { upnp_igd_refresh(lupnp->upnp_igd_ctxt); } const char* linphone_upnp_context_get_external_ipaddress(UpnpContext *lupnp) { const char* addr = NULL; if(lupnp != NULL) { ms_mutex_lock(&lupnp->mutex); addr = upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt); ms_mutex_unlock(&lupnp->mutex); } return addr; } int linphone_upnp_context_send_add_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry) { upnp_igd_port_mapping mapping; char description[128]; int ret; if(lupnp->state != LinphoneUpnpStateOk) { return -2; } // Compute port binding state if(port->state != LinphoneUpnpStateAdding) { port->to_remove = FALSE; switch(port->state) { case LinphoneUpnpStateKo: case LinphoneUpnpStateIdle: { port->retry = 0; port->state = LinphoneUpnpStateAdding; } break; case LinphoneUpnpStateRemoving: { port->to_add = TRUE; return 0; } break; default: return 0; } } // No retry if specified if(port->retry != 0 && !retry) { return -1; } if(port->retry >= UPNP_ADD_MAX_RETRY) { ret = -1; } else { linphone_upnp_port_binding_set_device_id(port, upnp_igd_get_device_id(lupnp->upnp_igd_ctxt)); mapping.cookie = linphone_upnp_port_binding_retain(port); lupnp->pending_bindings = ms_list_append(lupnp->pending_bindings, mapping.cookie); mapping.local_port = port->local_port; mapping.local_host = port->local_addr; if(port->external_port == -1) port->external_port = rand()%(0xffff - 1024) + 1024; mapping.remote_port = port->external_port; mapping.remote_host = ""; snprintf(description, 128, "%s %s at %s:%d", "Linphone", (port->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP": "UDP", port->local_addr, port->local_port); mapping.description = description; mapping.protocol = port->protocol; port->retry++; linphone_upnp_port_binding_log(ORTP_MESSAGE, "Try to add port binding", port); ret = upnp_igd_add_port_mapping(lupnp->upnp_igd_ctxt, &mapping); } if(ret != 0) { port->state = LinphoneUpnpStateKo; } return ret; } int linphone_upnp_context_send_remove_port_binding(UpnpContext *lupnp, UpnpPortBinding *port, bool_t retry) { upnp_igd_port_mapping mapping; int ret; if(lupnp->state != LinphoneUpnpStateOk) { return -2; } // Compute port binding state if(port->state != LinphoneUpnpStateRemoving) { port->to_add = FALSE; switch(port->state) { case LinphoneUpnpStateOk: { port->retry = 0; port->state = LinphoneUpnpStateRemoving; } break; case LinphoneUpnpStateAdding: { port->to_remove = TRUE; return 0; } break; default: return 0; } } // No retry if specified if(port->retry != 0 && !retry) { return 1; } if(port->retry >= UPNP_REMOVE_MAX_RETRY) { ret = -1; } else { linphone_upnp_port_binding_set_device_id(port, upnp_igd_get_device_id(lupnp->upnp_igd_ctxt)); mapping.cookie = linphone_upnp_port_binding_retain(port); lupnp->pending_bindings = ms_list_append(lupnp->pending_bindings, mapping.cookie); mapping.remote_port = port->external_port; mapping.remote_host = ""; mapping.protocol = port->protocol; port->retry++; linphone_upnp_port_binding_log(ORTP_MESSAGE, "Try to remove port binding", port); ret = upnp_igd_delete_port_mapping(lupnp->upnp_igd_ctxt, &mapping); } if(ret != 0) { port->state = LinphoneUpnpStateKo; } return ret; } /* * uPnP Core interfaces */ int linphone_core_update_upnp_audio_video(LinphoneCall *call, bool_t audio, bool_t video) { LinphoneCore *lc = call->core; UpnpContext *lupnp = lc->upnp; int ret = -1; if(lupnp == NULL) { return ret; } ms_mutex_lock(&lupnp->mutex); // Don't handle when the call if(lupnp->state == LinphoneUpnpStateOk && call->upnp_session != NULL) { ret = 0; /* * Audio part */ linphone_upnp_update_port_binding(lupnp, &call->upnp_session->audio->rtp, UPNP_IGD_IP_PROTOCOL_UDP, (audio)? call->media_ports[0].rtp_port:0, UPNP_CALL_RETRY_DELAY); linphone_upnp_update_port_binding(lupnp, &call->upnp_session->audio->rtcp, UPNP_IGD_IP_PROTOCOL_UDP, (audio)? call->media_ports[0].rtcp_port:0, UPNP_CALL_RETRY_DELAY); /* * Video part */ linphone_upnp_update_port_binding(lupnp, &call->upnp_session->video->rtp, UPNP_IGD_IP_PROTOCOL_UDP, (video)? call->media_ports[1].rtp_port:0, UPNP_CALL_RETRY_DELAY); linphone_upnp_update_port_binding(lupnp, &call->upnp_session->video->rtcp, UPNP_IGD_IP_PROTOCOL_UDP, (video)? call->media_ports[1].rtcp_port:0, UPNP_CALL_RETRY_DELAY); } ms_mutex_unlock(&lupnp->mutex); /* * Update uPnP call state */ linphone_upnp_call_process(call); return ret; } int linphone_core_update_upnp_from_remote_media_description(LinphoneCall *call, const SalMediaDescription *md) { bool_t audio = FALSE; bool_t video = FALSE; int i; const SalStreamDescription *stream; for (i = 0; i < md->nb_streams; i++) { stream = &md->streams[i]; if(stream->type == SalAudio) { audio = TRUE; } else if(stream->type == SalVideo) { video = TRUE; } } return linphone_core_update_upnp_audio_video(call, audio, video); } int linphone_core_update_upnp(LinphoneCore *lc, LinphoneCall *call) { return linphone_core_update_upnp_audio_video(call, call->audiostream!=NULL, call->videostream!=NULL); } void linphone_core_update_upnp_state_in_call_stats(LinphoneCall *call) { call->stats[LINPHONE_CALL_STATS_AUDIO].upnp_state = call->upnp_session->audio->state; call->stats[LINPHONE_CALL_STATS_VIDEO].upnp_state = call->upnp_session->video->state; } void linphone_upnp_update_stream_state(UpnpStream *stream) { if((stream->rtp == NULL || stream->rtp->state == LinphoneUpnpStateOk || stream->rtp->state == LinphoneUpnpStateIdle) && (stream->rtcp == NULL || stream->rtcp->state == LinphoneUpnpStateOk || stream->rtcp->state == LinphoneUpnpStateIdle)) { stream->state = LinphoneUpnpStateOk; } else if((stream->rtp != NULL && (stream->rtp->state == LinphoneUpnpStateAdding || stream->rtp->state == LinphoneUpnpStateRemoving)) || (stream->rtcp != NULL && (stream->rtcp->state == LinphoneUpnpStateAdding || stream->rtcp->state == LinphoneUpnpStateRemoving))) { stream->state = LinphoneUpnpStatePending; } else if((stream->rtp != NULL && stream->rtp->state == LinphoneUpnpStateKo) || (stream->rtcp != NULL && stream->rtcp->state == LinphoneUpnpStateKo)) { stream->state = LinphoneUpnpStateKo; } else { ms_error("Invalid stream %p state", stream); } } int linphone_upnp_call_process(LinphoneCall *call) { LinphoneCore *lc = call->core; UpnpContext *lupnp = lc->upnp; int ret = -1; LinphoneUpnpState oldState = 0, newState = 0; if(lupnp == NULL) { return ret; } ms_mutex_lock(&lupnp->mutex); // Don't handle when the call if(lupnp->state == LinphoneUpnpStateOk && call->upnp_session != NULL) { ret = 0; /* * Update Audio state */ linphone_upnp_update_stream_state(call->upnp_session->audio); /* * Update Video state */ linphone_upnp_update_stream_state(call->upnp_session->video); /* * Update stat */ linphone_core_update_upnp_state_in_call_stats(call); /* * Update session state */ oldState = call->upnp_session->state; if(call->upnp_session->audio->state == LinphoneUpnpStateOk && call->upnp_session->video->state == LinphoneUpnpStateOk) { call->upnp_session->state = LinphoneUpnpStateOk; } else if(call->upnp_session->audio->state == LinphoneUpnpStatePending || call->upnp_session->video->state == LinphoneUpnpStatePending) { call->upnp_session->state = LinphoneUpnpStatePending; } else if(call->upnp_session->audio->state == LinphoneUpnpStateKo || call->upnp_session->video->state == LinphoneUpnpStateKo) { call->upnp_session->state = LinphoneUpnpStateKo; } else { call->upnp_session->state = LinphoneUpnpStateIdle; } newState = call->upnp_session->state; } ms_mutex_unlock(&lupnp->mutex); /* When change is done proceed update */ if(oldState != LinphoneUpnpStateOk && oldState != LinphoneUpnpStateKo && (newState == LinphoneUpnpStateOk || newState == LinphoneUpnpStateKo)) { if(call->upnp_session->state == LinphoneUpnpStateOk) ms_message("uPnP IGD: uPnP for Call %p is ok", call); else ms_message("uPnP IGD: uPnP for Call %p is ko", call); switch (call->state) { case LinphoneCallUpdating: linphone_core_start_update_call(lc, call); break; case LinphoneCallUpdatedByRemote: linphone_core_start_accept_call_update(lc, call,call->prevstate,linphone_call_state_to_string(call->prevstate)); break; case LinphoneCallOutgoingInit: linphone_core_proceed_with_invite_if_ready(lc, call, NULL); break; case LinphoneCallIdle: linphone_core_notify_incoming_call(lc, call); break; default: break; } } return ret; } static const char *linphone_core_upnp_get_charptr_null(const char *str) { if(str != NULL) { return str; } return "(Null)"; } void linphone_upnp_update(UpnpContext *lupnp) { MSList *global_list = NULL; MSList *list = NULL; MSList *item; LinphoneCall *call; UpnpPortBinding *port_mapping, *port_mapping2; ms_message("uPnP IGD: Name:%s", linphone_core_upnp_get_charptr_null(upnp_igd_get_device_name(lupnp->upnp_igd_ctxt))); ms_message("uPnP IGD: Device:%s %s", linphone_core_upnp_get_charptr_null(upnp_igd_get_device_model_name(lupnp->upnp_igd_ctxt)), linphone_core_upnp_get_charptr_null(upnp_igd_get_device_model_number(lupnp->upnp_igd_ctxt))); ms_message("uPnP IGD: Refresh mappings"); if(lupnp->sip_udp != NULL) { global_list = ms_list_append(global_list, lupnp->sip_udp); } if(lupnp->sip_tcp != NULL) { global_list = ms_list_append(global_list, lupnp->sip_tcp); } if(lupnp->sip_tls != NULL) { global_list = ms_list_append(global_list, lupnp->sip_tls); } list = lupnp->lc->calls; while(list != NULL) { call = (LinphoneCall *)list->data; if(call->upnp_session != NULL) { if(call->upnp_session->audio->rtp != NULL) { global_list = ms_list_append(global_list, call->upnp_session->audio->rtp); } if(call->upnp_session->audio->rtcp != NULL) { global_list = ms_list_append(global_list, call->upnp_session->audio->rtcp); } if(call->upnp_session->video->rtp != NULL) { global_list = ms_list_append(global_list, call->upnp_session->video->rtp); } if(call->upnp_session->video->rtcp != NULL) { global_list = ms_list_append(global_list, call->upnp_session->video->rtcp); } } list = list->next; } list = linphone_upnp_config_list_port_bindings(lupnp->lc->config, upnp_igd_get_device_id(lupnp->upnp_igd_ctxt)); for(item = list;item != NULL; item = item->next) { port_mapping = (UpnpPortBinding *)item->data; port_mapping2 = linphone_upnp_port_binding_equivalent_in_list(global_list, port_mapping); if(port_mapping2 == NULL) { linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, TRUE); } else if(port_mapping2->state == LinphoneUpnpStateIdle){ /* Force to remove */ port_mapping2->state = LinphoneUpnpStateOk; } } ms_list_for_each(list, (void (*)(void*))linphone_upnp_port_binding_release); list = ms_list_free(list); // (Re)Add removed port bindings list = global_list; while(list != NULL) { port_mapping = (UpnpPortBinding *)list->data; linphone_upnp_context_send_remove_port_binding(lupnp, port_mapping, TRUE); linphone_upnp_context_send_add_port_binding(lupnp, port_mapping, TRUE); list = list->next; } global_list = ms_list_free(global_list); } void linphone_upnp_update_port_binding(UpnpContext *lupnp, UpnpPortBinding **port_mapping, upnp_igd_ip_protocol protocol, int port, int retry_delay) { const char *local_addr, *external_addr; time_t now = time(NULL); if(port != 0) { if(*port_mapping != NULL) { if(port != (*port_mapping)->local_port) { linphone_upnp_context_send_remove_port_binding(lupnp, *port_mapping, FALSE); *port_mapping = NULL; } } if(*port_mapping == NULL) { *port_mapping = linphone_upnp_port_binding_new_or_collect(lupnp->pending_bindings, protocol, port, port); } // Get addresses local_addr = upnp_igd_get_local_ipaddress(lupnp->upnp_igd_ctxt); external_addr = upnp_igd_get_external_ipaddress(lupnp->upnp_igd_ctxt); // Force binding update on local address change if(local_addr != NULL) { if(strncmp((*port_mapping)->local_addr, local_addr, sizeof((*port_mapping)->local_addr))) { linphone_upnp_context_send_remove_port_binding(lupnp, *port_mapping, FALSE); strncpy((*port_mapping)->local_addr, local_addr, sizeof((*port_mapping)->local_addr)); } } if(external_addr != NULL) { strncpy((*port_mapping)->external_addr, external_addr, sizeof((*port_mapping)->external_addr)); } // Add (if not already done) the binding if(now - (*port_mapping)->last_update >= retry_delay) { (*port_mapping)->last_update = now; linphone_upnp_context_send_add_port_binding(lupnp, *port_mapping, FALSE); } } else { if(*port_mapping != NULL) { linphone_upnp_context_send_remove_port_binding(lupnp, *port_mapping, FALSE); *port_mapping = NULL; } } } void linphone_upnp_update_config(UpnpContext* lupnp) { char key[64]; const MSList *item; UpnpPortBinding *port_mapping; /* Add configs */ for(item = lupnp->adding_configs;item!=NULL;item=item->next) { port_mapping = (UpnpPortBinding *)item->data; snprintf(key, sizeof(key), "%s-%s-%d-%d", port_mapping->device_id, (port_mapping->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP", port_mapping->external_port, port_mapping->local_port); lp_config_set_string(lupnp->lc->config, UPNP_SECTION_NAME, key, "uPnP"); linphone_upnp_port_binding_log(ORTP_DEBUG, "Configuration: Added port binding", port_mapping); } ms_list_for_each(lupnp->adding_configs,(void (*)(void*))linphone_upnp_port_binding_release); lupnp->adding_configs = ms_list_free(lupnp->adding_configs); /* Remove configs */ for(item = lupnp->removing_configs;item!=NULL;item=item->next) { port_mapping = (UpnpPortBinding *)item->data; snprintf(key, sizeof(key), "%s-%s-%d-%d", port_mapping->device_id, (port_mapping->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP", port_mapping->external_port, port_mapping->local_port); lp_config_set_string(lupnp->lc->config, UPNP_SECTION_NAME, key, NULL); linphone_upnp_port_binding_log(ORTP_DEBUG, "Configuration: Removed port binding", port_mapping); } ms_list_for_each(lupnp->removing_configs,(void (*)(void*))linphone_upnp_port_binding_release); lupnp->removing_configs = ms_list_free(lupnp->removing_configs); } void linphone_upnp_update_proxy(UpnpContext* lupnp, bool_t force) { LinphoneUpnpState ready_state; const MSList *item; time_t now = (force)? (lupnp->last_ready_check + UPNP_CORE_READY_CHECK) : time(NULL); /* Refresh registers if we are ready */ if(now - lupnp->last_ready_check >= UPNP_CORE_READY_CHECK) { lupnp->last_ready_check = now; ready_state = (_linphone_upnp_context_is_ready_for_register(lupnp))? LinphoneUpnpStateOk: LinphoneUpnpStateKo; if(ready_state != lupnp->last_ready_state) { for(item=linphone_core_get_proxy_config_list(lupnp->lc);item!=NULL;item=item->next) { LinphoneProxyConfig *cfg=(LinphoneProxyConfig*)item->data; if (linphone_proxy_config_register_enabled(cfg)) { if (ready_state != LinphoneUpnpStateOk) { // Only reset ithe registration if we require that upnp should be ok if(lupnp->lc->sip_conf.register_only_when_upnp_is_ok) { linphone_proxy_config_set_state(cfg, LinphoneRegistrationNone, "Registration impossible (uPnP not ready)"); } else { cfg->commit=TRUE; } } else { cfg->commit=TRUE; } } } lupnp->last_ready_state = ready_state; } } } bool_t linphone_core_upnp_hook(void *data) { LCSipTransports transport; UpnpContext *lupnp = (UpnpContext *)data; ms_mutex_lock(&lupnp->mutex); /* Update ports */ if(lupnp->state == LinphoneUpnpStateOk) { linphone_core_get_sip_transports(lupnp->lc, &transport); linphone_upnp_update_port_binding(lupnp, &lupnp->sip_udp, UPNP_IGD_IP_PROTOCOL_UDP, transport.udp_port, UPNP_CORE_RETRY_DELAY); linphone_upnp_update_port_binding(lupnp, &lupnp->sip_tcp, UPNP_IGD_IP_PROTOCOL_TCP, transport.tcp_port, UPNP_CORE_RETRY_DELAY); linphone_upnp_update_port_binding(lupnp, &lupnp->sip_tls, UPNP_IGD_IP_PROTOCOL_TCP, transport.tls_port, UPNP_CORE_RETRY_DELAY); } linphone_upnp_update_proxy(lupnp, FALSE); linphone_upnp_update_config(lupnp); ms_mutex_unlock(&lupnp->mutex); return TRUE; } int linphone_core_update_local_media_description_from_upnp(SalMediaDescription *desc, UpnpSession *session) { int i; SalStreamDescription *stream; UpnpStream *upnpStream; for (i = 0; i < desc->nb_streams; i++) { stream = &desc->streams[i]; if (!sal_stream_description_active(stream)) continue; upnpStream = NULL; if(stream->type == SalAudio) { upnpStream = session->audio; } else if(stream->type == SalVideo) { upnpStream = session->video; } if(upnpStream != NULL) { if(upnpStream->rtp != NULL && upnpStream->rtp->state == LinphoneUpnpStateOk) { strncpy(stream->rtp_addr, upnpStream->rtp->external_addr, LINPHONE_IPADDR_SIZE); stream->rtp_port = upnpStream->rtp->external_port; } if(upnpStream->rtcp != NULL && upnpStream->rtcp->state == LinphoneUpnpStateOk) { strncpy(stream->rtcp_addr, upnpStream->rtcp->external_addr, LINPHONE_IPADDR_SIZE); stream->rtcp_port = upnpStream->rtcp->external_port; } } } return 0; } /* * uPnP Port Binding */ UpnpPortBinding *linphone_upnp_port_binding_new() { UpnpPortBinding *port = NULL; port = ms_new0(UpnpPortBinding,1); ms_mutex_init(&port->mutex, NULL); port->state = LinphoneUpnpStateIdle; port->protocol = UPNP_IGD_IP_PROTOCOL_UDP; port->device_id = NULL; port->local_addr[0] = '\0'; port->local_port = -1; port->external_addr[0] = '\0'; port->external_port = -1; port->to_remove = FALSE; port->to_add = FALSE; port->ref = 1; port->last_update = 0; return port; } UpnpPortBinding *linphone_upnp_port_binding_new_with_parameters(upnp_igd_ip_protocol protocol, int local_port, int external_port) { UpnpPortBinding *port_binding = linphone_upnp_port_binding_new(); port_binding->protocol = protocol; port_binding->local_port = local_port; port_binding->external_port = external_port; return port_binding; } UpnpPortBinding *linphone_upnp_port_binding_new_or_collect(MSList *list, upnp_igd_ip_protocol protocol, int local_port, int external_port) { UpnpPortBinding *tmp_binding; UpnpPortBinding *end_binding; // Seek an binding with same protocol and local port end_binding = linphone_upnp_port_binding_new_with_parameters(protocol, local_port, -1); tmp_binding = linphone_upnp_port_binding_equivalent_in_list(list, end_binding); // Must be not attached to any struct if(tmp_binding != NULL && tmp_binding->ref == 1) { linphone_upnp_port_binding_release(end_binding); end_binding = linphone_upnp_port_binding_retain(tmp_binding); } else { end_binding->external_port = external_port; } return end_binding; } UpnpPortBinding *linphone_upnp_port_binding_copy(const UpnpPortBinding *port) { UpnpPortBinding *new_port = NULL; new_port = ms_new0(UpnpPortBinding,1); memcpy(new_port, port, sizeof(UpnpPortBinding)); new_port->device_id = NULL; linphone_upnp_port_binding_set_device_id(new_port, port->device_id); ms_mutex_init(&new_port->mutex, NULL); new_port->ref = 1; return new_port; } void linphone_upnp_port_binding_set_device_id(UpnpPortBinding *port, const char *device_id) { char *formated_device_id = linphone_upnp_format_device_id(device_id); if(formated_device_id != NULL && port->device_id != NULL) { if(strcmp(formated_device_id, port->device_id) == 0) { ms_free(formated_device_id); return; } } if(port->device_id != NULL) { ms_free(port->device_id); } port->device_id = formated_device_id; } void linphone_upnp_port_binding_log(int level, const char *msg, const UpnpPortBinding *port) { if(strlen(port->local_addr)) { ortp_log(level, "uPnP IGD: %s %s|%d->%s:%d (retry %d)", msg, (port->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP", port->external_port, port->local_addr, port->local_port, port->retry - 1); } else { ortp_log(level, "uPnP IGD: %s %s|%d->%d (retry %d)", msg, (port->protocol == UPNP_IGD_IP_PROTOCOL_TCP)? "TCP":"UDP", port->external_port, port->local_port, port->retry - 1); } } // Return true if the binding are equivalent. (Note external_port == -1 means "don't care") bool_t linphone_upnp_port_binding_equal(const UpnpPortBinding *port1, const UpnpPortBinding *port2) { return port1->protocol == port2->protocol && port1->local_port == port2->local_port && (port1->external_port == -1 || port2->external_port == -1 || port1->external_port == port2->external_port); } UpnpPortBinding *linphone_upnp_port_binding_equivalent_in_list(MSList *list, const UpnpPortBinding *port) { UpnpPortBinding *port_mapping; while(list != NULL) { port_mapping = (UpnpPortBinding *)list->data; if(linphone_upnp_port_binding_equal(port, port_mapping)) { return port_mapping; } list = list->next; } return NULL; } UpnpPortBinding *linphone_upnp_port_binding_retain(UpnpPortBinding *port) { ms_mutex_lock(&port->mutex); port->ref++; ms_mutex_unlock(&port->mutex); return port; } void linphone_upnp_port_binding_release(UpnpPortBinding *port) { ms_mutex_lock(&port->mutex); if(--port->ref == 0) { if(port->device_id != NULL) { ms_free(port->device_id); } ms_mutex_unlock(&port->mutex); ms_mutex_destroy(&port->mutex); ms_free(port); return; } ms_mutex_unlock(&port->mutex); } /* * uPnP Stream */ UpnpStream* linphone_upnp_stream_new() { UpnpStream *stream = ms_new0(UpnpStream,1); stream->state = LinphoneUpnpStateIdle; stream->rtp = NULL; stream->rtcp = NULL; return stream; } void linphone_upnp_stream_destroy(UpnpStream* stream) { if(stream->rtp != NULL) { linphone_upnp_port_binding_release(stream->rtp); stream->rtp = NULL; } if(stream->rtcp != NULL) { linphone_upnp_port_binding_release(stream->rtcp); stream->rtcp = NULL; } ms_free(stream); } /* * uPnP Session */ UpnpSession* linphone_upnp_session_new(LinphoneCall* call) { UpnpSession *session = ms_new0(UpnpSession,1); session->call = call; session->state = LinphoneUpnpStateIdle; session->audio = linphone_upnp_stream_new(); session->video = linphone_upnp_stream_new(); return session; } void linphone_upnp_session_destroy(UpnpSession *session) { LinphoneCore *lc = session->call->core; if(lc->upnp != NULL) { /* Remove bindings */ if(session->audio->rtp != NULL) { linphone_upnp_context_send_remove_port_binding(lc->upnp, session->audio->rtp, TRUE); } if(session->audio->rtcp != NULL) { linphone_upnp_context_send_remove_port_binding(lc->upnp, session->audio->rtcp, TRUE); } if(session->video->rtp != NULL) { linphone_upnp_context_send_remove_port_binding(lc->upnp, session->video->rtp, TRUE); } if(session->video->rtcp != NULL) { linphone_upnp_context_send_remove_port_binding(lc->upnp, session->video->rtcp, TRUE); } } session->call->stats[LINPHONE_CALL_STATS_AUDIO].upnp_state = LinphoneUpnpStateKo; session->call->stats[LINPHONE_CALL_STATS_VIDEO].upnp_state = LinphoneUpnpStateKo; linphone_upnp_stream_destroy(session->audio); linphone_upnp_stream_destroy(session->video); ms_free(session); } LinphoneUpnpState linphone_upnp_session_get_state(UpnpSession *session) { return session->state; } /* * uPnP Config */ struct linphone_upnp_config_list_port_bindings_struct { struct _LpConfig *lpc; MSList *retList; const char *device_id; }; static void linphone_upnp_config_list_port_bindings_cb(const char *entry, struct linphone_upnp_config_list_port_bindings_struct *cookie) { char device_id[UPNP_UUID_LEN + 1]; char protocol_str[4]; // TCP or UDP upnp_igd_ip_protocol protocol; int external_port; int local_port; int ret; bool_t valid = TRUE; UpnpPortBinding *port; ret = sscanf(entry, "%"UPNP_UUID_LEN_STR"[^-]-%3s-%i-%i", device_id, protocol_str, &external_port, &local_port); if(ret == 4) { // Handle only wanted device bindings if(device_id != NULL && strcmp(cookie->device_id, device_id) != 0) { return; } if(linphone_upnp_strncmpi(protocol_str, "TCP", 3) == 0) { protocol = UPNP_IGD_IP_PROTOCOL_TCP; } else if(linphone_upnp_strncmpi(protocol_str, "UDP", 3) == 0) { protocol = UPNP_IGD_IP_PROTOCOL_UDP; } else { valid = FALSE; } if(valid) { port = linphone_upnp_port_binding_new(); linphone_upnp_port_binding_set_device_id(port, device_id); port->state = LinphoneUpnpStateOk; port->protocol = protocol; port->external_port = external_port; port->local_port = local_port; cookie->retList = ms_list_append(cookie->retList, port); } } else { valid = FALSE; } if(!valid) { ms_warning("uPnP configuration invalid line: %s", entry); } } MSList *linphone_upnp_config_list_port_bindings(struct _LpConfig *lpc, const char *device_id) { char *formated_device_id = linphone_upnp_format_device_id(device_id); struct linphone_upnp_config_list_port_bindings_struct cookie = {lpc, NULL, formated_device_id}; lp_config_for_each_entry(lpc, UPNP_SECTION_NAME, (void(*)(const char *, void*))linphone_upnp_config_list_port_bindings_cb, &cookie); ms_free(formated_device_id); return cookie.retList; } void linphone_upnp_config_add_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port) { MSList *list; UpnpPortBinding *list_port; if(port->device_id == NULL) { ms_error("Can't remove port binding without device_id"); return; } list = lupnp->removing_configs; while(list != NULL) { list_port = (UpnpPortBinding *)list->data; if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) { lupnp->removing_configs = ms_list_remove(lupnp->removing_configs, list_port); linphone_upnp_port_binding_release(list_port); return; } list = ms_list_next(list); } list = lupnp->adding_configs; while(list != NULL) { list_port = (UpnpPortBinding *)list->data; if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) { return; } list = ms_list_next(list); } list_port = linphone_upnp_port_binding_copy(port); lupnp->adding_configs = ms_list_append(lupnp->adding_configs, list_port); } void linphone_upnp_config_remove_port_binding(UpnpContext *lupnp, const UpnpPortBinding *port) { MSList *list; UpnpPortBinding *list_port; if(port->device_id == NULL) { ms_error("Can't remove port binding without device_id"); return; } list = lupnp->adding_configs; while(list != NULL) { list_port = (UpnpPortBinding *)list->data; if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) { lupnp->adding_configs = ms_list_remove(lupnp->adding_configs, list_port); linphone_upnp_port_binding_release(list_port); return; } list = ms_list_next(list); } list = lupnp->removing_configs; while(list != NULL) { list_port = (UpnpPortBinding *)list->data; if(linphone_upnp_port_binding_equal(list_port, port) == TRUE) { return; } list = ms_list_next(list); } list_port = linphone_upnp_port_binding_copy(port); lupnp->removing_configs = ms_list_append(lupnp->removing_configs, list_port); }