[asterisk-commits] edguy3: branch edguy3/sip-identity-trunk r256147 - in /team/edguy3/sip-identi...
SVN commits to the Asterisk project
asterisk-commits at lists.digium.com
Sat Apr 3 07:49:56 CDT 2010
Author: edguy3
Date: Sat Apr 3 07:49:53 2010
New Revision: 256147
URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=256147
Log:
apply latest sip-identity-patch to latest trunk. Add automerge properties
Modified:
team/edguy3/sip-identity-trunk/ (props changed)
team/edguy3/sip-identity-trunk/channels/chan_sip.c
team/edguy3/sip-identity-trunk/channels/sip/include/sip.h
team/edguy3/sip-identity-trunk/configs/extensions.conf.sample
team/edguy3/sip-identity-trunk/configs/sip.conf.sample
Propchange: team/edguy3/sip-identity-trunk/
------------------------------------------------------------------------------
automerge = *
Propchange: team/edguy3/sip-identity-trunk/
------------------------------------------------------------------------------
automerge-email = edguy at emcsw.com
Modified: team/edguy3/sip-identity-trunk/channels/chan_sip.c
URL: http://svnview.digium.com/svn/asterisk/team/edguy3/sip-identity-trunk/channels/chan_sip.c?view=diff&rev=256147&r1=256146&r2=256147
==============================================================================
--- team/edguy3/sip-identity-trunk/channels/chan_sip.c (original)
+++ team/edguy3/sip-identity-trunk/channels/chan_sip.c Sat Apr 3 07:49:53 2010
@@ -163,6 +163,7 @@
/*** MODULEINFO
<depend>chan_local</depend>
+ <depend>curl</depend>
***/
/*! \page sip_session_timers SIP Session Timers in Asterisk Chan_sip
@@ -776,6 +777,26 @@
static int global_qualify_gap; /*!< Time between our group of peer pokes */
static int global_qualify_peers; /*!< Number of peers to poke at a given time */
+/*! \brief RFC 4474 Identity related static variables */
+static struct identity_struct {
+ enum identity_mode mode;/*!< flag whether to sign the invite, validate or sign and validate*/
+ char private_url_prefix[IDENTITY_PRIV_URL_PREFIX]; /*!< private url prefix is stored here used to fetch
+ the user private key from http or https server*/
+ char private_url_suffix[IDENTITY_PRIV_URL_SUFFIX]; /*!< private url suffix is stored here used to fetch
+ the user private key from http or https server
+ which can be .pem or .cer*/
+ char public_url_prefix[IDENTITY_PUB_URL_PREFIX]; /*!< public url prefix is stored here used to fetch the
+ user public key from http or https server used */
+ char public_url_suffix[IDENTITY_PUB_URL_SUFFIX]; /*!< public url suffix is stored here used to fetch the
+ user public key from http or https server used */
+ char private_path[AST_CONFIG_MAX_PATH]; /*!< location where private keys are stored */
+ char public_path[AST_CONFIG_MAX_PATH]; /*!< location where public keys are stored */
+ char cache_path[AST_CONFIG_MAX_PATH]; /*!< location where keys and certs are cached */
+} identity_general; /*! identity settings for overall system */
+
+AST_THREADSTORAGE(id_query_buf); /*! identity curl query buffer */
+AST_THREADSTORAGE(id_result_buf); /*! identity curl result buffer */
+
static enum st_mode global_st_mode; /*!< Mode of operation for Session-Timers */
static enum st_refresher global_st_refresher; /*!< Session-Timer refresher */
static int global_min_se; /*!< Lowest threshold for session refresh interval */
@@ -1068,7 +1089,7 @@
static void add_noncodec_to_sdp(const struct sip_pvt *p, int format,
struct ast_str **m_buf, struct ast_str **a_buf,
int debug);
-static enum sip_result add_sdp(struct sip_request *resp, struct sip_pvt *p, int oldsdp, int add_audio, int add_t38);
+static enum sip_result add_sdp(struct sip_request *resp, struct sip_pvt *p, int oldsdp, int add_audio, int add_t38, int first_message);
static void do_setnat(struct sip_pvt *p);
static void stop_media_flows(struct sip_pvt *p);
@@ -1524,6 +1545,927 @@
}
return res;
}
+
+
+/*
+ * Begin RFC4474 Identity definitions configuration values & functions
+ *
+ */
+
+/*! \brief helper function for rfc 4474 Return pointer to string with current SSL error
+ * \return Returns pointer to string with current SSL error
+ */
+static char* ssl_err(void)
+{
+ static char buf[1024];
+ ERR_error_string(ERR_get_error(), buf);
+ return buf;
+}
+
+/*! \brief helper function for rfc 4474 Return pointer to string with result explanation
+ * \param code the numeric (enum) code to be interpreted.
+ * \return Returns pointer to string with current SSL error
+ */
+static const char* ident_status(enum identity_result code)
+{
+ const char* rv = "SIP Identity status unknown";
+ switch(code) {
+ case IDENTITY_RES_NONE:
+ rv="SIP identity no result";
+ break;
+ case IDENTITY_RES_SIGN_OK:
+ rv="SIP identity sign ok";
+ break;
+ case IDENTITY_RES_SIGN_DISABLED:
+ rv="SIP identity sign disabled";
+ break;
+ case IDENTITY_RES_SIGN_BROKEN_KEY:
+ rv="SIP identity sign broken key";
+ break;
+ case IDENTITY_RES_SIGN_NO_PRIVATE_KEY:
+ rv="SIP identity sign no private key found";
+ break;
+ case IDENTITY_RES_VAL_OK:
+ rv="SIP identity validation ok";
+ break;
+ case IDENTITY_RES_VAL_DISABLED:
+ rv="SIP identity validation disabled";
+ break;
+ case IDENTITY_RES_VAL_BROKEN_CERT:
+ rv="SIP identity validation broken certificate";
+ break;
+ case IDENTITY_RES_VAL_NO_CERT:
+ rv="SIP identity validation no public certificate found";
+ }
+ return (rv);
+};
+
+
+/*! \brief helper function for rfc 4474 generate cache file name
+ * \return Returns pointer to filename. must be freed
+ */
+static char* id_cache_filename(const char*url,struct identity_struct *identity)
+{
+ char *p, *p2;
+ size_t len;
+ char buf[AST_CONFIG_MAX_PATH];
+ strncpy(buf,identity->cache_path,sizeof(buf));
+ len = strlen(buf);
+ p2 = buf + len;
+
+ ast_uri_encode(url,p2,sizeof(buf)-len,1);
+
+ /* substitute specials */
+ while ((p=strchr(buf,'<'))) {
+ *p='_';
+ }
+ while ((p=strchr(buf,'>'))) {
+ *p='_';
+ }
+ return ast_strdup(buf);
+}
+
+
+/*! \brief helper function for rfc 4474 Fill buffer with cached content.
+ * \param url url of source file
+ * \param data output buffer
+ * \param datasize size of output buffer
+ * \return Returns true if found. out will contain file contents
+ */
+static int id_check_cache(const char*url,char *data,size_t datasize,struct identity_struct *identity)
+{
+ FILE* stream;
+ int rv = FALSE;
+ char *cached_file=id_cache_filename(url,identity);
+ if (sipdebug) {
+ ast_debug(2,"** Identity key searching in id_check_cache %s => %s\n",url,cached_file);
+ }
+
+ /* does file exist? */
+ if ((stream = fopen (cached_file, "r")) != (FILE *)0) {
+ ast_debug(2,"id_check_cache: opened %s / %d \n",cached_file,datasize);
+ fread(data, datasize, 1, stream);
+ if ( ferror(stream) ) {
+ ast_log(LOG_ERROR,"id_check_cache: private key not loaded from %s\n",cached_file);
+ }
+ else {
+ ast_debug(2,"id_check_cache: private key loaded from %s\n",cached_file);
+ }
+ fclose(stream);
+ rv = TRUE;
+ }
+ ast_debug(2,"id_get_private_key: done %d\n",rv);
+ ast_free(cached_file);
+ return rv;
+}
+
+/*! \brief helper function for rfc 4474 store buffer to cache.
+ * \param url url of source file
+ * \param data output buffer
+ * \param datasize size of output buffer
+ * \return Returns void.
+ */
+static void id_store_cache(const char*url,char *data,size_t datasize,struct identity_struct *identity)
+{
+ FILE* stream;
+ char *cached_file=id_cache_filename(url,identity);
+ if (sipdebug) {
+ ast_debug(2,"** Identity info (datasize %d) to be stored in cache %s => %s\n",datasize,url,cached_file);
+ }
+
+ /* does file exist */
+ if ((stream = fopen (cached_file, "w")) != (FILE *)0) {
+ fwrite(data, datasize, 1, stream);
+ if (ferror(stream)) {
+ ast_log(LOG_ERROR,"id_store_cache: info NOT written to %s\n",cached_file);
+ }
+ else {
+ ast_log(LOG_NOTICE,"id_store_cache: info written to %s\n",cached_file);
+ }
+ fclose(stream);
+ }
+ else {
+ ast_log(LOG_ERROR,"** Identity id_store_cache cannot write to cache %s => %s\n",url,cached_file);
+ }
+ ast_free(cached_file);
+}
+
+
+/*! \brief helper function for rfc 4474 fetches public cert or private key for the user from server
+ * uses plain old socket to initiate http session to fetch keys from server
+ *
+ * \param type IDENTITY_PRIVATE for private key, IDENTITY_PUBLIC for public key
+ * \param user user name for whom the public or private key is fetched
+ * \param out where the fetched key is placed
+ * \param port port no to connect on the server
+ * \param url server name and path is fetched from url
+ * \return Returns integer result 1 is success, any other value means failure
+ */
+static int id_fetch_key_from_server(enum identity_key_type type,const char *user, char *out, size_t outsize,int port,const char *url_in,struct identity_struct *identity)
+{
+ int sockfd, portno, n,torecv=0,rcvd=0;
+ struct sockaddr_in serv_addr;
+ struct hostent *server;
+ struct ast_hostent ae;
+ char *p,*p1,*p2,tempbuf[20],buffer[IDENTITY_KEYBUF_SIZE],serveradd[PATH_MAX];
+ char url2[PATH_MAX];
+
+ /* ensure buffers are \0 terminated */
+ serveradd[0]='\0';
+ buffer[0]='\0';
+ out[0]='\0';
+
+ /* condition url */
+ p1=strstr(url_in,"<");
+ if (p1) {
+ p2=strstr(url_in,">");
+ if((p2-p1)>PATH_MAX) {
+ ast_log(LOG_ERROR, "Identity Error: URL too long %s\n",url2);
+ return -1;
+ }
+ memcpy(url2,p1+1,p2-p1-1);
+ ast_verbose("** Identity url:%s\n",url_in);
+ ast_verbose("** Identity url2:%s\n",url2);
+ }
+ else {
+ memcpy(url2,url_in,strlen(url_in)+1);
+ }
+
+ /* check local cache. */
+ if (id_check_cache(url2,out,outsize,identity)) {
+ if (sipdebug) {
+ ast_debug(2,"** Identity key found in cache %s\n%s\n",url2,out);
+ }
+ /* out is filled with contents */
+ return TRUE;
+ }
+
+ /* get url contents using CURL or a hand crafted method. ( curl gives us https, certs, etc. ) */
+ if (ast_custom_function_find("CURL")) {
+ /* use curl */
+ struct ast_str *query, *buffer;
+
+ /* allocate working buffers on thread */
+ if (!(query = ast_str_thread_get(&id_query_buf, 16))) {
+ ast_log(LOG_ERROR, "Identity Error: Memory allocation failure.\n");
+ return -1;
+ }
+
+ if (!(buffer = ast_str_thread_get(&id_result_buf, 16))) {
+ ast_log(LOG_ERROR, "Identity Error: Memory allocation failure.\n");
+ return -1;
+ }
+
+ ast_str_set(&query, 0, "${CURL(%s)}", url2);
+ ast_str_substitute_variables(&buffer, 0, NULL, ast_str_buffer(query));
+
+ /* copy results */
+ rcvd=ast_str_size(buffer);
+ if( rcvd > outsize) {
+ ast_log(LOG_ERROR, "Identity Error: http result from %s too large: %d.\n",url2, rcvd);
+ return -1;
+ }
+ if (rcvd>0) {
+ memcpy(out, ast_str_buffer(buffer),rcvd);
+ }
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,
+ "** Identity url [%s], results [\n%s]\n",
+ url2, out);
+ }
+
+ }
+ else {
+ /* use the el cheapo method to keep embedded guys happy */
+ /* check port no */
+ if (port < 1 || port > IDENTITY_MAXPORT) {
+ ast_log(LOG_ERROR, "Identity Error invalid port %d\n", port);
+ return -1;
+ }
+
+ /* format the http header buffer to fetch the keys from server */
+ p = strstr(url2, "://");
+ if (p!=NULL) {
+ p1=strstr(p+4,"/");
+ if (p1!=NULL) {
+ memcpy(serveradd,p+3,p1-(p+3) );
+ serveradd[p1-(p+3)]='\0';
+ strcpy(buffer, "GET ");
+ strcat(buffer, p1);
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,
+ "** Identity prv srv [%s], path [%s]\n",
+ serveradd, buffer);
+ }
+ }
+ }
+ if (p==NULL || p1==NULL) {
+ ast_log(
+ LOG_ERROR,
+ "** Identity error parsing private URL [%s], path [%s]\n",
+ serveradd, buffer);
+ return -1;
+ }
+
+ /* format the buffer to send to server */
+ strcat(buffer," HTTP/1.1\r\n");
+ strcat(buffer,"Host: ");
+ strcat(buffer,serveradd);
+ strcat(buffer,"\r\n");
+ strcat(buffer,"Accept: */*\r\n");
+ strcat(buffer,"\r\n");
+
+ server = ast_gethostbyname(serveradd,&ae);
+ if (server == NULL) {
+ ast_log(LOG_ERROR,
+ "** Identity ERROR cannot resolve hostname=%s\n", serveradd);
+ return -1;
+ }
+
+ /* populate connect to server addr */
+ portno = port;
+ if (sipdebug) {
+ ast_log(
+ LOG_NOTICE,
+ "** Identity id_fetch_key_from_server(); srv: %s, buff: \n%s\nport: %d\n",
+ serveradd, buffer, portno);
+ }
+
+ sockfd = socket(AF_INET, SOCK_STREAM, 0);
+ if (sockfd < 0) {
+ ast_log(LOG_ERROR,"** Identity Error opening socket\n");
+ return -1;
+ }
+ memset((char *) &serv_addr,'\0', sizeof(serv_addr));
+ serv_addr.sin_family = AF_INET;
+ memcpy((char *) &serv_addr.sin_addr.s_addr, (char *) server->h_addr,
+ server->h_length);
+ serv_addr.sin_port = htons(portno);
+ if (connect(sockfd,&serv_addr,sizeof(serv_addr)) < 0) {
+ ast_log(LOG_ERROR,"** Identity ERROR connecting \n");
+ return -1;
+ }
+ /* send to server the buffer */
+ n = write(sockfd,buffer,strlen(buffer));
+ if (n < 0) {
+ close(sockfd);
+ ast_log(LOG_ERROR,"** Identity ERROR writing to socket\n");
+ return -1;
+ }
+ /* read from server */
+ n = read(sockfd,buffer,sizeof(buffer));
+ if (n <= 0) {
+ close(sockfd);
+ ast_log(LOG_ERROR,"** Identity ERROR reading from socket\n");
+ return -1;
+ }
+ /* make sure content is null terminated */
+ buffer[(n<sizeof(buffer))?n:sizeof(buffer)-1]='\0';
+ /* write the read buffer from \r\n\r\n to out */
+ p=buffer;
+ p1=strstr(buffer,"\r\n\r\n");
+ if (p1!=NULL) {
+ memcpy(out,p1+4,strlen(buffer)-(p1+4-p));
+ out[strlen(buffer)-(p1+4-p)]='\0';
+ }
+ /* validate the response from host*/
+ p=strstr(buffer,"200 OK");
+ if (p==NULL) {
+ close(sockfd);
+ ast_log(LOG_ERROR,
+ "** Identity ERROR key not found on server [%s] %s\n",
+ serveradd, url2);
+ ast_log(LOG_ERROR,"********************\n");
+ ast_log(LOG_ERROR,"buffer %s\n",buffer);
+ ast_log(LOG_ERROR,"********************\n");
+ return -1;
+ }
+ /* get content length*/
+ p=strstr(buffer,"Content-Length:");
+ if (p!=NULL) {
+ char* p2 = p+sizeof("Content-Length:");
+ p1=strstr(p2,"\r\n");
+ if (p1!=NULL) {
+ memcpy(tempbuf,p2,p1-p2);
+ tempbuf[p1-p2]='\0';
+ torecv=strtol(tempbuf,NULL, 10);
+ }
+ }
+ /* check the length for validity */
+ if (torecv<=0) {
+ ast_log(LOG_ERROR, "** Identity ERROR nothing to receive [%d]\n",
+ torecv);
+ close(sockfd);
+ return -1;
+ }
+ /* find out what is received so far */
+ rcvd=strlen(out);
+ while (rcvd<torecv) {
+ buffer[0]='\0';
+ n = read(sockfd,buffer,sizeof(buffer));
+ if (n<0) {
+ ast_log(LOG_ERROR,
+ "** Identity ERROR socket read failed [%d]\n", torecv);
+ close(sockfd);
+ return -1;
+ }
+ memcpy(out+rcvd,buffer,n);
+ rcvd+=n;
+ }
+ out[rcvd]='\0';
+ close(sockfd);
+ }
+
+ if(rcvd>32) {
+ id_store_cache(url2,out,rcvd,identity);
+ }
+ return 1; /* success */
+}
+
+
+/*! \brief helper function for rfc 4474 gets private key from server and returns in EVP_PKEY format
+ *
+ * \param user user name for whom the private key is fetched
+ * \return Returns private key in EVP_PKEY or NULL if failed
+ */
+
+static EVP_PKEY* id_get_private_key(const char *user,struct identity_struct *identity)
+{
+ BIO *pem_bio;
+ EVP_PKEY *pkey;
+ char data[IDENTITY_KEYBUF_SIZE];
+ char url[AST_CONFIG_MAX_PATH];
+ char filename[AST_CONFIG_MAX_PATH];
+ FILE *stream;
+
+ data[0]='\0';
+
+ /* get private key from file system */
+ strcpy(filename,identity->private_path);
+ strcat(filename,user);
+
+ if (sipdebug) {
+ ast_debug(2,"id_get_private_key: private key potentially to be loaded from %s\n",filename);
+ }
+ if ((stream = fopen (filename, "r")) != (FILE *)0) {
+ fread(data, sizeof(data), 1, stream);
+ if (ferror(stream)){
+ ast_log(LOG_ERROR,"id_get_private_key: private key loading error from %s\n",filename);
+ }
+ else {
+ if (sipdebug) {
+ ast_debug(2,"id_get_private_key: private key was loaded from %s\n",filename);
+ }
+ }
+ fclose(stream);
+ }
+ else {
+ strcpy(url,identity->private_url_prefix);
+ strcat(url,user);
+ strcat(url,identity->private_url_suffix);
+ /* fetch the private key from server */
+ if (id_fetch_key_from_server(IDENTITY_PRIVATE,user,data,sizeof(data),IDENTITY_SERVER_PORT,url,identity)!=1) {
+ ast_log(LOG_ERROR,"** id_get_private_key: get private key failed %s\n",user);
+ return NULL;
+ }
+ }
+ /* return private key in EVP_PKEY format */
+ if (NULL == (pem_bio = BIO_new_mem_buf(data, strlen(data)))) {
+ ast_log(LOG_ERROR,"** id_get_private_key: unable to initialize BIO\n");
+ return NULL;
+ }
+ if (NULL == (pkey = PEM_read_bio_PrivateKey(pem_bio, NULL, 0, NULL))) {
+ ast_log(LOG_ERROR,"** id_get_private_key: could not decode private key from %s\n", ssl_err());
+ BIO_free(pem_bio);
+ return NULL;
+ }
+ BIO_free(pem_bio);
+ return pkey;
+}
+
+/*! \brief helper function for rfc 4474 gets public certificate from server and returns in X509 format
+ *
+ * \param user user name for whom the public key is fetched
+ * \param idinfohdr identity info header from sip request which contains the URL
+ * \return Returns private key in X509 or null
+ */
+static X509* id_get_public_cert(const char *user,const char *idinfohdr,struct identity_struct *identity)
+{
+ BIO *pem_bio;
+ X509 *cert;
+ char data[IDENTITY_KEYBUF_SIZE];
+ char filename[AST_CONFIG_MAX_PATH];
+ FILE *stream;
+
+ data[0]='\0';
+
+ /* get cert from file system */
+ strcpy(filename,identity->public_path);
+ strcat(filename,user);
+
+ if ((stream = fopen (filename, "r")) != (FILE *)0) {
+ fread(data, sizeof(data), 1, stream);
+ if (ferror(stream)) {
+ ast_log(LOG_ERROR,"id_get_public_cert: private key loading error from %s\n",filename);
+ }
+ else {
+ if (sipdebug) {
+ ast_debug(2,"id_get_public_cert: private key loaded from %s\n",filename);
+ }
+ }
+ fclose(stream);
+ }
+ else {
+ /* fetch the public cert from server */
+ if (id_fetch_key_from_server(IDENTITY_PUBLIC,user,data,sizeof(data),IDENTITY_SERVER_PORT,idinfohdr,identity)!=1) {
+ ast_log(LOG_ERROR,"** id_get_public_cert get public key failed %s\n",user);
+ return NULL;
+ }
+ }
+ if (sipdebug) {
+ ast_debug(2,"** id_get_public_cert: user: %s data: %s \n", user, data);
+ ast_verbose("** id_get_public_cert: user: %s data: %s \n", user, data);
+ }
+ /* return public key in EVP_PKEY format */
+ if (NULL == (pem_bio = BIO_new_mem_buf(data, strlen(data)))) {
+ ast_log(LOG_ERROR,"** id_get_public_cert: unable to initialize BIO\n");
+ return NULL;
+ }
+ if (NULL == (cert = PEM_read_bio_X509(pem_bio, NULL, 0, 0))) {
+ ast_log(LOG_ERROR,"** id_get_public_cert: could not decode public key from %s\n", ssl_err());
+ BIO_free(pem_bio);
+ return NULL;
+ }
+ BIO_free(pem_bio);
+ return cert;
+}
+
+
+/*! \brief helper function for rfc 4474 gets public key from server and returns in EVP_PKEY format
+ *
+ * \param user user name for whom the public key is fetched
+ * \param idinfohdr identity info header from sip request which contains the URL
+ * \return Returns private key in EVP_PKEY or null
+ */
+static EVP_PKEY* id_get_public_key(X509* cert)
+{
+ EVP_PKEY *pkey;
+ if (NULL == (pkey = X509_get_pubkey(cert))) {
+ ast_log(LOG_ERROR,"** id_get_public_key: could not get public key from %s\n", ssl_err());
+ }
+ return pkey;
+}
+
+/*! \brief helper function for rfc 4474 does SHA1 hashing
+ *
+ * \param buf pointer to data to be SHA1 hashed
+ * \param retbuf pointer to hashed data
+ * \return Returns pointer to hashed data
+ */
+static char* id_hashstr(const char *buf,char *retbuf)
+{
+ SHA_CTX sha;
+ SHA1_Init(&sha);
+ SHA1_Update(&sha, buf, strlen(buf));
+ SHA1_Final((unsigned char*)retbuf, &sha);
+ return retbuf;
+}
+/*! \brief main function for rfc 4474 does signing
+ *
+ * \param tosign pointer to data to be signed (RSA SHA1)
+ * \param res pointer to signed data
+ * \param user pointer to user for whom the key is fetched and data is signed using the private key of user
+ * \return Returns 1 for success any other value failure
+ */
+static int id_sign(const char *tosign,char *res,const char *user,struct identity_struct *identity)
+{
+ EVP_PKEY *pkey;
+ RSA *rsa;
+ unsigned char result[IDENTITY_SIGNBUF_SIZE];
+ unsigned int resultlen = sizeof(result);
+ char id[IDENTITY_SIGNBUF_SIZE];
+ unsigned int id_len = sizeof(id);
+
+ unsigned char sha1res[20];
+ /* hash the digest string */
+ if (id_hashstr(tosign,(char *) sha1res) == NULL) {
+ ast_log(LOG_ERROR,"** id_sign(): hash failed\n");
+ return FALSE;
+ }
+ /* get private key for user */
+ pkey = id_get_private_key(user,identity);
+ if (pkey == NULL) {
+ ast_log(LOG_ERROR,"** id_sign(): get private key failed\n");
+ return FALSE;
+ }
+
+ if (NULL == (rsa = EVP_PKEY_get1_RSA(pkey))) {
+ ast_log(LOG_ERROR,"** id_sign(): Unable to get RSA Key: %s\n", ssl_err());
+ EVP_PKEY_free(pkey);
+ return FALSE;
+ }
+ EVP_PKEY_free(pkey);
+ if (resultlen < RSA_size(rsa)) {
+ ast_log(LOG_ERROR,"** id_sign(): RSA Result too big: %d !>= %d\n", resultlen, RSA_size(rsa));
+ return FALSE;
+ }
+ /* do RSA sign */
+ if (RSA_sign(NID_sha1, sha1res, sizeof(sha1res), result, &resultlen, rsa) != 1) {
+ ast_log(LOG_ERROR,"** id_sign(): Error computing RSA results: %s\n", ssl_err());
+ RSA_free(rsa);
+ return FALSE;
+ }
+ RSA_free(rsa);
+ /* do base64 encoding */
+ ast_base64encode_full(id, result, resultlen, id_len, 0);
+ memcpy(res,id,strlen(id)+1);
+ return TRUE;
+}
+
+/*! \brief callback function for rfc 4474. Interprets errors
+ *
+ * \param ok status
+ * \param ctx pointer to x509 store cotext
+ * \return Returns 1 for success, 0 value failure
+ */
+static int verify_id_cb(int ok, X509_STORE_CTX *ctx)
+{
+ char buf[512]; /* cert subject buffer. size ok */
+
+ if (sipdebug && ctx->current_cert) {
+ X509_NAME_oneline( X509_get_subject_name(ctx->current_cert),buf, sizeof(buf));
+ ast_debug(2,"** verify_id_cb(%d): %s\n",ok,buf);
+ }
+ if (!ok) {
+ ast_log(LOG_ERROR,"** verify_id_cb: error %d at %d depth lookup:%s\n",ctx->error,
+ ctx->error_depth, X509_verify_cert_error_string(ctx->error));
+ /* allow self signing as OK */
+ if (ctx->error == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) {
+ ok=1;
+ }
+ }
+ return(ok);
+}
+
+
+
+
+/*! \brief main function for rfc 4474 does verifying the signature
+ *
+ * \param digeststr pointer to digest data
+ * \param identityhdr pointer to identity header data from sip request
+ * \param user pointer to user for whom the key is fetched and data is verified using the public key of user
+ * \param identity_info pointer to identity-info header data from sip request
+ * \return Returns 1 for success any other value failure
+ */
+static int id_verify(const char *digeststr,const char *identityhdr,const char *user,const char *identity_info,struct identity_struct *identity)
+{
+ unsigned char sig[IDENTITY_SIGNBUF_SIZE];
+ EVP_PKEY *pkey;
+ RSA *rsa=NULL;
+ X509 *cert=NULL;
+ int retlen;
+ unsigned char sha1res[20];
+ X509_NAME *nm=NULL;
+ X509_STORE *ctx=NULL;
+ X509_STORE_CTX *csc=NULL;
+ int cn_match;
+ int lastpos;
+ int rv=FALSE;
+
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,"** id_verify: digeststr: %s iden: %s user: %s idinfo: %s\n", digeststr, identityhdr, user, identity_info);
+ }
+
+ /* hash the digest string */
+ if (id_hashstr(digeststr, (char *)sha1res) == NULL) {
+ goto cleanup;
+ }
+
+ /* base 64 decode of the identity header */
+ retlen=ast_base64decode(sig, identityhdr, sizeof(sig));
+ if (retlen <=0 ) {
+ ast_log(LOG_ERROR,"** id_verify(): Cannot decode Identity header\n");
+ goto cleanup;
+ }
+ /* get public key for the user */
+ cert = id_get_public_cert(user,identity_info,identity);
+ if (cert == NULL) {
+ ast_log(LOG_ERROR,"** id_verify: Unable to get public cert: %s\n", ssl_err());
+ goto cleanup;
+ }
+ pkey = id_get_public_key(cert);
+ if (pkey == NULL) {
+ ast_log(LOG_ERROR,"** id_verify: Unable to get public Key: %s\n", ssl_err());
+ goto cleanup;
+ }
+ if (NULL == (rsa = EVP_PKEY_get1_RSA(pkey))) {
+ ast_log(LOG_ERROR,"** id_verify: Unable to get RSA Key: %s\n", ssl_err());
+ goto cleanup;
+ }
+ EVP_PKEY_free(pkey);
+
+ nm = X509_get_subject_name(cert);
+ if (sipdebug) {
+ char subject[256];
+ X509_NAME_oneline(nm, subject, sizeof(subject));
+ ast_log(LOG_NOTICE, "** id_verify Certificate Subject = %s\n", subject);
+ ast_log(LOG_NOTICE, "** id_verify: size-sha1res: %d retlen: %d \n", sizeof(sha1res), retlen);
+ }
+
+ /* do rsa verify */
+ if (RSA_verify(NID_sha1, sha1res, sizeof(sha1res),(unsigned char *) sig, retlen, rsa) != 1) {
+ ast_log(LOG_ERROR,"** id_verify: Verify failed: %s\n", ssl_err());
+ goto cleanup;
+ }
+ RSA_free(rsa);
+ rsa=NULL;
+
+ /* loop through the CNs */
+ cn_match = FALSE;
+ lastpos = -1 ;
+ do {
+ int name_len;
+ ASN1_STRING *tmp;
+ char *cname2;
+
+ lastpos = X509_NAME_get_index_by_NID(nm, NID_commonName, lastpos);
+ if (lastpos < 0) {
+ break;
+ }
+ tmp = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(nm,lastpos));
+ if (!tmp) {
+ ast_log(LOG_ERROR, "** id_verify: Error Processing certificate CN\n");
+ break;
+ }
+ name_len = ASN1_STRING_length(tmp);
+ cname2 = (char*)ast_malloc(name_len+1); /* remember ASN1 does not count \0 */
+ memcpy(cname2, ASN1_STRING_data(tmp), ASN1_STRING_length(tmp));
+ cname2[name_len]='\0';
+
+ cn_match = (0==strncmp(user,cname2,name_len+1));
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,"** id_verify: CNAME = %s match:%s\n",cname2,cn_match?"yes":"no");
+ }
+ ast_free(cname2);
+ }
+ while ( !cn_match );
+
+ if (!cn_match) {
+ ast_log(LOG_ERROR, "** id_verify: FAIL: user %s not found in cert CN\n",user);
+ goto cleanup;
+ }
+
+ if (sipdebug) {
+ ast_log(LOG_NOTICE, "** id_verify: Callerid(num) matches Common Name. ok\n");
+ }
+
+ /* now verify chain */
+ ctx=X509_STORE_new();
+ X509_STORE_set_verify_cb_func(ctx,verify_id_cb);
+
+ if (X509_STORE_set_default_paths(ctx) != 1) {
+ ast_log(LOG_ERROR, " id_verify: X509_STORE_set_default_paths failed\n");
+ goto cleanup;
+ }
+ csc = X509_STORE_CTX_new();
+ if (!csc) {
+ ast_log(LOG_ERROR, " id_verify: X509_STORE_CTX_new failed\n");
+ goto cleanup;
+ }
+ if (!X509_STORE_CTX_init(csc,ctx,cert,NULL)) {
+ ast_log(LOG_ERROR, " id_verify: X509_STORE_CTX_init failed\n");
+ goto cleanup;
+ }
+ if (!X509_verify_cert(csc)) {
+ ast_log(LOG_ERROR, " id_verify: X509_verify_cert failed.\n");
+ goto cleanup;
+ }
+
+ if (sipdebug) {
+ ast_log(LOG_NOTICE, "** id_verify: validates ok\n");
+ }
+
+ rv=TRUE;
+
+ /* verified ok. Now free everything */
+ cleanup: { /* gotos are not evil */
+ if(rsa)RSA_free(rsa);
+ if(ctx)X509_STORE_free(ctx);
+ if(csc)X509_STORE_CTX_free(csc);
+ if(cert)X509_free(cert);
+ }
+ return rv;
+}
+/*! \brief helper function for rfc 4474 gets the specific sip header requested
+ *
+ * \param req pointer to sip_request strucutre
+ * \param name pointer to header value being requested
+ * \param out pointer to buffer where result is stored
+ */
+static void id_get_sip_header(struct sip_request *req,const char *name,char *out,size_t size)
+{
+ const char *p1,*p;
+ p=get_header(req, name);
+
+ if (p!=NULL) {
+ p1=strstr(p,"\r\n");
+ if (p1!=NULL) {
+ size_t len = p1-p;
+ /* reduce size if too long - will likely fail signing */
+ if ( len >= size ) {
+ len=size-1;
+ ast_log(LOG_ERROR,"** id_verify(): sip header too long: %s\n", p);
+ }
+ memcpy(out,p,size);
+ out[len]='\0';
+ }
+ else {
+ strncpy(out,p,size);
+ out[size-1]='\0';
+ }
+ }
+ else { /* no \r\n found so it should be good to copy */
+ strncpy(out,p,size);
+ out[size-1]='\0'; /* ensure null terminated */
+ }
+}
+
+/*! \brief helper function for rfc 4474 gets the address of record
+ *
+ * \param req pointer to sip_request strucutre
+ * \param name pointer to Address of record value being requested
+ * \param out pointer to buffer where result is stored
+ */
+
+static void id_get_addr_of_record(struct sip_request *req,const char *name,char *out,size_t size)
+{
+ char HeaderReq[IDENTITY_HEADER_SIZE];
+ char *p,*p1;
+
+ id_get_sip_header(req, name,HeaderReq,sizeof(HeaderReq));
+ p=strchr(HeaderReq,'<');
+ if (p!=NULL) {
+ p1=strchr(p,'>');
+ if (p1!=NULL) {
+ if (p1-p-1 > size ) {
+ ast_log(LOG_ERROR,"** id_get_addr_of_record: field too large \n");
+ out[0]='\0';
+ }
+ else {
+ memcpy(out,p+1,p1-p-1);
+ out[p1-p-1]='\0';
+ }
+ }
+ }
+}
+
+/*! \brief entry function for rfc 4474 does signing or verifying signature
+ *
+ * \param req pointer to sip_request structure
+ * \param out pointer to where signing or verifying result is placed (out)
+ * \param sdp pointer to optional sdp data which can be passed as input
+ * \param type 0 for signing 1 for verifying
+ * \param userout pointer where user name is stored in case verifying is done (out)
+ */
+static int id_main(struct sip_request *req,char *out,const char *sdp,enum identity_action type,char *userout,size_t userout_size,struct identity_struct *identity)
+{
+ char digeststr[2*IDENTITY_HEADER_SIZE],signstr[IDENTITY_HEADER_SIZE],callid[IDENTITY_HEADER_SIZE],cseq[IDENTITY_HEADER_SIZE],date[IDENTITY_HEADER_SIZE];
+ char contactAor[IDENTITY_HEADER_SIZE],toAor[IDENTITY_HEADER_SIZE],fromAor[IDENTITY_HEADER_SIZE],user[40];
+ char idstoverify[IDENTITY_HEADER_SIZE],idinfo[IDENTITY_HEADER_SIZE];
+ char *p,*p1;
+ int rv=0; /* returned value. >1 is success */
+
+ /* ensure all buffers are null strings */
+ contactAor[0]=toAor[0]=fromAor[0]=user[0]= idstoverify[0]=idinfo[0]= digeststr[0]=signstr[0]=callid[0]=cseq[0]=date[0] ='\0';
+
+ if (type==IDENTITY_ACTION_VERIFY) { /* verify the signature*/
+ id_get_sip_header(req,"Identity-Info",idinfo,sizeof(idinfo));
+ if (strlen(idinfo)==0) {
+ return FALSE;
+ }
+ id_get_sip_header(req,"Identity",idstoverify,sizeof(idstoverify));
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,"\n** id_main()\n idstoverify: %s \n idinfo: %s \n ", idstoverify, idinfo);
+ }
+ }
+
+ /* get various header fields */
+ id_get_sip_header(req, "Call-ID",callid,sizeof(callid));
+ id_get_sip_header(req, "CSeq",cseq,sizeof(cseq));
+ id_get_sip_header(req, "Date",date,sizeof(date));
+ /* get address of record */
+ id_get_addr_of_record(req,"To",toAor,sizeof(toAor));
+ id_get_addr_of_record(req,"From",fromAor,sizeof(fromAor));
+ id_get_addr_of_record(req,"Contact",contactAor,sizeof(contactAor));
+ /* get user from from header */
+ if (strlen(fromAor)>0) {
+ p1=strstr(fromAor,":"); /* find sip: */
+ if (p1!=NULL) {
+ p=strstr(p1+1,"@");
+ memcpy(user,p1+1,p-(p1+1));
+ user[p-(p1+1)]='\0';
+ strncpy(userout,user,userout_size);
+ userout[userout_size-1]='\0'; /* ensure null terminated */
+ }
+ }
+ /* build the digest string fromAOR| toAOR| Call id| cSEQ|date|Contact AOR |SDP */
+ strcpy(digeststr,fromAor);
+ strcat(digeststr,"|");
+ strcat(digeststr,toAor);
+ strcat(digeststr,"|");
+ strcat(digeststr,callid);
+ strcat(digeststr,"|");
+ strcat(digeststr,cseq);
+ strcat(digeststr,"|");
+ strcat(digeststr,date);
+ strcat(digeststr,"|");
+ strcat(digeststr,contactAor);
+ strcat(digeststr,"|");
+ /*if sdp is not passed then find it*/
+ if (sdp==NULL) {
+ if (find_sdp(req)) {
+ int x;
+ for (x = 0; x < (req->lines); x++) {
+ char *p,*line2;
+ line2=ast_strdup(REQ_OFFSET_TO_STR(req, line[x]));
+ p=strstr(line2,"\r\n");
+ if (p!=NULL) {
+ *p='\0';
+ }
+ strcat(digeststr,line2);
+ strcat(digeststr,"\r\n");
+ ast_free(line2);
+ }
+ }
+ }
+ else {
+ strcat(digeststr,sdp);
+ }
+
+ /* call appropriate function depending on type */
+ if (type==IDENTITY_ACTION_SIGN) { /* sign */
+ if ( (rv = id_sign(digeststr,signstr,user,identity)) && sipdebug) {
+ ast_debug(2,"** id_main() Signing Success \n");
+ }
+
+ }
+ else if (type==IDENTITY_ACTION_VERIFY) { /* verify */
+ rv = id_verify(digeststr,idstoverify,user,idinfo,identity);
+ }
+
+ strcpy(out,signstr);
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,"** Identity id_main() size: %d digeststr: %s\n", strlen(digeststr), digeststr);
+ }
+ return rv;
+}
+
+/*
+ * End RFC4474 Identity definitions configuration values & functions
+ *
+ */
+
static void tcptls_packet_destructor(void *obj)
{
@@ -8905,12 +9847,17 @@
}
/*! \brief Add Session Description Protocol message
+ * \param resp
+ * \param p
+ * \param oldsdp
+ * \param add_audio
+ * \param add_t38
If oldsdp is TRUE, then the SDP version number is not incremented. This mechanism
is used in Session-Timers where RE-INVITEs are used for refreshing SIP sessions
without modifying the media session in any way.
*/
-static enum sip_result add_sdp(struct sip_request *resp, struct sip_pvt *p, int oldsdp, int add_audio, int add_t38)
+static enum sip_result add_sdp(struct sip_request *resp, struct sip_pvt *p, int oldsdp, int add_audio, int add_t38, int first_message)
{
int len = 0;
format_t alreadysent = 0;
@@ -8955,6 +9902,16 @@
char buf[SIPBUFSIZE];
char dummy_answer[256];
+ char signed_identity_digest[IDENTITY_SIGNBUF_SIZE]; /* signed identity digest header parameter */
+ char user[IDENTITY_SIGNBUF_SIZE]; /* user CALLERID(NUM) */
+ char idinfo[IDENTITY_SIGNBUF_SIZE]; /* identity-info header parameter */
+ char result_number[3]; /* Result code */
+ struct ast_str *sdp1; /* working copy of sdp for identity */
+
+ signed_identity_digest[0]='\0';
+ user[0]='\0';
+ idinfo[0]='\0';
+
/* Set the SDP session name */
snprintf(subject, sizeof(subject), "s=%s\r\n", ast_strlen_zero(global_sdpsession) ? "-" : global_sdpsession);
@@ -9212,6 +10169,84 @@
len += m_modem->used + a_modem->used;
add_header(resp, "Content-Type", "application/sdp");
+
+ /* add identity header can be done only here */
+
+ /* build a disposable SDP for signing. cant use real SDP because once add_line is called, cant add_header */
+ sdp1 = ast_str_create(1024);
+ ast_str_set(&sdp1,0,version);
+ ast_str_append(&sdp1,0,owner);
+ ast_str_append(&sdp1,0,subject);
+ ast_str_append(&sdp1,0,connection);
+
+ if (needvideo){
+ ast_str_append(&sdp1,0,bandwidth);
+ }
+
+ ast_str_append(&sdp1,0,session_time);
+ if (needaudio) {
+ ast_str_append(&sdp1,0,m_audio->str);
+ ast_str_append(&sdp1,0,a_audio->str);
+ ast_str_append(&sdp1,0,hold);
+ }
+
+ if (needvideo) { /* only if video response is appropriate */
+ ast_str_append(&sdp1,0,m_video->str);
+ ast_str_append(&sdp1,0,a_video->str);
+ ast_str_append(&sdp1,0,hold);
+ }
+ if (needtext) { /* only if text response is appropriate */
+ ast_str_append(&sdp1,0,m_text->str);
+ ast_str_append(&sdp1,0,a_text->str);
+ ast_str_append(&sdp1,0,hold);
+ }
+ if (add_t38) { /* only if t38 FAX response is appropriate */
+ ast_str_append(&sdp1,0,m_modem->str);
+ ast_str_append(&sdp1,0,a_modem->str);
+ ast_str_append(&sdp1,0,hold);
+ }
+
+ /* get and add the identity string */
+ struct identity_struct *identity = &identity_general;
+ /* FUTURE Select identity struct to use from domain or peer */
+ if (first_message && p->method==SIP_INVITE && !oldsdp && (identity->mode==IDENTITY_SIGN || identity->mode==IDENTITY_SIGN_AND_VALIDATE)) {
+ char idinfo[IDENTITY_SIGNBUF_SIZE];
+ /* call get digest string to get the signing result*/
+ id_get_sip_header(&p->initreq,"Identity-Info",idinfo,sizeof(idinfo));
+ if (debug) {
+ ast_debug(2,"set idinfo [%s]\n",idinfo);
+ }
+ id_main(resp,signed_identity_digest,ast_str_buffer(sdp1),IDENTITY_ACTION_SIGN,user,sizeof(user),identity);
+ ast_free(sdp1);
+ if (strlen(signed_identity_digest)>0) {
+ strcpy(idinfo,"<");
+ strcpy(&idinfo[1],identity->public_url_prefix);
+ strcat(idinfo,user);
+ strcpy(&idinfo[strlen(idinfo)],identity->public_url_suffix);
+ strcat(idinfo,">;alg=rsa-sha1");
+ add_header(resp, "Identity", signed_identity_digest);
+ add_header(resp, "Identity-Info",idinfo);
+ if (sipdebug) {
+ ast_log(LOG_NOTICE,"id [%s]\n idinfo [%s]\n",signed_identity_digest,idinfo);
+ }
+ p->initreq.sip_identity_result=IDENTITY_RES_SIGN_OK;
+ }
+ else {
+ p->initreq.sip_identity_result=IDENTITY_RES_SIGN_BROKEN_KEY; /* assume broken for now */
+ }
+ }
+ else {
+ p->initreq.sip_identity_result=IDENTITY_RES_SIGN_DISABLED;
+ }
+
+ /* set channel variable with result */
+ sprintf(result_number,"%2d",p->initreq.sip_identity_result);
+ pbx_builtin_setvar_helper(p->owner,"IDENTITY_RESULT",result_number);
+ if (sipdebug) {
+ ast_debug(2,"** Set Chan_Var IDENTITY_RESULT SIGN result to test: [%d], %s\n",p->initreq.sip_identity_result,ident_status(p->initreq.sip_identity_result));
+ }
+
+ /* Add content length now */
add_header_contentLength(resp, len);
add_line(resp, version);
add_line(resp, owner);
@@ -9271,7 +10306,7 @@
}
respprep(&resp, p, msg, req);
if (p->udptl) {
- add_sdp(&resp, p, 0, 0, 1);
+ add_sdp(&resp, p, 0, 0, 1, FALSE);
} else
ast_log(LOG_ERROR, "Can't add SDP to response, since we have no UDPTL session allocated. Call-ID %s\n", p->callid);
if (retrans && !p->pendinginvite)
@@ -9326,9 +10361,9 @@
ast_rtp_instance_activate(p->rtp);
try_suggested_sip_codec(p);
if (p->t38.state == T38_ENABLED) {
- add_sdp(&resp, p, oldsdp, TRUE, TRUE);
+ add_sdp(&resp, p, oldsdp, TRUE, TRUE, FALSE);
} else {
- add_sdp(&resp, p, oldsdp, TRUE, FALSE);
+ add_sdp(&resp, p, oldsdp, TRUE, FALSE, FALSE);
}
} else
ast_log(LOG_ERROR, "Can't add SDP to response, since we have no RTP session allocated. Call-ID %s\n", p->callid);
@@ -9417,9 +10452,9 @@
try_suggested_sip_codec(p);
if (t38version)
- add_sdp(&req, p, oldsdp, FALSE, TRUE);
+ add_sdp(&req, p, oldsdp, FALSE, TRUE, FALSE);
else
- add_sdp(&req, p, oldsdp, TRUE, FALSE);
+ add_sdp(&req, p, oldsdp, TRUE, FALSE, FALSE);
/* Use this as the basis */
initialize_initreq(p, &req);
@@ -9792,10 +10827,10 @@
memset(p->offered_media, 0, sizeof(p->offered_media));
if (p->udptl && p->t38.state == T38_LOCAL_REINVITE) {
ast_debug(1, "T38 is in state %d on channel %s\n", p->t38.state, p->owner ? p->owner->name : "<none>");
- add_sdp(&req, p, FALSE, FALSE, TRUE);
+ add_sdp(&req, p, FALSE, FALSE, TRUE, TRUE);
} else if (p->rtp) {
try_suggested_sip_codec(p);
- add_sdp(&req, p, FALSE, TRUE, FALSE);
+ add_sdp(&req, p, FALSE, TRUE, FALSE, TRUE);
}
} else if (p->notify) {
for (var = p->notify->headers; var; var = var->next)
@@ -10337,7 +11372,7 @@
add_header(&req, "Allow", ALLOWED_METHODS);
add_header(&req, "Supported", SUPPORTED_EXTENSIONS);
add_rpid(&req, p);
- add_sdp(&req, p, FALSE, TRUE, FALSE);
+ add_sdp(&req, p, FALSE, TRUE, FALSE, FALSE);
initialize_initreq(p, &req);
p->lastinvite = p->ocseq;
@@ -15130,6 +16165,39 @@
ast_cli(a->fd, " Save sys. name: %s\n", cli_yesno(sip_cfg.rtsave_sysname));
ast_cli(a->fd, " Auto Clear: %d\n", sip_cfg.rtautoclear);
}
+
+ struct identity_struct *identity = &identity_general;
+ /* FUTURE Select identity struct to use from domain or peer */
+ ast_cli(a->fd, "\nSIP Identity Settings:\n");
+ ast_cli(a->fd, "--------------------------\n");
+ if (identity->mode==IDENTITY_DISABLED) {
+ ast_cli(a->fd, " Mode: no (disabled)\n");
+ }
+ else {
+
+ char* mode;
+ switch(identity->mode) {
+ case IDENTITY_SIGN:
+ mode="sign";
+ break;
+ case IDENTITY_VALIDATE:
+ mode="sign";
+ break;
+ default:
+ mode="sign_and_val";
+ }
+
+ ast_cli(a->fd, " Mode: %s\n", mode);
+ ast_cli(a->fd, " Private URL Prefix: %s\n", identity->private_url_prefix);
+ ast_cli(a->fd, " Private URL Suffix: %s\n", identity->private_url_suffix);
+ ast_cli(a->fd, " Private File Path: %s\n", identity->private_path);
+ ast_cli(a->fd, " Public URL Prefix: %s\n", identity->public_url_prefix);
+ ast_cli(a->fd, " Public URL Suffix: %s\n", identity->public_url_suffix);
+ ast_cli(a->fd, " Public File Path: %s\n", identity->public_path);
+ ast_cli(a->fd, " Cache Path: %s\n", identity->cache_path);
+
+ }
+
ast_cli(a->fd, "\n----\n");
return CLI_SUCCESS;
}
@@ -19351,6 +20419,33 @@
[... 394 lines stripped ...]
More information about the asterisk-commits
mailing list