From 1c156b82d16bf1fa2013cafe900f3081415aab9a Mon Sep 17 00:00:00 2001 From: Matteo Brancaleoni <mbrancaleoni@espia.it> Date: Sun, 16 Feb 2003 06:00:12 +0000 Subject: [PATCH] Sun Feb 16 07:00:01 CET 2003 git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@616 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- CHANGES | 6 + Makefile | 2 +- apps/Makefile | 3 +- apps/app_authenticate.c | 154 ++++++++++++++++++++++++++ channels/chan_mgcp.c | 61 +++++++++- channels/chan_sip.c | 227 +++++++++++++++++++++++++++++++------- configs/sip.conf.sample | 2 + frame.c | 33 ++++++ include/asterisk/rtp.h | 15 +++ rtp.c | 204 +++++++++++++++++++++++++++++++++- sounds.txt | 2 + sounds/auth-incorrect.gsm | Bin 0 -> 7524 bytes 12 files changed, 660 insertions(+), 49 deletions(-) create mode 100755 apps/app_authenticate.c create mode 100755 sounds/auth-incorrect.gsm diff --git a/CHANGES b/CHANGES index 55cd0d7d57..d190dd4bf6 100755 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,9 @@ + -- Make HOLD on SIP make use of asterisk MOH + -- Add supervised transfer (tested with Pingtel only) + -- Allow maxexpirey and defaultexpirey to be runtime configurable for SIP + -- Preliminary codec 13 support (RFC3389) + -- Add app_authenticate for general purpose authentication + -- Optimize RTP and smoother -- Create special variable "EXTEN-n" where it is extension stripped by n MSD -- Fix uninitialized frame pointer in channel.c -- Add global variables support under [globals] of extensions.conf diff --git a/Makefile b/Makefile index 1d2bb9993c..d62c6a0793 100755 --- a/Makefile +++ b/Makefile @@ -144,7 +144,7 @@ datafiles: all exit 1; \ fi; \ done - for x in sounds/vm-* sounds/transfer* sounds/pbx-* sounds/ss-* sounds/beep* sounds/dir-* sounds/conf-* sounds/agent-* sounds/invalid* sounds/tt-*; do \ + for x in sounds/vm-* sounds/transfer* sounds/pbx-* sounds/ss-* sounds/beep* sounds/dir-* sounds/conf-* sounds/agent-* sounds/invalid* sounds/tt-* sounds/auth-*; do \ if grep -q "^%`basename $$x`%" sounds.txt; then \ install $$x $(ASTVARLIBDIR)/sounds ; \ else \ diff --git a/apps/Makefile b/apps/Makefile index 95f7275319..1d3ad29ad4 100755 --- a/apps/Makefile +++ b/apps/Makefile @@ -17,7 +17,8 @@ APPS=app_dial.so app_playback.so app_voicemail.so app_directory.so app_intercom. app_agi.so app_qcall.so app_adsiprog.so app_getcpeid.so app_milliwatt.so \ app_zapateller.so app_datetime.so app_setcallerid.so app_festival.so \ app_queue.so app_senddtmf.so app_parkandannounce.so app_striplsd.so \ - app_setcidname.so app_lookupcidname.so app_substring.so app_macro.so + app_setcidname.so app_lookupcidname.so app_substring.so app_macro.so \ + app_authenticate.so #APPS+=app_sql_postgres.so #APPS+=app_sql_odbc.so diff --git a/apps/app_authenticate.c b/apps/app_authenticate.c new file mode 100755 index 0000000000..69d2443511 --- /dev/null +++ b/apps/app_authenticate.c @@ -0,0 +1,154 @@ +/* + * Asterisk -- A telephony toolkit for Linux. + * + * Execute arbitrary authenticate commands + * + * Copyright (C) 1999, Mark Spencer + * + * Mark Spencer <markster@linux-support.net> + * + * This program is free software, distributed under the terms of + * the GNU General Public License + */ + +#include <asterisk/lock.h> +#include <asterisk/file.h> +#include <asterisk/logger.h> +#include <asterisk/channel.h> +#include <asterisk/pbx.h> +#include <asterisk/module.h> +#include <asterisk/app.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <stdlib.h> + +#include <pthread.h> + + +static char *tdesc = "Authentication Application"; + +static char *app = "Authenticate"; + +static char *synopsis = "Authenticate a user"; + +static char *descrip = +" Authenticate(password[|options]): Requires a user to enter a" +"given password in order to continue execution. If the\n" +"password begins with the '/' character, it is interpreted as\n" +"a file which contains a list of valid passwords (1 per line).\n" +"an optional set of opions may be provided by concatenating any\n" +"of the following letters:\n" +" a - Set account code to the password that is entered\n" +"\n" +"Returns 0 if the user enters a valid password within three\n" +"tries, or -1 otherwise (or on hangup).\n"; + +STANDARD_LOCAL_USER; + +LOCAL_USER_DECL; + +static int auth_exec(struct ast_channel *chan, void *data) +{ + int res=0; + int retries; + struct localuser *u; + char password[256]=""; + char passwd[256]; + char *opts; + char *prompt; + if (!data || !strlen(data)) { + ast_log(LOG_WARNING, "Authenticate requires an argument(password)\n"); + return -1; + } + LOCAL_USER_ADD(u); + if (chan->_state != AST_STATE_UP) { + res = ast_answer(chan); + if (res) { + LOCAL_USER_REMOVE(u); + return -1; + } + } + strncpy(password, data, sizeof(password) - 1); + opts=strchr(password, '|'); + if (opts) { + *opts = 0; + opts++; + } else + opts = ""; + /* Start asking for password */ + prompt = "agent-pass"; + for (retries = 0; retries < 3; retries++) { + res = ast_app_getdata(chan, prompt, passwd, sizeof(passwd) - 2, 0); + if (res < 0) + break; + res = 0; + if (password[0] == '/') { + /* Compare against a file */ + char tmp[80]; + FILE *f; + f = fopen(password, "r"); + if (f) { + char buf[256] = ""; + while(!feof(f)) { + fgets(buf, sizeof(buf), f); + if (!feof(f) && strlen(buf)) { + buf[strlen(buf) - 1] = '\0'; + if (strlen(buf) && !strcmp(passwd, buf)) + break; + } + } + fclose(f); + if (strlen(buf) && !strcmp(passwd, buf)) + break; + } else + ast_log(LOG_WARNING, "Unable to open file '%s' for authentication: %s\n", password, strerror(errno)); + } else { + /* Compare against a fixed password */ + if (!strcmp(passwd, password)) + break; + } + prompt="auth-incorrect"; + } + if ((retries < 3) && !res) { + if (strchr(opts, 'a')) + ast_cdr_setaccount(chan, passwd); + } else { + if (!res) + res = ast_streamfile(chan, "vm-goodbye", chan->language); + if (!res) + res = ast_waitstream(chan, ""); + res = -1; + } + LOCAL_USER_REMOVE(u); + return res; +} + +int unload_module(void) +{ + STANDARD_HANGUP_LOCALUSERS; + return ast_unregister_application(app); +} + +int load_module(void) +{ + return ast_register_application(app, auth_exec, synopsis, descrip); +} + +char *description(void) +{ + return tdesc; +} + +int usecount(void) +{ + int res; + STANDARD_USECOUNT(res); + return res; +} + +char *key() +{ + return ASTERISK_GPL_KEY; +} diff --git a/channels/chan_mgcp.c b/channels/chan_mgcp.c index 88a0d2cf01..08ef81499b 100755 --- a/channels/chan_mgcp.c +++ b/channels/chan_mgcp.c @@ -131,6 +131,7 @@ struct mgcp_endpoint { int outgoing; struct ast_channel *owner; struct ast_rtp *rtp; + struct sockaddr_in tmpdest; struct mgcp_endpoint *next; struct mgcp_gateway *parent; }; @@ -317,6 +318,7 @@ static int mgcp_hangup(struct ast_channel *ast) p->owner = NULL; if (strlen(p->cxident)) transmit_connection_del(p); + strcpy(p->cxident, ""); if (!p->alreadygone && (!p->outgoing || (ast->_state == AST_STATE_UP))) transmit_notify_request(p, "ro", 1); else @@ -324,8 +326,9 @@ static int mgcp_hangup(struct ast_channel *ast) ast->pvt->pvt = NULL; p->alreadygone = 0; p->outgoing = 0; - strcpy(p->cxident, ""); strcpy(p->callid, ""); + /* Reset temporary destination */ + memset(&p->tmpdest, 0, sizeof(p->tmpdest)); if (p->rtp) { ast_rtp_destroy(p->rtp); p->rtp = NULL; @@ -515,6 +518,7 @@ static struct ast_channel *mgcp_new(struct mgcp_endpoint *i, int state) tmp->pvt->indicate = mgcp_indicate; tmp->pvt->fixup = mgcp_fixup; tmp->pvt->send_digit = mgcp_senddigit; + tmp->pvt->bridge = ast_rtp_bridge; if (strlen(i->language)) strncpy(tmp->language, i->language, sizeof(tmp->language)-1); i->owner = tmp; @@ -951,8 +955,15 @@ static int add_sdp(struct mgcp_request *resp, struct mgcp_endpoint *p, struct as if (rtp) { ast_rtp_get_peer(rtp, &dest); } else { - dest.sin_addr = p->parent->ourip; - dest.sin_port = sin.sin_port; + if (p->tmpdest.sin_addr.s_addr) { + dest.sin_addr = p->tmpdest.sin_addr; + dest.sin_port = p->tmpdest.sin_port; + /* Reset temporary destination */ + memset(&p->tmpdest, 0, sizeof(p->tmpdest)); + } else { + dest.sin_addr = p->parent->ourip; + dest.sin_port = sin.sin_port; + } } printf("We're at %s port %d\n", inet_ntoa(p->parent->ourip), ntohs(sin.sin_port)); snprintf(v, sizeof(v), "v=0\r\n"); @@ -991,6 +1002,12 @@ static int transmit_modify_with_sdp(struct mgcp_endpoint *p, struct ast_rtp *rtp char local[256]; char tmp[80]; int x; + if (!strlen(p->cxident) && rtp) { + /* We don't have a CXident yet, store the destination and + wait a bit */ + ast_rtp_get_peer(rtp, &p->tmpdest); + return 0; + } snprintf(local, sizeof(local), "p:20"); for (x=1;x<= AST_FORMAT_MAX_AUDIO; x <<= 1) { if (p->capability & x) { @@ -1003,6 +1020,7 @@ static int transmit_modify_with_sdp(struct mgcp_endpoint *p, struct ast_rtp *rtp add_header(&resp, "L", local); add_header(&resp, "M", "sendrecv"); add_header(&resp, "X", p->txident); + add_header(&resp, "I", p->cxident); add_header(&resp, "S", ""); add_sdp(&resp, p, rtp); p->lastout = oseq; @@ -1278,8 +1296,14 @@ static int mgcpsock_read(int *id, int fd, short events, void *ignore) p = find_endpoint(NULL, ident, &sin); if (p) { handle_response(p, result, ident); - if ((c = get_header(&req, "I"))) - strncpy(p->cxident, c, sizeof(p->cxident) - 1); + if ((c = get_header(&req, "I"))) { + if (strlen(c)) { + strncpy(p->cxident, c, sizeof(p->cxident) - 1); + if (p->tmpdest.sin_addr.s_addr) { + transmit_modify_with_sdp(p, NULL); + } + } + } if (req.lines) process_sdp(p, &req); } @@ -1483,6 +1507,31 @@ struct mgcp_gateway *build_gateway(char *cat, struct ast_variable *v) return gw; } +static struct ast_rtp *mgcp_get_rtp_peer(struct ast_channel *chan) +{ + struct mgcp_endpoint *p; + p = chan->pvt->pvt; + if (p && p->rtp) + return p->rtp; + return NULL; +} + +static int mgcp_set_rtp_peer(struct ast_channel *chan, struct ast_rtp *rtp) +{ + struct mgcp_endpoint *p; + p = chan->pvt->pvt; + if (p) { + transmit_modify_with_sdp(p, rtp); + return 0; + } + return -1; +} + +static struct ast_rtp_protocol mgcp_rtp = { + get_rtp_info: mgcp_get_rtp_peer, + set_rtp_peer: mgcp_set_rtp_peer, +}; + int load_module() { struct ast_config *cfg; @@ -1590,6 +1639,8 @@ int load_module() ast_destroy(cfg); return -1; } + mgcp_rtp.type = type; + ast_rtp_proto_register(&mgcp_rtp); ast_cli_register(&cli_show_endpoints); /* And start the monitor for the first time */ restart_monitor(); diff --git a/channels/chan_sip.c b/channels/chan_sip.c index b9f235c3cc..a057eb89ef 100755 --- a/channels/chan_sip.c +++ b/channels/chan_sip.c @@ -32,6 +32,7 @@ #include <asterisk/cli.h> #include <asterisk/md5.h> #include <asterisk/app.h> +#include <asterisk/musiconhold.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> @@ -47,8 +48,12 @@ /* #define VOCAL_DATA_HACK */ #define SIPDUMPER -#define DEFAULT_EXPIREY 120 -#define MAX_EXPIREY 3600 +#define DEFAULT_DEFAULT_EXPIREY 120 +#define DEFAULT_MAX_EXPIREY 3600 + +static int max_expirey = DEFAULT_MAX_EXPIREY; +static int default_expirey = DEFAULT_DEFAULT_EXPIREY; + #define DEFAULT_MAXMS 2000 /* Must be faster than 2 seconds by default */ #define DEFAULT_MAXMS 2000 /* Must be faster than 2 seconds by default */ @@ -142,6 +147,7 @@ static struct sip_pvt { char refer_to[AST_MAX_EXTENSION]; /* Place to store REFER-TO extension */ char referred_by[AST_MAX_EXTENSION];/* Place to store REFERRED-BY extension */ char refer_contact[AST_MAX_EXTENSION];/* Place to store Contact info from a REFER extension */ + struct sip_pvt *refer_call; /* Call we are referring */ char record_route[256]; char record_route_info[256]; char remote_party_id[256]; @@ -728,6 +734,7 @@ static int sip_indicate(struct ast_channel *ast, int condition) } +#if 0 static int sip_bridge(struct ast_channel *c0, struct ast_channel *c1, int flags, struct ast_frame **fo, struct ast_channel **rc) { struct sip_pvt *p0, *p1; @@ -742,6 +749,7 @@ static int sip_bridge(struct ast_channel *c0, struct ast_channel *c1, int flags, ast_pthread_mutex_lock(&c1->lock); p0 = c0->pvt->pvt; p1 = c1->pvt->pvt; + ast_log(LOG_DEBUG, "Reinvite? %s: %s, %s: %s\n", c0->name, p0->canreinvite ? "yes" : "no", c1->name, p1->canreinvite ? "yes" : "no"); if (!p0->canreinvite || !p1->canreinvite) { /* Not gonna support reinvite */ ast_pthread_mutex_unlock(&c0->lock); @@ -796,6 +804,7 @@ static int sip_bridge(struct ast_channel *c0, struct ast_channel *c1, int flags, } return -1; } +#endif static struct ast_channel *sip_new(struct sip_pvt *i, int state, char *title) { @@ -808,7 +817,7 @@ static struct ast_channel *sip_new(struct sip_pvt *i, int state, char *title) tmp->nativeformats = capability; fmt = ast_best_codec(tmp->nativeformats); if (title) - snprintf(tmp->name, sizeof(tmp->name), "SIP/%s", title); + snprintf(tmp->name, sizeof(tmp->name), "SIP/%s-%04x", title, rand() & 0xffff); else snprintf(tmp->name, sizeof(tmp->name), "SIP/%s:%d", inet_ntoa(i->sa.sin_addr), ntohs(i->sa.sin_port)); tmp->type = type; @@ -830,7 +839,7 @@ static struct ast_channel *sip_new(struct sip_pvt *i, int state, char *title) tmp->pvt->indicate = sip_indicate; tmp->pvt->fixup = sip_fixup; tmp->pvt->send_digit = sip_senddigit; - tmp->pvt->bridge = sip_bridge; + tmp->pvt->bridge = ast_rtp_bridge; if (strlen(i->language)) strncpy(tmp->language, i->language, sizeof(tmp->language)-1); i->owner = tmp; @@ -1087,7 +1096,7 @@ static int sip_register(char *value, int lineno) if (secret) strncpy(reg->secret, secret, sizeof(reg->secret)-1); reg->expire = -1; - reg->refresh = DEFAULT_EXPIREY; + reg->refresh = default_expirey; reg->addr.sin_family = AF_INET; memcpy(®->addr.sin_addr, hp->h_addr, sizeof(®->addr.sin_addr)); reg->addr.sin_port = porta ? htons(atoi(porta)) : htons(DEFAULT_SIP_PORT); @@ -1237,11 +1246,21 @@ static int process_sdp(struct sip_pvt *p, struct sip_request *req) ast_log(LOG_WARNING, "No compatible codecs!\n"); return -1; } - if (p->owner && !(p->owner->nativeformats & p->capability)) { - ast_log(LOG_DEBUG, "Oooh, we need to change our formats since our peer supports only %d and not %d\n", p->capability, p->owner->nativeformats); - p->owner->nativeformats = p->capability; - ast_set_read_format(p->owner, p->owner->readformat); - ast_set_write_format(p->owner, p->owner->writeformat); + if (p->owner) { + if (p->owner->nativeformats & p->capability) { + ast_log(LOG_DEBUG, "Oooh, we need to change our formats since our peer supports only %d and not %d\n", p->capability, p->owner->nativeformats); + p->owner->nativeformats = p->capability; + ast_set_read_format(p->owner, p->owner->readformat); + ast_set_write_format(p->owner, p->owner->writeformat); + } + if (p->owner->bridge) { + /* Turn on/off music on hold if we are holding/unholding */ + if (sin.sin_addr.s_addr) { + ast_moh_stop(p->owner->bridge); + } else { + ast_moh_start(p->owner->bridge, NULL); + } + } } return 0; @@ -1830,7 +1849,7 @@ static int transmit_register(struct sip_registry *r, char *cmd, char *auth) if (auth) add_header(&req, "Authorization", auth); - snprintf(tmp, sizeof(tmp), "%d", DEFAULT_EXPIREY); + snprintf(tmp, sizeof(tmp), "%d", default_expirey); add_header(&req, "Expires", tmp); add_header(&req, "Event", "registration"); copy_request(&p->initreq, &req); @@ -1933,8 +1952,8 @@ static int parse_contact(struct sip_pvt *pvt, struct sip_peer *p, struct sip_req strcpy(p->username, ""); if (p->expire > -1) ast_sched_del(sched, p->expire); - if ((expirey < 1) || (expirey > MAX_EXPIREY)) - expirey = DEFAULT_EXPIREY; + if ((expirey < 1) || (expirey > max_expirey)) + expirey = max_expirey; p->expire = ast_sched_add(sched, expirey * 1000, expire_register, p); pvt->expirey = expirey; if (memcmp(&p->addr, &oldsin, sizeof(oldsin))) { @@ -2130,7 +2149,9 @@ static int get_refer_info(struct sip_pvt *p, struct sip_request *oreq) char tmp2[256] = "", *c2, *a2; char tmp3[256]; char tmp4[256]; + char tmp5[256] = ""; /* CallID to replace */ struct sip_request *req; + struct sip_pvt *p2; req = oreq; if (!req) @@ -2151,32 +2172,81 @@ static int get_refer_info(struct sip_pvt *p, struct sip_request *oreq) } c += 4; c2 += 4; - if ((a = strchr(c, '@')) || (a = strchr(c, ';'))) { + if ((a = strchr(c, '?'))) { + /* Search for arguemnts */ *a = '\0'; + a++; + if (!strncasecmp(a, "REPLACES=", strlen("REPLACES="))) { + strncpy(tmp5, a + strlen("REPLACES="), sizeof(tmp5) - 1); + if ((a = strchr(tmp5, '%'))) { + /* Yuck! Pingtel converts the '@' to a %40, icky icky! Convert + back to an '@' */ + if ((a[1] == '4') && (a[2] == '0')) { + *a = '@'; + memmove(a + 1, a+3, strlen(a + 3)); + } + } + if ((a = strchr(tmp5, '%'))) + *a = '\0'; + } } - if ((a2 = strchr(c2, '@')) || (a2 = strchr(c2, ';'))) { + + if ((a = strchr(c, '@'))) + *a = '\0'; + if ((a = strchr(c, ';'))) + *a = '\0'; + + + if ((a2 = strchr(c2, '@'))) *a2 = '\0'; - } + + if ((a2 = strchr(c2, ';'))) + *a2 = '\0'; + if (sipdebug) ast_verbose("Looking for %s in %s\n", c, p->context); ast_verbose("Looking for %s in %s\n", c2, p->context); - - if (ast_exists_extension(NULL, p->context, c, 1, NULL) && ast_exists_extension(NULL, p->context, c2, 1, NULL)) { - if (!oreq) - ast_log(LOG_DEBUG,"Something is wrong with this line.\n"); //This line is ignored for some reason.... - ast_log(LOG_DEBUG,"Assigning Extension %s to REFER-TO\n", c); - ast_log(LOG_DEBUG,"Assigning Extension %s to REFERRED-BY\n", c2); - ast_log(LOG_DEBUG,"Assigning Contact Info %s to REFER_CONTACT\n", tmp3); - ast_log(LOG_DEBUG,"Assigning Remote-Party-ID Info %s to REMOTE_PARTY_ID\n",tmp4); - strncpy(p->refer_to, c, sizeof(p->refer_to) - 1); - strncpy(p->referred_by, c2, sizeof(p->referred_by) - 1); - strncpy(p->refer_contact, tmp3, sizeof(p->refer_contact) - 1); - strncpy(p->remote_party_id, tmp4, sizeof(p->remote_party_id) - 1); + + if (strlen(tmp5)) { + /* This is a supervised transfer */ + ast_log(LOG_DEBUG,"Assigning Replace-Call-ID Info %s to REPLACE_CALL_ID\n",tmp5); + + strncpy(p->refer_to, "", sizeof(p->refer_to) - 1); + strncpy(p->referred_by, "", sizeof(p->referred_by) - 1); + strncpy(p->refer_contact, "", sizeof(p->refer_contact) - 1); + strncpy(p->remote_party_id, "", sizeof(p->remote_party_id) - 1); + p->refer_call = NULL; + ast_pthread_mutex_lock(&iflock); + /* Search interfaces and find the match */ + p2 = iflist; + while(p2) { + if (!strcmp(p2->callid, tmp5)) { + /* Go ahead and lock it before returning */ + ast_pthread_mutex_lock(&p2->lock); + p->refer_call = p2; + break; + } + p2 = p2->next; + } + ast_pthread_mutex_unlock(&iflock); + if (p->refer_call) return 0; - } - - if (ast_canmatch_extension(NULL, p->context, c, 1, NULL)) { + else + ast_log(LOG_NOTICE, "Supervised transfer requested, but unable to find callid '%s'\n", tmp5); + } else if (ast_exists_extension(NULL, p->context, c, 1, NULL) && ast_exists_extension(NULL, p->context, c2, 1, NULL)) { + /* This is an unsupervised transfer */ + ast_log(LOG_DEBUG,"Assigning Extension %s to REFER-TO\n", c); + ast_log(LOG_DEBUG,"Assigning Extension %s to REFERRED-BY\n", c2); + ast_log(LOG_DEBUG,"Assigning Contact Info %s to REFER_CONTACT\n", tmp3); + ast_log(LOG_DEBUG,"Assigning Remote-Party-ID Info %s to REMOTE_PARTY_ID\n",tmp4); + strncpy(p->refer_to, c, sizeof(p->refer_to) - 1); + strncpy(p->referred_by, c2, sizeof(p->referred_by) - 1); + strncpy(p->refer_contact, tmp3, sizeof(p->refer_contact) - 1); + strncpy(p->remote_party_id, tmp4, sizeof(p->remote_party_id) - 1); + p->refer_call = NULL; + return 0; + } else if (ast_canmatch_extension(NULL, p->context, c, 1, NULL)) { return 1; } @@ -2735,7 +2805,7 @@ retrylock: if (r->expire != -1) ast_sched_del(sched, r->expire); expires=atoi(get_header(req, "expires")); - if (!expires) expires=DEFAULT_EXPIREY; + if (!expires) expires=default_expirey; r->expire=ast_sched_add(sched, (expires-2)*1000, sip_reregister, r); } @@ -2879,6 +2949,37 @@ static int determine_firstline_parts( struct sip_request *req ) { return 1; } +static int attempt_transfer(struct sip_pvt *p1, struct sip_pvt *p2) +{ + if (!p1->owner || !p2->owner) { + ast_log(LOG_WARNING, "Transfer attempted without dual ownership?\n"); + return -1; + } + if (p1->owner->bridge) { + if (p2->owner->bridge) + ast_moh_stop(p2->owner->bridge); + ast_moh_stop(p1->owner->bridge); + ast_moh_stop(p1->owner); + ast_moh_stop(p2->owner); + if (ast_channel_masquerade(p2->owner, p1->owner->bridge)) { + ast_log(LOG_WARNING, "Failed to masquerade %s into %s\n", p2->owner->name, p1->owner->bridge->name); + return -1; + } + } else if (p2->owner->bridge) { + ast_moh_stop(p2->owner->bridge); + ast_moh_stop(p2->owner); + ast_moh_stop(p1->owner); + if (ast_channel_masquerade(p1->owner, p2->owner->bridge)) { + ast_log(LOG_WARNING, "Failed to masquerade %s into %s\n", p1->owner->name, p2->owner->bridge->name); + return -1; + } + } else { + ast_log(LOG_NOTICE, "Transfer attempted with no bridged calls to transfer\n"); + return -1; + } + return 0; +} + static int handle_request(struct sip_pvt *p, struct sip_request *req, struct sockaddr_in *sin) { struct sip_request resp; @@ -3048,16 +3149,23 @@ static int handle_request(struct sip_pvt *p, struct sip_request *req, struct soc transmit_response_with_allow(p, "404 Not Found", req); else if (res > 0) transmit_response_with_allow(p, "484 Address Incomplete", req); - else + else { transmit_response(p, "202 Accepted", req); - ast_log(LOG_DEBUG,"202 Accepted\n"); - c = p->owner; - if (c) { - transfer_to = c->bridge; - if (transfer_to) - ast_async_goto(transfer_to,"", p->refer_to,1, 1); + if (p->refer_call) { + ast_log(LOG_DEBUG,"202 Accepted (supervised)\n"); + attempt_transfer(p, p->refer_call); + ast_pthread_mutex_unlock(&p->refer_call->lock); + p->refer_call = NULL; + } else { + ast_log(LOG_DEBUG,"202 Accepted (blind)\n"); + c = p->owner; + if (c) { + transfer_to = c->bridge; + if (transfer_to) + ast_async_goto(transfer_to,"", p->refer_to,1, 1); + } + } } - } else if (!strcasecmp(cmd, "CANCEL") || !strcasecmp(cmd, "BYE")) { copy_request(&p->initreq, req); p->alreadygone = 1; @@ -3140,7 +3248,7 @@ static int sipsock_read(int *id, int fd, short events, void *ignore) /* Must have at least two headers */ return 1; } - /* Process request, with iflock held */ + /* Process request, with netlock held */ ast_pthread_mutex_lock(&netlock); p = find_call(&req, &sin); if (p) { @@ -3495,6 +3603,8 @@ static struct sip_peer *build_peer(char *name, struct ast_variable *v) peer->expirey = expirey; } peer->capability = capability; + /* Assume can reinvite */ + peer->canreinvite = 1; while(v) { if (!strcasecmp(v->name, "secret")) strncpy(peer->secret, v->value, sizeof(peer->secret)-1); @@ -3619,6 +3729,14 @@ static int reload_config() strncpy(context, v->value, sizeof(context)-1); } else if (!strcasecmp(v->name, "language")) { strncpy(language, v->value, sizeof(language)-1); + } else if (!strcasecmp(v->name, "maxexpirey")) { + max_expirey = atoi(v->value); + if (max_expirey < 1) + max_expirey = DEFAULT_MAX_EXPIREY; + } else if (!strcasecmp(v->name, "defaultexpirey")) { + default_expirey = atoi(v->value); + if (default_expirey < 1) + default_expirey = DEFAULT_DEFAULT_EXPIREY; } else if (!strcasecmp(v->name, "bindaddr")) { if (!(hp = gethostbyname(v->value))) { ast_log(LOG_WARNING, "Invalid address: %s\n", v->value); @@ -3743,6 +3861,31 @@ static int reload_config() return 0; } +static struct ast_rtp *sip_get_rtp_peer(struct ast_channel *chan) +{ + struct sip_pvt *p; + p = chan->pvt->pvt; + if (p && p->rtp && p->canreinvite) + return p->rtp; + return NULL; +} + +static int sip_set_rtp_peer(struct ast_channel *chan, struct ast_rtp *rtp) +{ + struct sip_pvt *p; + p = chan->pvt->pvt; + if (p) { + transmit_reinvite_with_sdp(p, rtp); + return 0; + } + return -1; +} + +static struct ast_rtp_protocol sip_rtp = { + get_rtp_info: sip_get_rtp_peer, + set_rtp_peer: sip_set_rtp_peer, +}; + int load_module() { int res; @@ -3761,6 +3904,8 @@ int load_module() ast_cli_register(&cli_show_registry); ast_cli_register(&cli_debug); ast_cli_register(&cli_no_debug); + sip_rtp.type = type; + ast_rtp_proto_register(&sip_rtp); sched = sched_context_create(); if (!sched) { ast_log(LOG_WARNING, "Unable to create schedule context\n"); diff --git a/configs/sip.conf.sample b/configs/sip.conf.sample index ed0ed6f931..dad7c15010 100755 --- a/configs/sip.conf.sample +++ b/configs/sip.conf.sample @@ -7,6 +7,8 @@ bindaddr = 0.0.0.0 ; Address to bind to context = default ; Default for incoming calls ;tos=lowdelay ;tos=184 +;maxexpirey=3600 ; Max length of incoming registration we allow +;defaultexpirey=120 ; Default length of incoming/outoing registration ;[snomsip] ;type=friend diff --git a/frame.c b/frame.c index e37e816b3b..fb255d83b6 100755 --- a/frame.c +++ b/frame.c @@ -35,10 +35,12 @@ struct ast_smoother { int size; int format; int readdata; + int optimizablestream; float samplesperbyte; struct ast_frame f; char data[SMOOTHER_SIZE]; char framedata[SMOOTHER_SIZE + AST_FRIENDLY_OFFSET]; + struct ast_frame *opt; int len; }; @@ -76,6 +78,28 @@ int ast_smoother_feed(struct ast_smoother *s, struct ast_frame *f) ast_log(LOG_WARNING, "Out of smoother space\n"); return -1; } + if ((f->datalen == s->size) && !s->opt) { + if (!s->len) { + /* Optimize by sending the frame we just got + on the next read, thus eliminating the douple + copy */ + s->opt = f; + return 0; + } else { + s->optimizablestream++; + if (s->optimizablestream > 10) { + /* For the past 10 rounds, we have input and output + frames of the correct size for this smoother, yet + we were unable to optimize because there was still + some cruft left over. Lets just drop the cruft so + we can move to a fully optimized path */ + s->len = 0; + s->opt = f; + return 0; + } + } + } else + s->optimizablestream = 0; memcpy(s->data + s->len, f->data, f->datalen); s->len += f->datalen; return 0; @@ -83,6 +107,15 @@ int ast_smoother_feed(struct ast_smoother *s, struct ast_frame *f) struct ast_frame *ast_smoother_read(struct ast_smoother *s) { + struct ast_frame *opt; + + /* IF we have an optimization frame, send it */ + if (s->opt) { + opt = s->opt; + s->opt = NULL; + return opt; + } + /* Make sure we have enough data */ if (s->len < s->size) { return NULL; diff --git a/include/asterisk/rtp.h b/include/asterisk/rtp.h index 8137eb38d1..30639a5cd3 100755 --- a/include/asterisk/rtp.h +++ b/include/asterisk/rtp.h @@ -17,6 +17,7 @@ #include <asterisk/frame.h> #include <asterisk/io.h> #include <asterisk/sched.h> +#include <asterisk/channel.h> #include <netinet/in.h> @@ -24,6 +25,14 @@ extern "C" { #endif +struct ast_rtp_protocol { + struct ast_rtp *(*get_rtp_info)(struct ast_channel *chan); /* Get RTP struct, or NULL if unwilling to transfer */ + int (*set_rtp_peer)(struct ast_channel *chan, struct ast_rtp *peer); /* Set RTP peer */ + int (*get_rtp_willing)(struct ast_channel *chan); /* Willing to native bridge */ + char *type; + struct ast_rtp_protocol *next; +}; + struct ast_rtp; typedef int (*ast_rtp_callback)(struct ast_rtp *rtp, struct ast_frame *f, void *data); @@ -58,6 +67,12 @@ int rtp2ast(int id); char *ast2rtpn(int id); +int ast_rtp_bridge(struct ast_channel *c0, struct ast_channel *c1, int flags, struct ast_frame **fo, struct ast_channel **rc); + +int ast_rtp_proto_register(struct ast_rtp_protocol *proto); + +void ast_rtp_proto_unregister(struct ast_rtp_protocol *proto); + #if defined(__cplusplus) || defined(c_plusplus) } #endif diff --git a/rtp.c b/rtp.c index 21bee69801..ca5d7a1f2e 100755 --- a/rtp.c +++ b/rtp.c @@ -30,6 +30,7 @@ #include <asterisk/logger.h> #include <asterisk/options.h> #include <asterisk/channel.h> +#include <asterisk/channel_pvt.h> #define TYPE_SILENCE 0x2 #define TYPE_HIGH 0x0 @@ -47,6 +48,7 @@ struct ast_rtp { unsigned int lastts; unsigned int lastrxts; int lasttxformat; + int lastrxformat; int dtmfcount; struct sockaddr_in us; struct sockaddr_in them; @@ -61,6 +63,8 @@ struct ast_rtp { ast_rtp_callback callback; }; +static struct ast_rtp_protocol *protos = NULL; + int ast_rtp_fd(struct ast_rtp *rtp) { return rtp->s; @@ -151,6 +155,49 @@ static struct ast_frame *process_rfc2833(struct ast_rtp *rtp, unsigned char *dat return f; } +static struct ast_frame *process_rfc3389(struct ast_rtp *rtp, unsigned char *data, int len) +{ + struct ast_frame *f = NULL; + /* Convert comfort noise into audio with various codecs. Unfortunately this doesn't + totally help us out becuase we don't have an engine to keep it going and we are not + guaranteed to have it every 20ms or anything */ +#if 0 + printf("RFC3389: %d bytes, format is %d\n", len, rtp->lastrxformat); +#endif + ast_log(LOG_NOTICE, "RFC3389 support incomplete. Turn off on client if possible\n"); + if (!rtp->lastrxformat) + return NULL; + switch(rtp->lastrxformat) { + case AST_FORMAT_ULAW: + rtp->f.frametype = AST_FRAME_VOICE; + rtp->f.subclass = AST_FORMAT_ULAW; + rtp->f.datalen = 160; + rtp->f.samples = 160; + memset(rtp->f.data, 0x7f, rtp->f.datalen); + f = &rtp->f; + break; + case AST_FORMAT_ALAW: + rtp->f.frametype = AST_FRAME_VOICE; + rtp->f.subclass = AST_FORMAT_ALAW; + rtp->f.datalen = 160; + rtp->f.samples = 160; + memset(rtp->f.data, 0x7e, rtp->f.datalen); /* XXX Is this right? XXX */ + f = &rtp->f; + break; + case AST_FORMAT_SLINEAR: + rtp->f.frametype = AST_FRAME_VOICE; + rtp->f.subclass = AST_FORMAT_SLINEAR; + rtp->f.datalen = 320; + rtp->f.samples = 160; + memset(rtp->f.data, 0x00, rtp->f.datalen); + f = &rtp->f; + break; + default: + ast_log(LOG_NOTICE, "Don't know how to handle RFC3389 for receive codec %d\n", rtp->lastrxformat); + } + return f; +} + static struct ast_frame *process_type121(struct ast_rtp *rtp, unsigned char *data, int len) { char resp = 0; @@ -247,6 +294,8 @@ struct ast_frame *ast_rtp_read(struct ast_rtp *rtp) } else if (payloadtype == 100) { /* CISCO's notso proprietary DTMF bridge */ f = process_rfc2833(rtp, rtp->rawdata + AST_FRIENDLY_OFFSET + hdrlen, res - hdrlen); + } else if (payloadtype == 13) { + f = process_rfc3389(rtp, rtp->rawdata + AST_FRIENDLY_OFFSET + hdrlen, res - hdrlen); } else { ast_log(LOG_NOTICE, "Unknown RTP codec %d received\n", payloadtype); } @@ -254,7 +303,8 @@ struct ast_frame *ast_rtp_read(struct ast_rtp *rtp) return f; else return &null_frame; - } + } else + rtp->lastrxformat = rtp->f.subclass; if (!rtp->lastrxts) rtp->lastrxts = timestamp; @@ -651,3 +701,155 @@ int ast_rtp_write(struct ast_rtp *rtp, struct ast_frame *_f) return 0; } + +void ast_rtp_proto_unregister(struct ast_rtp_protocol *proto) +{ + struct ast_rtp_protocol *cur, *prev; + cur = protos; + prev = NULL; + while(cur) { + if (cur == proto) { + if (prev) + prev->next = proto->next; + else + protos = proto->next; + return; + } + prev = cur; + cur = cur->next; + } +} + +int ast_rtp_proto_register(struct ast_rtp_protocol *proto) +{ + struct ast_rtp_protocol *cur; + cur = protos; + while(cur) { + if (cur->type == proto->type) { + ast_log(LOG_WARNING, "Tried to register same protocol '%s' twice\n", cur->type); + return -1; + } + cur = cur->next; + } + proto->next = protos; + protos = proto; + return 0; +} + +static struct ast_rtp_protocol *get_proto(struct ast_channel *chan) +{ + struct ast_rtp_protocol *cur; + cur = protos; + while(cur) { + if (cur->type == chan->type) { + return cur; + } + cur = cur->next; + } + return NULL; +} + +int ast_rtp_bridge(struct ast_channel *c0, struct ast_channel *c1, int flags, struct ast_frame **fo, struct ast_channel **rc) +{ + struct ast_frame *f; + struct ast_channel *who, *cs[3]; + struct ast_rtp *p0, *p1; + struct ast_rtp_protocol *pr0, *pr1; + void *pvt0, *pvt1; + int to; + + /* XXX Wait a half a second for things to settle up + this really should be fixed XXX */ + ast_autoservice_start(c0); + ast_autoservice_start(c1); + usleep(500000); + ast_autoservice_stop(c0); + ast_autoservice_stop(c1); + + /* if need DTMF, cant native bridge */ + if (flags & (AST_BRIDGE_DTMF_CHANNEL_0 | AST_BRIDGE_DTMF_CHANNEL_1)) + return -2; + ast_pthread_mutex_lock(&c0->lock); + ast_pthread_mutex_lock(&c1->lock); + pr0 = get_proto(c0); + pr1 = get_proto(c1); + if (!pr0) { + ast_log(LOG_WARNING, "Can't find native functions for channel '%s'\n", c0->name); + ast_pthread_mutex_unlock(&c0->lock); + ast_pthread_mutex_unlock(&c1->lock); + return -1; + } + if (!pr1) { + ast_log(LOG_WARNING, "Can't find native functions for channel '%s'\n", c1->name); + ast_pthread_mutex_unlock(&c0->lock); + ast_pthread_mutex_unlock(&c1->lock); + return -1; + } + pvt0 = c0->pvt->pvt; + pvt1 = c1->pvt->pvt; + p0 = pr0->get_rtp_info(c0); + p1 = pr1->get_rtp_info(c1); + if (!p0 || !p1) { + /* Somebody doesn't want to play... */ + ast_pthread_mutex_unlock(&c0->lock); + ast_pthread_mutex_unlock(&c1->lock); + return -2; + } + if (pr0->set_rtp_peer(c0, p1)) + ast_log(LOG_WARNING, "Channel '%s' failed to talk to '%s'\n", c0->name, c1->name); + if (pr1->set_rtp_peer(c1, p0)) + ast_log(LOG_WARNING, "Channel '%s' failed to talk back to '%s'\n", c1->name, c0->name); + ast_pthread_mutex_unlock(&c0->lock); + ast_pthread_mutex_unlock(&c1->lock); + cs[0] = c0; + cs[1] = c1; + cs[2] = NULL; + for (;;) { + if ((c0->pvt->pvt != pvt0) || + (c1->pvt->pvt != pvt1) || + (c0->masq || c0->masqr || c1->masq || c1->masqr)) { + ast_log(LOG_DEBUG, "Oooh, something is weird, backing out\n"); + if (c0->pvt->pvt == pvt0) { + if (pr0->set_rtp_peer(c0, NULL)) + ast_log(LOG_WARNING, "Channel '%s' failed to revert\n", c0->name); + } + if (c1->pvt->pvt == pvt1) { + if (pr1->set_rtp_peer(c1, NULL)) + ast_log(LOG_WARNING, "Channel '%s' failed to revert back\n", c1->name); + } + /* Tell it to try again later */ + return -3; + } + to = -1; + who = ast_waitfor_n(cs, 2, &to); + if (!who) { + ast_log(LOG_DEBUG, "Ooh, empty read...\n"); + continue; + } + f = ast_read(who); + if (!f || ((f->frametype == AST_FRAME_DTMF) && + (((who == c0) && (flags & AST_BRIDGE_DTMF_CHANNEL_0)) || + ((who == c1) && (flags & AST_BRIDGE_DTMF_CHANNEL_1))))) { + *fo = f; + *rc = who; + ast_log(LOG_DEBUG, "Oooh, got a %s\n", f ? "digit" : "hangup"); + if ((c0->pvt->pvt == pvt0) && (!c0->_softhangup)) { + if (pr0->set_rtp_peer(c0, NULL)) + ast_log(LOG_WARNING, "Channel '%s' failed to revert\n", c0->name); + } + if ((c1->pvt->pvt == pvt1) && (!c1->_softhangup)) { + if (pr1->set_rtp_peer(c1, NULL)) + ast_log(LOG_WARNING, "Channel '%s' failed to revert back\n", c1->name); + } + /* That's all we needed */ + return 0; + } else + ast_frfree(f); + /* Swap priority not that it's a big deal at this point */ + cs[2] = cs[0]; + cs[0] = cs[1]; + cs[1] = cs[2]; + + } + return -1; +} diff --git a/sounds.txt b/sounds.txt index bb08559bf8..33f57be4ae 100755 --- a/sounds.txt +++ b/sounds.txt @@ -14,6 +14,8 @@ %agent-user.gsm%Agent login. Please enter your agent number followed by the pound key. +%auth-incorrect.gsm%Login incorrect. Please enter your password followed by the pound key. + %beep.gsm%(this is a simple beep tone) %conf-getconfno.gsm%Please enter your conference number followed by the pound key. diff --git a/sounds/auth-incorrect.gsm b/sounds/auth-incorrect.gsm new file mode 100755 index 0000000000000000000000000000000000000000..d8ef6e1562a2af9dc2085cdaf82fe63c18cd8985 GIT binary patch literal 7524 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 DXEID> literal 0 HcmV?d00001 -- GitLab