Some changes to REFER handling in PJSUA-LIB:
 - added callback to report call transfer progress
 - changed the call transfer request callback name in pjsua
 - added "--norefersub" option in pjsua.
 - fixed bug when call transfer is done more than once in
   the same dialog (dialog usage can not be added)

Also removed 7xx status from the SIP status codes.

And added pjsip_parse_status_line() to parse sipfrag.




git-svn-id: https://svn.pjsip.org/repos/pjproject/trunk@780 74dad513-b988-da41-8d7b-12977e46ad98
diff --git a/pjsip-apps/src/pjsua/pjsua_app.c b/pjsip-apps/src/pjsua/pjsua_app.c
index 28bbb42..573494a 100644
--- a/pjsip-apps/src/pjsua/pjsua_app.c
+++ b/pjsip-apps/src/pjsua/pjsua_app.c
@@ -37,6 +37,7 @@
     pjsua_config	    cfg;
     pjsua_logging_config    log_cfg;
     pjsua_media_config	    media_cfg;
+    pj_bool_t		    no_refersub;
     pj_bool_t		    no_tcp;
     pj_bool_t		    no_udp;
     pjsua_transport_config  udp_cfg;
@@ -160,6 +161,7 @@
     puts  ("  --max-calls=N       Maximum number of concurrent calls (default:4, max:255)");
     puts  ("  --thread-cnt=N      Number of worker threads (default:1)");
     puts  ("  --duration=SEC      Set maximum call duration (default:no limit)");
+    puts  ("  --norefersub        Suppress event subscription when transfering calls");
 
     puts  ("");
     puts  ("When URL is specified, pjsua will immediately initiate call to that URL");
@@ -287,6 +289,7 @@
 	   OPT_RX_DROP_PCT, OPT_TX_DROP_PCT, OPT_EC_TAIL,
 	   OPT_NEXT_ACCOUNT, OPT_NEXT_CRED, OPT_MAX_CALLS, 
 	   OPT_DURATION, OPT_NO_TCP, OPT_NO_UDP, OPT_THREAD_CNT,
+	   OPT_NOREFERSUB,
     };
     struct pj_getopt_option long_options[] = {
 	{ "config-file",1, 0, OPT_CONFIG_FILE},
@@ -301,6 +304,7 @@
 	{ "ip-addr",	1, 0, OPT_IP_ADDR},
 	{ "no-tcp",     0, 0, OPT_NO_TCP},
 	{ "no-udp",     0, 0, OPT_NO_UDP},
+	{ "norefersub", 0, 0, OPT_NOREFERSUB},
 	{ "proxy",	1, 0, OPT_PROXY},
 	{ "outbound",	1, 0, OPT_OUTBOUND_PROXY},
 	{ "registrar",	1, 0, OPT_REGISTRAR},
@@ -456,6 +460,10 @@
 	    cfg->no_udp = PJ_TRUE;
 	    break;
 
+	case OPT_NOREFERSUB: /* norefersub */
+	    cfg->no_refersub = PJ_TRUE;
+	    break;
+
 	case OPT_NO_TCP: /* no-tcp */
 	    if (cfg->no_udp) {
 	      PJ_LOG(1,(THIS_FILE,"Error: can not disable both TCP and UDP"));
@@ -947,6 +955,15 @@
 	pj_strcat2(&cfg, line);
     }
 
+    /* No TCP ? */
+    if (config->no_tcp) {
+	pj_strcat2(&cfg, "--no-tcp\n");
+    }
+
+    /* No UDP ? */
+    if (config->no_udp) {
+	pj_strcat2(&cfg, "--no-udp\n");
+    }
 
     /* STUN */
     if (config->udp_cfg.stun_config.stun_port1) {
@@ -1089,6 +1106,12 @@
 	pj_strcat2(&cfg, line);
     }
 
+    /* norefersub ? */
+    if (config->no_refersub) {
+	pj_strcat2(&cfg, "--norefersub\n");
+    }
+
+
     pj_strcat2(&cfg, "\n#\n# Buddies:\n#\n");
 
     /* Add buddies. */
@@ -1473,6 +1496,30 @@
 }
 
 
