50c19d5005fdd0a55c0876bce2519e19be64f1b0
[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.3"
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     if (hook != NULL) {
130         query_context_t *context = client_data(pcy);
131         if (hook->counter >= 0 && hook->counter < MAX_COUNTERS && hook->cost > 0) {
132             context->context.counters[hook->counter] += hook->cost;
133             debug("request client=%s, from=<%s>, to=<%s>: added %d to counter %d (now %u)",
134                   query->client_name,
135                   query->sender == NULL ? "undefined" : query->sender,
136                   query->recipient == NULL ? "undefined" : query->recipient,
137                   hook->cost, hook->counter, context->context.counters[hook->counter]);
138         }
139     }
140     if (hook == NULL) {
141         warn("request client=%s, from=<%s>, to=<%s>: aborted",
142              query->client_name,
143              query->sender == NULL ? "undefined" : query->sender,
144              query->recipient == NULL ? "undefined" : query->recipient);
145         *ok = false;
146         return NULL;
147     } else if (hook->async) {
148         debug("request client=%s, from=<%s>, to=<%s>: "
149               "asynchronous filter from filter %s",
150                query->client_name,
151                query->sender == NULL ? "undefined" : query->sender,
152                query->recipient == NULL ? "undefined" : query->recipient,
153                filter->name);
154         *ok = true;
155         return NULL;
156     } else if (hook->postfix) {
157         info("request client=%s, from=<%s>, to=<%s>: "
158              "awswer %s from filter %s: \"%s\"",
159              query->client_name,
160              query->sender == NULL ? "undefined" : query->sender,
161              query->recipient == NULL ? "undefined" : query->recipient,
162              htokens[hook->type], filter->name, hook->value);
163         policy_answer(pcy, hook->value);
164         *ok = true;
165         return NULL;
166     } else {
167         debug("request client=%s, from=<%s>, to=<%s>: "
168                "awswer %s from filter %s: next filter %s",
169                query->client_name,
170                query->sender == NULL ? "undefined" : query->sender,
171                query->recipient == NULL ? "undefined" : query->recipient,
172                htokens[hook->type], filter->name,
173                (array_ptr(config->filters, hook->filter_id))->name);
174         return array_ptr(config->filters, hook->filter_id);
175     }
176 }
177
178 static bool policy_process(client_t *pcy, const config_t *mconfig)
179 {
180     query_context_t *context = client_data(pcy);
181     const query_t* query = &context->query;
182     const filter_t *filter;
183     if (mconfig->entry_points[query->state] == -1) {
184         warn("no filter defined for current protocol_state (%d)", query->state);
185         return false;
186     }
187     if (context->context.current_filter != NULL) {
188         filter = context->context.current_filter;
189     } else {
190         filter = array_ptr(mconfig->filters, mconfig->entry_points[query->state]);
191     }
192     context->context.current_filter = NULL;
193     while (true) {
194         bool  ok = false;
195         const filter_hook_t *hook = filter_run(filter, query, &context->context);
196         filter = next_filter(pcy, filter, query, hook, &ok);
197         if (filter == NULL) {
198             return ok;
199         }
200     }
201 }
202
203 static int policy_run(client_t *pcy, void* vconfig)
204 {
205     const config_t *mconfig = vconfig;
206     if (refresh) {
207         array_add(busy, pcy);
208         return 0;
209     }
210
211     query_context_t *context = client_data(pcy);
212     query_t         *query   = &context->query;
213     context->client = pcy;
214
215     buffer_t *buf   = client_input_buffer(pcy);
216     int search_offs = MAX(0, (int)(buf->len - 1));
217     int nb          = client_read(pcy);
218     const char *eoq;
219
220     if (nb < 0) {
221         if (errno == EAGAIN || errno == EINTR)
222             return 0;
223         UNIXERR("read");
224         return -1;
225     }
226     if (nb == 0) {
227         if (buf->len)
228             err("unexpected end of data");
229         return -1;
230     }
231
232     if (!(eoq = strstr(buf->data + search_offs, "\n\n"))) {
233         return 0;
234     }
235
236     if (!query_parse(query, buf->data)) {
237         return -1;
238     }
239     query->eoq = eoq + strlen("\n\n");
240
241     /* The instance changed => reset the static context */
242     if (query->instance == NULL || strcmp(context->context.instance, query->instance) != 0) {
243         filter_context_clean(&context->context);
244         m_strcat(context->context.instance, 64, query->instance);
245     }
246     client_io_none(pcy);
247     return policy_process(pcy, mconfig) ? 0 : -1;
248 }
249
250 static void policy_async_handler(filter_context_t *context,
251                                  const filter_hook_t *hook)
252 {
253     bool ok = false;
254     const filter_t *filter = context->current_filter;
255     query_context_t *qctx  = context->data;
256     query_t         *query = &qctx->query;
257     client_t        *server = qctx->client;
258
259     context->current_filter = next_filter(server, filter, query, hook, &ok);
260     if (context->current_filter != NULL) {
261         ok = policy_process(server, config);
262     }
263     if (!ok) {
264         client_release(server);
265     }
266     if (refresh && filter_running == 0) {
267         config_refresh(config);
268     }
269 }
270
271 static int postlicyd_init(void)
272 {
273     filter_async_handler_register(policy_async_handler);
274     return 0;
275 }
276
277 static void postlicyd_shutdown(void)
278 {
279     array_deep_wipe(busy, client_delete);
280 }
281 module_init(postlicyd_init);
282 module_exit(postlicyd_shutdown);
283
284 /* administrivia {{{ */
285
286 void usage(void)
287 {
288     fputs("usage: "DAEMON_NAME" [options] config\n"
289           "\n"
290           "Options:\n"
291           "    -l <port>    port to listen to\n"
292           "    -p <pidfile> file to write our pid to\n"
293           "    -f           stay in foreground\n"
294           "    -d           grow logging level\n"
295           "    -u           unsafe mode (don't drop privileges)\n"
296          , stderr);
297 }
298
299 /* }}} */
300
301 int main(int argc, char *argv[])
302 {
303     bool unsafe = false;
304     const char *pidfile = NULL;
305     bool daemonize = true;
306     int port = DEFAULT_PORT;
307     bool port_from_cli = false;
308
309     for (int c = 0; (c = getopt(argc, argv, "ufd" "l:p:")) >= 0; ) {
310         switch (c) {
311           case 'p':
312             pidfile = optarg;
313             break;
314           case 'u':
315             unsafe = true;
316             break;
317           case 'l':
318             port = atoi(optarg);
319             port_from_cli = true;
320             break;
321           case 'f':
322             daemonize = false;
323             break;
324           case 'd':
325             ++log_level;
326             break;
327           default:
328             usage();
329             return EXIT_FAILURE;
330         }
331     }
332
333     if (!daemonize) {
334         log_syslog = false;
335     }
336
337     if (argc - optind != 1) {
338         usage();
339         return EXIT_FAILURE;
340     }
341
342     info("starting %s v%s...", DAEMON_NAME, DAEMON_VERSION);
343
344     if (pidfile_open(pidfile) < 0) {
345         crit("unable to write pidfile %s", pidfile);
346         return EXIT_FAILURE;
347     }
348
349     if (drop_privileges(RUNAS_USER, RUNAS_GROUP) < 0) {
350         crit("unable to drop privileges");
351         return EXIT_FAILURE;
352     }
353
354     config = config_read(argv[optind]);
355     if (config == NULL) {
356         return EXIT_FAILURE;
357     }
358     if (port_from_cli || config->port == 0) {
359         config->port = port;
360     }
361
362     if (daemonize && daemon_detach() < 0) {
363         crit("unable to fork");
364         return EXIT_FAILURE;
365     }
366
367     pidfile_refresh();
368
369     if (start_listener(config->port) == NULL) {
370         return EXIT_FAILURE;
371     } else {
372         return server_loop(query_starter, query_stopper,
373                            policy_run, config_refresh, config);
374     }
375 }