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