+/**
+ * Call transfer request status.
+ */
+static void on_call_transfer_status(pjsua_call_id call_id,
+				    int status_code,
+				    const pj_str_t *status_text,
+				    pj_bool_t final,
+				    pj_bool_t *p_cont)
+{
+    PJ_LOG(3,(THIS_FILE, "Call %d: transfer status=%d (%.*s) %s",
+	      call_id, status_code,
+	      (int)status_text->slen, status_text->ptr,
+	      (final ? "[final]" : "")));
+
+    if (status_code/100 == 2) {
+	PJ_LOG(3,(THIS_FILE, 
+	          "Call %d: call transfered successfully, disconnecting call",
+		  call_id));
+	pjsua_call_hangup(call_id, PJSIP_SC_GONE, NULL, NULL);
+	*p_cont = PJ_FALSE;
+    }
+}
+
+
 /*
  * Print buddy list.
  */
@@ -2147,12 +2194,13 @@
 		    continue;
 		}
 
-		/* Add Refer-Sub: false in outgoing REFER request */
 		pjsua_msg_data_init(&msg_data);
-		pjsip_generic_string_hdr_init2(&refer_sub, &STR_REFER_SUB,
-					       &STR_FALSE);
-		pj_list_push_back(&msg_data.hdr_list, &refer_sub);
-
+		if (app_config.no_refersub) {
+		    /* Add Refer-Sub: false in outgoing REFER request */
+		    pjsip_generic_string_hdr_init2(&refer_sub, &STR_REFER_SUB,
+						   &STR_FALSE);
+		    pj_list_push_back(&msg_data.hdr_list, &refer_sub);
+		}
 		if (result.nb_result != NO_NB) {
 		    if (result.nb_result == -1 || result.nb_result == 0)
 			puts("You can't do that with transfer call!");
@@ -2167,9 +2215,6 @@
 		    tmp = pj_str(result.uri_result);
 		    pjsua_call_xfer( current_call, &tmp, &msg_data);
 		}
-
-		/* Hangup call regardless of xfer status */
-		pjsua_call_hangup(current_call, PJSIP_SC_GONE, NULL, NULL);
 	    }
 	    break;
 
@@ -2461,6 +2506,7 @@
     app_config.cfg.cb.on_buddy_state = &on_buddy_state;
     app_config.cfg.cb.on_pager = &on_pager;
     app_config.cfg.cb.on_typing = &on_typing;
+    app_config.cfg.cb.on_call_transfer_status = &on_call_transfer_status;
 
     /* Initialize pjsua */
     status = pjsua_init(&app_config.cfg, &app_config.log_cfg,
diff --git a/pjsip/include/pjsip/sip_msg.h b/pjsip/include/pjsip/sip_msg.h
index b27b270..6ae3308 100644
--- a/pjsip/include/pjsip/sip_msg.h
+++ b/pjsip/include/pjsip/sip_msg.h
@@ -431,9 +431,9 @@
     PJSIP_SC_DOES_NOT_EXIST_ANYWHERE = 604,
     PJSIP_SC_NOT_ACCEPTABLE_ANYWHERE = 606,
 
-    PJSIP_SC_TSX_TIMEOUT = 701,
+    PJSIP_SC_TSX_TIMEOUT = PJSIP_SC_REQUEST_TIMEOUT,
     /*PJSIP_SC_TSX_RESOLVE_ERROR = 702,*/
-    PJSIP_SC_TSX_TRANSPORT_ERROR = 703
+    PJSIP_SC_TSX_TRANSPORT_ERROR = PJSIP_SC_SERVICE_UNAVAILABLE
 
 } pjsip_status_code;
 
diff --git a/pjsip/include/pjsip/sip_parser.h b/pjsip/include/pjsip/sip_parser.h
index d0eb838..72c103a 100644
--- a/pjsip/include/pjsip/sip_parser.h
+++ b/pjsip/include/pjsip/sip_parser.h
@@ -24,7 +24,7 @@
  * @brief SIP Message Parser
  */
 
