# Confirm patch # Joe R. Jah # September 16, 2000 # # Adopted from: # http://www.waste.org/~oxymoron/majordomo/ftp/newconfirm.patch # and fixed its duplicate key bug. # # This patch for 1.94.5 provides more robust handling for open+confirm # un/subscription policies. The patch also fixes a bug for closed # unsubscribe_policy. It adds a new address for Majordomo for handling # subscription confirmations. When a user requests a subscription, they # are sent a message with a randomly generated key in the subject and # the body explaining the confirmation process and telling them what # address made the request. To confirm, they just have to hit reply and # launch off the message. Other messages to the confirmation address # will be bounced back with instructions for contacting the proper address. # # This should be more secure than the old auth cookie method and easier for # users. It also makes administration simpler by not requiring additional # owner approval for open lists when address_a subscribes address_b # (confirmation makes this redundant). # # It requires the following entries in majordomo.cf: # # $confirm_address = "confirm\@$whereami"; # $confirm_expire = 14*24*3600;# Seconds to wait before expiring (2 weeks) # $confirm_randev = "/dev/random";# Leave undefined to use rand instead # $confirm_keys = "$listdir/confirm.keys";# Or some other MD-writable place # # It also requires to add an alias at the confirm address to call majordomo # with the -a switch: # # confirm: "|/usr/local/majordomo/wrapper majordomo -a" # # To apply the patch: # # - Save the patch to a file on your Majordomo server. # - Login as Majordomo user, (e.g. majordom.) # - In Majordomo home run the following command: # # patch < /path/to/majordomo.5 # --- majordomo-1.94.5/majordomo Thu Jan 13 09:29:31 2000 +++ majordomo Sat Sep 16 19:28:13 2000 @@ -27,7 +27,11 @@ # Read and execute the .cf file $cf = $ENV{"MAJORDOMO_CF"} || "/etc/majordomo.cf"; -while ($ARGV[0]) { # parse for config file or default list +# Set the random number seed for the rand operator, (for versions of Perl +# before 5.004) +srand time^$$; + +while ($ARGV[0]) { # parse for config file, default list, or auth if ($ARGV[0] =~ /^-C$/i) { # sendmail v8 clobbers case $cf = $ARGV[1]; shift(@ARGV); @@ -36,6 +40,9 @@ $deflist = $ARGV[1]; shift(@ARGV); shift(@ARGV); + } elsif ($ARGV[0] eq "-a") { # We're at the confirm address + $confirm_mode=1; + shift(@ARGV); } else { die "Unknown argument $ARGV[0]\n"; } @@ -125,7 +132,7 @@ # if somebody has set $reply_to to be our own input address, there's a problem. if (&addr_match($reply_to, $whoami)) { - &abort( "$whoami punting to avoid mail loop.\n"); + &abort( "$whoami not replying to $1 to avoid mail loop.\n"); exit 0; } @@ -160,11 +167,137 @@ select((select(REPLY), $| = 1)[0]); +if($confirm_mode) # search the message for keys +{ + local(*KEYS, *NEW); + local(%keyaddr,%keycmd,%keytime, %keylist, @keys); + local(@a,$c,$t,$l,$k, $key, $msg); + + print STDERR "$0: processing confirm message.\n" if $DEBUG; + + &lopen(KEYS,"","$confirm_keys") || + &abort("Couldn't read key file $confirm_keys: $!"); + + (local($mode, $uid, $gid)=(stat(KEYS))[2,4,5]) || + &abort("Can't stat key file $confirm_keys: $!"); + open(NEW,">$confirm_keys.new") || + &abort("Can't open $confirm_keys.new: $!"); + chmod($mode, "$confirm_keys.new") || + &abort("chmod($mode, \"$confirm_keys.new\"): $!"); + chown($uid, $gid, "$confirm_keys.new") || + &abort("chown($uid, $gid, \"$confirm_keys.new\"): $!"); + + while() + { + # Break line into fields, using all trailing fields for address + ($k,$t,$c,$l,@a)=split(/\t/,$_); + if($t>time) # Is the key still valid + { + $keyaddr{$k}=join("\t",@a); + $keyaddr{$k}=~ s/\n$//; + $keycmd{$k}=$c; + $keytime{$k}=$t; + $keylist{$k}=$l; + + push(@keys,$_); + } + } + + if($hdrs{'subject'} =~ /\b([A-F0-9]{7}-[A-F0-9]{7})\b/i) + { + if($keytime{$1}) + { + $key=$1; + } + } + + $msg.="> Subject: $hdrs{'subject'}\n>\n"; + + if(!$key) + { + while(<>) + { + $msg.="> $_"; + + while(/\b([A-F0-9]{7}-[A-F0-9]{7})\b/ig) # look all over for keys + { + if($keytime{$1}) + { + $key=$1; + last; + } + } + } + } + + while(<>) # eat up remaining input + { + $msg.="> $_"; + } + + if($key) + { + print STDERR "$0: found key $key-> $keycmd{$key} $keylist{$key} $keyaddr{$key}.\n" if $DEBUG; + + $confirmed=1; + if($keycmd{$key} eq "subscribe") + { + print REPLY ">>>> Confirmation for \"subscribe $keylist{$key} $keyaddr{$key}\": $key\n"; + &do_subscribe($keylist{$key},$keyaddr{$key}) + } + elsif($keycmd{$key} eq "unsubscribe") + { + print REPLY ">>>> Confirmation for \"unsubscribe $keylist{$key} $keyaddr{$key}\": $key\n"; + &do_unsubscribe($keylist{$key},$keyaddr{$key}) + } + } + else + { + print REPLY <<"EOM"; +**** Confirmation message: no valid key found or key expired! +**** Only send confirmation messages to $confirm_address, +**** Other list commands should be directed to $whoami. +**** If you have any questions or problems, please contact +**** "$whoami_owner". +**** Here is the message you sent: + +$msg +EOM + ; + print STDERR "$0: No key found.\n" if $DEBUG; + } + + foreach $_ (@keys) + { + if(!$key || !/^$key/) + { + print NEW $_ + ||&abort("Error writing $confirm_keys.new: $!"); + } + } + + close(NEW) || &abort("Error closing $confirm_keys.new: $!"); + + link("$confirm_keys", "$confirm_keys.old") || + &abort("link(\"$confirm_keys\", \"$confirm_keys.old\"): $!"); + unlink("$confirm_keys"); + link("$confirm_keys.new", "$confirm_keys") || + &abort("link(\"$confirm_keys.new\", \"$confirm_keys\"): $!"); + unlink("$confirm_keys.old"); + unlink("$confirm_keys.new"); + + &lclose(KEYS); + + $count=1; + &done(); +} + print STDERR "$0: processing commands in message body.\n" if $DEBUG; # Process the rest of the message as commands while (<>) { $approved = 0; # all requests start as un-approved + $confirmed = 0; # all requests start as un-confirmed $quietnonmember = 0; # show non-member on unsubscribe while ( /\\\s*$/ ) { # if the last non-whitespace &chop_nl($_); # character is '\', chop the nl @@ -226,7 +359,6 @@ elsif ($cmd eq "help") { &do_help(@parts); } elsif ($cmd eq "get") { &do_get(@parts); } elsif ($cmd eq "index") { &do_index(@parts); } - elsif ($cmd eq "auth") { &do_auth(@parts); } else { &squawk("Command '$cmd' not recognized."); $count--; # if we get to here, it wasn't really a command @@ -264,33 +396,32 @@ local($sub_policy) = $config_opts{$clean_list,"subscribe_policy"}; - # check to see if this is a list with a 'confirm' subscribe policy, - # and check the cookie if so. - # + # check to see if this is a list with a 'confirm' subscribe policy if (! $approved - && (($sub_policy =~ /confirm/) - && (&gen_cookie($sm, $clean_list, $subscriber) ne $auth_info))) + && (($sub_policy =~ /confirm/) && !$confirmed)) { # We want to send the stripped address in the confirmation # message if strip = yes. if (&cf_ck_bool($clean_list,"strip")) { $subscriber = (&ParseAddrs($subscriber))[0]; } - &send_confirm("subscribe", $clean_list, $subscriber); + &send_confirm("subscribe", $clean_list, $subscriber, $reply_to); return 0; } # Check to see if this request is approved, or if the list is an # auto-approve list, or if the list is an open list and the - # subscriber is the person making the request + # subscriber is the person making the request, + # or if it's open and they've confirmed it if ($approved || ($sub_policy =~ /auto/i && # I don't think this check is doing the right thing. Chan 95/10/19 &check_and_request($sm, $clean_list, $subscriber, "check_only")) || (($sub_policy !~ /closed/ ) - && &addr_match($reply_to, $subscriber, - (&cf_ck_bool($clean_list,"mungedomain") ? 2 : undef))) + && ($confirmed || + &addr_match($reply_to, $subscriber, + (&cf_ck_bool($clean_list,"mungedomain") ? 2 : undef)))) ) { # Either the request is approved, or the list is open and the # subscriber is the requester, so check to see if they're @@ -364,6 +495,7 @@ # and check to see if the list is valid local($sm) = "unsubscribe"; local($list, $clean_list, @args) = &get_listname($sm, 1, @_); + local(*NEW); # figure out who's trying to unsubscribe, and check it's a valid address local($subscriber) = join(" ", @args); @@ -400,36 +532,36 @@ print STDERR "do_unsubscribe: valid list, valid subscriber.\n" if $DEBUG; - # check to see if this is a list with a 'confirm' unsubscribe policy, - # and check the cookie if so and the subscriber is not the person - # making the request. - # + # Check to see if subscribe confirmation is required + # If so, then we will send a confirmation message + + local($sub_policy) = $config_opts{$clean_list,"unsubscribe_policy"}; + if (! $approved - && ! ((&addr_match($reply_to, $subscriber, - (&cf_ck_bool($clean_list,"mungedomain") - ? 2 : undef)))) - && (($unsub_policy =~ /confirm/) - && (&gen_cookie($sm, $clean_list, $subscriber) ne $auth_info))) - { + && (($sub_policy =~ /confirm/) && !$confirmed)) + { # We want to send the stripped address in the confirmation # message if strip = yes. if (&cf_ck_bool($clean_list,"strip")) { - $subscriber = (&ParseAddrs($subscriber))[0]; + $subscriber = (&ParseAddrs($subscriber))[0]; } - &send_confirm("unsubscribe", $clean_list, $subscriber); - return 0; - } + &send_confirm("unsubscribe", $clean_list, $subscriber, $reply_to); + return 0; + } # Check to see if this request is approved, if the unsub policy is - # auto, or if the subscriber is the person making the request (even - # on a closed list, folks can unsubscribe themselves without the - # owner's approval). + # auto, or if the subscriber is the person making the request. If + # the request has been confirmed and the policy is open, we don't + # need to match if ($approved || ($unsub_policy =~ /auto/i && &check_and_request($sm, $clean_list, $subscriber, "check_only")) - || ((&addr_match($reply_to, $subscriber, - (&cf_ck_bool($clean_list,"mungedomain") ? 2 : undef))))) { + || ($unsub_policy !~ /closed/i && + &check_and_request($sm, $clean_list, $subscriber, "check_only")) + + && ($confirmed || &addr_match($reply_to, $subscriber, + (&cf_ck_bool($clean_list,"mungedomain") ? 2 : undef)))) { # Either the request is approved, or the subscriber is the # requester, so drop them from the list @@ -504,29 +636,6 @@ } } -sub do_auth { - # Check to see we've got all the arguments; the address is allowed to - # contain spaces, so since our argument list was split on spaces we - # have to join them back together. - local($auth_info, $cmd, $list, @sub) = @_; - if ( !length($auth_info) - || ($cmd ne 'subscribe' - && $cmd ne 'unsubscribe') # can only authorize [un]subscribes at the moment - ) { - &squawk("auth: needs key"); - return 0; - } - $sub = join(' ',@sub); - if ( $cmd eq "subscribe" ) { - &do_subscribe($list, $sub); - } - elsif ( $cmd eq "unsubscribe" ) { - &do_unsubscribe($list, $sub); - } - - -} - sub do_approve { # Check to see we've got all the arguments local($sm) = "approve"; @@ -1609,31 +1718,36 @@ sub send_confirm { local($cmd) = shift; + local($cmdtext); local($list) = &valid_list($listdir, shift); - local($subscriber) = @_; - local($cookie) = &gen_cookie($cmd, $list, $subscriber); + local($subscriber) = shift; + local($requestor) = shift; + local($cookie) = &add_key($cmd, $list, $subscriber); + + $cmdtext="subscribed to" if $cmd eq "subscribe"; + $cmdtext="unsubscribed from" if $cmd eq "unsubscribe"; + &set_mail_from($confirm_address); + local(*AUTH); - &sendmail(AUTH, $subscriber, "Confirmation for $cmd $list"); + &sendmail(AUTH, $subscriber, "Confirm $cmd $list ($cookie)"); print AUTH <<"EOM"; -Someone (possibly you) has requested that your email address be added -to or deleted from the mailing list "$list\@$whereami". +Someone ($reply_to) has requested +that your email address be $cmdtext the mailing list +"$list\@$whereami". + +If you really want this action to be taken, send a reply to this +message to $confirm_address including the following confirmation key +anywhere in the subject or the body: -If you really want this action to be taken, please send the following -commands (exactly as shown) back to "$whoami": + KEY: $cookie - auth $cookie $cmd $list $subscriber +You must reply in two weeks or this request will expire. If you do not want this action to be taken, simply ignore this message and the request will be disregarded. -If your mailer will not allow you to send the entire command as a single -line, you may split it using backslashes, like so: - - auth $cookie $cmd $list \\ - $subscriber - If you have any questions about the policy of the list owner, please contact "$list-approval\@$whereami". @@ -1643,12 +1757,14 @@ EOM close(AUTH); + &set_mail_from($whoami); + print REPLY <<"EOM"; **** Your request to $whoami: **** **** $cmd $list $subscriber **** -**** must be authenticated. To accomplish this, another request must be +**** must be authenticated. To accomplish this, a confirmation must be **** sent in with an authorization key, which has been sent to: **** $subscriber **** @@ -1898,24 +2014,43 @@ } } -sub gen_cookie { - local($combined) = join('/', $cookie_seed ? $cookie_seed : $homedir, @_); - local($cookie) = 0; - local($i, $carry); - - # Because of backslashing and all of the splitting on whitespace and - # joining that goes on, we need to ignore whitespace. - $combined =~ s/\s//g; - - for ($i = 0; $i < length($combined); $i++) { - $cookie ^= ord(substr($combined, $i)); - $carry = ($cookie >> 28) & 0xf; - $cookie <<= 4; - $cookie |= $carry; +sub add_key +{ + local($cmd, $list, $addr) = @_; + local(*KEYS, $binkey, $asckey, $time); + + if($confirm_randev && -f $confirm_randev) + { + local(*RAND); + open(RAND,"$confirm_randev") || + abort("Couldn't open $confirm_randev: $!"); + + for(1..7) + { + $binkey .= getc RAND; + } + close RAND; + } + else + { + for(1..7) + { + $binkey .= pack("c",rand 256); + } } - return (sprintf("%08x", $cookie)); -} + $asckey = unpack("h14",$binkey); + $asckey =~ s/(.{7})(.{7})/\U$1-$2/; + + $time=time+$confirm_expire; + + &lopen(KEYS,">>","$confirm_keys") || + &abort("Couldn't append key file $confirm_keys: $!"); + print KEYS "$asckey\t$time\t$cmd\t$list\t$addr\n"; + &lclose(KEYS) || &abort("Error closing $confirm_keys: $!"); + + return $asckey; +} # Extracts the list name from the argument list to the do_* functions # or uses the default list name, depending on invocation options and