1/*	$NetBSD: postconf_edit.c,v 1.3 2023/12/23 20:30:44 christos Exp $	*/
2
3/*++
4/* NAME
5/*	postconf_edit 3
6/* SUMMARY
7/*	edit main.cf or master.cf
8/* SYNOPSIS
9/*	#include <postconf.h>
10/*
11/*	void	pcf_edit_main(mode, argc, argv)
12/*	int	mode;
13/*	int	argc;
14/*	char	**argv;
15/*
16/*	void	pcf_edit_master(mode, argc, argv)
17/*	int	mode;
18/*	int	argc;
19/*	char	**argv;
20/* DESCRIPTION
21/*	pcf_edit_main() edits the \fBmain.cf\fR configuration file.
22/*	It replaces or adds parameter settings given as "\fIname=value\fR"
23/*	pairs given on the command line, or removes parameter
24/*	settings given as "\fIname\fR" on the command line.  The
25/*	file is copied to a temporary file then renamed into place.
26/*
27/*	pcf_edit_master() edits the \fBmaster.cf\fR configuration
28/*	file.  The file is copied to a temporary file then renamed
29/*	into place. Depending on the flags in \fBmode\fR:
30/* .IP PCF_MASTER_ENTRY
31/*	With PCF_EDIT_CONF, pcf_edit_master() replaces or adds
32/*	entire master.cf entries, specified on the command line as
33/*	"\fIname/type = name type private unprivileged chroot wakeup
34/*	process_limit command...\fR".
35/*
36/*	With PCF_EDIT_EXCL or PCF_COMMENT_OUT, pcf_edit_master()
37/*	removes or comments out entries specified on the command
38/*	line as "\fIname/type\fR.
39/* .IP PCF_MASTER_FLD
40/*	With PCF_EDIT_CONF, pcf_edit_master() replaces the value
41/*	of specific service attributes, specified on the command
42/*	line as "\fIname/type/attribute = value\fR".
43/* .IP PCF_MASTER_PARAM
44/*	With PCF_EDIT_CONF, pcf_edit_master() replaces or adds the
45/*	value of service parameters, specified on the command line
46/*	as "\fIname/type/parameter = value\fR".
47/*
48/*	With PCF_EDIT_EXCL, pcf_edit_master() removes service
49/*	parameters specified on the command line as "\fIparametername\fR".
50/* DIAGNOSTICS
51/*	Problems are reported to the standard error stream.
52/* FILES
53/*	/etc/postfix/main.cf, Postfix configuration parameters
54/*	/etc/postfix/main.cf.tmp, temporary name
55/*	/etc/postfix/master.cf, Postfix configuration parameters
56/*	/etc/postfix/master.cf.tmp, temporary name
57/* LICENSE
58/* .ad
59/* .fi
60/*	The Secure Mailer license must be distributed with this software.
61/* AUTHOR(S)
62/*	Wietse Venema
63/*	IBM T.J. Watson Research
64/*	P.O. Box 704
65/*	Yorktown Heights, NY 10598, USA
66/*--*/
67
68/* System library. */
69
70#include <sys_defs.h>
71#include <string.h>
72#include <ctype.h>
73
74/* Utility library. */
75
76#include <msg.h>
77#include <mymalloc.h>
78#include <htable.h>
79#include <vstring.h>
80#include <vstring_vstream.h>
81#include <edit_file.h>
82#include <readlline.h>
83#include <stringops.h>
84#include <split_at.h>
85
86/* Global library. */
87
88#include <mail_params.h>
89
90/* Application-specific. */
91
92#include <postconf.h>
93
94#define STR(x) vstring_str(x)
95
96/* pcf_find_cf_info - pass-through non-content line, return content or null */
97
98static char *pcf_find_cf_info(VSTRING *buf, VSTREAM *dst)
99{
100    char   *cp;
101
102    for (cp = STR(buf); ISSPACE(*cp) /* including newline */ ; cp++)
103	 /* void */ ;
104    /* Pass-through comment, all-whitespace, or empty line. */
105    if (*cp == '#' || *cp == 0) {
106	vstream_fputs(STR(buf), dst);
107	return (0);
108    } else {
109	return (cp);
110    }
111}
112
113/* pcf_next_cf_line - return next content line, pass non-content */
114
115static char *pcf_next_cf_line(VSTRING *buf, VSTREAM *src, VSTREAM *dst, int *lineno)
116{
117    char   *cp;
118
119    while (vstring_get(buf, src) != VSTREAM_EOF) {
120	if (lineno)
121	    *lineno += 1;
122	if ((cp = pcf_find_cf_info(buf, dst)) != 0)
123	    return (cp);
124    }
125    return (0);
126}
127
128/* pcf_gobble_cf_line - accumulate multi-line content, pass non-content */
129
130static void pcf_gobble_cf_line(VSTRING *full_entry_buf, VSTRING *line_buf,
131			            VSTREAM *src, VSTREAM *dst, int *lineno)
132{
133    int     ch;
134
135    vstring_strcpy(full_entry_buf, STR(line_buf));
136    for (;;) {
137	if ((ch = VSTREAM_GETC(src)) != VSTREAM_EOF)
138	    vstream_ungetc(src, ch);
139	if ((ch != '#' && !ISSPACE(ch))
140	    || vstring_get(line_buf, src) == VSTREAM_EOF)
141	    break;
142	lineno += 1;
143	if (pcf_find_cf_info(line_buf, dst))
144	    vstring_strcat(full_entry_buf, STR(line_buf));
145    }
146}
147
148/* pcf_edit_main - edit main.cf file */
149
150void    pcf_edit_main(int mode, int argc, char **argv)
151{
152    const char *path;
153    EDIT_FILE *ep;
154    VSTREAM *src;
155    VSTREAM *dst;
156    VSTRING *buf = vstring_alloc(100);
157    VSTRING *key = vstring_alloc(10);
158    char   *cp;
159    char   *pattern;
160    char   *edit_value;
161    HTABLE *table;
162    struct cvalue {
163	char   *value;
164	int     found;
165    };
166    struct cvalue *cvalue;
167    HTABLE_INFO **ht_info;
168    HTABLE_INFO **ht;
169    int     interesting;
170    const char *err;
171
172    /*
173     * Store command-line parameters for quick lookup.
174     */
175    table = htable_create(argc);
176    while ((cp = *argv++) != 0) {
177	if (strchr(cp, '\n') != 0)
178	    msg_fatal("-e, -X, or -# accepts no multi-line input");
179	while (ISSPACE(*cp))
180	    cp++;
181	if (*cp == '#')
182	    msg_fatal("-e, -X, or -# accepts no comment input");
183	if (mode & PCF_EDIT_CONF) {
184	    if ((err = split_nameval(cp, &pattern, &edit_value)) != 0)
185		msg_fatal("%s: \"%s\"", err, cp);
186	} else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
187	    if (*cp == 0)
188		msg_fatal("-X or -# requires non-blank parameter names");
189	    if (strchr(cp, '=') != 0)
190		msg_fatal("-X or -# requires parameter names without value");
191	    pattern = cp;
192	    trimblanks(pattern, 0);
193	    edit_value = 0;
194	} else {
195	    msg_panic("pcf_edit_main: unknown mode %d", mode);
196	}
197	if ((cvalue = htable_find(table, pattern)) != 0) {
198	    msg_warn("ignoring earlier request: '%s = %s'",
199		     pattern, cvalue->value);
200	    htable_delete(table, pattern, myfree);
201	}
202	cvalue = (struct cvalue *) mymalloc(sizeof(*cvalue));
203	cvalue->value = edit_value;
204	cvalue->found = 0;
205	htable_enter(table, pattern, (void *) cvalue);
206    }
207
208    /*
209     * Open a temp file for the result. This uses a deterministic name so we
210     * don't leave behind thrash with random names.
211     */
212    path = pcf_get_main_path();
213    if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
214	msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
215    dst = ep->tmp_fp;
216
217    /*
218     * Open the original file for input.
219     */
220    if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
221	/* OK to delete, since we control the temp file name exclusively. */
222	(void) unlink(ep->tmp_path);
223	msg_fatal("open %s for reading: %m", path);
224    }
225
226    /*
227     * Copy original file to temp file, while replacing parameters on the
228     * fly. Issue warnings for names found multiple times.
229     */
230#define STR(x) vstring_str(x)
231
232    interesting = 0;
233    while ((cp = pcf_next_cf_line(buf, src, dst, (int *) 0)) != 0) {
234	/* Copy, skip or replace continued text. */
235	if (cp > STR(buf)) {
236	    if (interesting == 0)
237		vstream_fputs(STR(buf), dst);
238	    else if (mode & PCF_COMMENT_OUT)
239		vstream_fprintf(dst, "#%s", STR(buf));
240	}
241	/* Copy or replace start of logical line. */
242	else {
243	    vstring_strncpy(key, cp, strcspn(cp, CHARS_SPACE "="));
244	    cvalue = (struct cvalue *) htable_find(table, STR(key));
245	    if ((interesting = !!cvalue) != 0) {
246		if (cvalue->found++ == 1)
247		    msg_warn("%s: multiple entries for \"%s\"", path, STR(key));
248		if (mode & PCF_EDIT_CONF)
249		    vstream_fprintf(dst, "%s = %s\n", STR(key), cvalue->value);
250		else if (mode & PCF_COMMENT_OUT)
251		    vstream_fprintf(dst, "#%s", cp);
252	    } else {
253		vstream_fputs(STR(buf), dst);
254	    }
255	}
256    }
257
258    /*
259     * Generate new entries for parameters that were not found.
260     */
261    if (mode & PCF_EDIT_CONF) {
262	for (ht_info = ht = htable_list(table); *ht; ht++) {
263	    cvalue = (struct cvalue *) ht[0]->value;
264	    if (cvalue->found == 0)
265		vstream_fprintf(dst, "%s = %s\n", ht[0]->key, cvalue->value);
266	}
267	myfree((void *) ht_info);
268    }
269
270    /*
271     * When all is well, rename the temp file to the original one.
272     */
273    if (vstream_fclose(src))
274	msg_fatal("read %s: %m", path);
275    if (edit_file_close(ep) != 0)
276	msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
277
278    /*
279     * Cleanup.
280     */
281    vstring_free(buf);
282    vstring_free(key);
283    htable_free(table, myfree);
284}
285
286 /*
287  * Data structure to hold a master.cf edit request.
288  */
289typedef struct {
290    int     match_count;		/* hit count */
291    const char *raw_text;		/* unparsed command-line argument */
292    char   *parsed_text;		/* destructive parse */
293    ARGV   *service_pattern;		/* service name, type, ... */
294    int     field_number;		/* attribute field number */
295    const char *param_pattern;		/* parameter name */
296    char   *edit_value;			/* value substring */
297} PCF_MASTER_EDIT_REQ;
298
299/* pcf_edit_master - edit master.cf file */
300
301void    pcf_edit_master(int mode, int argc, char **argv)
302{
303    const char *myname = "pcf_edit_master";
304    const char *path;
305    EDIT_FILE *ep;
306    VSTREAM *src;
307    VSTREAM *dst;
308    VSTRING *line_buf = vstring_alloc(100);
309    VSTRING *parse_buf = vstring_alloc(100);
310    int     lineno;
311    PCF_MASTER_ENT *new_entry;
312    VSTRING *full_entry_buf = vstring_alloc(100);
313    char   *cp;
314    char   *pattern;
315    int     service_name_type_matched;
316    const char *err;
317    PCF_MASTER_EDIT_REQ *edit_reqs;
318    PCF_MASTER_EDIT_REQ *req;
319    int     num_reqs = argc;
320    const char *edit_opts = "-Me, -Fe, -Pe, -X, or -#";
321    char   *service_name;
322    char   *service_type;
323
324    /*
325     * Sanity check.
326     */
327    if (num_reqs <= 0)
328	msg_panic("%s: empty argument list", myname);
329
330    /*
331     * Preprocessing: split pattern=value, then split the pattern components.
332     */
333    edit_reqs = (PCF_MASTER_EDIT_REQ *) mymalloc(sizeof(*edit_reqs) * num_reqs);
334    for (req = edit_reqs; *argv != 0; req++, argv++) {
335	req->match_count = 0;
336	req->raw_text = *argv;
337	cp = req->parsed_text = mystrdup(req->raw_text);
338	if (strchr(cp, '\n') != 0)
339	    msg_fatal("%s accept no multi-line input", edit_opts);
340	while (ISSPACE(*cp))
341	    cp++;
342	if (*cp == '#')
343	    msg_fatal("%s accept no comment input", edit_opts);
344	/* Separate the pattern from the value. */
345	if (mode & PCF_EDIT_CONF) {
346	    if ((err = split_nameval(cp, &pattern, &req->edit_value)) != 0)
347		msg_fatal("%s: \"%s\"", err, req->raw_text);
348#if 0
349	    if ((mode & PCF_MASTER_PARAM)
350	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
351		msg_fatal("whitespace in parameter value: \"%s\"",
352			  req->raw_text);
353#endif
354	} else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
355	    if (strchr(cp, '=') != 0)
356		msg_fatal("-X or -# requires names without value");
357	    pattern = cp;
358	    trimblanks(pattern, 0);
359	    req->edit_value = 0;
360	} else {
361	    msg_panic("%s: unknown mode %d", myname, mode);
362	}
363
364#define PCF_MASTER_MASK (PCF_MASTER_ENTRY | PCF_MASTER_FLD | PCF_MASTER_PARAM)
365
366	/*
367	 * Split name/type or name/type/whatever pattern into components.
368	 */
369	switch (mode & PCF_MASTER_MASK) {
370	case PCF_MASTER_ENTRY:
371	    if ((req->service_pattern =
372		 pcf_parse_service_pattern(pattern, 2, 2)) == 0)
373		msg_fatal("-Me, -MX or -M# requires service_name/type");
374	    break;
375	case PCF_MASTER_FLD:
376	    if ((req->service_pattern =
377		 pcf_parse_service_pattern(pattern, 3, 3)) == 0)
378		msg_fatal("-Fe or -FX requires service_name/type/field_name");
379	    req->field_number =
380		pcf_parse_field_pattern(req->service_pattern->argv[2]);
381	    if (pcf_is_magic_field_pattern(req->field_number))
382		msg_fatal("-Fe does not accept wild-card field name");
383	    if ((mode & PCF_EDIT_CONF)
384		&& req->field_number < PCF_MASTER_FLD_CMD
385	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
386		msg_fatal("-Fe does not accept whitespace in non-command field");
387	    break;
388	case PCF_MASTER_PARAM:
389	    if ((req->service_pattern =
390		 pcf_parse_service_pattern(pattern, 3, 3)) == 0)
391		msg_fatal("-Pe or -PX requires service_name/type/parameter");
392	    req->param_pattern = req->service_pattern->argv[2];
393	    if (PCF_IS_MAGIC_PARAM_PATTERN(req->param_pattern))
394		msg_fatal("-Pe does not accept wild-card parameter name");
395	    if ((mode & PCF_EDIT_CONF)
396	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
397		msg_fatal("-Pe does not accept whitespace in parameter value");
398	    break;
399	default:
400	    msg_panic("%s: unknown edit mode %d", myname, mode);
401	}
402    }
403
404    /*
405     * Open a temp file for the result. This uses a deterministic name so we
406     * don't leave behind thrash with random names.
407     */
408    path = pcf_get_master_path();
409    if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
410	msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
411    dst = ep->tmp_fp;
412
413    /*
414     * Open the original file for input.
415     */
416    if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
417	/* OK to delete, since we control the temp file name exclusively. */
418	(void) unlink(ep->tmp_path);
419	msg_fatal("open %s for reading: %m", path);
420    }
421
422    /*
423     * Copy original file to temp file, while replacing service entries on
424     * the fly.
425     */
426    service_name_type_matched = 0;
427    new_entry = 0;
428    lineno = 0;
429    while ((cp = pcf_next_cf_line(parse_buf, src, dst, &lineno)) != 0) {
430	vstring_strcpy(line_buf, STR(parse_buf));
431
432	/*
433	 * Copy, skip or replace continued text.
434	 */
435	if (cp > STR(parse_buf)) {
436	    if (service_name_type_matched == 0)
437		vstream_fputs(STR(line_buf), dst);
438	    else if (mode & PCF_COMMENT_OUT)
439		vstream_fprintf(dst, "#%s", STR(line_buf));
440	}
441
442	/*
443	 * Copy or replace (start of) logical line.
444	 */
445	else {
446	    service_name_type_matched = 0;
447
448	    /*
449	     * Parse out the service name and type.
450	     */
451	    if ((service_name = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0
452		|| (service_type = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0)
453		msg_fatal("file %s: line %d: specify service name and type "
454			  "on the same line", path, lineno);
455	    if (strchr(service_name, '='))
456		msg_fatal("file %s: line %d: service name syntax \"%s\" is "
457			  "unsupported with %s", path, lineno, service_name,
458			  edit_opts);
459	    if (service_type[strcspn(service_type, "=/")] != 0)
460		msg_fatal("file %s: line %d: "
461			"service type syntax \"%s\" is unsupported with %s",
462			  path, lineno, service_type, edit_opts);
463
464	    /*
465	     * Match each service pattern.
466	     *
467	     * Additional care is needed when a request adds or replaces an
468	     * entire service definition, instead of a specific field or
469	     * parameter. Given a command "postconf -M name1/type1='name2
470	     * type2 ...'", where name1 and name2 may differ, and likewise
471	     * for type1 and type2:
472	     *
473	     * - First, if an existing service definition a) matches the service
474	     * pattern 'name1/type1', or b) matches the name and type in the
475	     * new service definition 'name2 type2 ...', remove the service
476	     * definition.
477	     *
478	     * - Then, after an a) or b) type match, add a new service
479	     * definition for 'name2 type2 ...', but only after the first
480	     * match.
481	     *
482	     * - Finally, if a request had no a) or b) type match for any
483	     * master.cf service definition, add a new service definition for
484	     * 'name2 type2 ...'.
485	     */
486	    for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
487		PCF_MASTER_ENT *tentative_entry = 0;
488		int     use_tentative_entry = 0;
489
490		/* Additional care for whole service definition requests. */
491		if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
492		    tentative_entry = (PCF_MASTER_ENT *)
493			mymalloc(sizeof(*tentative_entry));
494		    if ((err = pcf_parse_master_entry(tentative_entry,
495						      req->edit_value)) != 0)
496			msg_fatal("%s: \"%s\"", err, req->raw_text);
497		}
498		if (PCF_MATCH_SERVICE_PATTERN(req->service_pattern,
499					      service_name,
500					      service_type)) {
501		    service_name_type_matched = 1;	/* Sticky flag */
502		    req->match_count += 1;
503
504		    /*
505		     * Generate replacement master.cf entries.
506		     */
507		    if ((mode & PCF_EDIT_CONF)
508			|| ((mode & PCF_MASTER_PARAM) && (mode & PCF_EDIT_EXCL))) {
509			switch (mode & PCF_MASTER_MASK) {
510
511			    /*
512			     * Replace master.cf entry field or parameter
513			     * value.
514			     */
515			case PCF_MASTER_FLD:
516			case PCF_MASTER_PARAM:
517			    if (new_entry == 0) {
518				/* Gobble up any continuation lines. */
519				pcf_gobble_cf_line(full_entry_buf, line_buf,
520						   src, dst, &lineno);
521				new_entry = (PCF_MASTER_ENT *)
522				    mymalloc(sizeof(*new_entry));
523				if ((err = pcf_parse_master_entry(new_entry,
524						 STR(full_entry_buf))) != 0)
525				    msg_fatal("file %s: line %d: %s",
526					      path, lineno, err);
527			    }
528			    if (mode & PCF_MASTER_FLD) {
529				pcf_edit_master_field(new_entry,
530						      req->field_number,
531						      req->edit_value);
532			    } else {
533				pcf_edit_master_param(new_entry, mode,
534						      req->param_pattern,
535						      req->edit_value);
536			    }
537			    break;
538
539			    /*
540			     * Replace entire master.cf entry.
541			     */
542			case PCF_MASTER_ENTRY:
543			    if (req->match_count == 1)
544				use_tentative_entry = 1;
545			    break;
546			default:
547			    msg_panic("%s: unknown edit mode %d", myname, mode);
548			}
549		    }
550		} else if (tentative_entry != 0
551			 && PCF_MATCH_SERVICE_PATTERN(tentative_entry->argv,
552						      service_name,
553						      service_type)) {
554		    service_name_type_matched = 1;	/* Sticky flag */
555		    req->match_count += 1;
556		    if (req->match_count == 1)
557			use_tentative_entry = 1;
558		}
559		if (tentative_entry != 0) {
560		    if (use_tentative_entry) {
561			if (new_entry != 0)
562			    pcf_free_master_entry(new_entry);
563			new_entry = tentative_entry;
564		    } else {
565			pcf_free_master_entry(tentative_entry);
566		    }
567		}
568	    }
569
570	    /*
571	     * Pass through or replace the current input line.
572	     */
573	    if (new_entry) {
574		pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
575		pcf_free_master_entry(new_entry);
576		new_entry = 0;
577	    } else if (service_name_type_matched == 0) {
578		vstream_fputs(STR(line_buf), dst);
579	    } else if (mode & PCF_COMMENT_OUT) {
580		vstream_fprintf(dst, "#%s", STR(line_buf));
581	    }
582	}
583    }
584
585    /*
586     * Postprocessing: when editing entire service entries, generate new
587     * entries for services not found. Otherwise (editing fields or
588     * parameters), "service not found" is a fatal error.
589     */
590    for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
591	if (req->match_count == 0) {
592	    if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
593		new_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*new_entry));
594		if ((err = pcf_parse_master_entry(new_entry, req->edit_value)) != 0)
595		    msg_fatal("%s: \"%s\"", err, req->raw_text);
596		pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
597		pcf_free_master_entry(new_entry);
598	    } else if ((mode & PCF_MASTER_ENTRY) == 0) {
599		msg_warn("unmatched service_name/type: \"%s\"", req->raw_text);
600	    }
601	}
602    }
603
604    /*
605     * When all is well, rename the temp file to the original one.
606     */
607    if (vstream_fclose(src))
608	msg_fatal("read %s: %m", path);
609    if (edit_file_close(ep) != 0)
610	msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
611
612    /*
613     * Cleanup.
614     */
615    vstring_free(line_buf);
616    vstring_free(parse_buf);
617    vstring_free(full_entry_buf);
618    for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
619	argv_free(req->service_pattern);
620	myfree(req->parsed_text);
621    }
622    myfree((void *) edit_reqs);
623}
624