1 /* spmfilter - mail filtering framework
2  * Copyright (C) 2009-2013 Sebastian Jaekel, Axel Steiner and SpaceNet AG
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 3 of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #define _GNU_SOURCE
19 
20 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <unistd.h>
26 #include <errno.h>
27 #include <fcntl.h>
28 #include <dlfcn.h>
29 
30 #include "smf_modules.h"
31 #include "smf_header.h"
32 #include "smf_envelope.h"
33 #include "smf_message.h"
34 #include "smf_nexthop.h"
35 #include "smf_trace.h"
36 #include "smf_internal.h"
37 #include "smf_dict.h"
38 #include "smf_smtp.h"
39 
40 #define THIS_MODULE "modules"
41 
message_file_mtime(SMFSession_T * session)42 static time_t message_file_mtime(SMFSession_T *session) {
43   struct stat fstat;
44 
45   if (stat(session->message_file, &fstat) != 0) {
46     STRACE(TRACE_ERR, session->id, "%s: %s", session->message_file, strerror(errno));
47     return 0; /* Ignore... */
48   }
49 
50   return fstat.st_mtime;
51 }
52 
_header_destroy(void * data)53 void _header_destroy(void *data) {
54     SMFHeader_T *h = (SMFHeader_T *)data;
55     smf_header_free(h);
56 }
57 
_copy_header(SMFListElem_T * elem)58 void *_copy_header(SMFListElem_T *elem) {
59     SMFHeader_T *o = (SMFHeader_T *)smf_list_data(elem);
60     SMFHeader_T *n = smf_header_new();
61     char *s = NULL;
62     int i;
63 
64     s = smf_header_get_name(o);
65     smf_header_set_name(n, s);
66 
67     for(i=0; i < smf_header_get_count(o); i++) {
68         s = smf_header_get_value(o, i);
69         smf_header_set_value(n, s, 0);
70     }
71 
72     return n;
73 }
74 
smf_modules_engine_load(SMFSettings_T * settings)75 int smf_modules_engine_load(SMFSettings_T *settings) {
76     void *module = NULL;
77     LoadEngine load_engine = NULL;
78     char *engine_path = NULL;
79     char *error = NULL;
80     int ret;
81 
82     /* check if engine module starts with lib */
83     if (settings->lib_dir != NULL)
84         engine_path = smf_internal_build_module_path(settings->lib_dir, settings->engine);
85     else
86         engine_path = smf_internal_build_module_path(LIB_DIR, settings->engine);
87 
88     if ((module = dlopen(engine_path, RTLD_LAZY)) == NULL) {
89         TRACE(TRACE_ERR,"failed to load library [%s]: %s", engine_path,dlerror());
90         free(engine_path);
91         return -1;
92     }
93     dlerror();
94 
95     load_engine = dlsym(module,"load");
96     if ((error = dlerror()) != NULL)  {
97         TRACE(TRACE_ERR,"library error: %s", error);
98         free(error);
99         free(engine_path);
100         return -1;
101     }
102 
103     ret = load_engine(settings);
104 
105     if (dlclose(module) != 0) {
106         TRACE(TRACE_ERR,"failed to unload module [%s]",engine_path);
107     }
108 
109     free(engine_path);
110 
111     return ret;
112 }
113 
114 /* initialize the processing queue */
smf_modules_pqueue_init(int (* loaderr)(SMFSettings_T * settings,SMFSession_T * session),int (* processerr)(SMFSettings_T * settings,SMFSession_T * session,int retval),int (* nhoperr)(SMFSettings_T * settings,SMFSession_T * session))115 SMFProcessQueue_T *smf_modules_pqueue_init(
116         int (*loaderr)(SMFSettings_T *settings, SMFSession_T *session),
117         int (*processerr)(SMFSettings_T *settings, SMFSession_T *session, int retval),
118         int (*nhoperr)(SMFSettings_T *settings, SMFSession_T *session)) {
119     SMFProcessQueue_T *q;
120 
121     q = (SMFProcessQueue_T *)calloc(1, sizeof(SMFProcessQueue_T));
122     if(q == NULL) {
123         TRACE(TRACE_ERR, "failed to allocate memory for process queue!");
124         return(NULL);
125     }
126 
127     q->load_error = loaderr;
128     q->processing_error = processerr;
129     q->nexthop_error = nhoperr;
130     return q;
131 }
132 
133 /* build full filename to modules states dir */
smf_modules_stf_path(SMFSettings_T * settings,SMFSession_T * session)134 static char *smf_modules_stf_path(SMFSettings_T *settings, SMFSession_T *session) {
135     char *hex = NULL;
136     char *mid = NULL;
137     char *buf = NULL;
138     SMFMessage_T *msg = NULL;
139     msg = smf_envelope_get_message(session->envelope);
140 
141     mid = smf_message_get_message_id(msg);
142     hex = smf_core_md5sum(mid);
143     asprintf(&buf,"%s/%s.%s.modules", settings->queue_dir, session->id, hex);
144     free(hex);
145 
146     return(buf);
147 }
148 
149 /* load the list of processed modules from file */
smf_modules_stf_processed_modules(FILE * fh)150 static SMFDict_T *smf_modules_stf_processed_modules(FILE *fh) {
151     SMFDict_T *d;
152     char *buf = NULL;
153     size_t n;
154     char **parts = NULL;
155     int nparts;
156 
157     d = smf_dict_new();
158     fseek(fh, 0, SEEK_SET); /* rewind the file */
159 
160     while(getline(&buf,&n,fh) >= 0) {
161         parts = smf_core_strsplit(buf, ":", &nparts);
162         assert(nparts == 2);
163         smf_dict_set(d, parts[0], parts[1]);
164     }
165 
166     free(buf);
167     if (parts != NULL) {
168         free(parts[0]);
169         free(parts[1]);
170         free(parts[2]);
171         free(parts);
172     }
173     return d;
174 }
175 
176 /* write an entry to the state file */
smf_modules_stf_write_entry(FILE * fh,char * mod)177 static int smf_modules_stf_write_entry(FILE *fh, char *mod) {
178     fprintf(fh, "%s:ok\n", mod);
179     return(0);
180 }
181 
smf_module_create(const char * name)182 SMFModule_T *smf_module_create(const char *name) {
183     return smf_module_create_callback(name, NULL);
184 }
185 
smf_module_create_handle(const char * name)186 static void *smf_module_create_handle(const char *name) {
187     struct stat fstat;
188     void *handle;
189     char *path;
190 
191     if (stat(name, &fstat) == 0 && S_ISREG(fstat.st_mode)) {
192         path = strdup(name);
193     } else if ((path = smf_internal_build_module_path(LIB_DIR, name)) == NULL) {
194         TRACE(TRACE_ERR, "failed to build module path for [%s]", name);
195         return NULL;
196     }
197 
198     if ((handle = dlopen(path, RTLD_LAZY)) == NULL) {
199         TRACE(TRACE_ERR, "failed to load module [%s]: %s", name, dlerror());
200         free(path);
201         return NULL;
202     }
203 
204     free(path);
205 
206     return handle;
207 }
208 
smf_module_create_callback(const char * name,ModuleLoadFunction callback)209 SMFModule_T *smf_module_create_callback(const char *name, ModuleLoadFunction callback) {
210     SMFModule_T *module;
211 
212     assert(name);
213 
214     if ((module = malloc(sizeof(SMFModule_T))) == NULL) {
215         return NULL;
216     }
217 
218     module->name = strdup(name);
219 
220     if (callback == NULL) {
221         module->type = 0;
222         module->u.handle = smf_module_create_handle(name);
223     } else {
224         module->type = 1;
225         module->u.callback = callback;
226     }
227 
228     TRACE(TRACE_DEBUG, "module %s loaded", name);
229 
230     return module;
231 }
232 
smf_module_destroy(SMFModule_T * module)233 int smf_module_destroy(SMFModule_T *module) {
234     int result = 0;
235 
236     assert(module);
237 
238     if (module->type == 0) {
239         if (module->u.handle != NULL && dlclose(module->u.handle) != 0) {
240             TRACE(TRACE_ERR, "failed to unload module [%s]", module->name);
241             result = -1;
242         }
243     }
244 
245     free(module->name);
246     free(module);
247 
248     return result;
249 }
250 
smf_module_invoke(SMFSettings_T * settings,SMFModule_T * module,SMFSession_T * session)251 int smf_module_invoke(SMFSettings_T *settings, SMFModule_T *module, SMFSession_T *session) {
252     ModuleLoadFunction runner;
253     time_t mtime_before, mtime_after;
254     int result;
255 
256     assert(module);
257     assert(session);
258 
259     if (module->type == 0) {
260         dlerror(); // Clear any errors
261         if ((runner = dlsym(module->u.handle, "load")) == NULL) {
262             TRACE(TRACE_ERR, "failed to locate 'load'-symbol in module '%s': %s",
263                   module->name, dlerror());
264               return -1;
265           }
266     } else {
267         runner = module->u.callback;
268     }
269 
270     if (session->message_file != NULL)
271       mtime_before = message_file_mtime(session);
272 
273     result = runner(settings,session);
274 
275     if (result == 0 && session->message_file != NULL) {
276       mtime_after = message_file_mtime(session);
277 
278       if (mtime_after > mtime_before) {
279         // Spoolfile has change. Reload the message inside the session
280         SMFMessage_T *message_new = smf_message_new();
281         result = smf_message_from_file(&message_new, session->message_file, 0);
282 
283         if (result == 0) {
284           smf_message_free(session->envelope->message);
285           session->envelope->message = message_new;
286         }
287       }
288     }
289 
290     return result;
291 }
292 
smf_modules_process(SMFProcessQueue_T * q,SMFSession_T * session,SMFSettings_T * settings)293 int smf_modules_process(
294         SMFProcessQueue_T *q, SMFSession_T *session, SMFSettings_T *settings) {
295     FILE *stfh = NULL;
296     char *stf_filename = NULL;
297     SMFDict_T *modlist;
298     SMFMessage_T *msg = NULL;
299     SMFList_T *initial_headers = NULL;
300     SMFListElem_T *elem = NULL;
301     SMFModule_T *curmod;
302     int ret = 0;
303     int mod_count;
304     char *header = NULL;
305     NexthopFunction nexthop;
306 
307     /* initialize message file  and load processed modules */
308     stf_filename = smf_modules_stf_path(settings,session);
309 
310     stfh = fopen(stf_filename, "a+");
311     if(stfh == NULL) {
312         STRACE(TRACE_ERR, session->id, "failed to open message state file %s: %s (%d)", stf_filename, strerror(errno),errno);
313 
314         if(stf_filename != NULL)
315             free(stf_filename);
316 
317         return -1;
318     }
319 
320     if (smf_list_new(&initial_headers,_header_destroy) != 0) {
321         STRACE(TRACE_ERR,session->id, "failed to create header list");
322         free(stf_filename);
323         fclose(stfh);
324         return -1;
325     }
326 
327     msg = smf_envelope_get_message(session->envelope);
328     elem = smf_list_head(msg->headers);
329     while(elem != NULL) {
330         smf_list_append(initial_headers,_copy_header(elem));
331         elem = elem->next;
332     }
333 
334     if (settings->add_header == 1)
335         asprintf(&header,"X-Spmfilter: ");
336 
337     /* fetch user data */
338     if (smf_internal_fetch_user_data(settings,session) != 0)
339         STRACE(TRACE_ERR, session->id, "failed to load local user data");
340 
341     mod_count = 0;
342     modlist = smf_modules_stf_processed_modules(stfh);
343     elem = smf_list_head(settings->modules);
344     while(elem != NULL) {
345         curmod = (SMFModule_T *)smf_list_data(elem);
346         elem = elem->next;
347 
348         /* check if the module is in our modlist, if yes, the module has
349          * already been processed and can be skipped */
350         if(smf_dict_get(modlist, curmod->name) != NULL) {
351             STRACE(TRACE_INFO, session->id, "skipping module [%s]", curmod->name);
352             continue;
353         }
354 
355         STRACE(TRACE_DEBUG,session->id,"invoke module [%s]", curmod->name);
356         ret = smf_module_invoke(settings, curmod, session);
357 
358         if(ret != 0) {
359             ret = q->processing_error(settings,session,ret);
360 
361             if(ret == 0) {
362                 STRACE(TRACE_ERR, session->id, "module [%s] failed, stopping processing!", curmod->name);
363                 smf_dict_free(modlist);
364                 fclose(stfh);
365                 free(stf_filename);
366                 free(header);
367                 smf_list_free(initial_headers);
368                 return -1;
369             } else if(ret == 1) {
370                 STRACE(TRACE_WARNING, session->id, "module [%s] stopped processing!", curmod->name);
371                 smf_dict_free(modlist);
372                 fclose(stfh);
373                 if(unlink(stf_filename) != 0)
374                     STRACE(TRACE_ERR,session->id,"Failed to unlink state file [%s]", stf_filename);
375                 free(stf_filename);
376                 free(header);
377                 smf_list_free(initial_headers);
378                 return 1;
379             } else if(ret == 2) {
380                 STRACE(TRACE_DEBUG,session->id,"module [%s] stopped processing, turning to nexthop processing!",curmod->name);
381                 break;
382             }
383         } else {
384             STRACE(TRACE_DEBUG, session->id, "module [%s] finished successfully", curmod->name);
385             smf_modules_stf_write_entry(stfh, curmod->name);
386         }
387 
388         mod_count++;
389         if (settings->add_header == 1) {
390             if (mod_count == smf_list_size(settings->modules))
391                 smf_core_strcat_printf(&header, "%s", curmod->name);
392             else
393                 smf_core_strcat_printf(&header, "%s, ", curmod->name);
394         }
395     }
396 
397     /* close file, cleanup modlist and remove state file */
398     STRACE(TRACE_DEBUG, session->id,"module processing finished successfully.");
399     fclose(stfh);
400     smf_dict_free(modlist);
401 
402     if(unlink(stf_filename) != 0) {
403         STRACE(TRACE_ERR,session->id,"failed to unlink state file [%s]: %s (%d)", stf_filename,strerror(errno),errno);
404     }
405     free(stf_filename);
406 
407     if ((ret == 0) || (ret == 2)) {
408         if (settings->add_header == 1) {
409             smf_message_set_header(msg, header);
410             free(header);
411         }
412 
413         if ((ret = smf_modules_flush_dirty(settings,session,initial_headers)) != 0)
414             STRACE(TRACE_ERR,session->id,"message flush failed");
415 
416         /* queue is done, if we're still here check for next hop and
417          * deliver
418          */
419         if (ret == 0 && (nexthop = smf_nexthop_find(settings)) != NULL) {
420             if ((ret = nexthop(settings, session)) != 0)
421                 q->nexthop_error(settings, session);
422         }
423     }
424     smf_list_free(initial_headers);
425 
426     return ret;
427 }
428 
429 
430 /** Flush modified message headers to queue file */
smf_modules_flush_dirty(SMFSettings_T * settings,SMFSession_T * session,SMFList_T * initial_headers)431 int smf_modules_flush_dirty(SMFSettings_T *settings, SMFSession_T *session, SMFList_T *initial_headers) {
432     SMFHeader_T *h_init = NULL;
433     SMFHeader_T *h_msg = NULL;
434     SMFListElem_T *elem_init = NULL;
435     SMFListElem_T *elem_msg = NULL;
436     SMFMessage_T *msg = NULL;
437     int dirty = 0;
438     int found = 0;
439     int i = 0;
440     char *value1 = NULL;
441     char *value2 = NULL;
442 
443     STRACE(TRACE_DEBUG,session->id,"flushing header information to filesystem");
444 
445     msg = smf_envelope_get_message(session->envelope);
446 
447     elem_msg = smf_list_head(msg->headers);
448 
449     /* check if message headers have changed during session */
450     if (msg->headers->size != initial_headers->size) {
451         dirty = 1;
452     } else {
453         while(elem_msg != NULL) {
454             h_msg = (SMFHeader_T *)smf_list_data(elem_msg);
455             found = 0;
456             elem_init = smf_list_head(initial_headers);
457             while (elem_init != NULL) {
458                 h_init = (SMFHeader_T *)smf_list_data(elem_init);
459                 /* ok, we found the header name in the initial list */
460                 if (strcmp(h_msg->name,h_init->name)==0) {
461                     found = 1;
462                     /* lets check if the values are the same */
463                     if (smf_header_get_count(h_msg) != smf_header_get_count(h_init)) {
464                         dirty = 1;
465                     } else {
466                         for (i=0;i<smf_header_get_count(h_msg);i++) {
467                             value1 = smf_header_get_value(h_msg,i);
468                             value2 = smf_header_get_value(h_init,i);
469                             if ((value1 != NULL) && (value2 !=NULL) && (strcmp(value1,value2) != 0)) {
470                                 dirty = 1;
471                             }
472                         }
473                     }
474                     break;
475                 }
476                 elem_init = elem_init->next;
477             }
478 
479             if (found == 0) {
480                 dirty = 1;
481                 break;
482             }
483             elem_msg = elem_msg->next;
484         }
485     }
486 
487     /* merge new headers with message content to new queue file */
488     if (dirty == 1) {
489         char tmpname[PATH_MAX];
490         int fd;
491         FILE *new = NULL;
492         FILE *old = NULL;
493 
494         snprintf(tmpname, sizeof(tmpname), "%s/XXXXXX", settings->queue_dir);
495 
496         if ((fd = mkstemp(tmpname)) == -1) {
497             STRACE(TRACE_ERR,session->id,"failed to create temporary file: %s (%d)",strerror(errno),errno);
498             return -1;
499         }
500 
501         if (smf_message_to_fd(msg, fd) == -1) {
502             STRACE(TRACE_ERR,session->id,"unable to write temporary file [%s]: %s",tmpname, strerror(errno));
503             return -1;
504         }
505 
506         close(fd);
507 
508         if((new = fopen(tmpname, "a"))==NULL) {
509             STRACE(TRACE_ERR,session->id,"unable to open temporary file: %s (%d)",strerror(errno), errno);
510             return -1;
511         }
512 
513         if((old = fopen(session->message_file, "r"))==NULL) {
514             STRACE(TRACE_ERR,session->id,"unable to open queue file: %s (%d)",strerror(errno), errno);
515             fclose(new);
516             return -1;
517         }
518 
519         if (smf_message_write_skip_header(old, new) == -1) {
520             fclose(old);
521             fclose(new);
522             return -1;
523         }
524 
525         fclose(old);
526         fclose(new);
527 
528         if (unlink(session->message_file)!=0) {
529             STRACE(TRACE_ERR,session->id,"failed to remove queue file: %s (%d)",strerror(errno),errno);
530             return -1;
531         }
532 
533         if (rename(tmpname,session->message_file)!=0) {
534             STRACE(TRACE_ERR,session->id,"failed to rename queue file: %s (%d)",strerror(errno),errno);
535             return -1;
536         }
537     }
538 
539     return 0;
540 }
541