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