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