-#include <pjsip/sip_types.h>
+#include <pjsip/sip_msg.h>
 #include <pjlib-util/scanner.h>
 #include <pj/list.h>
 
@@ -191,6 +191,19 @@
 				     unsigned options);
 
 /**
+ * Parse SIP status line.
+ *
+ * @param buf		Text buffer to parse.
+ * @param size		The size of the buffer.
+ * @param status_line	Structure to receive the parsed elements.
+ *
+ * @return		PJ_SUCCESS if a status line is parsed successfully.
+ */
+PJ_DECL(pj_status_t) pjsip_parse_status_line(char *buf, pj_size_t size,
+					     pjsip_status_line *status_line);
+
+
+/**
  * Parse a packet buffer and build a full SIP message from the packet. This
  * function parses all parts of the message, including request/status line,
  * all headers, and the message body. The message body however is only 
diff --git a/pjsip/include/pjsua-lib/pjsua.h b/pjsip/include/pjsua-lib/pjsua.h
index 2b11df0..39d5caf 100644
--- a/pjsip/include/pjsua-lib/pjsua.h
+++ b/pjsip/include/pjsua-lib/pjsua.h
@@ -309,9 +309,32 @@
      * is not defined, the default behavior is to accept the
      * transfer.
      */
-    void (*on_call_transfered)(pjsua_call_id call_id,
-			       const pj_str_t *dst,
-			       pjsip_status_code *code);
+    void (*on_call_transfer_request)(pjsua_call_id call_id,
+				     const pj_str_t *dst,
+				     pjsip_status_code *code);
+
+    /**
+     * Notify application of the status of previously sent call
+     * transfer request. Application can monitor the status of the
+     * call transfer request, for example to decide whether to 
+     * terminate existing call.
+     *
+     * @param call_id	    Call ID.
+     * @param status_code   Status progress of the transfer request.
+     * @param status_text   Status progress text.
+     * @param final	    If non-zero, no further notification will
+     *			    be reported. The status_code specified in
+     *			    this callback is the final status.
+     * @param p_cont	    Initially will be set to non-zero, application
+     *			    can set this to FALSE if it no longer wants
+     *			    to receie further notification (for example,
+     *			    after it hangs up the call).
+     */
+    void (*on_call_transfer_status)(pjsua_call_id call_id,
+				    int status_code,
+				    const pj_str_t *status_text,
+				    pj_bool_t final,
+				    pj_bool_t *p_cont);
 
     /**
      * Notify application when registration status has changed.
diff --git a/pjsip/src/pjsip/sip_dialog.c b/pjsip/src/pjsip/sip_dialog.c
index aab0670..8c2f287 100644
--- a/pjsip/src/pjsip/sip_dialog.c
+++ b/pjsip/src/pjsip/sip_dialog.c
@@ -793,9 +793,23 @@
      */
     for (index=0; index<dlg->usage_cnt; ++index) {
 	if (dlg->usage[index] == mod) {
-	    pj_assert(!"This module is already registered");
+	    /* Module may be registered more than once in the same dialog.
+	     * For example, when call transfer fails, application may retry
+	     * call transfer on the same dialog.
+	     * So return PJ_SUCCESS here.
+	     */
+	    PJ_LOG(4,(dlg->obj_name, 
+		      "Module %.*s already registered as dialog usage, "
+		      "updating the data %p",
+		      (int)mod->name.slen, mod->name.ptr, mod_data));
+	    dlg->mod_data[mod->id] = mod_data;
+
 	    pjsip_dlg_dec_lock(dlg);
-	    return PJSIP_ETYPEEXISTS;
+	    return PJ_SUCCESS;
+
+	    //pj_assert(!"This module is already registered");
+	    //pjsip_dlg_dec_lock(dlg);
+	    //return PJSIP_ETYPEEXISTS;
 	}
 
 	if (dlg->usage[index]->priority > mod->priority)
diff --git a/pjsip/src/pjsip/sip_parser.c b/pjsip/src/pjsip/sip_parser.c
index b3f13bd..c3f456a 100644
--- a/pjsip/src/pjsip/sip_parser.c
+++ b/pjsip/src/pjsip/sip_parser.c
@@ -1471,6 +1471,36 @@
     pj_scan_get_newline( scanner );
 }
 
