Version 0.4
[apps/pfixtools.git] / postlicyd / main-postlicyd.c
1 /******************************************************************************/
2 /*          pfixtools: a collection of postfix related tools                  */
3 /*          ~~~~~~~~~                                                         */
4 /*  ________________________________________________________________________  */
5 /*                                                                            */
6 /*  Redistribution and use in source and binary forms, with or without        */
7 /*  modification, are permitted provided that the following conditions        */
8 /*  are met:                                                                  */
9 /*                                                                            */
10 /*  1. Redistributions of source code must retain the above copyright         */
11 /*     notice, this list of conditions and the following disclaimer.          */
12 /*  2. Redistributions in binary form must reproduce the above copyright      */
13 /*     notice, this list of conditions and the following disclaimer in the    */
14 /*     documentation and/or other materials provided with the distribution.   */
15 /*  3. The names of its contributors may not be used to endorse or promote    */
16 /*     products derived from this software without specific prior written     */
17 /*     permission.                                                            */
18 /*                                                                            */
19 /*  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND   */
20 /*  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE     */
21 /*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR        */
22 /*  PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS    */
23 /*  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR    */
24 /*  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF      */
25 /*  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS  */
26 /*  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN   */
27 /*  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)   */
28 /*  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF    */
29 /*  THE POSSIBILITY OF SUCH DAMAGE.                                           */
30 /******************************************************************************/
31
32 /*
33  * Copyright © 2006-2007 Pierre Habouzit
34  * Copyright © 2008 Florent Bruneau
35  */
36
37 #include <getopt.h>
38
39 #include "buffer.h"
40 #include "common.h"
41 #include "policy_tokens.h"
42 #include "server.h"
43 #include "config.h"
44 #include "query.h"
45
46 #define DAEMON_NAME             "postlicyd"
47 #define DAEMON_VERSION          "0.4"
48 #define DEFAULT_PORT            10000
49 #define RUNAS_USER              "nobody"
50 #define RUNAS_GROUP             "nogroup"
51
52 DECLARE_MAIN
53
54 typedef struct query_context_t {
55     query_t query;
56     filter_context_t context;
57     client_t *client;
58 } query_context_t;
59
60 static config_t *config  = NULL;
61 static bool refresh      = false;
62 static PA(client_t) busy = ARRAY_INIT;
63
64 static void *query_starter(listener_t* server)
65 {
66     query_context_t *context = p_new(query_context_t, 1);
67     filter_context_prepare(&context->context, context);
68     return context;
69 }
70
71 static void query_stopper(void *data)
72 {
73     query_context_t **context = data;
74     if (*context) {
75         filter_context_wipe(&(*context)->context);
76         p_delete(context);
77     }
78 }
79
80 static bool config_refresh(void *mconfig)
81 {
82     refresh = true;
83     if (filter_running > 0) {
84         return true;
85     }
86     bool ret = config_reload(mconfig);
87     foreach (client_t **server, busy) {
88         client_io_ro(*server);
89     }}
90     array_len(busy) = 0;
91     refresh = false;
92     return ret;
93 }
94
95 static void policy_answer(client_t *pcy, const char *message)
96 {
97     query_context_t *context = client_data(pcy);
98     const query_t* query = &context->query;
99     buffer_t *buf = client_output_buffer(pcy);
100
101     /* Write reply "action=ACTION [text]" */
102     buffer_addstr(buf, "action=");
103     buffer_ensure(buf, m_strlen(message) + 64);
104
105     ssize_t size = array_size(*buf) - array_len(*buf);
106     ssize_t format_size = query_format(array_ptr(*buf, array_len(*buf)),
107                                        size, message, query);
108     if (format_size == -1) {
109         buffer_addstr(buf, message);
110     } else if (format_size > size) {
111         buffer_ensure(buf, format_size + 1);
112         query_format(array_ptr(*buf, array_len(*buf)),
113                      array_size(*buf) - array_len(*buf),
114                      message, query);
115         array_len(*buf) += format_size;
116     } else {
117         array_len(*buf) += format_size;
118     }
119     buffer_addstr(buf, "\n\n");
120
121     /* Finalize query. */
122     buf = client_input_buffer(pcy);
123     buffer_consume(buf, query->eoq - buf->data);
124     client_io_rw(pcy);
125 }
126
127 static const filter_t *next_filter(client_t *pcy, const filter_t *filter,
128                                    const query_t *query, const filter_hook_t *hook, bool *ok) {
129 #define MESSAGE_FORMAT "request client=%s from=<%s> to=<%s> at %s: "
130 #define MESSAGE_PARAMS query->client_name,                                          \
131                   query->sender == NULL ? "undefined" : query->sender,              \
132                   query->recipient == NULL ? "undefined" : query->recipient,        \
133                   smtp_state_names[query->state]
134
135     if (hook != NULL) {
136         query_context_t *context = client_data(pcy);
137         if (hook->counter >= 0 && hook->counter < MAX_COUNTERS && hook->cost > 0) {
138             context->context.counters[hook->counter] += hook->cost;
139             debug(MESSAGE_FORMAT "added %d to counter %d (now %u)", MESSAGE_PARAMS,
140                   hook->cost, hook->counter, context->context.counters[hook->counter]);
141         }
142     }
143     if (hook == NULL) {
144         warn(MESSAGE_FORMAT "aborted", MESSAGE_PARAMS);
145         *ok = false;
146         return NULL;
147     } else if (hook->async) {
148         debug(MESSAGE_FORMAT "asynchronous filter from filter %s",
149               MESSAGE_PARAMS, filter->name);
150         *ok = true;
151         return NULL;
152     } else if (hook->postfix) {
153         info(MESSAGE_FORMAT "awswer %s from filter %s: \"%s\"", MESSAGE_PARAMS,
154              htokens[hook->type], filter->name, hook->value);
155         policy_answer(pcy, hook->value);
156         *ok = true;
157         return NULL;
158     } else {
159         debug(MESSAGE_FORMAT "awswer %s from filter %s: next filter %s",
160               MESSAGE_PARAMS, htokens[hook->type], filter->name,
161               (array_ptr(config->filters, hook->filter_id))->name);
162         return array_ptr(config->filters, hook->filter_id);
163     }
164 #undef MESSAGE_PARAMS
165 #undef MESSAGE_FORMAT
166 }
167
168 static bool policy_process(client_t *pcy, const config_t *mconfig)
169 {
170     query_context_t *context = client_data(pcy);
171     const query_t* query = &context->query;
172     const filter_t *filter;
173     if (mconfig->entry_points[query->state] == -1) {
174         warn("no filter defined for current protocol_state (%s)", smtp_state_names[query->state]);
175         return false;
176     }
177     if (context->context.current_filter != NULL) {
178         filter = context->context.current_filter;
179     } else {
180         filter = array_ptr(mconfig->filters, mconfig->entry_points[query->state]);
181     }
182     context->context.current_filter = NULL;
183     while (true) {
184         bool  ok = false;
185         const filter_hook_t *hook = filter_run(filter, query, &context->context);
186         filter = next_filter(pcy, filter, query, hook, &ok);
187         if (filter == NULL) {
188             return ok;
189         }
190     }
191 }
192
193 static int policy_run(client_t *pcy, void* vconfig)
194 {
195     const config_t *mconfig = vconfig;
196     if (refresh) {
197         array_add(busy, pcy);
198         return 0;
199     }
200
201     query_context_t *context = client_data(pcy);
202     query_t         *query   = &context->query;
203     context->client = pcy;
204
205     buffer_t *buf   = client_input_buffer(pcy);
206     int search_offs = MAX(0, (int)(buf->len - 1));
207     int nb          = client_read(pcy);
208     const char *eoq;
209
210     if (nb < 0) {
211         if (errno == EAGAIN || errno == EINTR)
212             return 0;
213         UNIXERR("read");
214         return -1;
215     }
216     if (nb == 0) {
217         if (buf->len)
218             err("unexpected end of data");
219         return -1;
220     }
221
222     if (!(eoq = strstr(buf->data + search_offs, "\n\n"))) {
223         return 0;
224     }
225
226     if (!query_parse(query, buf->data)) {
227         return -1;
228     }
229     query->eoq = eoq + strlen("\n\n");
230
231     /* The instance changed => reset the static context */
232     if (query->instance == NULL || strcmp(context->context.instance, query->instance) != 0) {
233         filter_context_clean(&context->context);
234         m_strcat(context->context.instance, 64, query->instance);
235     }
236     client_io_none(pcy);
237     return policy_process(pcy, mconfig) ? 0 : -1;
238 }
239
240 static void policy_async_handler(filter_context_t *context,
241                                  const filter_hook_t *hook)
242 {
243     bool ok = false;
244     const filter_t *filter = context->current_filter;
245     query_context_t *qctx  = context->data;
246     query_t         *query = &qctx->query;
247     client_t        *server = qctx->client;
248
249     context->current_filter = next_filter(server, filter, query, hook, &ok);
250     if (context->current_filter != NULL) {
251         ok = policy_process(server, config);
252     }
253     if (!ok) {
254         client_release(server);
255     }
256     if (refresh && filter_running == 0) {
257         config_refresh(config);
258     }
259 }
260
261 static int postlicyd_init(void)
262 {
263     filter_async_handler_register(policy_async_handler);
264     return 0;
265 }
266
267 static void postlicyd_shutdown(void)
268 {
269     array_deep_wipe(busy, client_delete);
270 }
271 module_init(postlicyd_init);
272 module_exit(postlicyd_shutdown);
273
274 /* administrivia {{{ */
275
276 void usage(void)
277 {
278     fputs("usage: "DAEMON_NAME" [options] config\n"
279           "\n"
280           "Options:\n"
281           "    -l <port>    port to listen to\n"
282           "    -p <pidfile> file to write our pid to\n"
283           "    -f           stay in foreground\n"
284           "    -d           grow logging level\n"
285           "    -u           unsafe mode (don't drop privileges)\n"
286          , stderr);
287 }
288
289 /* }}} */
290
291 int main(int argc, char *argv[])
292 {
293     bool unsafe = false;
294     const char *pidfile = NULL;
295     bool daemonize = true;
296     int port = DEFAULT_PORT;
297     bool port_from_cli = false;
298
299     for (int c = 0; (c = getopt(argc, argv, "ufd" "l:p:")) >= 0; ) {
300         switch (c) {
301           case 'p':
302             pidfile = optarg;
303             break;
304           case 'u':
305             unsafe = true;
306             break;
307           case 'l':
308             port = atoi(optarg);
309             port_from_cli = true;
310             break;
311           case 'f':
312             daemonize = false;
313             break;
314           case 'd':
315             ++log_level;
316             break;
317           default:
318             usage();
319             return EXIT_FAILURE;
320         }
321     }
322
323     if (!daemonize) {
324         log_syslog = false;
325     }
326
327     if (argc - optind != 1) {
328         usage();
329         return EXIT_FAILURE;
330     }
331
332     info("%s v%s...", DAEMON_NAME, DAEMON_VERSION);
333
334     if (pidfile_open(pidfile) < 0) {
335         crit("unable to write pidfile %s", pidfile);
336         return EXIT_FAILURE;
337     }
338
339     if (drop_privileges(RUNAS_USER, RUNAS_GROUP) < 0) {
340         crit("unable to drop privileges");
341         return EXIT_FAILURE;
342     }
343
344     config = config_read(argv[optind]);
345     if (config == NULL) {
346         return EXIT_FAILURE;
347     }
348     if (port_from_cli || config->port == 0) {
349         config->port = port;
350     }
351
352     if (daemonize && daemon_detach() < 0) {
353         crit("unable to fork");
354         return EXIT_FAILURE;
355     }
356
357     pidfile_refresh();
358
359     if (start_listener(config->port) == NULL) {
360         return EXIT_FAILURE;
361     } else {
362         return server_loop(query_starter, query_stopper,
363                            policy_run, config_refresh, config);
364     }
365 }