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