+
+/*
+ * Public API to parse SIP status line.
+ */
+PJ_DEF(pj_status_t) pjsip_parse_status_line( char *buf, pj_size_t size,
+					     pjsip_status_line *status_line)
+{
+    pj_scanner scanner;
+    PJ_USE_EXCEPTION;
+
+    pj_bzero(status_line, sizeof(*status_line));
+    pj_scan_init(&scanner, buf, size, 0, &on_syntax_error);
+
+    PJ_TRY {
+	int_parse_status_line(&scanner, status_line);
+    } 
+    PJ_CATCH_ANY {
+	/* Tolerate the error if it is caused only by missing newline */
+	if (status_line->code == 0 && status_line->reason.slen == 0) {
+	    pj_scan_fini(&scanner);
+	    return PJSIP_EINVALIDMSG;
+	}
+    }
+    PJ_END;
+
+    pj_scan_fini(&scanner);
+    return PJ_SUCCESS;
+}
+
+
 /* Parse ending of header. */
 static void parse_hdr_end( pj_scanner *scanner )
 {
diff --git a/pjsip/src/pjsua-lib/pjsua_call.c b/pjsip/src/pjsua-lib/pjsua_call.c
index d3b88a7..36e5f73 100644
--- a/pjsip/src/pjsua-lib/pjsua_call.c
+++ b/pjsip/src/pjsua-lib/pjsua_call.c
@@ -71,7 +71,19 @@
  * Callback called by event framework when the xfer subscription state
  * has changed.
  */
-static void xfer_on_evsub_state( pjsip_evsub *sub, pjsip_event *event);
+static void xfer_client_on_evsub_state( pjsip_evsub *sub, pjsip_event *event);
+static void xfer_server_on_evsub_state( pjsip_evsub *sub, pjsip_event *event);
+
+/*
+ * Callback called by event framework when NOTIFY is received for outgoing
+ * REFER subscription.
+ */
+static void xfer_on_rx_notify(pjsip_evsub *sub, 
+			      pjsip_rx_data *rdata,
+			      int *p_st_code,
+			      pj_str_t **p_st_text,
+			      pjsip_hdr *res_hdr,
+			      pjsip_msg_body **p_body);
 
 /*
  * Reset call descriptor.
@@ -1078,7 +1090,7 @@
    
     /* Create xfer client subscription. */
     pj_bzero(&xfer_cb, sizeof(xfer_cb));
-    xfer_cb.on_evsub_state = &xfer_on_evsub_state;
+    xfer_cb.on_evsub_state = &xfer_client_on_evsub_state;
 
     status = pjsip_xfer_create_uac(call->inv->dlg, &xfer_cb, &sub);
     if (status != PJ_SUCCESS) {
@@ -1087,6 +1099,9 @@
 	return status;
     }
 
+    /* Associate this call with the client subscription */
+    pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, call);
+
     /*
      * Create REFER request.
      */
@@ -2144,7 +2159,163 @@
  * Callback called by event framework when the xfer subscription state
  * has changed.
  */
