xref: /netbsd-src/external/ibm-public/postfix/dist/src/postconf/postconf_edit.c (revision 9fb66d812c00ebfb445c0b47dea128f32aa6fe96)
1 /*	$NetBSD: postconf_edit.c,v 1.2 2017/02/14 01:16:46 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 
98 static 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 
115 static 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 
130 static 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 
150 void    pcf_edit_main(int mode, int argc, char **argv)
151 {
152     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 	cvalue = (struct cvalue *) mymalloc(sizeof(*cvalue));
198 	cvalue->value = edit_value;
199 	cvalue->found = 0;
200 	htable_enter(table, pattern, (void *) cvalue);
201     }
202 
203     /*
204      * Open a temp file for the result. This uses a deterministic name so we
205      * don't leave behind thrash with random names.
206      */
207     pcf_set_config_dir();
208     path = concatenate(var_config_dir, "/", MAIN_CONF_FILE, (char *) 0);
209     if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
210 	msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
211     dst = ep->tmp_fp;
212 
213     /*
214      * Open the original file for input.
215      */
216     if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
217 	/* OK to delete, since we control the temp file name exclusively. */
218 	(void) unlink(ep->tmp_path);
219 	msg_fatal("open %s for reading: %m", path);
220     }
221 
222     /*
223      * Copy original file to temp file, while replacing parameters on the
224      * fly. Issue warnings for names found multiple times.
225      */
226 #define STR(x) vstring_str(x)
227 
228     interesting = 0;
229     while ((cp = pcf_next_cf_line(buf, src, dst, (int *) 0)) != 0) {
230 	/* Copy, skip or replace continued text. */
231 	if (cp > STR(buf)) {
232 	    if (interesting == 0)
233 		vstream_fputs(STR(buf), dst);
234 	    else if (mode & PCF_COMMENT_OUT)
235 		vstream_fprintf(dst, "#%s", STR(buf));
236 	}
237 	/* Copy or replace start of logical line. */
238 	else {
239 	    vstring_strncpy(key, cp, strcspn(cp, CHARS_SPACE "="));
240 	    cvalue = (struct cvalue *) htable_find(table, STR(key));
241 	    if ((interesting = !!cvalue) != 0) {
242 		if (cvalue->found++ == 1)
243 		    msg_warn("%s: multiple entries for \"%s\"", path, STR(key));
244 		if (mode & PCF_EDIT_CONF)
245 		    vstream_fprintf(dst, "%s = %s\n", STR(key), cvalue->value);
246 		else if (mode & PCF_COMMENT_OUT)
247 		    vstream_fprintf(dst, "#%s", cp);
248 	    } else {
249 		vstream_fputs(STR(buf), dst);
250 	    }
251 	}
252     }
253 
254     /*
255      * Generate new entries for parameters that were not found.
256      */
257     if (mode & PCF_EDIT_CONF) {
258 	for (ht_info = ht = htable_list(table); *ht; ht++) {
259 	    cvalue = (struct cvalue *) ht[0]->value;
260 	    if (cvalue->found == 0)
261 		vstream_fprintf(dst, "%s = %s\n", ht[0]->key, cvalue->value);
262 	}
263 	myfree((void *) ht_info);
264     }
265 
266     /*
267      * When all is well, rename the temp file to the original one.
268      */
269     if (vstream_fclose(src))
270 	msg_fatal("read %s: %m", path);
271     if (edit_file_close(ep) != 0)
272 	msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
273 
274     /*
275      * Cleanup.
276      */
277     myfree(path);
278     vstring_free(buf);
279     vstring_free(key);
280     htable_free(table, myfree);
281 }
282 
283  /*
284   * Data structure to hold a master.cf edit request.
285   */
286 typedef struct {
287     int     match_count;		/* hit count */
288     const char *raw_text;		/* unparsed command-line argument */
289     char   *parsed_text;		/* destructive parse */
290     ARGV   *service_pattern;		/* service name, type, ... */
291     int     field_number;		/* attribute field number */
292     const char *param_pattern;		/* parameter name */
293     char   *edit_value;			/* value substring */
294 } PCF_MASTER_EDIT_REQ;
295 
296 /* pcf_edit_master - edit master.cf file */
297 
298 void    pcf_edit_master(int mode, int argc, char **argv)
299 {
300     const char *myname = "pcf_edit_master";
301     char   *path;
302     EDIT_FILE *ep;
303     VSTREAM *src;
304     VSTREAM *dst;
305     VSTRING *line_buf = vstring_alloc(100);
306     VSTRING *parse_buf = vstring_alloc(100);
307     int     lineno;
308     PCF_MASTER_ENT *new_entry;
309     VSTRING *full_entry_buf = vstring_alloc(100);
310     char   *cp;
311     char   *pattern;
312     int     service_name_type_matched;
313     const char *err;
314     PCF_MASTER_EDIT_REQ *edit_reqs;
315     PCF_MASTER_EDIT_REQ *req;
316     int     num_reqs = argc;
317     const char *edit_opts = "-Me, -Fe, -Pe, -X, or -#";
318     char   *service_name;
319     char   *service_type;
320 
321     /*
322      * Sanity check.
323      */
324     if (num_reqs <= 0)
325 	msg_panic("%s: empty argument list", myname);
326 
327     /*
328      * Preprocessing: split pattern=value, then split the pattern components.
329      */
330     edit_reqs = (PCF_MASTER_EDIT_REQ *) mymalloc(sizeof(*edit_reqs) * num_reqs);
331     for (req = edit_reqs; *argv != 0; req++, argv++) {
332 	req->match_count = 0;
333 	req->raw_text = *argv;
334 	cp = req->parsed_text = mystrdup(req->raw_text);
335 	if (strchr(cp, '\n') != 0)
336 	    msg_fatal("%s accept no multi-line input", edit_opts);
337 	while (ISSPACE(*cp))
338 	    cp++;
339 	if (*cp == '#')
340 	    msg_fatal("%s accept no comment input", edit_opts);
341 	/* Separate the pattern from the value. */
342 	if (mode & PCF_EDIT_CONF) {
343 	    if ((err = split_nameval(cp, &pattern, &req->edit_value)) != 0)
344 		msg_fatal("%s: \"%s\"", err, req->raw_text);
345 #if 0
346 	    if ((mode & PCF_MASTER_PARAM)
347 	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
348 		msg_fatal("whitespace in parameter value: \"%s\"",
349 			  req->raw_text);
350 #endif
351 	} else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) {
352 	    if (strchr(cp, '=') != 0)
353 		msg_fatal("-X or -# requires names without value");
354 	    pattern = cp;
355 	    trimblanks(pattern, 0);
356 	    req->edit_value = 0;
357 	} else {
358 	    msg_panic("%s: unknown mode %d", myname, mode);
359 	}
360 
361 #define PCF_MASTER_MASK (PCF_MASTER_ENTRY | PCF_MASTER_FLD | PCF_MASTER_PARAM)
362 
363 	/*
364 	 * Split name/type or name/type/whatever pattern into components.
365 	 */
366 	switch (mode & PCF_MASTER_MASK) {
367 	case PCF_MASTER_ENTRY:
368 	    if ((req->service_pattern =
369 		 pcf_parse_service_pattern(pattern, 2, 2)) == 0)
370 		msg_fatal("-Me, -MX or -M# requires service_name/type");
371 	    break;
372 	case PCF_MASTER_FLD:
373 	    if ((req->service_pattern =
374 		 pcf_parse_service_pattern(pattern, 3, 3)) == 0)
375 		msg_fatal("-Fe or -FX requires service_name/type/field_name");
376 	    req->field_number =
377 		pcf_parse_field_pattern(req->service_pattern->argv[2]);
378 	    if (pcf_is_magic_field_pattern(req->field_number))
379 		msg_fatal("-Fe does not accept wild-card field name");
380 	    if ((mode & PCF_EDIT_CONF)
381 		&& req->field_number < PCF_MASTER_FLD_CMD
382 	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
383 		msg_fatal("-Fe does not accept whitespace in non-command field");
384 	    break;
385 	case PCF_MASTER_PARAM:
386 	    if ((req->service_pattern =
387 		 pcf_parse_service_pattern(pattern, 3, 3)) == 0)
388 		msg_fatal("-Pe or -PX requires service_name/type/parameter");
389 	    req->param_pattern = req->service_pattern->argv[2];
390 	    if (PCF_IS_MAGIC_PARAM_PATTERN(req->param_pattern))
391 		msg_fatal("-Pe does not accept wild-card parameter name");
392 	    if ((mode & PCF_EDIT_CONF)
393 	    && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)])
394 		msg_fatal("-Pe does not accept whitespace in parameter value");
395 	    break;
396 	default:
397 	    msg_panic("%s: unknown edit mode %d", myname, mode);
398 	}
399     }
400 
401     /*
402      * Open a temp file for the result. This uses a deterministic name so we
403      * don't leave behind thrash with random names.
404      */
405     pcf_set_config_dir();
406     path = concatenate(var_config_dir, "/", MASTER_CONF_FILE, (char *) 0);
407     if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0)
408 	msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX);
409     dst = ep->tmp_fp;
410 
411     /*
412      * Open the original file for input.
413      */
414     if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) {
415 	/* OK to delete, since we control the temp file name exclusively. */
416 	(void) unlink(ep->tmp_path);
417 	msg_fatal("open %s for reading: %m", path);
418     }
419 
420     /*
421      * Copy original file to temp file, while replacing service entries on
422      * the fly.
423      */
424     service_name_type_matched = 0;
425     new_entry = 0;
426     lineno = 0;
427     while ((cp = pcf_next_cf_line(parse_buf, src, dst, &lineno)) != 0) {
428 	vstring_strcpy(line_buf, STR(parse_buf));
429 
430 	/*
431 	 * Copy, skip or replace continued text.
432 	 */
433 	if (cp > STR(parse_buf)) {
434 	    if (service_name_type_matched == 0)
435 		vstream_fputs(STR(line_buf), dst);
436 	    else if (mode & PCF_COMMENT_OUT)
437 		vstream_fprintf(dst, "#%s", STR(line_buf));
438 	}
439 
440 	/*
441 	 * Copy or replace (start of) logical line.
442 	 */
443 	else {
444 	    service_name_type_matched = 0;
445 
446 	    /*
447 	     * Parse out the service name and type.
448 	     */
449 	    if ((service_name = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0
450 		|| (service_type = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0)
451 		msg_fatal("file %s: line %d: specify service name and type "
452 			  "on the same line", path, lineno);
453 	    if (strchr(service_name, '='))
454 		msg_fatal("file %s: line %d: service name syntax \"%s\" is "
455 			  "unsupported with %s", path, lineno, service_name,
456 			  edit_opts);
457 	    if (service_type[strcspn(service_type, "=/")] != 0)
458 		msg_fatal("file %s: line %d: "
459 			"service type syntax \"%s\" is unsupported with %s",
460 			  path, lineno, service_type, edit_opts);
461 
462 	    /*
463 	     * Match each service pattern.
464 	     */
465 	    for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
466 		if (PCF_MATCH_SERVICE_PATTERN(req->service_pattern,
467 					      service_name,
468 					      service_type)) {
469 		    service_name_type_matched = 1;	/* Sticky flag */
470 		    req->match_count += 1;
471 
472 		    /*
473 		     * Generate replacement master.cf entries.
474 		     */
475 		    if ((mode & PCF_EDIT_CONF)
476 			|| ((mode & PCF_MASTER_PARAM) && (mode & PCF_EDIT_EXCL))) {
477 			switch (mode & PCF_MASTER_MASK) {
478 
479 			    /*
480 			     * Replace master.cf entry field or parameter
481 			     * value.
482 			     */
483 			case PCF_MASTER_FLD:
484 			case PCF_MASTER_PARAM:
485 			    if (new_entry == 0) {
486 				/* Gobble up any continuation lines. */
487 				pcf_gobble_cf_line(full_entry_buf, line_buf,
488 						   src, dst, &lineno);
489 				new_entry = (PCF_MASTER_ENT *)
490 				    mymalloc(sizeof(*new_entry));
491 				if ((err = pcf_parse_master_entry(new_entry,
492 						 STR(full_entry_buf))) != 0)
493 				    msg_fatal("file %s: line %d: %s",
494 					      path, lineno, err);
495 			    }
496 			    if (mode & PCF_MASTER_FLD) {
497 				pcf_edit_master_field(new_entry,
498 						      req->field_number,
499 						      req->edit_value);
500 			    } else {
501 				pcf_edit_master_param(new_entry, mode,
502 						      req->param_pattern,
503 						      req->edit_value);
504 			    }
505 			    break;
506 
507 			    /*
508 			     * Replace entire master.cf entry.
509 			     */
510 			case PCF_MASTER_ENTRY:
511 			    if (new_entry != 0)
512 				pcf_free_master_entry(new_entry);
513 			    new_entry = (PCF_MASTER_ENT *)
514 				mymalloc(sizeof(*new_entry));
515 			    if ((err = pcf_parse_master_entry(new_entry,
516 						     req->edit_value)) != 0)
517 				msg_fatal("%s: \"%s\"", err, req->raw_text);
518 			    break;
519 			default:
520 			    msg_panic("%s: unknown edit mode %d", myname, mode);
521 			}
522 		    }
523 		}
524 	    }
525 
526 	    /*
527 	     * Pass through or replace the current input line.
528 	     */
529 	    if (new_entry) {
530 		pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
531 		pcf_free_master_entry(new_entry);
532 		new_entry = 0;
533 	    } else if (service_name_type_matched == 0) {
534 		vstream_fputs(STR(line_buf), dst);
535 	    } else if (mode & PCF_COMMENT_OUT) {
536 		vstream_fprintf(dst, "#%s", STR(line_buf));
537 	    }
538 	}
539     }
540 
541     /*
542      * Postprocessing: when editing entire service entries, generate new
543      * entries for services not found. Otherwise (editing fields or
544      * parameters), "service not found" is a fatal error.
545      */
546     for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
547 	if (req->match_count == 0) {
548 	    if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) {
549 		new_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*new_entry));
550 		if ((err = pcf_parse_master_entry(new_entry, req->edit_value)) != 0)
551 		    msg_fatal("%s: \"%s\"", err, req->raw_text);
552 		pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry);
553 		pcf_free_master_entry(new_entry);
554 	    } else if ((mode & PCF_MASTER_ENTRY) == 0) {
555 		msg_warn("unmatched service_name/type: \"%s\"", req->raw_text);
556 	    }
557 	}
558     }
559 
560     /*
561      * When all is well, rename the temp file to the original one.
562      */
563     if (vstream_fclose(src))
564 	msg_fatal("read %s: %m", path);
565     if (edit_file_close(ep) != 0)
566 	msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX);
567 
568     /*
569      * Cleanup.
570      */
571     myfree(path);
572     vstring_free(line_buf);
573     vstring_free(parse_buf);
574     vstring_free(full_entry_buf);
575     for (req = edit_reqs; req < edit_reqs + num_reqs; req++) {
576 	argv_free(req->service_pattern);
577 	myfree(req->parsed_text);
578     }
579     myfree((void *) edit_reqs);
580 }
581