Write a postlicyd(8) man page.
[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 CONTRIBUTORS ``AS IS'' AND ANY EXPRESS   */
20 /*  OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED         */
21 /*  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE    */
22 /*  DISCLAIMED.  IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY         */
23 /*  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL        */
24 /*  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS   */
25 /*  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)     */
26 /*  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,       */
27 /*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN  */
28 /*  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE           */
29 /*  POSSIBILITY OF SUCH DAMAGE.                                               */
30 /*                                                                            */
31 /*   Copyright (c) 2006-2008 the Authors                                      */
32 /*   see AUTHORS and source files for details                                 */
33 /******************************************************************************/
34
35 /*
36  * Copyright © 2006-2007 Pierre Habouzit
37  * Copyright © 2008 Florent Bruneau
38  */
39
40 #include <getopt.h>
41
42 #include "buffer.h"
43 #include "common.h"
44 #include "policy_tokens.h"
45 #include "server.h"
46 #include "config.h"
47 #include "query.h"
48
49 #define DAEMON_NAME             "postlicyd"
50 #define DAEMON_VERSION          "0.5"
51 #define DEFAULT_PORT            10000
52 #define RUNAS_USER              "nobody"
53 #define RUNAS_GROUP             "nogroup"
54
55 DECLARE_MAIN
56
57 typedef struct query_context_t {
58     query_t query;
59     filter_context_t context;
60     client_t *client;
61 } query_context_t;
62
63 static config_t *config  = NULL;
64 static bool refresh      = false;
65 static PA(client_t) busy = ARRAY_INIT;
66
67 static void *query_starter(listener_t* server)
68 {
69     query_context_t *context = p_new(query_context_t, 1);
70     filter_context_prepare(&context->context, context);
71     return context;
72 }
73
74 static void query_stopper(void *data)
75 {
76     query_context_t **context = data;
77     if (*context) {
78         filter_context_wipe(&(*context)->context);
79         p_delete(context);
80     }
81 }
82
83 static bool config_refresh(void *mconfig)
84 {
85     refresh = true;
86     if (filter_running > 0) {
87         return true;
88     }
89     log_state = "refreshing ";
90     info("reloading configuration");
91     bool ret = config_reload(mconfig);
92     log_state = "";
93     foreach (client_t **server, busy) {
94         client_io_ro(*server);
95     }}
96     array_len(busy) = 0;
97     refresh = false;
98     return ret;
99 }
100
101 static void policy_answer(client_t *pcy, const char *message)
102 {
103     query_context_t *context = client_data(pcy);
104     const query_t* query = &context->query;
105     buffer_t *buf = client_output_buffer(pcy);
106
107     /* Write reply "action=ACTION [text]" */
108     buffer_addstr(buf, "action=");
109     if (!query_format_buffer(buf, message, query)) {
110         buffer_addstr(buf, message);
111     }
112     buffer_addstr(buf, "\n\n");
113
114     /* Finalize query. */
115     buf = client_input_buffer(pcy);
116     buffer_consume(buf, query->eoq - buf->data);
117     client_io_rw(pcy);
118 }
119
120 static const filter_t *next_filter(client_t *pcy, const filter_t *filter,
121                                    const query_t *query, const filter_hook_t *hook, bool *ok) {
122     char log_prefix[BUFSIZ];
123     log_prefix[0] = '\0';
124
125 #define log_reply(Level, Msg, ...)                                             \
126     if (log_level >= LOG_ ## Level) {                                          \
127         if (log_prefix[0] == '\0') {                                           \
128             query_format(log_prefix, BUFSIZ,                                   \
129                          config->log_format && config->log_format[0] ?         \
130                             config->log_format : DEFAULT_LOG_FORMAT, query);   \
131         }                                                                      \
132         __log(LOG_ ## Level, "%s: " Msg, log_prefix, ##__VA_ARGS__);           \
133     }
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             log_reply(DEBUG, "added %d to counter %d (now %u)",
140                       hook->cost, hook->counter,
141                       context->context.counters[hook->counter]);
142         }
143     }
144     if (hook == NULL) {
145         log_reply(WARNING, "aborted");
146         *ok = false;
147         return NULL;
148     } else if (hook->async) {
149         log_reply(WARNING, "asynchronous filter from filter %s", filter->name);
150         *ok = true;
151         return NULL;
152     } else if (hook->postfix) {
153         log_reply(INFO, "answer %s from filter %s: \"%s\"",
154                   htokens[hook->type], filter->name, hook->value);
155         policy_answer(pcy, hook->value);
156         *ok = true;
157         return NULL;
158     } else {
159         log_reply(DEBUG, "answer %s from filter %s: next filter %s",
160                   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 log_reply
165 }
166
167 static bool policy_process(client_t *pcy, const config_t *mconfig)
168 {
169     query_context_t *context = client_data(pcy);
170     const query_t* query = &context->query;
171     const filter_t *filter;
172     if (mconfig->entry_points[query->state] == -1) {
173         warn("no filter defined for current protocol_state (%s)", smtp_state_names[query->state].str);
174         return false;
175     }
176     if (context->context.current_filter != NULL) {
177         filter = context->context.current_filter;
178     } else {
179         filter = array_ptr(mconfig->filters, mconfig->entry_points[query->state]);
180     }
181     context->context.current_filter = NULL;
182     while (true) {
183         bool  ok = false;
184         const filter_hook_t *hook = filter_run(filter, query, &context->context);
185         filter = next_filter(pcy, filter, query, hook, &ok);
186         if (filter == NULL) {
187             return ok;
188         }
189     }
190 }
191
192 static int policy_run(client_t *pcy, void* vconfig)
193 {
194     const config_t *mconfig = vconfig;
195     if (refresh) {
196         array_add(busy, pcy);
197         return 0;
198     }
199
200     query_context_t *context = client_data(pcy);
201     query_t         *query   = &context->query;
202     context->client = pcy;
203
204     buffer_t *buf   = client_input_buffer(pcy);
205     int search_offs = MAX(0, (int)(buf->len - 1));
206     int nb          = client_read(pcy);
207     const char *eoq;
208
209     if (nb < 0) {
210         if (errno == EAGAIN || errno == EINTR)
211             return 0;
212         UNIXERR("read");
213         return -1;
214     }
215     if (nb == 0) {
216         if (buf->len)
217             err("unexpected end of data");
218         return -1;
219     }
220
221     if (!(eoq = strstr(buf->data + search_offs, "\n\n"))) {
222         return 0;
223     }
224
225     if (!query_parse(query, buf->data)) {
226         return -1;
227     }
228     query->eoq = eoq + strlen("\n\n");
229
230     /* The instance changed => reset the static context */
231     if (query->instance.str == NULL || query->instance.len == 0
232         || strcmp(context->context.instance, query->instance.str) != 0) {
233         filter_context_clean(&context->context);
234         m_strcat(context->context.instance, 64, query->instance.str);
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           "    -c           check-conf\n"
287          , stderr);
288 }
289
290 /* }}} */
291
292 int main(int argc, char *argv[])
293 {
294     bool unsafe = false;
295     const char *pidfile = NULL;
296     bool daemonize = true;
297     int port = DEFAULT_PORT;
298     bool port_from_cli = false;
299     bool check_conf = false;
300
301     for (int c = 0; (c = getopt(argc, argv, "hufdc" "l:p:")) >= 0; ) {
302         switch (c) {
303           case 'p':
304             pidfile = optarg;
305             break;
306           case 'u':
307             unsafe = true;
308             break;
309           case 'l':
310             port = atoi(optarg);
311             port_from_cli = true;
312             break;
313           case 'f':
314             daemonize = false;
315             break;
316           case 'd':
317             ++log_level;
318             break;
319           case 'c':
320             check_conf = true;
321             daemonize  = false;
322             unsafe     = true;
323             break;
324           default:
325             usage();
326             return EXIT_FAILURE;
327         }
328     }
329
330     if (!daemonize) {
331         log_syslog = false;
332     }
333
334     if (argc - optind != 1) {
335         usage();
336         return EXIT_FAILURE;
337     }
338
339     if (check_conf) {
340         return config_check(argv[optind]) ? EXIT_SUCCESS : EXIT_FAILURE;
341     }
342     info("%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 }