Fix checking for CONFIG_STATUS_FILEINVALID so that modules don't crash upon trying...
[asterisk/asterisk.git] / cdr / cdr_csv.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Mark Spencer <markster@digium.com>
7  *
8  * Includes code and algorithms from the Zapata library.
9  *
10  * See http://www.asterisk.org for more information about
11  * the Asterisk project. Please do not directly contact
12  * any of the maintainers of this project for assistance;
13  * the project provides a web site, mailing lists and IRC
14  * channels for your use.
15  *
16  * This program is free software, distributed under the terms of
17  * the GNU General Public License Version 2. See the LICENSE file
18  * at the top of the source tree.
19  */
20
21 /*! \file
22  *
23  * \brief Comma Separated Value CDR records.
24  *
25  * \author Mark Spencer <markster@digium.com>
26  * 
27  * \arg See also \ref AstCDR
28  * \ingroup cdr_drivers
29  */
30
31 #include "asterisk.h"
32
33 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
34
35 #include <time.h>
36
37 #include "asterisk/paths.h"     /* use ast_config_AST_LOG_DIR */
38 #include "asterisk/config.h"
39 #include "asterisk/channel.h"
40 #include "asterisk/cdr.h"
41 #include "asterisk/module.h"
42 #include "asterisk/utils.h"
43 #include "asterisk/lock.h"
44
45 #define CSV_LOG_DIR "/cdr-csv"
46 #define CSV_MASTER  "/Master.csv"
47
48 #define DATE_FORMAT "%Y-%m-%d %T"
49
50 static int usegmtime = 0;
51 static int loguniqueid = 0;
52 static int loguserfield = 0;
53 static int loaded = 0;
54 static char *config = "cdr.conf";
55
56 /* #define CSV_LOGUNIQUEID 1 */
57 /* #define CSV_LOGUSERFIELD 1 */
58
59 /*----------------------------------------------------
60   The values are as follows:
61
62
63   "accountcode",        accountcode is the account name of detail records, Master.csv contains all records *
64                         Detail records are configured on a channel basis, IAX and SIP are determined by user *
65                         DAHDI is determined by channel in dahdi.conf 
66   "source",
67   "destination",
68   "destination context", 
69   "callerid",
70   "channel",
71   "destination channel",        (if applicable)
72   "last application",   Last application run on the channel 
73   "last app argument",  argument to the last channel 
74   "start time", 
75   "answer time", 
76   "end time", 
77   duration,             Duration is the whole length that the entire call lasted. ie. call rx'd to hangup  
78                         "end time" minus "start time" 
79   billable seconds,     the duration that a call was up after other end answered which will be <= to duration  
80                         "end time" minus "answer time" 
81   "disposition",        ANSWERED, NO ANSWER, BUSY 
82   "amaflags",           DOCUMENTATION, BILL, IGNORE etc, specified on a per channel basis like accountcode. 
83   "uniqueid",           unique call identifier 
84   "userfield"           user field set via SetCDRUserField 
85 ----------------------------------------------------------*/
86
87 static char *name = "csv";
88
89 AST_MUTEX_DEFINE_STATIC(mf_lock);
90 AST_MUTEX_DEFINE_STATIC(acf_lock);
91
92 static int load_config(int reload)
93 {
94         struct ast_config *cfg;
95         struct ast_variable *var;
96         const char *tmp;
97         struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
98
99         usegmtime = 0;
100         loguniqueid = 0;
101         loguserfield = 0;
102
103         if (!(cfg = ast_config_load(config, config_flags)) || cfg == CONFIG_STATUS_FILEINVALID) {
104                 ast_log(LOG_WARNING, "unable to load config: %s\n", config);
105                 return 0;
106         } else if (cfg == CONFIG_STATUS_FILEUNCHANGED)
107                 return 1;
108
109         if (!(var = ast_variable_browse(cfg, "csv"))) {
110                 ast_config_destroy(cfg);
111                 return 0;
112         }
113         
114         if ((tmp = ast_variable_retrieve(cfg, "csv", "usegmtime"))) {
115                 usegmtime = ast_true(tmp);
116                 if (usegmtime)
117                         ast_debug(1, "logging time in GMT\n");
118         }
119
120         if ((tmp = ast_variable_retrieve(cfg, "csv", "loguniqueid"))) {
121                 loguniqueid = ast_true(tmp);
122                 if (loguniqueid)
123                         ast_debug(1, "logging CDR field UNIQUEID\n");
124         }
125
126         if ((tmp = ast_variable_retrieve(cfg, "csv", "loguserfield"))) {
127                 loguserfield = ast_true(tmp);
128                 if (loguserfield)
129                         ast_debug(1, "logging CDR user-defined field\n");
130         }
131
132         ast_config_destroy(cfg);
133         return 1;
134 }
135
136 static int append_string(char *buf, char *s, size_t bufsize)
137 {
138         int pos = strlen(buf), spos = 0, error = -1;
139
140         if (pos >= bufsize - 4)
141                 return -1;
142
143         buf[pos++] = '\"';
144
145         while(pos < bufsize - 3) {
146                 if (!s[spos]) {
147                         error = 0;
148                         break;
149                 }
150                 if (s[spos] == '\"')
151                         buf[pos++] = '\"';
152                 buf[pos++] = s[spos];
153                 spos++;
154         }
155
156         buf[pos++] = '\"';
157         buf[pos++] = ',';
158         buf[pos++] = '\0';
159
160         return error;
161 }
162
163 static int append_int(char *buf, int s, size_t bufsize)
164 {
165         char tmp[32];
166         int pos = strlen(buf);
167
168         snprintf(tmp, sizeof(tmp), "%d", s);
169
170         if (pos + strlen(tmp) > bufsize - 3)
171                 return -1;
172
173         strncat(buf, tmp, bufsize - strlen(buf) - 1);
174         pos = strlen(buf);
175         buf[pos++] = ',';
176         buf[pos++] = '\0';
177
178         return 0;
179 }
180
181 static int append_date(char *buf, struct timeval when, size_t bufsize)
182 {
183         char tmp[80] = "";
184         struct ast_tm tm;
185
186         if (strlen(buf) > bufsize - 3)
187                 return -1;
188
189         if (ast_tvzero(when)) {
190                 strncat(buf, ",", bufsize - strlen(buf) - 1);
191                 return 0;
192         }
193
194         ast_localtime(&when, &tm, usegmtime ? "GMT" : NULL);
195         ast_strftime(tmp, sizeof(tmp), DATE_FORMAT, &tm);
196
197         return append_string(buf, tmp, bufsize);
198 }
199
200 static int build_csv_record(char *buf, size_t bufsize, struct ast_cdr *cdr)
201 {
202
203         buf[0] = '\0';
204         /* Account code */
205         append_string(buf, cdr->accountcode, bufsize);
206         /* Source */
207         append_string(buf, cdr->src, bufsize);
208         /* Destination */
209         append_string(buf, cdr->dst, bufsize);
210         /* Destination context */
211         append_string(buf, cdr->dcontext, bufsize);
212         /* Caller*ID */
213         append_string(buf, cdr->clid, bufsize);
214         /* Channel */
215         append_string(buf, cdr->channel, bufsize);
216         /* Destination Channel */
217         append_string(buf, cdr->dstchannel, bufsize);
218         /* Last Application */
219         append_string(buf, cdr->lastapp, bufsize);
220         /* Last Data */
221         append_string(buf, cdr->lastdata, bufsize);
222         /* Start Time */
223         append_date(buf, cdr->start, bufsize);
224         /* Answer Time */
225         append_date(buf, cdr->answer, bufsize);
226         /* End Time */
227         append_date(buf, cdr->end, bufsize);
228         /* Duration */
229         append_int(buf, cdr->duration, bufsize);
230         /* Billable seconds */
231         append_int(buf, cdr->billsec, bufsize);
232         /* Disposition */
233         append_string(buf, ast_cdr_disp2str(cdr->disposition), bufsize);
234         /* AMA Flags */
235         append_string(buf, ast_cdr_flags2str(cdr->amaflags), bufsize);
236         /* Unique ID */
237         if (loguniqueid)
238                 append_string(buf, cdr->uniqueid, bufsize);
239         /* append the user field */
240         if(loguserfield)
241                 append_string(buf, cdr->userfield,bufsize);     
242         /* If we hit the end of our buffer, log an error */
243         if (strlen(buf) < bufsize - 5) {
244                 /* Trim off trailing comma */
245                 buf[strlen(buf) - 1] = '\0';
246                 strncat(buf, "\n", bufsize - strlen(buf) - 1);
247                 return 0;
248         }
249         return -1;
250 }
251
252 static int writefile(char *s, char *acc)
253 {
254         char tmp[PATH_MAX];
255         FILE *f;
256
257         if (strchr(acc, '/') || (acc[0] == '.')) {
258                 ast_log(LOG_WARNING, "Account code '%s' insecure for writing file\n", acc);
259                 return -1;
260         }
261
262         snprintf(tmp, sizeof(tmp), "%s/%s/%s.csv", ast_config_AST_LOG_DIR,CSV_LOG_DIR, acc);
263
264         ast_mutex_lock(&acf_lock);
265         if (!(f = fopen(tmp, "a"))) {
266                 ast_mutex_unlock(&acf_lock);
267                 ast_log(LOG_ERROR, "Unable to open file %s : %s\n", tmp, strerror(errno));
268                 return -1;
269         }
270         fputs(s, f);
271         fflush(f);
272         fclose(f);
273         ast_mutex_unlock(&acf_lock);
274
275         return 0;
276 }
277
278
279 static int csv_log(struct ast_cdr *cdr)
280 {
281         FILE *mf = NULL;
282         /* Make sure we have a big enough buf */
283         char buf[1024];
284         char csvmaster[PATH_MAX];
285         snprintf(csvmaster, sizeof(csvmaster),"%s/%s/%s", ast_config_AST_LOG_DIR, CSV_LOG_DIR, CSV_MASTER);
286 #if 0
287         printf("[CDR] %s ('%s' -> '%s') Dur: %ds Bill: %ds Disp: %s Flags: %s Account: [%s]\n", cdr->channel, cdr->src, cdr->dst, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), ast_cdr_flags2str(cdr->amaflags), cdr->accountcode);
288 #endif
289         if (build_csv_record(buf, sizeof(buf), cdr)) {
290                 ast_log(LOG_WARNING, "Unable to create CSV record in %d bytes.  CDR not recorded!\n", (int)sizeof(buf));
291                 return 0;
292         }
293         
294         /* because of the absolutely unconditional need for the
295            highest reliability possible in writing billing records,
296            we open write and close the log file each time */
297         ast_mutex_lock(&mf_lock);
298         if ((mf = fopen(csvmaster, "a"))) {
299                 fputs(buf, mf);
300                 fflush(mf); /* be particularly anal here */
301                 fclose(mf);
302                 mf = NULL;
303                 ast_mutex_unlock(&mf_lock);
304         } else {
305                 ast_mutex_unlock(&mf_lock);
306                 ast_log(LOG_ERROR, "Unable to re-open master file %s : %s\n", csvmaster, strerror(errno));
307         }
308         
309         if (!ast_strlen_zero(cdr->accountcode)) {
310                 if (writefile(buf, cdr->accountcode))
311                         ast_log(LOG_WARNING, "Unable to write CSV record to account file '%s' : %s\n", cdr->accountcode, strerror(errno));
312         }
313
314         return 0;
315 }
316
317 static int unload_module(void)
318 {
319         ast_cdr_unregister(name);
320         loaded = 0;
321         return 0;
322 }
323
324 static int load_module(void)
325 {
326         int res;
327         
328         if(!load_config(0))
329                 return AST_MODULE_LOAD_DECLINE;
330
331         if ((res = ast_cdr_register(name, ast_module_info->description, csv_log))) {
332                 ast_log(LOG_ERROR, "Unable to register CSV CDR handling\n");
333         } else {
334                 loaded = 1;
335         }
336         return res;
337 }
338
339 static int reload(void)
340 {
341         if (load_config(1)) {
342                 loaded = 1;
343         } else {
344                 loaded = 0;
345                 ast_log(LOG_WARNING, "No [csv] section in cdr.conf.  Unregistering backend.\n");
346                 ast_cdr_unregister(name);
347         }
348
349         return 0;
350 }
351
352 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "Comma Separated Values CDR Backend",
353                 .load = load_module,
354                 .unload = unload_module,
355                 .reload = reload,
356                );