/*++ /* NAME /* postconf_edit 3 /* SUMMARY /* edit main.cf or master.cf /* SYNOPSIS /* #include /* /* void pcf_edit_main(mode, argc, argv) /* int mode; /* int argc; /* char **argv; /* /* void pcf_edit_master(mode, argc, argv) /* int mode; /* int argc; /* char **argv; /* DESCRIPTION /* pcf_edit_main() edits the \fBmain.cf\fR configuration file. /* It replaces or adds parameter settings given as "\fIname=value\fR" /* pairs given on the command line, or removes parameter /* settings given as "\fIname\fR" on the command line. The /* file is copied to a temporary file then renamed into place. /* /* pcf_edit_master() edits the \fBmaster.cf\fR configuration /* file. The file is copied to a temporary file then renamed /* into place. Depending on the flags in \fBmode\fR: /* .IP PCF_MASTER_ENTRY /* With PCF_EDIT_CONF, pcf_edit_master() replaces or adds /* entire master.cf entries, specified on the command line as /* "\fIname/type = name type private unprivileged chroot wakeup /* process_limit command...\fR". /* /* With PCF_EDIT_EXCL or PCF_COMMENT_OUT, pcf_edit_master() /* removes or comments out entries specified on the command /* line as "\fIname/type\fR. /* .IP PCF_MASTER_FLD /* With PCF_EDIT_CONF, pcf_edit_master() replaces the value /* of specific service attributes, specified on the command /* line as "\fIname/type/attribute = value\fR". /* .IP PCF_MASTER_PARAM /* With PCF_EDIT_CONF, pcf_edit_master() replaces or adds the /* value of service parameters, specified on the command line /* as "\fIname/type/parameter = value\fR". /* /* With PCF_EDIT_EXCL, pcf_edit_master() removes service /* parameters specified on the command line as "\fIparametername\fR". /* DIAGNOSTICS /* Problems are reported to the standard error stream. /* FILES /* /etc/postfix/main.cf, Postfix configuration parameters /* /etc/postfix/main.cf.tmp, temporary name /* /etc/postfix/master.cf, Postfix configuration parameters /* /etc/postfix/master.cf.tmp, temporary name /* LICENSE /* .ad /* .fi /* The Secure Mailer license must be distributed with this software. /* AUTHOR(S) /* Wietse Venema /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA /*--*/ /* System library. */ #include #include #include /* Utility library. */ #include #include #include #include #include #include #include #include #include /* Global library. */ #include /* Application-specific. */ #include #define STR(x) vstring_str(x) /* pcf_find_cf_info - pass-through non-content line, return content or null */ static char *pcf_find_cf_info(VSTRING *buf, VSTREAM *dst) { char *cp; for (cp = STR(buf); ISSPACE(*cp) /* including newline */ ; cp++) /* void */ ; /* Pass-through comment, all-whitespace, or empty line. */ if (*cp == '#' || *cp == 0) { vstream_fputs(STR(buf), dst); return (0); } else { return (cp); } } /* pcf_next_cf_line - return next content line, pass non-content */ static char *pcf_next_cf_line(VSTRING *buf, VSTREAM *src, VSTREAM *dst, int *lineno) { char *cp; while (vstring_get(buf, src) != VSTREAM_EOF) { if (lineno) *lineno += 1; if ((cp = pcf_find_cf_info(buf, dst)) != 0) return (cp); } return (0); } /* pcf_gobble_cf_line - accumulate multi-line content, pass non-content */ static void pcf_gobble_cf_line(VSTRING *full_entry_buf, VSTRING *line_buf, VSTREAM *src, VSTREAM *dst, int *lineno) { int ch; vstring_strcpy(full_entry_buf, STR(line_buf)); for (;;) { if ((ch = VSTREAM_GETC(src)) != VSTREAM_EOF) vstream_ungetc(src, ch); if ((ch != '#' && !ISSPACE(ch)) || vstring_get(line_buf, src) == VSTREAM_EOF) break; lineno += 1; if (pcf_find_cf_info(line_buf, dst)) vstring_strcat(full_entry_buf, STR(line_buf)); } } /* pcf_edit_main - edit main.cf file */ void pcf_edit_main(int mode, int argc, char **argv) { char *path; EDIT_FILE *ep; VSTREAM *src; VSTREAM *dst; VSTRING *buf = vstring_alloc(100); VSTRING *key = vstring_alloc(10); char *cp; char *pattern; char *edit_value; HTABLE *table; struct cvalue { char *value; int found; }; struct cvalue *cvalue; HTABLE_INFO **ht_info; HTABLE_INFO **ht; int interesting; const char *err; /* * Store command-line parameters for quick lookup. */ table = htable_create(argc); while ((cp = *argv++) != 0) { if (strchr(cp, '\n') != 0) msg_fatal("-e, -X, or -# accepts no multi-line input"); while (ISSPACE(*cp)) cp++; if (*cp == '#') msg_fatal("-e, -X, or -# accepts no comment input"); if (mode & PCF_EDIT_CONF) { if ((err = split_nameval(cp, &pattern, &edit_value)) != 0) msg_fatal("%s: \"%s\"", err, cp); } else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) { if (*cp == 0) msg_fatal("-X or -# requires non-blank parameter names"); if (strchr(cp, '=') != 0) msg_fatal("-X or -# requires parameter names without value"); pattern = cp; trimblanks(pattern, 0); edit_value = 0; } else { msg_panic("pcf_edit_main: unknown mode %d", mode); } if ((cvalue = htable_find(table, pattern)) != 0) { msg_warn("ignoring earlier request: '%s = %s'", pattern, cvalue->value); htable_delete(table, pattern, myfree); } cvalue = (struct cvalue *) mymalloc(sizeof(*cvalue)); cvalue->value = edit_value; cvalue->found = 0; htable_enter(table, pattern, (void *) cvalue); } /* * Open a temp file for the result. This uses a deterministic name so we * don't leave behind thrash with random names. */ pcf_set_config_dir(); path = concatenate(var_config_dir, "/", MAIN_CONF_FILE, (char *) 0); if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0) msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX); dst = ep->tmp_fp; /* * Open the original file for input. */ if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) { /* OK to delete, since we control the temp file name exclusively. */ (void) unlink(ep->tmp_path); msg_fatal("open %s for reading: %m", path); } /* * Copy original file to temp file, while replacing parameters on the * fly. Issue warnings for names found multiple times. */ #define STR(x) vstring_str(x) interesting = 0; while ((cp = pcf_next_cf_line(buf, src, dst, (int *) 0)) != 0) { /* Copy, skip or replace continued text. */ if (cp > STR(buf)) { if (interesting == 0) vstream_fputs(STR(buf), dst); else if (mode & PCF_COMMENT_OUT) vstream_fprintf(dst, "#%s", STR(buf)); } /* Copy or replace start of logical line. */ else { vstring_strncpy(key, cp, strcspn(cp, CHARS_SPACE "=")); cvalue = (struct cvalue *) htable_find(table, STR(key)); if ((interesting = !!cvalue) != 0) { if (cvalue->found++ == 1) msg_warn("%s: multiple entries for \"%s\"", path, STR(key)); if (mode & PCF_EDIT_CONF) vstream_fprintf(dst, "%s = %s\n", STR(key), cvalue->value); else if (mode & PCF_COMMENT_OUT) vstream_fprintf(dst, "#%s", cp); } else { vstream_fputs(STR(buf), dst); } } } /* * Generate new entries for parameters that were not found. */ if (mode & PCF_EDIT_CONF) { for (ht_info = ht = htable_list(table); *ht; ht++) { cvalue = (struct cvalue *) ht[0]->value; if (cvalue->found == 0) vstream_fprintf(dst, "%s = %s\n", ht[0]->key, cvalue->value); } myfree((void *) ht_info); } /* * When all is well, rename the temp file to the original one. */ if (vstream_fclose(src)) msg_fatal("read %s: %m", path); if (edit_file_close(ep) != 0) msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX); /* * Cleanup. */ myfree(path); vstring_free(buf); vstring_free(key); htable_free(table, myfree); } /* * Data structure to hold a master.cf edit request. */ typedef struct { int match_count; /* hit count */ const char *raw_text; /* unparsed command-line argument */ char *parsed_text; /* destructive parse */ ARGV *service_pattern; /* service name, type, ... */ int field_number; /* attribute field number */ const char *param_pattern; /* parameter name */ char *edit_value; /* value substring */ } PCF_MASTER_EDIT_REQ; /* pcf_edit_master - edit master.cf file */ void pcf_edit_master(int mode, int argc, char **argv) { const char *myname = "pcf_edit_master"; char *path; EDIT_FILE *ep; VSTREAM *src; VSTREAM *dst; VSTRING *line_buf = vstring_alloc(100); VSTRING *parse_buf = vstring_alloc(100); int lineno; PCF_MASTER_ENT *new_entry; VSTRING *full_entry_buf = vstring_alloc(100); char *cp; char *pattern; int service_name_type_matched; const char *err; PCF_MASTER_EDIT_REQ *edit_reqs; PCF_MASTER_EDIT_REQ *req; int num_reqs = argc; const char *edit_opts = "-Me, -Fe, -Pe, -X, or -#"; char *service_name; char *service_type; /* * Sanity check. */ if (num_reqs <= 0) msg_panic("%s: empty argument list", myname); /* * Preprocessing: split pattern=value, then split the pattern components. */ edit_reqs = (PCF_MASTER_EDIT_REQ *) mymalloc(sizeof(*edit_reqs) * num_reqs); for (req = edit_reqs; *argv != 0; req++, argv++) { req->match_count = 0; req->raw_text = *argv; cp = req->parsed_text = mystrdup(req->raw_text); if (strchr(cp, '\n') != 0) msg_fatal("%s accept no multi-line input", edit_opts); while (ISSPACE(*cp)) cp++; if (*cp == '#') msg_fatal("%s accept no comment input", edit_opts); /* Separate the pattern from the value. */ if (mode & PCF_EDIT_CONF) { if ((err = split_nameval(cp, &pattern, &req->edit_value)) != 0) msg_fatal("%s: \"%s\"", err, req->raw_text); #if 0 if ((mode & PCF_MASTER_PARAM) && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)]) msg_fatal("whitespace in parameter value: \"%s\"", req->raw_text); #endif } else if (mode & (PCF_COMMENT_OUT | PCF_EDIT_EXCL)) { if (strchr(cp, '=') != 0) msg_fatal("-X or -# requires names without value"); pattern = cp; trimblanks(pattern, 0); req->edit_value = 0; } else { msg_panic("%s: unknown mode %d", myname, mode); } #define PCF_MASTER_MASK (PCF_MASTER_ENTRY | PCF_MASTER_FLD | PCF_MASTER_PARAM) /* * Split name/type or name/type/whatever pattern into components. */ switch (mode & PCF_MASTER_MASK) { case PCF_MASTER_ENTRY: if ((req->service_pattern = pcf_parse_service_pattern(pattern, 2, 2)) == 0) msg_fatal("-Me, -MX or -M# requires service_name/type"); break; case PCF_MASTER_FLD: if ((req->service_pattern = pcf_parse_service_pattern(pattern, 3, 3)) == 0) msg_fatal("-Fe or -FX requires service_name/type/field_name"); req->field_number = pcf_parse_field_pattern(req->service_pattern->argv[2]); if (pcf_is_magic_field_pattern(req->field_number)) msg_fatal("-Fe does not accept wild-card field name"); if ((mode & PCF_EDIT_CONF) && req->field_number < PCF_MASTER_FLD_CMD && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)]) msg_fatal("-Fe does not accept whitespace in non-command field"); break; case PCF_MASTER_PARAM: if ((req->service_pattern = pcf_parse_service_pattern(pattern, 3, 3)) == 0) msg_fatal("-Pe or -PX requires service_name/type/parameter"); req->param_pattern = req->service_pattern->argv[2]; if (PCF_IS_MAGIC_PARAM_PATTERN(req->param_pattern)) msg_fatal("-Pe does not accept wild-card parameter name"); if ((mode & PCF_EDIT_CONF) && req->edit_value[strcspn(req->edit_value, PCF_MASTER_BLANKS)]) msg_fatal("-Pe does not accept whitespace in parameter value"); break; default: msg_panic("%s: unknown edit mode %d", myname, mode); } } /* * Open a temp file for the result. This uses a deterministic name so we * don't leave behind thrash with random names. */ pcf_set_config_dir(); path = concatenate(var_config_dir, "/", MASTER_CONF_FILE, (char *) 0); if ((ep = edit_file_open(path, O_CREAT | O_WRONLY, 0644)) == 0) msg_fatal("open %s%s: %m", path, EDIT_FILE_SUFFIX); dst = ep->tmp_fp; /* * Open the original file for input. */ if ((src = vstream_fopen(path, O_RDONLY, 0)) == 0) { /* OK to delete, since we control the temp file name exclusively. */ (void) unlink(ep->tmp_path); msg_fatal("open %s for reading: %m", path); } /* * Copy original file to temp file, while replacing service entries on * the fly. */ service_name_type_matched = 0; new_entry = 0; lineno = 0; while ((cp = pcf_next_cf_line(parse_buf, src, dst, &lineno)) != 0) { vstring_strcpy(line_buf, STR(parse_buf)); /* * Copy, skip or replace continued text. */ if (cp > STR(parse_buf)) { if (service_name_type_matched == 0) vstream_fputs(STR(line_buf), dst); else if (mode & PCF_COMMENT_OUT) vstream_fprintf(dst, "#%s", STR(line_buf)); } /* * Copy or replace (start of) logical line. */ else { service_name_type_matched = 0; /* * Parse out the service name and type. */ if ((service_name = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0 || (service_type = mystrtok(&cp, PCF_MASTER_BLANKS)) == 0) msg_fatal("file %s: line %d: specify service name and type " "on the same line", path, lineno); if (strchr(service_name, '=')) msg_fatal("file %s: line %d: service name syntax \"%s\" is " "unsupported with %s", path, lineno, service_name, edit_opts); if (service_type[strcspn(service_type, "=/")] != 0) msg_fatal("file %s: line %d: " "service type syntax \"%s\" is unsupported with %s", path, lineno, service_type, edit_opts); /* * Match each service pattern. * * Additional care is needed when a request adds or replaces an * entire service definition, instead of a specific field or * parameter. Given a command "postconf -M name1/type1='name2 * type2 ...'", where name1 and name2 may differ, and likewise * for type1 and type2: * * - First, if an existing service definition a) matches the service * pattern 'name1/type1', or b) matches the name and type in the * new service definition 'name2 type2 ...', remove the service * definition. * * - Then, after an a) or b) type match, add a new service * definition for 'name2 type2 ...', but only after the first * match. * * - Finally, if a request had no a) or b) type match for any * master.cf service definition, add a new service definition for * 'name2 type2 ...'. */ for (req = edit_reqs; req < edit_reqs + num_reqs; req++) { PCF_MASTER_ENT *tentative_entry = 0; int use_tentative_entry = 0; /* Additional care for whole service definition requests. */ if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) { tentative_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*tentative_entry)); if ((err = pcf_parse_master_entry(tentative_entry, req->edit_value)) != 0) msg_fatal("%s: \"%s\"", err, req->raw_text); } if (PCF_MATCH_SERVICE_PATTERN(req->service_pattern, service_name, service_type)) { service_name_type_matched = 1; /* Sticky flag */ req->match_count += 1; /* * Generate replacement master.cf entries. */ if ((mode & PCF_EDIT_CONF) || ((mode & PCF_MASTER_PARAM) && (mode & PCF_EDIT_EXCL))) { switch (mode & PCF_MASTER_MASK) { /* * Replace master.cf entry field or parameter * value. */ case PCF_MASTER_FLD: case PCF_MASTER_PARAM: if (new_entry == 0) { /* Gobble up any continuation lines. */ pcf_gobble_cf_line(full_entry_buf, line_buf, src, dst, &lineno); new_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*new_entry)); if ((err = pcf_parse_master_entry(new_entry, STR(full_entry_buf))) != 0) msg_fatal("file %s: line %d: %s", path, lineno, err); } if (mode & PCF_MASTER_FLD) { pcf_edit_master_field(new_entry, req->field_number, req->edit_value); } else { pcf_edit_master_param(new_entry, mode, req->param_pattern, req->edit_value); } break; /* * Replace entire master.cf entry. */ case PCF_MASTER_ENTRY: if (req->match_count == 1) use_tentative_entry = 1; break; default: msg_panic("%s: unknown edit mode %d", myname, mode); } } } else if (tentative_entry != 0 && PCF_MATCH_SERVICE_PATTERN(tentative_entry->argv, service_name, service_type)) { service_name_type_matched = 1; /* Sticky flag */ req->match_count += 1; if (req->match_count == 1) use_tentative_entry = 1; } if (tentative_entry != 0) { if (use_tentative_entry) { if (new_entry != 0) pcf_free_master_entry(new_entry); new_entry = tentative_entry; } else { pcf_free_master_entry(tentative_entry); } } } /* * Pass through or replace the current input line. */ if (new_entry) { pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry); pcf_free_master_entry(new_entry); new_entry = 0; } else if (service_name_type_matched == 0) { vstream_fputs(STR(line_buf), dst); } else if (mode & PCF_COMMENT_OUT) { vstream_fprintf(dst, "#%s", STR(line_buf)); } } } /* * Postprocessing: when editing entire service entries, generate new * entries for services not found. Otherwise (editing fields or * parameters), "service not found" is a fatal error. */ for (req = edit_reqs; req < edit_reqs + num_reqs; req++) { if (req->match_count == 0) { if ((mode & PCF_MASTER_ENTRY) && (mode & PCF_EDIT_CONF)) { new_entry = (PCF_MASTER_ENT *) mymalloc(sizeof(*new_entry)); if ((err = pcf_parse_master_entry(new_entry, req->edit_value)) != 0) msg_fatal("%s: \"%s\"", err, req->raw_text); pcf_print_master_entry(dst, PCF_FOLD_LINE, new_entry); pcf_free_master_entry(new_entry); } else if ((mode & PCF_MASTER_ENTRY) == 0) { msg_warn("unmatched service_name/type: \"%s\"", req->raw_text); } } } /* * When all is well, rename the temp file to the original one. */ if (vstream_fclose(src)) msg_fatal("read %s: %m", path); if (edit_file_close(ep) != 0) msg_fatal("close %s%s: %m", path, EDIT_FILE_SUFFIX); /* * Cleanup. */ myfree(path); vstring_free(line_buf); vstring_free(parse_buf); vstring_free(full_entry_buf); for (req = edit_reqs; req < edit_reqs + num_reqs; req++) { argv_free(req->service_pattern); myfree(req->parsed_text); } myfree((void *) edit_reqs); }