Add the ability to negate matches for the daemon's "refuse options".
authorWayne Davison <wayne@opencoder.net>
Mon, 18 May 2020 04:29:11 +0000 (21:29 -0700)
committerWayne Davison <wayne@opencoder.net>
Mon, 18 May 2020 04:29:11 +0000 (21:29 -0700)
NEWS
options.c
rsyncd.conf.yo

diff --git a/NEWS b/NEWS
index 23b7f034dd1d64ed00dea36df06c98df49883ad3..d29ea3c4c67d4b49aa315e02c1659ee0389afaa4 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,9 @@ Changes since 3.1.3:
       script. Its value is the user-specified port number (set via --port or an
       rsync:// URL) or 0 if the user didn't override the port.
 
+    - Added negated matching to the daemon's "refuse options" setting by using
+      match strings that start with a "!" (such as "!compress*").
+
     - Added a status output based on a signal (via both SIGINFO & SIGVTALRM).
 
     - Added a --copy-as=USER option to give some extra security to root-run
index a66930064eb6463487cfff25280efbb33bccacaf..ebf31a603a294adfbeedee0b3f6ef65a24585e36 100644 (file)
--- a/options.c
+++ b/options.c
@@ -1143,75 +1143,145 @@ void option_error(void)
 }
 
 
+static void set_one_refuse_option(int negated, const char *ref, const struct poptOption *list_end)
+{
+       struct poptOption *op;
+       char shortName[2];
+       int is_wild = strpbrk(ref, "*?[") != NULL;
+       int found_match = 0;
+
+       shortName[1] = '\0';
+
+       if (strcmp("a", ref) == 0 || strcmp("archive", ref) == 0) {
+               ref = "[ardlptgoD]";
+               is_wild = 1;
+       }
+
+       for (op = long_options; op != list_end; op++) {
+               *shortName = op->shortName;
+               if ((op->longName && wildmatch(ref, op->longName))
+                || (*shortName && wildmatch(ref, shortName))) {
+                       if (*op->descrip == 'a' || *op->descrip == 'r')
+                               op->descrip = negated ? "accepted" : "refused";
+                       else if (!is_wild)
+                               op->descrip = negated ? "ACCEPTED" : "REFUSED";
+                       found_match = 1;
+                       if (!is_wild)
+                               break;
+               }
+       }
+
+       if (!found_match)
+               rprintf(FLOG, "No match for refuse-options string \"%s\"\n", ref);
+}
+
+
 /**
  * Tweak the option table to disable all options that the rsyncd.conf
  * file has told us to refuse.
  **/
