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
pcf_find_cf_info(VSTRING * buf,VSTREAM * dst)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
pcf_next_cf_line(VSTRING * buf,VSTREAM * src,VSTREAM * dst,int * lineno)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
pcf_gobble_cf_line(VSTRING * full_entry_buf,VSTRING * line_buf,VSTREAM * src,VSTREAM * dst,int * lineno)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
pcf_edit_main(int mode,int argc,char ** argv)150 void 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 */
289 typedef 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
pcf_edit_master(int mode,int argc,char ** argv)301 void 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