/* $Id$ */ /* * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com) * Copyright (C) 2003-2008 Benny Prijono * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** * sipecho.c * * - Accepts incoming calls and echoes back SDP and any media. * - Specify URI in cmdline argument to make call * - Accepts registration too! */ /* Include all headers. */ #include #include #include #include #include /* For logging purpose. */ #define THIS_FILE "sipecho.c" #include "util.h" /* Settings */ #define MAX_CALLS 8 typedef struct call_t { pjsip_inv_session *inv; } call_t; static struct app_t { pj_caching_pool cp; pj_pool_t *pool; pjsip_endpoint *sip_endpt; //pjmedia_endpt *med_endpt; call_t call[MAX_CALLS]; pj_bool_t quit; pj_thread_t *worker_thread; pj_bool_t enable_msg_logging; } app; /* * Prototypes: */ static void call_on_media_update(pjsip_inv_session *inv, pj_status_t status); static void call_on_state_changed(pjsip_inv_session *inv, pjsip_event *e); static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer); static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e); static pj_bool_t on_rx_request( pjsip_rx_data *rdata ); /* Globals */ static int sip_af; static int sip_port = 5060; static pj_bool_t sip_tcp; /* This is a PJSIP module to be registered by application to handle * incoming requests outside any dialogs/transactions. The main purpose * here is to handle incoming INVITE request message, where we will * create a dialog and INVITE session for it. */ static pjsip_module mod_sipecho = { NULL, NULL, /* prev, next. */ { "mod-sipecho", 11 }, /* Name. */ -1, /* Id */ PJSIP_MOD_PRIORITY_APPLICATION, /* Priority */ NULL, /* load() */ NULL, /* start() */ NULL, /* stop() */ NULL, /* unload() */ &on_rx_request, /* on_rx_request() */ NULL, /* on_rx_response() */ NULL, /* on_tx_request. */ NULL, /* on_tx_response() */ NULL, /* on_tsx_state() */ }; /* Notification on incoming messages */ static pj_bool_t logging_on_rx_msg(pjsip_rx_data *rdata) { if (!app.enable_msg_logging) return PJ_FALSE; PJ_LOG(3,(THIS_FILE, "RX %d bytes %s from %s %s:%d:\n" "%.*s\n" "--end msg--", rdata->msg_info.len, pjsip_rx_data_get_info(rdata), rdata->tp_info.transport->type_name, rdata->pkt_info.src_name, rdata->pkt_info.src_port, (int)rdata->msg_info.len, rdata->msg_info.msg_buf)); return PJ_FALSE; } /* Notification on outgoing messages */ static pj_status_t logging_on_tx_msg(pjsip_tx_data *tdata) { if (!app.enable_msg_logging) return PJ_SUCCESS; PJ_LOG(3,(THIS_FILE, "TX %d bytes %s to %s %s:%d:\n" "%.*s\n" "--end msg--", (tdata->buf.cur - tdata->buf.start), pjsip_tx_data_get_info(tdata), tdata->tp_info.transport->type_name, tdata->tp_info.dst_name, tdata->tp_info.dst_port, (int)(tdata->buf.cur - tdata->buf.start), tdata->buf.start)); return PJ_SUCCESS; } /* The module instance. */ static pjsip_module msg_logger = { NULL, NULL, /* prev, next. */ { "mod-msg-log", 13 }, /* Name. */ -1, /* Id */ PJSIP_MOD_PRIORITY_TRANSPORT_LAYER-1,/* Priority */ NULL, /* load() */ NULL, /* start() */ NULL, /* stop() */ NULL, /* unload() */ &logging_on_rx_msg, /* on_rx_request() */ &logging_on_rx_msg, /* on_rx_response() */ &logging_on_tx_msg, /* on_tx_request. */ &logging_on_tx_msg, /* on_tx_response() */ NULL, /* on_tsx_state() */ }; static int worker_proc(void *arg) { PJ_UNUSED_ARG(arg); while (!app.quit) { pj_time_val interval = { 0, 20 }; pjsip_endpt_handle_events(app.sip_endpt, &interval); } return 0; } static void hangup_all(void) { unsigned i; for (i=0; iinv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED) { pj_status_t status; pjsip_tx_data *tdata; status = pjsip_inv_end_session(call->inv, PJSIP_SC_BUSY_HERE, NULL, &tdata); if (status==PJ_SUCCESS && tdata) pjsip_inv_send_msg(call->inv, tdata); } } } static void destroy_stack(void) { enum { WAIT_CLEAR = 5000, WAIT_INTERVAL = 500 }; unsigned i; PJ_LOG(3,(THIS_FILE, "Shutting down..")); /* Wait until all clear */ hangup_all(); for (i=0; iinv && call->inv->state <= PJSIP_INV_STATE_CONFIRMED) break; } if (j==MAX_CALLS) return; pj_thread_sleep(WAIT_INTERVAL); } app.quit = PJ_TRUE; if (app.worker_thread) { pj_thread_join(app.worker_thread); app.worker_thread = NULL; } //if (app.med_endpt) //pjmedia_endpt_destroy(app.med_endpt); if (app.sip_endpt) pjsip_endpt_destroy(app.sip_endpt); if (app.pool) pj_pool_release(app.pool); dump_pool_usage(THIS_FILE, &app.cp); pj_caching_pool_destroy(&app.cp); } #define CHECK_STATUS() do { if (status != PJ_SUCCESS) return status; } while (0) static pj_status_t init_stack() { pj_sockaddr addr; pjsip_inv_callback inv_cb; pj_status_t status; pj_log_set_level(3); status = pjlib_util_init(); CHECK_STATUS(); pj_caching_pool_init(&app.cp, NULL, 0); app.pool = pj_pool_create( &app.cp.factory, "sipecho", 512, 512, 0); status = pjsip_endpt_create(&app.cp.factory, NULL, &app.sip_endpt); CHECK_STATUS(); pj_log_set_level(4); pj_sockaddr_init((pj_uint16_t)sip_af, &addr, NULL, (pj_uint16_t)sip_port); if (sip_af == pj_AF_INET()) { if (sip_tcp) { status = pjsip_tcp_transport_start( app.sip_endpt, &addr.ipv4, 1, NULL); } else { status = pjsip_udp_transport_start( app.sip_endpt, &addr.ipv4, NULL, 1, NULL); } } else if (sip_af == pj_AF_INET6()) { status = pjsip_udp_transport_start6(app.sip_endpt, &addr.ipv6, NULL, 1, NULL); } else { status = PJ_EAFNOTSUP; } pj_log_set_level(3); CHECK_STATUS(); status = pjsip_tsx_layer_init_module(app.sip_endpt) || pjsip_ua_init_module( app.sip_endpt, NULL ); CHECK_STATUS(); pj_bzero(&inv_cb, sizeof(inv_cb)); inv_cb.on_state_changed = &call_on_state_changed; inv_cb.on_new_session = &call_on_forked; inv_cb.on_media_update = &call_on_media_update; inv_cb.on_rx_offer = &call_on_rx_offer; status = pjsip_inv_usage_init(app.sip_endpt, &inv_cb) || pjsip_100rel_init_module(app.sip_endpt) || pjsip_endpt_register_module( app.sip_endpt, &mod_sipecho) || pjsip_endpt_register_module( app.sip_endpt, &msg_logger) || //pjmedia_endpt_create(&app.cp.factory, // pjsip_endpt_get_ioqueue(app.sip_endpt), // 0, &app.med_endpt) || pj_thread_create(app.pool, "sipecho", &worker_proc, NULL, 0, 0, &app.worker_thread); CHECK_STATUS(); return PJ_SUCCESS; } static void destroy_call(call_t *call) { call->inv = NULL; } static pjmedia_sdp_attr * find_remove_sdp_attrs(unsigned *cnt, pjmedia_sdp_attr *attr[], unsigned cnt_attr_to_remove, const char* attr_to_remove[]) { pjmedia_sdp_attr *found_attr = NULL; int i; for (i=0; i<(int)*cnt; ++i) { unsigned j; for (j=0; jname, attr_to_remove[j])==0) { if (!found_attr) found_attr = attr[i]; pj_array_erase(attr, sizeof(attr[0]), *cnt, i); --(*cnt); --i; break; } } } return found_attr; } static pjmedia_sdp_session *create_answer(int call_num, pj_pool_t *pool, const pjmedia_sdp_session *offer) { const char* dir_attrs[] = { "sendrecv", "sendonly", "recvonly", "inactive" }; const char *ice_attrs[] = {"ice-pwd", "ice-ufrag", "candidate"}; pjmedia_sdp_session *answer = pjmedia_sdp_session_clone(pool, offer); pjmedia_sdp_attr *sess_dir_attr = NULL; unsigned mi; PJ_LOG(3,(THIS_FILE, "Call %d: creating answer:", call_num)); answer->name = pj_str("sipecho"); sess_dir_attr = find_remove_sdp_attrs(&answer->attr_count, answer->attr, PJ_ARRAY_SIZE(dir_attrs), dir_attrs); for (mi=0; mimedia_count; ++mi) { pjmedia_sdp_media *m = answer->media[mi]; pjmedia_sdp_attr *m_dir_attr; pjmedia_sdp_attr *dir_attr; const char *our_dir = NULL; pjmedia_sdp_conn *c; /* Match direction */ m_dir_attr = find_remove_sdp_attrs(&m->attr_count, m->attr, PJ_ARRAY_SIZE(dir_attrs), dir_attrs); dir_attr = m_dir_attr ? m_dir_attr : sess_dir_attr; if (dir_attr) { if (pj_strcmp2(&dir_attr->name, "sendonly")==0) our_dir = "recvonly"; else if (pj_strcmp2(&dir_attr->name, "inactive")==0) our_dir = "inactive"; else if (pj_strcmp2(&dir_attr->name, "recvonly")==0) our_dir = "inactive"; if (our_dir) { dir_attr = PJ_POOL_ZALLOC_T(pool, pjmedia_sdp_attr); dir_attr->name = pj_str((char*)our_dir); m->attr[m->attr_count++] = dir_attr; } } /* Remove ICE attributes */ find_remove_sdp_attrs(&m->attr_count, m->attr, PJ_ARRAY_SIZE(ice_attrs), ice_attrs); /* Done */ c = m->conn ? m->conn : answer->conn; PJ_LOG(3,(THIS_FILE, " Media %d, %.*s: %s <--> %.*s:%d", mi, (int)m->desc.media.slen, m->desc.media.ptr, (our_dir ? our_dir : "sendrecv"), (int)c->addr.slen, c->addr.ptr, m->desc.port)); } return answer; } static void call_on_state_changed( pjsip_inv_session *inv, pjsip_event *e) { call_t *call = (call_t*)inv->mod_data[mod_sipecho.id]; if (!call) return; PJ_UNUSED_ARG(e); if (inv->state == PJSIP_INV_STATE_DISCONNECTED) { PJ_LOG(3,(THIS_FILE, "Call %d: DISCONNECTED [reason=%d (%s)]", call - app.call, inv->cause, pjsip_get_status_text(inv->cause)->ptr)); destroy_call(call); } else { PJ_LOG(3,(THIS_FILE, "Call %d: state changed to %s", call - app.call, pjsip_inv_state_name(inv->state))); } } static void call_on_rx_offer(pjsip_inv_session *inv, const pjmedia_sdp_session *offer) { call_t *call = (call_t*) inv->mod_data[mod_sipecho.id]; pjsip_inv_set_sdp_answer(inv, create_answer((int)(call - app.call), inv->pool_prov, offer)); } static void call_on_forked(pjsip_inv_session *inv, pjsip_event *e) { PJ_UNUSED_ARG(inv); PJ_UNUSED_ARG(e); } static pj_bool_t on_rx_request( pjsip_rx_data *rdata ) { pj_sockaddr hostaddr; char temp[80], hostip[PJ_INET6_ADDRSTRLEN]; pj_str_t local_uri; pjsip_dialog *dlg; pjsip_rdata_sdp_info *sdp_info; pjmedia_sdp_session *answer = NULL; pjsip_tx_data *tdata = NULL; call_t *call = NULL; unsigned i; pj_status_t status; PJ_LOG(3,(THIS_FILE, "RX %.*s from %s", (int)rdata->msg_info.msg->line.req.method.name.slen, rdata->msg_info.msg->line.req.method.name.ptr, rdata->pkt_info.src_name)); if (rdata->msg_info.msg->line.req.method.id == PJSIP_REGISTER_METHOD) { /* Let me be a registrar! */ pjsip_hdr hdr_list, *h; pjsip_msg *msg; int expires = -1; pj_list_init(&hdr_list); msg = rdata->msg_info.msg; h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_EXPIRES, NULL); if (h) { expires = ((pjsip_expires_hdr*)h)->ivalue; pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h)); PJ_LOG(3,(THIS_FILE, " Expires=%d", expires)); } if (expires != 0) { h = (pjsip_hdr*)pjsip_msg_find_hdr(msg, PJSIP_H_CONTACT, NULL); if (h) pj_list_push_back(&hdr_list, pjsip_hdr_clone(rdata->tp_info.pool, h)); } pjsip_endpt_respond(app.sip_endpt, &mod_sipecho, rdata, 200, NULL, &hdr_list, NULL, NULL); return PJ_TRUE; } if (rdata->msg_info.msg->line.req.method.id != PJSIP_INVITE_METHOD) { if (rdata->msg_info.msg->line.req.method.id != PJSIP_ACK_METHOD) { pj_str_t reason = pj_str("Go away"); pjsip_endpt_respond_stateless( app.sip_endpt, rdata, 400, &reason, NULL, NULL); } return PJ_TRUE; } sdp_info = pjsip_rdata_get_sdp_info(rdata); if (!sdp_info || !sdp_info->sdp) { pj_str_t reason = pj_str("Require valid offer"); pjsip_endpt_respond_stateless( app.sip_endpt, rdata, 400, &reason, NULL, NULL); } for (i=0; i", hostip, sip_port); local_uri = pj_str(temp); status = pjsip_dlg_create_uas( pjsip_ua_instance(), rdata, &local_uri, &dlg); if (status == PJ_SUCCESS) answer = create_answer((int)(call-app.call), dlg->pool, sdp_info->sdp); if (status == PJ_SUCCESS) status = pjsip_inv_create_uas( dlg, rdata, answer, 0, &call->inv); if (status == PJ_SUCCESS) status = pjsip_inv_initial_answer(call->inv, rdata, 100, NULL, NULL, &tdata); if (status == PJ_SUCCESS) status = pjsip_inv_send_msg(call->inv, tdata); if (status == PJ_SUCCESS) status = pjsip_inv_answer(call->inv, 180, NULL, NULL, &tdata); if (status == PJ_SUCCESS) status = pjsip_inv_send_msg(call->inv, tdata); if (status == PJ_SUCCESS) status = pjsip_inv_answer(call->inv, 200, NULL, NULL, &tdata); if (status == PJ_SUCCESS) status = pjsip_inv_send_msg(call->inv, tdata); if (status != PJ_SUCCESS) { pjsip_endpt_respond_stateless( app.sip_endpt, rdata, 500, NULL, NULL, NULL); destroy_call(call); } else { call->inv->mod_data[mod_sipecho.id] = call; } return PJ_TRUE; } static void call_on_media_update( pjsip_inv_session *inv, pj_status_t status) { PJ_UNUSED_ARG(inv); PJ_UNUSED_ARG(status); } static void usage() { printf("\nUsage: sipecho OPTIONS\n"); printf("\n"); printf("where OPTIONS:\n"); printf(" --local-port, -p PORT Bind to port PORT.\n"); printf(" --tcp, -t Listen to TCP instead.\n"); printf(" --ipv6, -6 Use IPv6 instead.\n"); printf(" --help, -h Show this help page.\n"); } /* main() * * If called with argument, treat argument as SIP URL to be called. * Otherwise wait for incoming calls. */ int main(int argc, char *argv[]) { struct pj_getopt_option long_options[] = { { "local-port", 1, 0, 'p' }, { "tcp", 0, 0, 't' }, { "ipv6", 0, 0, '6' }, { "help", 0, 0, 'h' } }; int c, option_index; pj_log_set_level(5); pj_init(); sip_af = pj_AF_INET(); pj_optind = 0; while ((c = pj_getopt_long(argc, argv, "p:t6h", long_options, &option_index)) != -1) { switch (c) { case 'p': sip_port = atoi(pj_optarg); break; case 't': sip_tcp = PJ_TRUE; break; case 'h': usage(); return 0; case '6': sip_af = pj_AF_INET6(); break; default: PJ_LOG(1,(THIS_FILE, "Argument \"%s\" is not valid. Use --help to see help", argv[pj_optind-1])); return -1; } } if (init_stack()) goto on_error; /* If URL is specified, then make call immediately. */ if (pj_optind != argc) { pj_sockaddr hostaddr; char hostip[PJ_INET6_ADDRSTRLEN+2]; char temp[80]; call_t *call; pj_str_t dst_uri = pj_str(argv[pj_optind]); pj_str_t local_uri; pjsip_dialog *dlg; pj_status_t status; pjsip_tx_data *tdata; if (pj_gethostip(sip_af, &hostaddr) != PJ_SUCCESS) { PJ_LOG(1,(THIS_FILE, "Unable to retrieve local host IP")); goto on_error; } pj_sockaddr_print(&hostaddr, hostip, sizeof(hostip), 2); pj_ansi_sprintf(temp, "", hostip, sip_port); local_uri = pj_str(temp); call = &app.call[0]; status = pjsip_dlg_create_uac( pjsip_ua_instance(), &local_uri, /* local URI */ &local_uri, /* local Contact */ &dst_uri, /* remote URI */ &dst_uri, /* remote target */ &dlg); /* dialog */ if (status != PJ_SUCCESS) { app_perror(THIS_FILE, "Unable to create UAC dialog", status); return 1; } status = pjsip_inv_create_uac( dlg, NULL, 0, &call->inv); if (status != PJ_SUCCESS) goto on_error; call->inv->mod_data[mod_sipecho.id] = call; status = pjsip_inv_invite(call->inv, &tdata); if (status != PJ_SUCCESS) goto on_error; status = pjsip_inv_send_msg(call->inv, tdata); if (status != PJ_SUCCESS) goto on_error; puts("Press ENTER to quit..."); } else { puts("Ready for incoming calls. Press ENTER to quit..."); } for (;;) { char s[10]; printf("\nMenu:\n" " h Hangup all calls\n" " l %s message logging\n" " q Quit\n", (app.enable_msg_logging? "Disable" : "Enable")); if (fgets(s, sizeof(s), stdin) == NULL) continue; if (s[0]=='q') break; switch (s[0]) { case 'l': app.enable_msg_logging = !app.enable_msg_logging; break; case 'h': hangup_all(); break; } } destroy_stack(); puts("Bye bye.."); return 0; on_error: puts("An error has occurred. run a debugger.."); return 1; }