diff options
-rw-r--r-- | CHANGES | 8 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | configs/modules.conf.sample | 3 | ||||
-rw-r--r-- | configs/phoneprov.conf.sample | 55 | ||||
-rw-r--r-- | doc/tex/asterisk.tex | 5 | ||||
-rw-r--r-- | doc/tex/phoneprov.tex | 301 | ||||
-rw-r--r-- | funcs/func_strings.c | 39 | ||||
-rw-r--r-- | include/asterisk/localtime.h | 1 | ||||
-rw-r--r-- | main/acl.c | 5 | ||||
-rw-r--r-- | main/stdtime/localtime.c | 115 | ||||
-rw-r--r-- | phoneprov/000000000000-directory.xml | 6 | ||||
-rw-r--r-- | phoneprov/000000000000-phone.cfg | 1 | ||||
-rw-r--r-- | phoneprov/000000000000.cfg | 2 | ||||
-rw-r--r-- | phoneprov/polycom.xml | 37 | ||||
-rw-r--r-- | res/res_phoneprov.c | 1012 |
15 files changed, 1588 insertions, 6 deletions
@@ -436,3 +436,11 @@ Miscellaneous * Added a new codec translation module, codec_resample, which re-samples signed linear audio between 8 kHz and 16 kHz to help support wideband codecs. + * Added a new module, res_phoneprov, which allows auto-provisioning of phones + based on configuration templates that use Asterisk dialplan function and + variable substitution. It should be possible to create phone profiles and + templates that work for the majority of phones provisioned over http. It + is currently only intended to provision a single user account per phone. + An example profile and set of templates for Polycom phones is provided. + NOTE: Polycom firmware is not included, but should be placed in + AST_DATA_DIR/phoneprov/configs to match up with the included templates. @@ -450,6 +450,10 @@ datafiles: _all # Should static HTTP be installed during make samples or even with its own target ala # webvoicemail? There are portions here that *could* be customized but might also be # improved a lot. I'll put it here for now. + mkdir -p $(DESTDIR)$(ASTDATADIR)/phoneprov + for x in phoneprov/*; do \ + $(INSTALL) -m 644 $$x $(DESTDIR)$(ASTDATADIR)/phoneprov ; \ + done mkdir -p $(DESTDIR)$(ASTDATADIR)/static-http for x in static-http/*; do \ $(INSTALL) -m 644 $$x $(DESTDIR)$(ASTDATADIR)/static-http ; \ diff --git a/configs/modules.conf.sample b/configs/modules.conf.sample index 8610dbca5..0fee96120 100644 --- a/configs/modules.conf.sample +++ b/configs/modules.conf.sample @@ -18,6 +18,9 @@ autoload=yes ;preload => res_odbc.so ;preload => res_config_odbc.so ; +; res_phoneprov requires func_strings.so to be loaded: +preload => func_strings.so +; ; Uncomment the following if you wish to use the Speech Recognition API ;preload => res_speech.so ; diff --git a/configs/phoneprov.conf.sample b/configs/phoneprov.conf.sample new file mode 100644 index 000000000..3b82490c1 --- /dev/null +++ b/configs/phoneprov.conf.sample @@ -0,0 +1,55 @@ +[general] +;serveraddr=192.168.1.1 ; Address to send to the phone to use as server address. +serveriface=eth0 ; Same as above, except an ethernet interface. + ; Useful for when the interface uses DHCP. + ; There is no default for either of the above, and only one should be set. +serverport=5060 ; Port to send to the phone to use as server port. Default is 5060. +default_profile=polycom ; The default profile to use if none specified in users.conf + +; You can define profiles for different phones specifying what files to register +; with the provisioning server. You can define either static files, or dynamically +; generated files that can have dynamic names and point to templates that variables +; can be substituted into. You can also set arbitrary variables for the profiles +; templates to have access to. Example: + +;[example] +;mime_type => application/octet-stream +;static_file => example/firmware +;static_file => example/default.cfg,text/xml +;${TOUPPER(${MAC})}.cfg => templates/example-mac.cfg +;setvar => DB_CIDNAME=${ODBC_CID_NAME_LOOKUP(${USERNAME})} + +; Dynamically generated files have a filename registered with variable substitution +; with variables obtained while reading users.conf. + +; Built in variables and the options in users.conf that they come from +; MAC (macaddress) +; USERNAME (username) +; DISPLAY_NAME (fullname) +; SECRET (secret) +; LABEL (label) +; CALLERID (cid_number) +; VOCIEMAIL_EXTEN (vmexten) +; EXTENSION_LENGTH (localextenlength) + +; Built-in variables and the options in phoneprov.conf that they come from +; SERVER (server) +; SERVER_PORT (serverport) + +[polycom] +staticdir => configs/ ; Sub directory of AST_DATA_DIR/phoneprov that static files reside + ; in. This allows a request to /phoneprov/sip.cfg to pull the file + ; from /phoneprov/configs/sip.cfg +mime_type => text/xml ; Default mime type to use if one isn't specified or the + ; extension isn't recognized +static_file => bootrom.ld,application/octet-stream ; Static files the phone will download +static_file => bootrom.ver,plain/text ; static_file => filename,mime-type +static_file => sip.ld,application/octet-stream +static_file => sip.ver,plain/text +static_file => sip.cfg +static_file => custom.cfg +${TOLOWER(${MAC})}.cfg => 000000000000.cfg ; Dynamically generated files. +${TOLOWER(${MAC})}-phone.cfg => 000000000000-phone.cfg ; (relative to AST_DATA_DIR/phoneprov) +config/${TOLOWER(${MAC})} => polycom.xml ; Dynamic Filename => template file +${TOLOWER(${MAC})}-directory.xml => 000000000000-directory.xml +setvar => CUSTOM_CONFIG=/var/lib/asterisk/phoneprov/configs/custom.cfg ; Custom variable diff --git a/doc/tex/asterisk.tex b/doc/tex/asterisk.tex index ca0a0dc6b..0feb3d7d9 100644 --- a/doc/tex/asterisk.tex +++ b/doc/tex/asterisk.tex @@ -32,7 +32,7 @@ \author{Asterisk Development Team \\ Asterisk.org} -\title{Asterisk Reference Information \\ Version ASTERISKVERSION} +\title{Asterisk Reference Information \\ Version } \begin{document} \maketitle @@ -132,6 +132,9 @@ reference purposes. \section{Queue Logs} \input{queuelog.tex} +\chapter{Phone Provisioning} + \input{phoneprov.tex} + \chapter{Development} \section{Backtrace} \input{backtrace.tex} diff --git a/doc/tex/phoneprov.tex b/doc/tex/phoneprov.tex new file mode 100644 index 000000000..d813a7f45 --- /dev/null +++ b/doc/tex/phoneprov.tex @@ -0,0 +1,301 @@ +\section{Introduction} + +Asterisk includes basic phone provisioning support through the res\_phoneprov module. The +current implementation is based on a templating system using Asterisk dialplan function +and variable substitution and obtains information to substitute into those templates from +\path{phoneprov.conf} and \path{users.conf}. A profile and set of templates is provided +for provisioning Polycom phones. Note that res\_phoneprov is currently limited to +provisioning a single user per device. + +\section{Configuration of phoneprov.conf} + +The configuration file, \path{phoneprov.conf}, is used to set up the built-in variables +SEVER and SERVER\_PORT, to define a default phone profile to use, and to define different +phone profiles available for provisioning. + +\subsection{The [general] section} + +Below is a sample of the general section of \path{phoneprov.conf}: + +\begin{astlisting} +\begin{verbatim} +[general] +;serveriface=eth0 +serveraddr=192.168.1.1 +serverport=5060 +default_profile=polycom +\end{verbatim} +\end{astlisting} + +There are two choices for setting the SERVER variable. If the IP address of the server is +known, or the hostname resolvable by the phones, the appropriate \textbf{serveraddr} +value should be set. Alternatively, the network interface that the server listens on can +be set by specifying a \textbf{serveriface} and SERVER will be set to the IP address of +that interface. Only one of these options should be set. + +The SERVER\_PORT variable is set by setting the \textbf{serverport}. If serverport is +not specified, it is set to a default value of 5060. + +Any user set for auto-provisioning in users.conf without a specified profile will be +assumed to belong to the profile set with \textbf{default\_profile}. + +\subsection{Creating phone profiles} + +A phone profile is basically a list of files that a particular group of phones needs to +function. For most phone types there are files that are identical for all phones +(firmware, for instance) as well as a configuration file that is specific to individual +phones. res\_phoneprov breaks these two groups of files into static files and dynamic +files, respectively. A sample profile: + +\begin{astlisting} +\begin{verbatim} +[polycom] +staticdir => configs/ +mime_type => text/xml +setvar => CUSTOM_CONFIG=/var/lib/asterisk/phoneprov/configs/custom.cfg +static_file => bootrom.ld,application/octet-stream +static_file => bootrom.ver,plain/text +static_file => sip.ld,application/octet-stream +static_file => sip.ver,plain/text +static_file => sip.cfg +static_file => custom.cfg +${TOLOWER(${MAC})}.cfg => 000000000000.cfg +${TOLOWER(${MAC})}-phone.cfg => 000000000000-phone.cfg +config/${TOLOWER(${MAC})} => polycom.xml +${TOLOWER(${MAC})}-directory.xml => 000000000000-directory.xml +\end{verbatim} +\end{astlisting} + +A \textbf{static\_file} is set by specifying the file name, relative to +\path{AST\_DATA\_DIR/phoneprov}. The mime-type of the file can optionally be specified +after a comma. If \textbf{staticdir} is set, all static files will be relative to the +subdirectory of AST\_DATA\_DIR/phoneprov specified. + +Since phone-specific config files generally have file names based on phone-specifc data, +dynamic filenames in res\_phoneprov can be defined with Asterisk dialplan function and +variable substitution. In the above example, \$\{TOLOWER(\$\{MAC\})\}.cfg $\Rightarrow$ +000000000000.cfg would define a relative URI to be served that matches the format of +MACADDRESS.cfg, all lower case. A request for that file would then point to the template +found at AST\_DATA\_DIR/phoneprov/000000000000.cfg. The template can be followed by a +comma and mime-type. Notice that the dynamic filename (URI) can contain contain +directories. Since these files are dynamically generated, the config file itself does not +reside on the filesystem--only the template. To view the generated config file, open it +in a web browser. If the config file is XML, Firefox should display it. Some browsers +will require viewing the source of the page requested. + +A default mime-type for the profile can be defined by setting \textbf{mime-type}. If a +custom variable is required for a template, it can be specified with \textbf{setvar}. +Variable substitution on this value is done while building the route list, so +\$\{USERNAME\} would expand to the username of the users.conf user that registers the +dynamic filename. + +NOTE: Any dialplan function that is used for generation of dynamic file names MUST be +loaded before res\_phoneprov. Add "preload $\Rightarrow$ modulename.so" to +\path{modules.conf} for required functions. In the example above, "preload $\Rightarrow$ +func\_strings.so" would be required. + +\section{Configuration of users.conf} + +The asterisk-gui sets up extensions, SIP/IAX2 peers, and a host of other settings. +User-specific settings are stored in users.conf. If the asterisk-gui is not being used, +manual entries to users.conf can be made. + +\subsection{The [general] section} + +There are only two settings in the general section of \path{users.conf} that apply to +phone provisioning: localextenlength which maps to template variable EXTENSION\_LENGTH +and \textbf{vmexten} which maps to the VOICEMAIL\_EXTEN variable. + +\subsection{Invdividual Users} + +To enable auto-provisioning of a phone, the user in \path{users.conf} needs to have: + +\begin{astlisting} +\begin{verbatim} +... +autoprov=yes +macaddress=deadbeef4dad +profile=polycom +\end{verbatim} +\end{astlisting} + +The profile is optional if a \textbf{default\_profile} is set in \path{phoneprov.conf}. +The following is a sample users.conf entry, with the template variables commented next to +the settings: + +\begin{astlisting} +\begin{verbatim} +[6001] +callwaiting = yes +context = numberplan-custom-1 +hasagent = no +hasdirectory = yes +hasiax = no +hasmanager = no +hassip = yes +hasvoicemail = yes +host = dynamic +mailbox = 6001 +threewaycalling = yes +deletevoicemail = no +autoprov = yes +profile = polycom +canreinvite = no +nat = no +fullname = User Two ; ${DISPLAY_NAME} +secret = test ; ${SECRET} +username = 6001 ; ${USERNAME} +macaddress = deadbeef4dad ; ${MAC} +label = 6001 ; ${LABEL} +cid_number = 6001 ; ${CALLERID} +\end{verbatim} +\end{astlisting} + +The variables above, are the user-specfic variables that can be substituted into dynamic +filenames and config templates. + +\section{Templates} + +Configuration templates are a generic way to configure phones with text-based +configuration files. Templates can use any loaded dialplan function and all of the +variables created by \path{phoneprov.conf} and \path{users.conf}. A short example is the +included 000000000000.cfg Polycom template: + +\begin{astlisting} +\begin{verbatim} +<?xml version="1.0" standalone="yes"?> + <APPLICATION + APP_FILE_PATH="sip.ld" + CONFIG_FILES="${IF($[${STAT(e|${CUSTOM_CONFIG})}] ? "custom.cfg, +")}config/${TOLOWER(${MAC})}, sip.cfg" + MISC_FILES="" LOG_FILE_DIRECTORY="" + /> +\end{verbatim} +\end{astlisting} + +This template uses dialplan functions, expressions, and a couple of variables to generate +a config file to instruct the Polycom where to pull other needed config files. If a phone +with MAC address 0xDEADBEEF4DAD requests this config file, and the filename that is +stored in variable CUSTOM\_CONFIG does not exist, then the generated output would be: + +\begin{astlisting} +\begin{verbatim} +<?xml version="1.0" standalone="yes"?> + <APPLICATION + APP_FILE_PATH="sip.ld" + CONFIG_FILES="config/deadbeef4dad, sip.cfg" + MISC_FILES="" LOG_FILE_DIRECTORY="" + /> +\end{verbatim} +\end{astlisting} + +The Polycom phone would then download both sip.cfg (which would be registered in +\path{phoneprov.conf} as a static file) and config/deadbeef4dad (which would be +registered as a dynamic file pointing to another template, polycom.xml). + +res\_phoneprov also registers its own dialplan function: PP\_EACH\_USER. This function +was designed to be able to print out a particular string for each user that +res\_phoneprov knows about. An example use of this function is the template for a Polycom +contact directory: + +\begin{astlisting} +\begin{verbatim} +<?xml version="1.0" standalone="yes"?> +<directory> + <item_list> + ${PP_EACH_USER(<item><fn>%{DISPLAY_NAME}</fn><ct>%{CALLERID}</ct><bw>1</bw></item>|${MAC})} + </item_list> +</directory> +\end{verbatim} +\end{astlisting} + +PP\_EACH\_USER takes two arguments. The first is the string to be printed for each user. +Any variables that are to be substituted need to be in the format \%\{VARNAME\} so that +Asterisk doesn't try to substitute the variable immediately before it is passed to +PP\_EACH\_USER. The second, optional, argument is a MAC address to exclude from the list +iterated over (so, in this case, a phone won't be listed in its own contact directory). + +\section{Putting it all together} + +Make sure that \path{manager.conf} has: + +\begin{astlisting} +\begin{verbatim} +[general] +enabled = yes +webenabled = yes +\end{verbatim} +\end{astlisting} + +and that \path{http.conf} has: + +\begin{astlisting} +\begin{verbatim} +[general] +enabled = yes +bindaddr = 192.168.1.1 ; Your IP here ;-) +bindport = 8088 ; Or port 80 if it is the only http server running on the machine +\end{verbatim} +\end{astlisting} + +With \path{phoneprov.conf} and \path{users.conf} in place, start Astersik. From the CLI, +type "http show status". An example output: +\begin{astlisting} +\begin{verbatim} +HTTP Server Status: +Prefix: /asterisk +Server Enabled and Bound to 192.168.1.1:8088 + +Enabled URI's: +/asterisk/httpstatus => Asterisk HTTP General Status +/asterisk/phoneprov/... => Asterisk HTTP Phone Provisioning Tool +/asterisk/manager => HTML Manager Event Interface +/asterisk/rawman => Raw HTTP Manager Event Interface +/asterisk/static/... => Asterisk HTTP Static Delivery +/asterisk/mxml => XML Manager Event Interface + +Enabled Redirects: + None. + +POST mappings: +None. +\end{verbatim} +\end{astlisting} + +There should be a phoneprov URI listed. Next, from the CLI, type "phoneprov show routes" +and verify that the information there is correct. An example output for Polycom phones +woud look like: + +\begin{astlisting} +\begin{verbatim} +Static routes + +Relative URI Physical location +sip.ver configs/sip.ver +sip.ld configs/sip.ld +bootrom.ver configs/bootrom.ver +sip.cfg configs/sip.cfg +bootrom.ld configs/bootrom.ld +custom.cfg configs/custom.cfg + +Dynamic routes + +Relative URI Template +deadbeef4dad.cfg 000000000000.cfg +deadbeef4dad-directory.xml 000000000000-directory.xml +deadbeef4dad-phone.cfg 000000000000-phone.cfg +config/deadbeef4dad polycom.xml +\end{verbatim} +\end{astlisting} + +With the above examples, the phones would be pointed to +\url{http://192.168.1.1:8080/asterisk/phoneprov} for pulling config files. Templates +would all be placed in AST\_DATA\_DIR/phoneprov and static files would be placed in +AST\_DATA\_DIR/phoneprov/configs. Examples of valid URIs would be: + +\begin{itemize} +\item http://192.168.1.1:8080/asterisk/phoneprov/sip.cfg +\item http://192.168.1.1:8080/asterisk/phoneprov/deadbeef4dad.cfg +\item http://192.168.1.1:8080/asterisk/phoneprov/config/deadbeef4dad +\end{itemize} + diff --git a/funcs/func_strings.c b/funcs/func_strings.c index da234949a..54730e108 100644 --- a/funcs/func_strings.c +++ b/funcs/func_strings.c @@ -30,6 +30,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include <regex.h> +#include <ctype.h> #include "asterisk/module.h" #include "asterisk/channel.h" @@ -800,6 +801,40 @@ static struct ast_custom_function keypadhash_function = { .desc = "Example: ${KEYPADHASH(Les)} returns \"537\"\n", }; +static int string_toupper(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) +{ + char *bufptr = buf, *dataptr = data; + + while ((bufptr < buf + len - 1) && (*bufptr++ = toupper(*dataptr++))); + + return 0; +} + +static struct ast_custom_function toupper_function = { + .name = "TOUPPER", + .synopsis = "Convert the string to upper case.", + .syntax = "TOUPPER(<string>)", + .read = string_toupper, + .desc = "Example: ${TOUPPER(Example)} returns \"EXAMPLE\"\n", +}; + +static int string_tolower(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) +{ + char *bufptr = buf, *dataptr = data; + + while ((bufptr < buf + len - 1) && (*bufptr++ = tolower(*dataptr++))); + + return 0; +} + +static struct ast_custom_function tolower_function = { + .name = "TOLOWER", + .synopsis = "Convert the string to lower case.", + .syntax = "TOLOWER(<string>)", + .read = string_tolower, + .desc = "Example: ${TOLOWER(Example)} returns \"example\"\n", +}; + static int unload_module(void) { int res = 0; @@ -818,6 +853,8 @@ static int unload_module(void) res |= ast_custom_function_unregister(&hashkeys_function); res |= ast_custom_function_unregister(&hash_function); res |= ast_unregister_application(app_clearhash); + res |= ast_custom_function_unregister(&toupper_function); + res |= ast_custom_function_unregister(&tolower_function); return res; } @@ -840,6 +877,8 @@ static int load_module(void) res |= ast_custom_function_register(&hashkeys_function); res |= ast_custom_function_register(&hash_function); res |= ast_register_application(app_clearhash, exec_clearhash, syn_clearhash, desc_clearhash); + res |= ast_custom_function_register(&toupper_function); + res |= ast_custom_function_register(&tolower_function); return res; } diff --git a/include/asterisk/localtime.h b/include/asterisk/localtime.h index 25dd92e34..dd92871e9 100644 --- a/include/asterisk/localtime.h +++ b/include/asterisk/localtime.h @@ -41,6 +41,7 @@ struct ast_tm { }; struct ast_tm *ast_localtime(const struct timeval *timep, struct ast_tm *p_tm, const char *zone); +void ast_get_dst_info(const time_t * const timep, int *dst_enabled, time_t *dst_start, time_t *dst_end, int *gmt_off, const char * const zone); struct timeval ast_mktime(struct ast_tm * const tmp, const char *zone); int ast_strftime(char *buf, size_t len, const char *format, const struct ast_tm *tm); diff --git a/main/acl.c b/main/acl.c index ff0d25d09..ed149b963 100644 --- a/main/acl.c +++ b/main/acl.c @@ -39,11 +39,6 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/lock.h" #include "asterisk/srv.h" -struct my_ifreq { - char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "eth0", "ppp0", etc. */ - struct sockaddr_in ifru_addr; -}; - /* Free HA structure */ void ast_free_ha(struct ast_ha *ha) { diff --git a/main/stdtime/localtime.c b/main/stdtime/localtime.c index c132e901c..2d60beefd 100644 --- a/main/stdtime/localtime.c +++ b/main/stdtime/localtime.c @@ -1144,6 +1144,121 @@ struct ast_tm *ast_localtime(const struct timeval *timep, struct ast_tm *tmp, co } /* +** This function provides informaton about daylight savings time +** for the given timezone. This includes whether it can determine +** if daylight savings is used for this timezone, the UTC times for +** when daylight savings transitions, and the offset in seconds from +** UTC. +*/ + +void ast_get_dst_info(const time_t * const timep, int *dst_enabled, time_t *dst_start, time_t *dst_end, int *gmt_off, const char * const zone) +{ + int i; + int transition1 = -1; + int transition2 = -1; + time_t seconds; + int bounds_exceeded = 0; + time_t t = *timep; + const struct state *sp; + + if (NULL == dst_enabled) + return; + *dst_enabled = 0; + + if (NULL == dst_start || NULL == dst_end || NULL == gmt_off) + return; + + *gmt_off = 0; + + sp = ast_tzset(zone); + if (NULL == sp) + return; + + /* If the desired time exceeds the bounds of the defined time transitions + * then give give up on determining DST info and simply look for gmt offset + * This requires that I adjust the given time using increments of Gregorian + * repeats to place the time within the defined time transitions in the + * timezone structure. + */ + if ((sp->goback && t < sp->ats[0]) || + (sp->goahead && t > sp->ats[sp->timecnt - 1])) { + time_t tcycles; + int_fast64_t icycles; + + if (t < sp->ats[0]) + seconds = sp->ats[0] - t; + else seconds = t - sp->ats[sp->timecnt - 1]; + --seconds; + tcycles = seconds / YEARSPERREPEAT / AVGSECSPERYEAR; + ++tcycles; + icycles = tcycles; + if (tcycles - icycles >= 1 || icycles - tcycles >= 1) + return; + seconds = icycles; + seconds *= YEARSPERREPEAT; + seconds *= AVGSECSPERYEAR; + if (t < sp->ats[0]) + t += seconds; + else + t -= seconds; + + if (t < sp->ats[0] || t > sp->ats[sp->timecnt - 1]) + return; /* "cannot happen" */ + + bounds_exceeded = 1; + } + + if (sp->timecnt == 0 || t < sp->ats[0]) { + /* I have no transition times or I'm before time */ + *dst_enabled = 0; + /* Find where I can get gmtoff */ + i = 0; + while (sp->ttis[i].tt_isdst) + if (++i >= sp->typecnt) { + i = 0; + break; + } + *gmt_off = sp->ttis[i].tt_gmtoff; + return; + } + + for (i = 1; i < sp->timecnt; ++i) { + if (t < sp->ats[i]) { + transition1 = sp->types[i - 1]; + transition2 = sp->types[i]; + break; + } + } + /* if I found transition times that do not bounded the given time and these correspond to + or the bounding zones do not reflect a changes in day light savings, then I do not have dst active */ + if (i >= sp->timecnt || 0 > transition1 || 0 > transition2 || + (sp->ttis[transition1].tt_isdst == sp->ttis[transition2].tt_isdst)) { + *dst_enabled = 0; + *gmt_off = sp->ttis[sp->types[sp->timecnt -1]].tt_gmtoff; + } else { + /* I have valid daylight savings information. */ + if(sp->ttis[transition2].tt_isdst) + *gmt_off = sp->ttis[transition1].tt_gmtoff; + else + *gmt_off = sp->ttis[transition2].tt_gmtoff; + + /* If I adjusted the time earlier, indicate that the dst is invalid */ + if (!bounds_exceeded) { + *dst_enabled = 1; + /* Determine which of the bounds is the start of daylight savings and which is the end */ + if(sp->ttis[transition2].tt_isdst) { + *dst_start = sp->ats[i]; + *dst_end = sp->ats[i -1]; + } else { + *dst_start = sp->ats[i -1]; + *dst_end = sp->ats[i]; + } + } + } + return; +} + +/* ** gmtsub is to gmtime as localsub is to localtime. */ diff --git a/phoneprov/000000000000-directory.xml b/phoneprov/000000000000-directory.xml new file mode 100644 index 000000000..a79e6eda0 --- /dev/null +++ b/phoneprov/000000000000-directory.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" standalone="yes"?> +<directory> + <item_list> + ${PP_EACH_USER(<item><fn>%{DISPLAY_NAME}</fn><ct>%{CALLERID}</ct><bw>1</bw></item>|${MAC})} + </item_list> +</directory> diff --git a/phoneprov/000000000000-phone.cfg b/phoneprov/000000000000-phone.cfg new file mode 100644 index 000000000..c04a5cb00 --- /dev/null +++ b/phoneprov/000000000000-phone.cfg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="yes"?> diff --git a/phoneprov/000000000000.cfg b/phoneprov/000000000000.cfg new file mode 100644 index 000000000..a1377382e --- /dev/null +++ b/phoneprov/000000000000.cfg @@ -0,0 +1,2 @@ +<?xml version="1.0" standalone="yes"?> + <APPLICATION APP_FILE_PATH="sip.ld" CONFIG_FILES="${IF($[${STAT(e|${CUSTOM_CONFIG})}] ? "custom.cfg, ")}config/${TOLOWER(${MAC})}, sip.cfg" MISC_FILES="" LOG_FILE_DIRECTORY=""/> diff --git a/phoneprov/polycom.xml b/phoneprov/polycom.xml new file mode 100644 index 000000000..05195a862 --- /dev/null +++ b/phoneprov/polycom.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<phone1> + <reg reg.1.displayName="${DISPLAY_NAME}" reg.1.address="${USERNAME}" reg.1.label="${LABEL}" reg.1.type="private" reg.1.thirdPartyName="" reg.1.auth.userId="${USERNAME}" reg.1.auth.password="${SECRET}" reg.1.server.1.address="${SERVER}" reg.1.server.1.port="${SERVER_PORT}" reg.1.server.1.transport="DNSnaptr" reg.1.server.1.expires="300" reg.1.server.1.register="1" reg.1.server.1.retryTimeOut="" reg.1.server.1.retryMaxCount="" reg.1.server.1.expires.lineSeize="" reg.1.acd-login-logout="0" reg.1.acd-agent-available="0" reg.1.ringType="2" reg.1.lineKeys="2" reg.1.callsPerLineKey="1"/> + <call> + <donotdisturb call.donotdisturb.perReg="1" /> + <autoOffHook call.autoOffHook.1.enabled="0" call.autoOffHook.1.contact="" call.autoOffHook.2.enabled="0" call.autoOffHook.2.contact="" call.autoOffHook.3.enabled="0" call.autoOffHook.3.contact="" call.autoOffHook.4.enabled="0" call.autoOffHook.4.contact="" call.autoOffHook.5.enabled="0" call.autoOffHook.5.contact="" call.autoOffHook.6.enabled="0" call.autoOffHook.6.contact=""/> + <serverMissedCall call.serverMissedCall.1.enabled="0" call.serverMissedCall.2.enabled="0" call.serverMissedCall.3.enabled="0" call.serverMissedCall.4.enabled="0" call.serverMissedCall.5.enabled="0" call.serverMissedCall.6.enabled="0"/> + </call> + <divert divert.1.contact="" divert.1.autoOnSpecificCaller="1" divert.1.sharedDisabled="1" divert.2.contact="" divert.2.autoOnSpecificCaller="1" divert.2.sharedDisabled="1" divert.3.contact="" divert.3.autoOnSpecificCaller="1" divert.3.sharedDisabled="1" divert.4.contact="" divert.4.autoOnSpecificCaller="1" divert.4.sharedDisabled="1" divert.5.contact="" divert.5.autoOnSpecificCaller="1" divert.5.sharedDisabled="1" divert.6.contact="" divert.6.autoOnSpecificCaller="1" divert.6.sharedDisabled="1"> + <fwd divert.fwd.1.enabled="1" divert.fwd.2.enabled="1" divert.fwd.3.enabled="1" divert.fwd.4.enabled="1" divert.fwd.5.enabled="1" divert.fwd.6.enabled="1"/> + <busy divert.busy.1.enabled="1" divert.busy.1.contact="" divert.busy.2.enabled="1" divert.busy.2.contact="" divert.busy.3.enabled="1" divert.busy.3.contact="" divert.busy.4.enabled="1" divert.busy.4.contact="" divert.busy.5.enabled="1" divert.busy.5.contact="" divert.busy.6.enabled="1" divert.busy.6.contact=""/> + <noanswer divert.noanswer.1.enabled="1" divert.noanswer.1.timeout="60" divert.noanswer.1.contact="" divert.noanswer.2.enabled="1" divert.noanswer.2.timeout="60" divert.noanswer.2.contact="" divert.noanswer.3.enabled="1" divert.noanswer.3.timeout="60" divert.noanswer.3.contact="" divert.noanswer.4.enabled="1" divert.noanswer.4.timeout="60" divert.noanswer.4.contact="" divert.noanswer.5.enabled="1" divert.noanswer.5.timeout="60" divert.noanswer.5.contact="" divert.noanswer.6.enabled="1" divert.noanswer.6.timeout="60" divert.noanswer.6.contact=""/> + <dnd divert.dnd.1.enabled="0" divert.dnd.1.contact="" divert.dnd.2.enabled="0" divert.dnd.2.contact="" divert.dnd.3.enabled="0" divert.dnd.3.contact="" divert.dnd.4.enabled="0" divert.dnd.4.contact="" divert.dnd.5.enabled="0" divert.dnd.5.contact="" divert.dnd.6.enabled="0" divert.dnd.6.contact=""/> + </divert> + <dialplan + dialplan.1.impossibleMatchHandling="2" dialplan.1.removeEndOfDial="0" + /> + <digitmap + dialplan.1.digitmap="" dialplan.1.digitmap.timeOut="3" + /> + <routing> + <server dialplan.1.routing.server.1.address="" dialplan.1.routing.server.1.port="" dialplan.2.routing.server.1.address="" dialplan.2.routing.server.1.port="" dialplan.3.routing.server.1.address="" dialplan.3.routing.server.1.port="" dialplan.4.routing.server.1.address="" dialplan.4.routing.server.1.port="" dialplan.5.routing.server.1.address="" dialplan.5.routing.server.1.port="" dialplan.6.routing.server.1.address="" dialplan.6.routing.server.1.port=""/> + <emergency dialplan.1.routing.emergency.1.value="" dialplan.1.routing.emergency.1.server.1="" dialplan.2.routing.emergency.1.value="" dialplan.2.routing.emergency.1.server.1="" dialplan.3.routing.emergency.1.value="" dialplan.3.routing.emergency.1.server.1="" dialplan.4.routing.emergency.1.value="" dialplan.4.routing.emergency.1.server.1="" dialplan.5.routing.emergency.1.value="" dialplan.5.routing.emergency.1.server.1="" dialplan.6.routing.emergency.1.value="" dialplan.6.routing.emergency.1.server.1=""/> + </routing> + <msg msg.bypassInstantMessage="1"> + <mwi msg.mwi.1.callBackMode="contact" msg.mwi.1.callBack="${VOICEMAIL_EXTEN}" /> + </msg> + <nat nat.ip="" nat.signalPort="" nat.mediaPortStart=""/> + <user_preferences up.oneTouchVoiceMail="1" up.welcomeSoundEnabled="0" /> + <volume voice.volume.persist.handset="1" voice.volume.persist.headset="1" /> + <SNTP tcpIpApp.sntp.address="time" tcpIpApp.sntp.gmtOffset="-21600" /> + <HTTPD httpd.enabled="1" /> + <feature + feature.1.name="presence" feature.1.enabled="1" + feature.8.name="calllist-missed" feature.8.enabled="1" + /> +</phone1> diff --git a/res/res_phoneprov.c b/res/res_phoneprov.c new file mode 100644 index 000000000..f03c4919c --- /dev/null +++ b/res/res_phoneprov.c @@ -0,0 +1,1012 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 1999 - 2008, Digium, Inc. + * + * Mark Spencer <markster@digium.com> + * Matthew Brooks <mbrooks@digium.com> + * Terry Wilson <twilson@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \Phone provisioning application for the asterisk internal http server + * + * \author Matthew Brooks <mbrooks@digium.com> + * \author Terry Wilson <twilson@digium.com> + */ + +#include "asterisk.h" + +#include <sys/ioctl.h> + +ASTERISK_FILE_VERSION(__FILE__, "$Revision: 96773 $") + +#include "asterisk/file.h" +#include "asterisk/paths.h" +#include "asterisk/pbx.h" +#include "asterisk/cli.h" +#include "asterisk/module.h" +#include "asterisk/http.h" +#include "asterisk/utils.h" +#include "asterisk/app.h" +#include "asterisk/strings.h" +#include "asterisk/stringfields.h" +#include "asterisk/options.h" +#include "asterisk/config.h" +#include "asterisk/acl.h" +#include "asterisk/astobj2.h" +#include "asterisk/version.h" + +#ifdef LOW_MEMORY +#define MAX_PROFILE_BUCKETS 1 +#define MAX_ROUTE_BUCKETS 1 +#else +#define MAX_PROFILE_BUCKETS 17 +#define MAX_ROUTE_BUCKETS 563 +#endif /* LOW_MEMORY */ + +#define VAR_BUF_SIZE 4096 + +/*! \brief for use in lookup_iface */ +struct my_ifreq { + char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "eth0", "ppp0", etc. */ + struct sockaddr_in ifru_addr; +}; + +/*! \brief for use in lookup_iface */ +static struct in_addr __ourip = { .s_addr = 0x00000000, }; + +/* \note This enum and the pp_variable_list must be in the same order or + * bad things happen! */ +enum pp_variables { + PP_MACADDRESS, + PP_USERNAME, + PP_FULLNAME, + PP_SECRET, + PP_LABEL, + PP_CALLERID, + PP_TIMEZONE, + PP_VAR_LIST_LENGTH, /* This entry must always be the last in the list */ +}; + +/*! \brief Lookup table to translate between users.conf property names and + * variables for use in phoneprov templates */ +static const struct pp_variable_lookup { + enum pp_variables id; + const char * const user_var; + const char * const template_var; +} pp_variable_list[] = { + { PP_MACADDRESS, "macaddress", "MAC" }, + { PP_USERNAME, "username", "USERNAME" }, + { PP_FULLNAME, "fullname", "DISPLAY_NAME" }, + { PP_SECRET, "secret", "SECRET" }, + { PP_LABEL, "label", "LABEL" }, + { PP_CALLERID, "cid_number", "CALLERID" }, + { PP_TIMEZONE, "timezone", "TIMEZONE" }, +}; + +/*! \brief structure to hold file data */ +struct phoneprov_file { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(format); /*!< After variable substitution, becomes route->uri */ + AST_STRING_FIELD(template); /*!< Template/physical file location */ + AST_STRING_FIELD(mime_type);/*!< Mime-type of the file */ + ); + AST_LIST_ENTRY(phoneprov_file) entry; +}; + +/*! \brief structure to hold phone profiles read from phoneprov.conf */ +struct phone_profile { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(name); /*!< Name of phone profile */ + AST_STRING_FIELD(default_mime_type); /*!< Default mime type if it isn't provided */ + AST_STRING_FIELD(staticdir); /*!< Subdirectory that static files are stored in */ + ); + struct varshead *headp; /*!< List of variables set with 'setvar' in phoneprov.conf */ + AST_LIST_HEAD_NOLOCK(, phoneprov_file) static_files; /*!< List of static files */ + AST_LIST_HEAD_NOLOCK(, phoneprov_file) dynamic_files; /*!< List of dynamic files */ +}; + +/*! \brief structure to hold users read from users.conf */ +struct user { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(name); /*!< Name of user */ + AST_STRING_FIELD(macaddress); /*!< Mac address of user's phone */ + ); + struct phone_profile *profile; /*!< Profile the phone belongs to */ + struct varshead *headp; /*!< List of variables to substitute into templates */ + AST_LIST_ENTRY(user) entry; +}; + +/*! \brief structure to hold http routes (valid URIs, and the files they link to) */ +struct http_route { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(uri); /*!< The URI requested */ + ); + struct phoneprov_file *file; /*!< The file that links to the URI */ + struct user *user; /*!< The user that has variables to substitute into the file + * NULL in the case of a static route */ +}; + +static struct ao2_container *profiles; +static struct ao2_container *http_routes; +AST_RWLIST_HEAD_STATIC(users, user); + +/*! \brief Extensions whose mime types we think we know */ +static struct { + char *ext; + char *mtype; +} mimetypes[] = { + { "png", "image/png" }, + { "xml", "text/xml" }, + { "jpg", "image/jpeg" }, + { "js", "application/x-javascript" }, + { "wav", "audio/x-wav" }, + { "mp3", "audio/mpeg" }, +}; + +char global_server[80] = ""; /*!< Server to substitute into templates */ +char global_serverport[6] = "5060"; /*!< Server port to substitute into templates */ +char global_default_profile[80] = ""; /*!< Default profile to use if one isn't specified */ + +/*! \brief List of global variables currently available: VOICEMAIL_EXTEN, EXTENSION_LENGTH */ +struct varshead global_variables; + +/*! \brief Return mime type based on extension */ +static char *ftype2mtype(const char *ftype) +{ + int x; + + if (ast_strlen_zero(ftype)) + return NULL; + + for (x = 0;x < ARRAY_LEN(mimetypes);x++) { + if (!strcasecmp(ftype, mimetypes[x].ext)) + return mimetypes[x].mtype; + } + + return NULL; +} + +/* iface is the interface (e.g. eth0); address is the return value */ +static int lookup_iface(const char *iface, struct in_addr *address) +{ + int mysock, res = 0; + struct my_ifreq ifreq; + + memset(&ifreq, 0, sizeof(ifreq)); + ast_copy_string(ifreq.ifrn_name, iface, sizeof(ifreq.ifrn_name)); + + mysock = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP); + if (mysock < 0) { + ast_log(LOG_ERROR, "Failed to create socket: %s\n", strerror(errno)); + return -1; + } + + res = ioctl(mysock, SIOCGIFADDR, &ifreq); + + close(mysock); + + if (res < 0) { + ast_log(LOG_WARNING, "Unable to get IP of %s: %s\n", iface, strerror(errno)); + memcpy(address, &__ourip, sizeof(__ourip)); + return -1; + } else { + memcpy(address, &ifreq.ifru_addr.sin_addr, sizeof(ifreq.ifru_addr.sin_addr)); + return 0; + } +} + +static struct phone_profile *unref_profile(struct phone_profile *prof) +{ + ao2_ref(prof, -1); + + return NULL; +} + +/*! \brief Return a phone profile looked up by name */ +static struct phone_profile *find_profile(const char *name) +{ + struct phone_profile tmp = { + .name = name, + }; + + return ao2_find(profiles, &tmp, OBJ_POINTER); +} + +static int profile_hash_fn(const void *obj, const int flags) +{ + const struct phone_profile *profile = obj; + + return ast_str_hash(profile->name); +} + +static int profile_cmp_fn(void *obj, void *arg, int flags) +{ + const struct phone_profile *profile1 = obj, *profile2 = arg; + + return !strcasecmp(profile1->name, profile2->name) ? CMP_MATCH : 0; +} + +static void delete_file(struct phoneprov_file *file) +{ + ast_string_field_free_memory(file); + free(file); +} + +static void profile_destructor(void *obj) +{ + struct phone_profile *profile = obj; + struct phoneprov_file *file; + struct ast_var_t *var; + + while ((file = AST_LIST_REMOVE_HEAD(&profile->static_files, entry))) + delete_file(file); + + while ((file = AST_LIST_REMOVE_HEAD(&profile->dynamic_files, entry))) + delete_file(file); + + while ((var = AST_LIST_REMOVE_HEAD(profile->headp, entries))) + ast_var_delete(var); + + free(profile->headp); + ast_string_field_free_memory(profile); +} + +static struct http_route *unref_route(struct http_route *route) +{ + ao2_ref(route, -1); + + return NULL; +} + +static int routes_hash_fn(const void *obj, const int flags) +{ + const struct http_route *route = obj; + + return ast_str_hash(route->uri); +} + +static int routes_cmp_fn(void *obj, void *arg, int flags) +{ + const struct http_route *route1 = obj, *route2 = arg; + + return !strcmp(route1->uri, route2->uri) ? CMP_MATCH : 0; +} + +static void route_destructor(void *obj) +{ + struct http_route *route = obj; + + ast_string_field_free_memory(route); +} + +/*! \brief Read a TEXT file into a string and return the length */ +static int load_file(const char *filename, char **ret) +{ + int len = 0; + FILE *f; + + if (!(f = fopen(filename, "r"))) { + *ret = NULL; + return -1; + } + + fseek(f, 0, SEEK_END); + len = ftell(f); + fseek(f, 0, SEEK_SET); + if (!(*ret = ast_malloc(len + 1))) + return -2; + + if (len != fread(*ret, sizeof(char), len, f)) { + free(*ret); + *ret = NULL; + return -3; + } + + fclose(f); + + (*ret)[len] = '\0'; + + return len; +} + +/*! \brief Callback that is executed everytime an http request is received by this module */ +static struct ast_str *phoneprov_callback(struct server_instance *ser, const char *uri, struct ast_variable *vars, int *status, char **title, int *contentlength) +{ + struct http_route *route; + struct http_route search_route = { + .uri = uri, + }; + struct ast_str *result = ast_str_create(512); + char path[PATH_MAX]; + char *file = NULL; + int len; + int fd; + char buf[256]; + struct timeval tv = ast_tvnow(); + struct ast_tm tm; + + if (!(route = ao2_find(http_routes, &search_route, OBJ_POINTER))) + goto out404; + + snprintf(path, sizeof(path), "%s/phoneprov/%s", ast_config_AST_DATA_DIR, route->file->template); + + if (!route->user) { /* Static file */ + + fd = open(path, O_RDONLY); + if (fd < 0) + goto out500; + + len = lseek(fd, 0, SEEK_END); + lseek(fd, 0, SEEK_SET); + if (len < 0) { + ast_log(LOG_WARNING, "Could not load file: %s (%d)\n", path, len); + close(fd); + goto out500; + } + + ast_strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S %Z", ast_localtime(&tv, &tm, "GMT")); + fprintf(ser->f, "HTTP/1.1 200 OK\r\n" + "Server: Asterisk/%s\r\n" + "Date: %s\r\n" + "Connection: close\r\n" + "Cache-Control: no-cache, no-store\r\n" + "Content-Length: %d\r\n" + "Content-Type: %s\r\n\r\n", + ast_get_version(), buf, len, route->file->mime_type); + + while ((len = read(fd, buf, sizeof(buf))) > 0) + fwrite(buf, 1, len, ser->f); + + close(fd); + route = unref_route(route); + return NULL; + } else { /* Dynamic file */ + int bufsize; + char *tmp; + + len = load_file(path, &file); + if (len < 0) { + ast_log(LOG_WARNING, "Could not load file: %s (%d)\n", path, len); + if (file) + ast_free(file); + goto out500; + } + + if (!file) + goto out500; + + /* XXX This is a hack -- maybe sum length of all variables in route->user->headp and add that? */ + bufsize = len + VAR_BUF_SIZE; + + /* malloc() instead of alloca() here, just in case the file is bigger than + * we have enough stack space for. */ + if (!(tmp = ast_calloc(1, bufsize))) { + if (file) + ast_free(file); + goto out500; + } + + pbx_substitute_variables_varshead(route->user->headp, file, tmp, bufsize); + + if (file) + ast_free(file); + + ast_str_append(&result, 0, + "Content-Type: %s\r\n" + "Content-length: %d\r\n" + "\r\n" + "%s", route->file->mime_type, (int) strlen(tmp), tmp); + + if (tmp) + ast_free(tmp); + + route = unref_route(route); + + return result; + } + +out404: + *status = 404; + *title = strdup("Not Found"); + *contentlength = 0; + return ast_http_error(404, "Not Found", NULL, "Nothing to see here. Move along."); + +out500: + route = unref_route(route); + *status = 500; + *title = strdup("Internal Server Error"); + *contentlength = 0; + return ast_http_error(500, "Internal Error", NULL, "An internal error has occured."); +} + +/*! \brief Build a route structure and add it to the list of available http routes + \param pp_file File to link to the route + \param user User to link to the route (NULL means static route) + \param uri URI of the route +*/ +static void build_route(struct phoneprov_file *pp_file, struct user *user, char *uri) +{ + struct http_route *route; + + if (!(route = ao2_alloc(sizeof(*route), route_destructor))) + return; + + if (ast_string_field_init(route, 32)) { + ast_log(LOG_ERROR, "Couldn't create string fields for %s\n", pp_file->format); + route = unref_route(route); + return; + } + + ast_string_field_set(route, uri, S_OR(uri, pp_file->format)); + route->user = user; + route->file = pp_file; + + ao2_link(http_routes, route); + + route = unref_route(route); +} + +/*! \brief Build a phone profile and add it to the list of phone profiles + \param name the name of the profile + \param v ast_variable from parsing phoneprov.conf +*/ +static void build_profile(const char *name, struct ast_variable *v) +{ + struct phone_profile *profile; + struct ast_var_t *var; + + if (!(profile = ao2_alloc(sizeof(*profile), profile_destructor))) + return; + + if (ast_string_field_init(profile, 32)) { + profile = unref_profile(profile); + return; + } + + if (!(profile->headp = ast_calloc(1, sizeof(*profile->headp)))) { + profile = unref_profile(profile); + return; + } + + AST_LIST_HEAD_INIT_NOLOCK(&profile->static_files); + AST_LIST_HEAD_INIT_NOLOCK(&profile->dynamic_files); + + ast_string_field_set(profile, name, name); + for (; v; v = v->next) { + if (!strcasecmp(v->name, "mime_type")) { + ast_string_field_set(profile, default_mime_type, v->value); + } else if (!strcasecmp(v->name, "setvar")) { + struct ast_var_t *var; + char *value_copy = ast_strdupa(v->value); + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(varname); + AST_APP_ARG(varval); + ); + + AST_NONSTANDARD_APP_ARGS(args, value_copy, '='); + do { + if (ast_strlen_zero(args.varname) || ast_strlen_zero(args.varval)) + break; + args.varname = ast_strip(args.varname); + args.varval = ast_strip(args.varval); + if (ast_strlen_zero(args.varname) || ast_strlen_zero(args.varval)) + break; + if ((var = ast_var_assign(args.varname, args.varval))) + AST_LIST_INSERT_TAIL(profile->headp, var, entries); + } while (0); + } else if (!strcasecmp(v->name, "staticdir")) { + ast_string_field_set(profile, staticdir, v->value); + } else { + struct phoneprov_file *pp_file; + char *file_extension; + char *value_copy = ast_strdupa(v->value); + + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(filename); + AST_APP_ARG(mimetype); + ); + + if (!(pp_file = ast_calloc(1, sizeof(*pp_file)))) { + profile = unref_profile(profile); + return; + } + if (ast_string_field_init(pp_file, 32)) { + ast_free(pp_file); + profile = unref_profile(profile); + return; + } + + if ((file_extension = strrchr(pp_file->format, '.'))) + file_extension++; + + AST_STANDARD_APP_ARGS(args, value_copy); + + /* Mime type order of preference + * 1) Specific mime-type defined for file in profile + * 2) Mime determined by extension + * 3) Default mime type specified in profile + * 4) text/plain + */ + ast_string_field_set(pp_file, mime_type, S_OR(args.mimetype, (S_OR(S_OR(ftype2mtype(file_extension), profile->default_mime_type), "text/plain")))); + + if (!strcasecmp(v->name, "static_file")) { + ast_string_field_set(pp_file, format, args.filename); + ast_string_field_build(pp_file, template, "%s%s", profile->staticdir, args.filename); + AST_LIST_INSERT_TAIL(&profile->static_files, pp_file, entry); + /* Add a route for the static files, as their filenames won't change per-user */ + build_route(pp_file, NULL, NULL); + } else { + ast_string_field_set(pp_file, format, v->name); + ast_string_field_set(pp_file, template, args.filename); + AST_LIST_INSERT_TAIL(&profile->dynamic_files, pp_file, entry); + } + } + } + + /* Append the global variables to the variables list for this profile. + * This is for convenience later, when we need to provide a single + * variable list for use in substitution. */ + AST_LIST_TRAVERSE(&global_variables, var, entries) { + struct ast_var_t *new_var; + if ((new_var = ast_var_assign(var->name, var->value))) + AST_LIST_INSERT_TAIL(profile->headp, new_var, entries); + } + + ao2_link(profiles, profile); + + profile = unref_profile(profile); +} + +/*! \brief Free all memory associated with a user */ +static void delete_user(struct user *user) +{ + struct ast_var_t *var; + + while ((var = AST_LIST_REMOVE_HEAD(user->headp, entries))) + ast_var_delete(var); + + ast_free(user->headp); + ast_string_field_free_memory(user); + user->profile = unref_profile(user->profile); + free(user); +} + +/*! \brief Destroy entire user list */ +static void delete_users(void) +{ + struct user *user; + + AST_RWLIST_WRLOCK(&users); + while ((user = AST_LIST_REMOVE_HEAD(&users, entry))) + delete_user(user); + AST_RWLIST_UNLOCK(&users); +} + +/*! \brief Set all timezone-related variables based on a zone (i.e. America/New_York) + \param headp pointer to list of user variables + \param zone A time zone. NULL sets variables based on timezone of the machine +*/ +static void set_timezone_variables(struct varshead *headp, const char *zone) +{ + time_t utc_time; + int dstenable; + time_t dststart; + time_t dstend; + struct ast_tm tm_info; + int tzoffset; + char buffer[21]; + struct ast_var_t *var; + struct timeval tv; + + time(&utc_time); + ast_get_dst_info(&utc_time, &dstenable, &dststart, &dstend, &tzoffset, zone); + snprintf(buffer, sizeof(buffer), "%d", tzoffset); + var = ast_var_assign("TZOFFSET", buffer); + if (var) + AST_LIST_INSERT_TAIL(headp, var, entries); + + if (!dstenable) + return; + + if ((var = ast_var_assign("DST_ENABLE", "1"))) + AST_LIST_INSERT_TAIL(headp, var, entries); + + tv.tv_sec = dststart; + ast_localtime(&tv, &tm_info, zone); + + snprintf(buffer, sizeof(buffer), "%d", tm_info.tm_mon+1); + if ((var = ast_var_assign("DST_START_MONTH", buffer))) + AST_LIST_INSERT_TAIL(headp, var, entries); + + snprintf(buffer, sizeof(buffer), "%d", tm_info.tm_mday); + if ((var = ast_var_assign("DST_START_MDAY", buffer))) + AST_LIST_INSERT_TAIL(headp, var, entries); + + snprintf(buffer, sizeof(buffer), "%d", tm_info.tm_hour); + if ((var = ast_var_assign("DST_START_HOUR", buffer))) + AST_LIST_INSERT_TAIL(headp, var, entries); + + tv.tv_sec = dstend; + ast_localtime(&tv, &tm_info, zone); + + snprintf(buffer, sizeof(buffer), "%d", tm_info.tm_mon + 1); + if ((var = ast_var_assign("DST_END_MONTH", buffer))) + AST_LIST_INSERT_TAIL(headp, var, entries); + + snprintf(buffer, sizeof(buffer), "%d", tm_info.tm_mday); + if ((var = ast_var_assign("DST_END_MDAY", buffer))) + AST_LIST_INSERT_TAIL(headp, var, entries); + + snprintf(buffer, sizeof(buffer), "%d", tm_info.tm_hour); + if ((var = ast_var_assign("DST_END_HOUR", buffer))) + AST_LIST_INSERT_TAIL(headp, var, entries); +} + +/*! \brief Build and return a user structure based on gathered config data */ +static struct user *build_user(struct ast_config *cfg, const char *name, const char *mac, struct phone_profile *profile) +{ + struct user *user; + struct ast_var_t *var; + const char *tmp; + int i; + + if (!(user = ast_calloc(1, sizeof(*user)))) { + profile = unref_profile(profile); + return NULL; + } + + if (!(user->headp = ast_calloc(1, sizeof(*user->headp)))) { + profile = unref_profile(profile); + ast_free(user); + return NULL; + } + + if (ast_string_field_init(user, 32)) { + profile = unref_profile(profile); + delete_user(user); + return NULL; + } + + ast_string_field_set(user, name, name); + ast_string_field_set(user, macaddress, mac); + user->profile = profile; /* already ref counted by find_profile */ + + for (i = 0; i < PP_VAR_LIST_LENGTH; i++) { + tmp = ast_variable_retrieve(cfg, name, pp_variable_list[i].user_var); + + if (i == PP_TIMEZONE) { + /* perfectly ok if tmp is NULL, will set variables based on server's time zone */ + set_timezone_variables(user->headp, tmp); + } + + if (tmp && (var = ast_var_assign(pp_variable_list[i].template_var, tmp))) + AST_LIST_INSERT_TAIL(user->headp, var, entries); + } + + if (!ast_strlen_zero(global_server)) { + if ((var = ast_var_assign("SERVER", global_server))) + AST_LIST_INSERT_TAIL(user->headp, var, entries); + if (!ast_strlen_zero(global_serverport)) { + if ((var = ast_var_assign("SERVER_PORT", global_serverport))) + AST_LIST_INSERT_TAIL(user->headp, var, entries); + } + } + + /* Append profile variables here, and substitute variables on profile + * setvars, so that we can use user specific variables in them */ + AST_LIST_TRAVERSE(user->profile->headp, var, entries) { + char expand_buf[VAR_BUF_SIZE] = {0,}; + struct ast_var_t *var2; + + pbx_substitute_variables_varshead(user->headp, var->value, expand_buf, sizeof(expand_buf)); + if ((var2 = ast_var_assign(var->name, expand_buf))) + AST_LIST_INSERT_TAIL(user->headp, var2, entries); + } + + return user; +} + +/*! \brief Add an http route for dynamic files attached to the profile of the user */ +static int build_user_routes(struct user *user) +{ + struct phoneprov_file *pp_file; + + AST_LIST_TRAVERSE(&user->profile->dynamic_files, pp_file, entry) { + char expand_buf[VAR_BUF_SIZE] = { 0, }; + + pbx_substitute_variables_varshead(user->headp, pp_file->format, expand_buf, sizeof(expand_buf)); + build_route(pp_file, user, expand_buf); + } + + return 0; +} + +/* \brief Parse config files and create appropriate structures */ +static int set_config(void) +{ + struct ast_config *phoneprov_cfg, *users_cfg; + char *cat; + struct ast_variable *v; + struct ast_flags config_flags = { 0 }; + + if (!(phoneprov_cfg = ast_config_load("phoneprov.conf", config_flags))) { + ast_log(LOG_ERROR, "Unable to load config phoneprov.conf\n"); + return -1; + } + + cat = NULL; + while ((cat = ast_category_browse(phoneprov_cfg, cat))) { + if (!strcasecmp(cat, "general")) { + for (v = ast_variable_browse(phoneprov_cfg, cat); v; v = v->next) { + if (!strcasecmp(v->name, "serveraddr")) + ast_copy_string(global_server, v->value, sizeof(global_server)); + else if (!strcasecmp(v->name, "serveriface")) { + struct in_addr addr; + lookup_iface(v->value, &addr); + ast_copy_string(global_server, ast_inet_ntoa(addr), sizeof(global_server)); + } else if (!strcasecmp(v->name, "serverport")) + ast_copy_string(global_serverport, v->value, sizeof(global_serverport)); + else if (!strcasecmp(v->name, "default_profile")) + ast_copy_string(global_default_profile, v->value, sizeof(global_default_profile)); + } + if (ast_strlen_zero(global_server)) + ast_log(LOG_WARNING, "No serveraddr/serveriface set in phoneprov.conf. Breakage likely.\n"); + } else + build_profile(cat, ast_variable_browse(phoneprov_cfg, cat)); + } + + ast_config_destroy(phoneprov_cfg); + + if (!(users_cfg = ast_config_load("users.conf", config_flags))) { + ast_log(LOG_WARNING, "Unable to load users.cfg\n"); + return 0; + } + + cat = NULL; + while ((cat = ast_category_browse(users_cfg, cat))) { + const char *tmp, *mac; + struct user *user; + struct phone_profile *profile; + struct ast_var_t *var; + + if (!strcasecmp(cat, "general")) { + for (v = ast_variable_browse(users_cfg, cat); v; v = v->next) { + if (!strcasecmp(v->name, "vmexten")) { + if ((var = ast_var_assign("VOICEMAIL_EXTEN", v->value))) + AST_LIST_INSERT_TAIL(&global_variables, var, entries); + } + if (!strcasecmp(v->name, "localextenlength")) { + if ((var = ast_var_assign("EXTENSION_LENGTH", v->value))) + AST_LIST_INSERT_TAIL(&global_variables, var, entries); + } + } + } + + if (!strcasecmp(cat, "authentication")) + continue; + + if (!((tmp = ast_variable_retrieve(users_cfg, cat, "autoprov")) && ast_true(tmp))) + continue; + + if (!(mac = ast_variable_retrieve(users_cfg, cat, "macaddress"))) { + ast_log(LOG_WARNING, "autoprov set for %s, but no mac address - skipping.\n", cat); + continue; + } + + tmp = S_OR(ast_variable_retrieve(users_cfg, cat, "profile"), global_default_profile); + if (ast_strlen_zero(tmp)) { + ast_log(LOG_WARNING, "No profile for user [%s] with mac '%s' - skipping\n", cat, mac); + continue; + } + + if (!(profile = find_profile(tmp))) { + ast_log(LOG_WARNING, "Could not look up profile '%s' - skipping.\n", tmp); + continue; + } + + if (!(user = build_user(users_cfg, cat, mac, profile))) { + ast_log(LOG_WARNING, "Could not create user %s - skipping.\n", cat); + continue; + } + + if (build_user_routes(user)) { + ast_log(LOG_WARNING, "Could not create http routes for %s - skipping\n", user->name); + delete_user(user); + continue; + } + + AST_RWLIST_WRLOCK(&users); + AST_RWLIST_INSERT_TAIL(&users, user, entry); + AST_RWLIST_UNLOCK(&users); + } + + ast_config_destroy(users_cfg); + + return 0; +} + +/*! \brief Delete all http routes, freeing their memory */ +static void delete_routes(void) +{ + struct ao2_iterator i; + struct http_route *route; + + i = ao2_iterator_init(http_routes, 0); + while ((route = ao2_iterator_next(&i))) { + ao2_unlink(http_routes, route); + route = unref_route(route); + } +} + +/*! \brief Delete all phone profiles, freeing their memory */ +static void delete_profiles(void) +{ + struct ao2_iterator i; + struct phone_profile *profile; + + i = ao2_iterator_init(profiles, 0); + while ((profile = ao2_iterator_next(&i))) { + ao2_unlink(profiles, profile); + profile = unref_profile(profile); + } +} + +/*! \brief A dialplan function that can be used to print a string for each phoneprov user */ +static int pp_each_user_exec(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) +{ + char *tmp, expand_buf[VAR_BUF_SIZE] = {0,}; + struct user *user; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(string); + AST_APP_ARG(exclude_mac); + ); + AST_STANDARD_APP_ARGS(args, data); + + /* Fix data by turning %{ into ${ */ + while ((tmp = strstr(args.string, "%{"))) + *tmp = '$'; + + AST_RWLIST_RDLOCK(&users); + AST_RWLIST_TRAVERSE(&users, user, entry) { + if (!ast_strlen_zero(args.exclude_mac) && !strcasecmp(user->macaddress, args.exclude_mac)) + continue; + pbx_substitute_variables_varshead(user->headp, args.string, expand_buf, sizeof(expand_buf)); + ast_build_string(&buf, &len, expand_buf); + } + AST_RWLIST_UNLOCK(&users); + + return 0; +} + +static struct ast_custom_function pp_each_user_function = { + .name = "PP_EACH_USER", + .synopsis = "Generate a string for each phoneprov user", + .syntax = "PP_EACH_USER(<string>|<exclude_mac>)", + .desc = + "Pass in a string, with phoneprov variables you want substituted in the format of\n" + "%{VARNAME}, and you will get the string rendered for each user in phoneprov\n" + "excluding ones with MAC address <exclude_mac>. Probably not useful outside of\n" + "res_phoneprov.\n" + "\nExample: ${PP_EACH_USER(<item><fn>%{DISPLAY_NAME}</fn></item>|${MAC})", + .read = pp_each_user_exec, +}; + +/*! \brief CLI command to list static and dynamic routes */ +static char *handle_show_routes(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ +#define FORMAT "%-40.40s %-30.30s\n" + struct ao2_iterator i; + struct http_route *route; + + switch(cmd) { + case CLI_INIT: + e->command = "phoneprov show routes"; + e->usage = + "Usage: phoneprov show routes\n" + " Lists all registered phoneprov http routes.\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + /* This currently iterates over routes twice, but it is the only place I've needed + * to really separate static an dynamic routes, so I've just left it this way. */ + ast_cli(a->fd, "Static routes\n\n"); + ast_cli(a->fd, FORMAT, "Relative URI", "Physical location"); + i = ao2_iterator_init(http_routes, 0); + while ((route = ao2_iterator_next(&i))) { + if (!route->user) + ast_cli(a->fd, FORMAT, route->uri, route->file->template); + route = unref_route(route); + } + + ast_cli(a->fd, "\nDynamic routes\n\n"); + ast_cli(a->fd, FORMAT, "Relative URI", "Template"); + + i = ao2_iterator_init(http_routes, 0); + while ((route = ao2_iterator_next(&i))) { + if (route->user) + ast_cli(a->fd, FORMAT, route->uri, route->file->template); + route = unref_route(route); + } + + return CLI_SUCCESS; +} + +static struct ast_cli_entry pp_cli[] = { + AST_CLI_DEFINE(handle_show_routes, "Show registered phoneprov http routes"), +}; + +static struct ast_http_uri phoneprovuri = { + .callback = phoneprov_callback, + .description = "Asterisk HTTP Phone Provisioning Tool", + .uri = "phoneprov", + .has_subtree = 1, +}; + +static int load_module(void) +{ + profiles = ao2_container_alloc(MAX_PROFILE_BUCKETS, profile_hash_fn, profile_cmp_fn); + + http_routes = ao2_container_alloc(MAX_ROUTE_BUCKETS, routes_hash_fn, routes_cmp_fn); + + AST_LIST_HEAD_INIT_NOLOCK(&global_variables); + + ast_custom_function_register(&pp_each_user_function); + ast_cli_register_multiple(pp_cli, ARRAY_LEN(pp_cli)); + + set_config(); + ast_http_uri_link(&phoneprovuri); + + return 0; +} + +static int unload_module(void) +{ + struct ast_var_t *var; + + ast_http_uri_unlink(&phoneprovuri); + ast_custom_function_unregister(&pp_each_user_function); + ast_cli_unregister_multiple(pp_cli, ARRAY_LEN(pp_cli)); + + delete_routes(); + delete_users(); + delete_profiles(); + ao2_ref(profiles, -1); + ao2_ref(http_routes, -1); + + while ((var = AST_LIST_REMOVE_HEAD(&global_variables, entries))) + ast_var_delete(var); + + return 0; +} + +static int reload(void) +{ + delete_routes(); + delete_users(); + delete_profiles(); + set_config(); + + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "HTTP Phone Provisioning", + .load = load_module, + .unload = unload_module, + .reload = reload, + ); |