-static void set_refuse_options(char *bp)
+static void set_refuse_options(void)
 {
-       struct poptOption *op;
-       char *cp, shortname[2];
-       int is_wild, found_match;
+       struct poptOption *op, *list_end = NULL;
+       char *cp, *ref = lp_refuse_options(module_id);
+       int negated;
 
-       shortname[1] = '\0';
+       if (!ref)
+               ref = "";
 
-       while (1) {
-               while (*bp == ' ') bp++;
-               if (!*bp)
+       if (!*ref && !am_daemon) /* A simple optimization */
+               return;
+
+       /* We abuse the descrip field in poptOption to make it easy to flag which options
+        * are refused (since we don't use it otherwise).  Start by marking all options
+        * as accepted except for some that are marked as ACCEPTED (non-wild-matched). */
+       for (op = long_options; ; op++) {
+               const char *longName = op->longName ? op->longName : "";
+               if (!op->longName && !op->shortName) {
+                       list_end = op;
                        break;
-               if ((cp = strchr(bp, ' ')) != NULL)
-                       *cp= '\0';
-               is_wild = strpbrk(bp, "*?[") != NULL;
-               found_match = 0;
-               for (op = long_options; ; op++) {
-                       *shortname = op->shortName;
-                       if (!op->longName && !*shortname)
-                               break;
-                       if ((op->longName && wildmatch(bp, op->longName))
-                           || (*shortname && wildmatch(bp, shortname))) {
-                               if (op->argInfo == POPT_ARG_VAL)
-                                       op->argInfo = POPT_ARG_NONE;
-                               op->val = (op - long_options) + OPT_REFUSED_BASE;
-                               found_match = 1;
-                               /* These flags are set to let us easily check
-                                * an implied option later in the code. */
-                               switch (*shortname) {
-                               case 'r': case 'd': case 'l': case 'p':
-                               case 't': case 'g': case 'o': case 'D':
-                                       refused_archive_part = op->val;
-                                       break;
-                               case 'z':
-                                       refused_compress = op->val;
-                                       break;
-                               case '\0':
-                                       if (wildmatch("delete", op->longName))
-                                               refused_delete = op->val;
-                                       else if (wildmatch("delete-before", op->longName))
-                                               refused_delete_before = op->val;
-                                       else if (wildmatch("delete-during", op->longName))
-                                               refused_delete_during = op->val;
-                                       else if (wildmatch("partial", op->longName))
-                                               refused_partial = op->val;
-                                       else if (wildmatch("progress", op->longName))
-                                               refused_progress = op->val;
-                                       else if (wildmatch("inplace", op->longName))
-                                               refused_inplace = op->val;
-                                       else if (wildmatch("no-iconv", op->longName))
-                                               refused_no_iconv = op->val;
-                                       break;
-                               }
-                               if (!is_wild)
-                                       break;
-                       }
-               }
-               if (!found_match) {
-                       rprintf(FLOG, "No match for refuse-options string \"%s\"\n",
-                               bp);
                }
+               /* These options are protected from wild-card matching, but the user is free to
+                * shoot themselves in the foot if they specify the option explicitly. */
+               if (op->shortName == 'e'
+                || op->shortName == '0' /* --from0 just modifies --files-from, so refuse that instead (or not) */
+                || op->shortName == 's' /* --protect-args is always OK */
+                || op->shortName == 'n' /* --dry-run is always OK */
+                || strcmp("server", longName) == 0
+                || strcmp("sender", longName) == 0
+                || strcmp("iconv", longName) == 0
+                || strcmp("no-iconv", longName) == 0
+                || strcmp("checksum-seed", longName) == 0
+                || strcmp("write-devices", longName) == 0 /* disable wild-match (it gets refused below) */
+                || strcmp("log-format", longName) == 0)
+                       op->descrip = "ACCEPTED";
+               else
+                       op->descrip = "accepted";
+       }
+       assert(list_end != NULL);
+
+       if (am_daemon) /* Refused by default, but can be accepted via "!write-devices" */
+               set_one_refuse_option(0, "write-devices", list_end);
+
+       while (1) {
+               while (*ref == ' ') ref++;
+               if (!*ref)
+                       break;
+               if ((cp = strchr(ref, ' ')) != NULL)
+                       *cp = '\0';
+               negated = *ref == '!';
+               if (negated && ref[1])
+                       ref++;
+               set_one_refuse_option(negated, ref, list_end);
                if (!cp)
                        break;
                *cp = ' ';
-               bp = cp + 1;
+               ref = cp + 1;
+       }
+
+       if (am_daemon) {
+#ifdef ICONV_OPTION
+               if (!*lp_charset(module_id))
+                       set_one_refuse_option(0, "iconv", list_end);
+#endif
+               set_one_refuse_option(0, "log-file", list_end);
+       }
+
+       /* Now we use the descrip values to actually mark the options for refusal. */
+       for (op = long_options; op != list_end; op++) {
+               int refused = *op->descrip == 'r' || *op->descrip == 'R';
+               op->descrip = NULL;
+               if (!refused)
+                       continue;
+               if (op->argInfo == POPT_ARG_VAL)
+                       op->argInfo = POPT_ARG_NONE;
+               op->val = (op - long_options) + OPT_REFUSED_BASE;
+               /* The following flags are set to let us easily check an implied option later in the code. */
+               switch (op->shortName) {
+               case 'r': case 'd': case 'l': case 'p':
+               case 't': case 'g': case 'o': case 'D':
+                       refused_archive_part = op->val;
+                       break;
+               case 'z':
+                       refused_compress = op->val;
+                       break;
+               case '\0':
+                       if (strcmp("delete", op->longName) == 0)
+                               refused_delete = op->val;
+                       else if (strcmp("delete-before", op->longName) == 0)
+                               refused_delete_before = op->val;
+                       else if (strcmp("delete-during", op->longName) == 0)
+                               refused_delete_during = op->val;
+                       else if (strcmp("partial", op->longName) == 0)
+                               refused_partial = op->val;
+                       else if (strcmp("progress", op->longName) == 0)
+                               refused_progress = op->val;
+                       else if (strcmp("inplace", op->longName) == 0)
+                               refused_inplace = op->val;
+                       else if (strcmp("no-iconv", op->longName) == 0)
+                               refused_no_iconv = op->val;
+                       break;
+               }
        }
 }
 
@@ -1327,7 +1397,6 @@ static void popt_unalias(poptContext con, const char *opt)
 int parse_arguments(int *argc_p, const char ***argv_p)
 {
        static poptContext pc;
-       char *ref = lp_refuse_options(module_id);
        const char *arg, **argv = *argv_p;
        int argc = *argc_p;
        int opt;
@@ -1337,16 +1406,8 @@ int parse_arguments(int *argc_p, const char ***argv_p)
                strlcpy(err_buf, "argc is zero!\n", sizeof err_buf);
                return 0;
        }
-       if (ref && *ref)
-               set_refuse_options(ref);
-       if (am_daemon) {
-               set_refuse_options("log-file*");
-#ifdef ICONV_OPTION
-               if (!*lp_charset(module_id))
-                       set_refuse_options("iconv");
-#endif
-               set_refuse_options("write-devices");
-       }
+
+       set_refuse_options();
 
 #ifdef ICONV_OPTION
        if (!am_daemon && protect_args <= 0 && (arg = getenv("RSYNC_ICONV")) != NULL && *arg)
@@ -1555,7 +1616,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
 
                case 'U':
                        if (++preserve_atimes > 1)
-                           open_noatime = 1;
+                               open_noatime = 1;
                        break;
 
                case 'v':
index 8f004ae6ce89f95f39597f50f1d30eda7e724190..c3bc3dd17bb97be7dc71beb3aa61969d318a3830 100644 (file)
@@ -735,28 +735,73 @@ is specified in seconds. A value of zero means no timeout and is the
 default. A good choice for anonymous rsync daemons may be 600 (giving
 a 10 minute timeout).
 
-dit(bf(refuse options)) This parameter allows you to
-specify a space-separated list of rsync command line options that will
-be refused by your rsync daemon.
+dit(bf(refuse options)) This parameter allows you to specify a space-separated
+list of rsync command line options that will be refused by your rsync daemon.
 You may specify the full option name, its one-letter abbreviation, or a
-wild-card string that matches multiple options.
+wild-card string that matches multiple options. Beginning in 3.2.0, you can
+also negate a match term by starting it with a "!".
+
+When an option is refused, the daemon prints an error message and exits.
+
 For example, this would refuse bf(--checksum) (bf(-c)) and all the various
 delete options:
 
-quote(tt(    refuse options = c delete))
+verb(    refuse options = c delete)
 
 The reason the above refuses all delete options is that the options imply
 bf(--delete), and implied options are refused just like explicit options.
+
+The use of a negated match allows you to fine-tune your refusals after a
+wild-card, such as this:
+
+verb(    refuse options = delete-* !delete-during)
+
+Negated matching can also turn your list of refused options into a list of
+accepted options. To do this, begin the list with a "*" (to refuse all options)
+and then specify one or more negated matches to allow.  For example:
+
+verb(    refuse options = * !a !v !compress*)
+
+Don't worry that the "*" will refuse certain vital options such as
+bf(--server), bf(--no-iconv), bf(--protect-args), etc. These important options
+are not matched by a wild-card, so they must be overridden by their exact name.
+For instance, if you're forcing iconv transfers you could use something like
+this:
+
+verb(    refuse options = * no-iconv !a !v)
+
+As an additional aid (beginning in 3.2.0), refusing (or "!refusing") the "a" or
+"archive"  option also affects all the options that the bf(--archive) option
+implies (bf(-rdlptgoD)), but only if the option  is matched explicitly (not
+using a wildcard). If you want to do something tricky, you can use "archive*"
+to avoid this side-effect, but keep in mind that no normal rsync client ever
+sends the actual archive option to the server.
+
 As an additional safety feature, the refusal of "delete" also refuses
 bf(remove-source-files) when the daemon is the sender; if you want the latter
-without the former, instead refuse "delete-*" -- that refuses all the
-delete modes without affecting bf(--remove-source-files).
+without the former, instead refuse "delete-*" as that refuses all the delete
+modes without affecting bf(--remove-source-files). (Keep in mind that the
+client's bf(--delete) option typically enables bf(--delete-during).)
 
-When an option is refused, the daemon prints an error message and exits.
-To prevent all compression when serving files,
-you can use "dont compress = *" (see below)
-instead of "refuse options = compress" to avoid returning an error to a
-client that requests compression.
+When un-refusing delete options, you should either specify "!delete*" (to
+accept all delete options) or specify a limited set that includes "delete",
+such as:
+
+verb(    refuse options = * !a !delete !delete-during)
+
+... whereas this accepts any delete option except bf(--delete-after):
+
+verb(    refuse options = * !a !delete* delete-after)
+
+A note on refusing "compress" -- it is better to set the "dont compress" daemon
+option to "*" because that disables compression silently instead of returning
+an error that forces the client to remove the bf(-z) option.
+
+If you are un-refusing the compress option, you probably want to match
+"!compress*" so that you also allow the bf(--compress-level) option.
+
+Finally, the "write-devices" option is refused by default, but can be
+explicitly enabled with "!write-devices".
 
 dit(bf(dont compress)) This parameter allows you to select
 filenames based on wildcard patterns that should not be compressed