-static void xfer_on_evsub_state( pjsip_evsub *sub, pjsip_event *event)
+static void xfer_client_on_evsub_state( pjsip_evsub *sub, pjsip_event *event)
+{
+    
+    PJ_UNUSED_ARG(event);
+
+    /*
+     * When subscription is accepted (got 200/OK to REFER), check if 
+     * subscription suppressed.
+     */
+    if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_ACCEPTED) {
+
+	pjsip_rx_data *rdata;
+	pjsip_generic_string_hdr *refer_sub;
+	const pj_str_t REFER_SUB = { "Refer-Sub", 9 };
+	pjsua_call *call;
+
+	call = pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+
+	/* Must be receipt of response message */
+	pj_assert(event->type == PJSIP_EVENT_TSX_STATE && 
+		  event->body.tsx_state.type == PJSIP_EVENT_RX_MSG);
+	rdata = event->body.tsx_state.src.rdata;
+
+	/* Find Refer-Sub header */
+	refer_sub = (pjsip_generic_string_hdr*)
+		    pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, 
+					       &REFER_SUB, NULL);
+
+	/* Check if subscription is suppressed */
+	if (refer_sub && pj_stricmp2(&refer_sub->hvalue, "false")==0) {
+	    /* Since no subscription is desired, assume that call has been
+	     * transfered successfully.
+	     */
+	    if (call && pjsua_var.ua_cfg.cb.on_call_transfer_status) {
+		const pj_str_t ACCEPTED = { "Accepted", 8 };
+		pj_bool_t cont = PJ_FALSE;
+		(*pjsua_var.ua_cfg.cb.on_call_transfer_status)(call->index, 
+							       200,
+							       &ACCEPTED,
+							       PJ_TRUE,
+							       &cont);
+	    }
+
+	    /* Yes, subscription is suppressed.
+	     * Terminate our subscription now.
+	     */
+	    PJ_LOG(4,(THIS_FILE, "Xfer subscription suppressed, terminating "
+				 "event subcription..."));
+	    pjsip_evsub_terminate(sub, PJ_TRUE);
+
+	} else {
+	    /* Notify application about call transfer progress. 
+	     * Initially notify with 100/Accepted status.
+	     */
+	    if (call && pjsua_var.ua_cfg.cb.on_call_transfer_status) {
+		const pj_str_t ACCEPTED = { "Accepted", 8 };
+		pj_bool_t cont = PJ_FALSE;
+		(*pjsua_var.ua_cfg.cb.on_call_transfer_status)(call->index, 
+							       100,
+							       &ACCEPTED,
+							       PJ_FALSE,
+							       &cont);
+	    }
+	}
+    }
+    /*
+     * On incoming NOTIFY, notify application about call transfer progress.
+     */
+    else if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_ACTIVE ||
+	     pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) 
+    {
+	pjsua_call *call;
+	pjsip_msg *msg;
+	pjsip_msg_body *body;
+	pjsip_status_line status_line;
+	pj_bool_t is_last;
+	pj_bool_t cont;
+	pj_status_t status;
+
+	call = pjsip_evsub_get_mod_data(sub, pjsua_var.mod.id);
+
+	/* When subscription is terminated, clear the xfer_sub member of 
+	 * the inv_data.
+	 */
+	if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_TERMINATED) {
+	    pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+	    PJ_LOG(4,(THIS_FILE, "Xfer client subscription terminated"));
+
+	}
+
+	if (!call || !event || !pjsua_var.ua_cfg.cb.on_call_transfer_status) {
+	    /* Application is not interested with call progress status */
+	    return;
+	}
+
+	/* This better be a NOTIFY request */
+	if (event->type == PJSIP_EVENT_TSX_STATE &&
+	    event->body.tsx_state.type == PJSIP_EVENT_RX_MSG)
+	{
+	    pjsip_rx_data *rdata;
+
+	    rdata = event->body.tsx_state.src.rdata;
+
+	    /* Check if there's body */
+	    msg = rdata->msg_info.msg;
+	    body = msg->body;
+	    if (!body) {
+		PJ_LOG(4,(THIS_FILE, 
+			  "Warning: received NOTIFY without message body"));
+		return;
+	    }
+
+	    /* Check for appropriate content */
+	    if (pj_stricmp2(&body->content_type.type, "message") != 0 ||
+		pj_stricmp2(&body->content_type.subtype, "sipfrag") != 0)
+	    {
+		PJ_LOG(4,(THIS_FILE, 
+			  "Warning: received NOTIFY with non message/sipfrag "
+			  "content"));
+		return;
+	    }
+
+	    /* Try to parse the content */
+	    status = pjsip_parse_status_line(body->data, body->len, 
+					     &status_line);
+	    if (status != PJ_SUCCESS) {
+		PJ_LOG(4,(THIS_FILE, 
+			  "Warning: received NOTIFY with invalid "
+			  "message/sipfrag content"));
+		return;
+	    }
+
+	} else {
+	    status_line.code = 500;
+	    status_line.reason = *pjsip_get_status_text(500);
+	}
+
+	/* Notify application */
+	is_last = (pjsip_evsub_get_state(sub)==PJSIP_EVSUB_STATE_TERMINATED);
+	cont = !is_last;
+	(*pjsua_var.ua_cfg.cb.on_call_transfer_status)(call->index, 
+						       status_line.code,
+						       &status_line.reason,
+						       is_last, &cont);
+
+	if (!cont) {
+	    pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
+	}
+    }
+}
+
+
+/*
+ * Callback called by event framework when the xfer subscription state
+ * has changed.
+ */
+static void xfer_server_on_evsub_state( pjsip_evsub *sub, pjsip_event *event)
 {
     
     PJ_UNUSED_ARG(event);
@@ -2163,38 +2334,7 @@
 	pjsip_evsub_set_mod_data(sub, pjsua_var.mod.id, NULL);
 	call->xfer_sub = NULL;
 
-	PJ_LOG(4,(THIS_FILE, "Xfer subscription terminated"));
-
-    }
-    /*
-     * When subscription is accepted (got 200/OK to REFER), check if 
-     * subscription suppressed.
-     */
-    else if (pjsip_evsub_get_state(sub) == PJSIP_EVSUB_STATE_ACCEPTED) {
-
-	pjsip_rx_data *rdata;
-	pjsip_generic_string_hdr *refer_sub;
-	const pj_str_t REFER_SUB = { "Refer-Sub", 9 };
-
-	/* Must be receipt of response message */
-	pj_assert(event->type == PJSIP_EVENT_TSX_STATE && 
-		  event->body.tsx_state.type == PJSIP_EVENT_RX_MSG);
-	rdata = event->body.tsx_state.src.rdata;
-
-	/* Find Refer-Sub header */
-	refer_sub = (pjsip_generic_string_hdr*)
-		    pjsip_msg_find_hdr_by_name(rdata->msg_info.msg, 
-					       &REFER_SUB, NULL);
-
-	/* Check if subscription is suppressed */
-	if (refer_sub && pj_stricmp2(&refer_sub->hvalue, "false")==0) {
-	    /* Yes, subscription is suppressed.
-	     * Terminate our subscription now.
-	     */
-	    PJ_LOG(4,(THIS_FILE, "Xfer subscription suppressed, terminating "
-				 "event subcription..."));
-	    pjsip_evsub_terminate(sub, PJ_TRUE);
-	}
+	PJ_LOG(4,(THIS_FILE, "Xfer server subscription terminated"));
     }
 }
 
@@ -2246,9 +2386,10 @@
 
     /* Notify callback */
     code = PJSIP_SC_OK;
-    if (pjsua_var.ua_cfg.cb.on_call_transfered)
-	(*pjsua_var.ua_cfg.cb.on_call_transfered)(existing_call->index,
-						  &refer_to->hvalue, &code);
+    if (pjsua_var.ua_cfg.cb.on_call_transfer_request)
+	(*pjsua_var.ua_cfg.cb.on_call_transfer_request)(existing_call->index,
+							&refer_to->hvalue, 
+							&code);
 
     if (code < 200)
 	code = 200;
@@ -2304,7 +2445,7 @@
 
 	/* Init callback */
 	pj_bzero(&xfer_cb, sizeof(xfer_cb));
-	xfer_cb.on_evsub_state = &xfer_on_evsub_state;
+	xfer_cb.on_evsub_state = &xfer_server_on_evsub_state;
 
 	/* Init additional header list to be sent with REFER response */
 	pj_list_init(&hdr_list);