Added a new module, res_phoneprov, which allows auto-provisioning of phones
authorTerry Wilson <twilson@digium.com>
Wed, 9 Jan 2008 21:37:26 +0000 (21:37 +0000)
committerTerry Wilson <twilson@digium.com>
Wed, 9 Jan 2008 21:37:26 +0000 (21:37 +0000)
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.

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@97634 65c4cc65-6c06-0410-ace0-fbb531ad65f3

15 files changed:
CHANGES
Makefile
configs/modules.conf.sample
configs/phoneprov.conf.sample [new file with mode: 0644]
doc/tex/asterisk.tex
doc/tex/phoneprov.tex [new file with mode: 0644]
funcs/func_strings.c
include/asterisk/localtime.h
main/acl.c
main/stdtime/localtime.c
phoneprov/000000000000-directory.xml [new file with mode: 0644]
phoneprov/000000000000-phone.cfg [new file with mode: 0644]
phoneprov/000000000000.cfg [new file with mode: 0644]
phoneprov/polycom.xml [new file with mode: 0644]
res/res_phoneprov.c [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index ad60f60..3080c2f 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -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. 
index e1b96ad..19c385e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -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 ; \
index 8610dbc..0fee961 100644 (file)
@@ -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 (file)
index 0000000..3b82490
--- /dev/null
@@ -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
index ca0a0dc..0feb3d7 100644 (file)
@@ -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 (file)
index 0000000..d813a7f
--- /dev/null
@@ -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}
+
index da23494..54730e1 100644 (file)
@@ -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;
 }
index 25dd92e..dd92871 100644 (file)
@@ -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);
 
index ff0d25d..ed149b9 100644 (file)
@@ -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)
 {
index c132e90..2d60bee 100644 (file)
@@ -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 (file)
index 0000000..a79e6ed
--- /dev/null
@@ -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 (file)
index 0000000..c04a5cb
--- /dev/null
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="yes"?>
diff --git a/phoneprov/000000000000.cfg b/phoneprov/000000000000.cfg
new file mode 100644 (file)
index 0000000..a137738
--- /dev/null
@@ -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 (file)
index 0000000..05195a8
--- /dev/null
@@ -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 (file)
index 0000000..f03c491
--- /dev/null
@@ -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,
+       );