chiark / gitweb /
support genopts
[d.git] / service
1 #!/usr/bin/perl -w
2 our $usage1 = <<'END';
3 usage: ../fyvzl [OPTIONS DATABASE-FILE ACTION ARG
4 options
5   -l<minrandlength>   (for create/choose, number of letters)
6   -m<maxperuser>
7   -d<dom>
8   -q<qualdom>
9   -C     (show comments in output)
10   -h     (display help)
11 END
12 our $usage2 = <<'END';
13 actions
14   create [GENOPTS] [REDIRECT] [#COMMENT]  (default for REDIRECT is username)
15   choose [GENOPTS] [REDIRECT] [#COMMENT]  (generate and interactively allocate)
16   update ADDR [REDIRECT] [#COMMENT]
17   show ADDR
18   list
19   list-actions
20 empty string for REDIRECT means reject
21 END
22 our $usage3 = <<'END';
23 privileged actions
24   list-user USER
25   insert-exact ADDR USER REDIRECT COMMENT
26   donate ADDR USER
27   enable-user|disable-user USER
28 END
29 #/
30
31 use strict;
32
33 use DBI;
34 use POSIX;
35
36 our $minrandlength = 6;
37 our $randlength;
38 our $maxrandlength = 50;
39
40 our $maxperuser = 10000;
41 our $qualdom;
42 our $dbh;
43 our $dom;
44 our $user;
45 our $priv;
46 our $showcomment;
47 our $genmethod = 'alphanum';
48
49 sub nextarg () {
50     die "too few arguments\n" unless @ARGV;
51     my $v = shift @ARGV;
52     die "option too late on command line\n" if $v =~ m/^-/;
53     return $v;
54 }
55
56 sub addr2localpart ($) {
57     my ($addr) = @_;
58     return $addr if $addr !~ m/\@/;
59     die "address not in correct domain (\@$dom)\n" unless $' eq $dom; #';
60     return $`; #`;
61 }
62
63 sub nextarg_addr () {
64     return addr2localpart nextarg;
65 }
66
67 sub nomoreargs () {
68     die "too many arguments\n" if @ARGV;
69 }
70
71 sub isdisabled ($) {
72     my ($u) = @_;
73     our $dis_q;
74     $dis_q ||= $dbh->prepare("SELECT * FROM disabled_users WHERE user=?");
75     $dis_q->execute($u);
76     my $row = $dis_q->fetchrow_arrayref();
77     return !!$row;
78 }
79
80 sub puser ($) {
81     my ($u) = @_;
82     our $last_u;
83     if (!defined $last_u or $last_u ne $u) {
84         print "# user $u ".(isdisabled($u) ? 'disabled' : 'enabled')."\n";
85         $last_u = $u;
86     }
87 }
88
89 sub prow ($) {
90     my ($row) = @_;
91     my $u = $row->{'user'};
92     puser($u);
93     my $pa = $row->{'localpart'};
94     $pa .= '@'.$dom if defined $dom;
95     if (length $row->{'redirect'}) {
96         print "$pa: $row->{'redirect'}" or die $!;
97     } else {
98         print "# reject $pa" or die $!;
99     }
100     if ($showcomment || !$priv) {
101         print " #$row->{'comment'}" or die $!;
102     }
103     print "\n" or die $!;
104 }
105
106 sub goodrand ($) {
107     my ($lim) = @_;
108     for (;;) {
109         my $ch;
110         read(R, $ch, 1) == 1 or die $!;
111         my $o = ord $ch;
112         my $v = $o % $lim;
113         next unless $o-$v+$lim < 256;
114 #       print STDERR "goodrand($lim)=$v\n";
115         return $v;
116     }
117 }
118
119 sub qualify (\$) {
120     my ($ref) = @_;
121     if (defined $$ref && $$ref =~ m/[^\041-\177]/) {
122         die "bad characters in redirection target\n";
123     }
124     if (defined $$ref && length $$ref && $$ref !~ m/\@/) {
125         die "unqualified redirection target\n" unless defined $qualdom;
126         $$ref .= '@'.$qualdom;
127     }
128 }
129
130 sub insertrow ($) {
131     my ($row) = @_;
132     my $stmt =
133         "INSERT INTO addrs (".
134         (join ",", sort keys %$row).
135         ") VALUES (".
136         (join ",", map { "?" } sort keys %$row).
137         ") ";
138     $dbh->do($stmt, {}, map { $row->{$_} } sort keys %$row);
139 }
140
141 sub rhsargs ($) {
142     my ($defrow) = @_;
143     my $row = { };
144     while (@ARGV) {
145         $_ = shift @ARGV;
146         my $f = (s/^\#// ? 'comment' : 'redirect');
147         die "$f supplied twice\n" if exists $row->{$f};
148         $row->{$f} = $_;
149     }
150     foreach my $f (keys %$defrow) {
151         next if defined $row->{$f};
152         $row->{$f} = $defrow->{$f};
153     }
154     qualify $row->{'redirect'};
155     return $row;
156 }
157
158 sub local_part_inuse ($) {
159     my ($s) = @_;
160     our $checkexist_q ||=
161         $dbh->prepare("SELECT localpart FROM addrs WHERE localpart=?");
162     $checkexist_q->execute($s);
163     my $row = $checkexist_q->fetchrow_arrayref();
164     return !!$row;
165 }
166
167 sub gen_local_part_alphanum {
168     my $s = chr(ord('a')+goodrand(26));
169     while (length $s < $randlength) {
170         my $v = goodrand(36);
171         $s .= chr($v < 26
172                   ? ord('a')+($v)
173                   : ord('0')+($v-26));
174     }
175     return $s;
176 }
177
178 sub generate_local_part () {
179     my $s;
180     for (;;) {
181         { no strict qw(refs); $s = &{"gen_local_part_$genmethod"}; }
182 #       print STDERR "$s\n";
183         last if !local_part_inuse($s);
184     }
185     return $s;
186 }
187
188 sub prepare_create () {
189     my $countq = $dbh->prepare("SELECT count(*) FROM addrs WHERE user=?");
190     $countq->execute($user);
191     my ($count) = $countq->fetchrow_array();
192     die unless defined $count;
193     die "too many aliases for this user\n" if $count >= $maxperuser;
194     open R, "/dev/urandom" or die $!;
195     binmode R;
196 }
197
198 sub genopt_alphanum {
199     local ($_) = @_;
200     if (m/^-l(\d+)$/) {
201         $randlength = $1;
202         die "length out of range $minrandlength..$maxrandlength\n"
203             unless ($minrandlength<=$randlength &&
204                     $randlength<=$maxrandlength);
205     } else {
206         die "unknown alphanumeric generation option\n";
207     }
208 }
209
210 sub gendefaults_alphanum {
211     $randlength ||= $minrandlength;
212 }
213
214 sub genopts {
215     while (@ARGV && $ARGV[0] =~ m/^-/) {
216         my $arg = shift @ARGV;
217         last if $arg =~ m/^--?$/;
218         { no strict qw(refs); &{"genopt_$genmethod"}($arg); }
219     }
220     { no strict qw(refs); &{"gendefaults_$genmethod"}(); }
221 }
222
223 sub action_create {
224     genopts;
225     my $newrow = rhsargs({'redirect'=>$user, 'comment'=>''});
226     prepare_create();
227     $newrow->{'user'} = $user;
228     $newrow->{'localpart'} = generate_local_part();
229     insertrow($newrow);
230     $dbh->commit();
231     prow($newrow);
232 }
233
234 sub action_choose {
235     genopts;
236     my $template = rhsargs({'redirect'=>$user, 'comment'=>''});
237     $template->{'user'} = $user;
238     prepare_create();
239     puser($user);
240     my %s;
241     while (keys %s < 10) {
242         my $s = generate_local_part();
243         next if $s{$s};
244         print $s or die $!;
245         print "\@$dom" or die $! if $dom;
246         print "\n" or die $!;
247         $s{$s} = 1;
248     }
249     print "# ready - enter addrs or local-parts to create,".
250         " then \`.' on a line by itself\n"
251         or die $!;
252
253     while (<STDIN>) {
254         chomp;
255         last if m/^\.$/;
256         my $s;
257         if (eval {
258             $s = addr2localpart $_;
259             $s{$s} or die "not an (as-yet-unused) suggestion\n";
260             delete $s{$s};
261             die "just taken in the meantime (bad luck!)\n"
262                 if local_part_inuse $s;
263             1;
264         }) {
265             my $newrow = { %$template, 'localpart' => $s };
266             $dbh->commit();
267             prow($newrow);
268         } else {
269             $dbh->rollback();
270             print "! error: $@" or die $!;
271         }
272     }
273 }
274
275 sub selectrow ($) {
276     my ($localpart) = @_;
277     our $row_q ||= $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
278     $row_q->execute($localpart);
279     return $row_q->fetchrow_hashref();
280 }
281
282 sub begin_row ($) {
283     my ($localpart) = @_;
284     my $q = $dbh->prepare("SELECT * FROM addrs WHERE localpart=?");
285     my $row = selectrow $localpart;
286     die "unknown localpart\n" unless defined $row;
287     die "not owned by you\n" unless $priv || $row->{user} eq $user;
288     return $row;
289 }
290
291 sub action_update {
292     my $localpart = nextarg_addr;
293     my $updrow = rhsargs({});
294     nomoreargs;
295     begin_row($localpart);
296     foreach my $f (qw(redirect comment)) {
297         my $v = $updrow->{$f};
298         next unless defined $v;
299         $dbh->do("UPDATE addrs SET $f=? WHERE localpart=?",
300                  {}, $v, $localpart);
301     }
302     my $row = selectrow $localpart;
303     $dbh->commit;
304     prow($row);
305 }
306
307 sub action_show {
308     my $localpart = nextarg_addr;
309     nomoreargs;
310     my $row = begin_row($localpart);
311     prow($row);
312 }
313
314 sub listq ($) {
315     my ($q) = @_;
316     while (my $row = $q->fetchrow_hashref()) {
317         prow($row);
318     }
319 }
320
321 sub action_list {
322     nomoreargs;
323     my $q = $dbh->prepare("SELECT * FROM addrs WHERE user=?".
324                           " ORDER BY localpart");
325     $q->execute($user);
326     listq($q);
327 }
328
329 sub action_list_user {
330     die unless $priv;
331     $user = nextarg;
332     nomoreargs;
333     action_list;
334 }
335
336 sub action_list_all {
337     die unless $priv;
338     nomoreargs;
339     my $q = $dbh->prepare("SELECT * FROM addrs".
340                           " ORDER BY user, localpart");
341     $q->execute();
342     listq($q)
343 }
344
345 sub action_insert_exact {
346     die unless $priv;
347     my $row = { };
348     $row->{'localpart'} = nextarg_addr;
349     $row->{'user'} = $user = nextarg;
350     $row->{'redirect'} = nextarg;
351     $row->{'comment'} = nextarg;
352     nomoreargs;
353     insertrow($row);
354     $dbh->commit;
355 }
356
357 sub action_donate {
358     die unless $priv;
359     my $localpart = nextarg_addr;
360     my $newuser = nextarg;
361     nomoreargs;
362     begin_row($localpart);
363     $dbh->do('UPDATE addrs SET user=? WHERE localpart=?',
364              {}, $newuser, $localpart);
365     $dbh->commit;
366 }
367
368 sub action_enable_user {
369     die unless $priv;
370     $user = nextarg;
371     nomoreargs;
372     $dbh->do('DELETE FROM disabled_users WHERE user=?',{},$user);
373     $dbh->commit;
374 }
375
376 sub action_disable_user {
377     die unless $priv;
378     $user = nextarg;
379     nomoreargs;
380     $dbh->do('INSERT INTO disabled_users VALUES user (?)',{},$user);
381     $dbh->commit;
382 }
383
384 sub action_list_actions {
385     print $usage2 or die $!;
386 }
387
388 while (@ARGV) {
389     last unless $ARGV[0] =~ m/^-/;
390     $_ = shift @ARGV;
391     last if m/^--?$/;
392     for (;;) {
393         last unless m/^-./;
394         if (s/^-l(\d+)$//) {
395             $minrandlength = $1;
396         } elsif (s/^-m(\d+)$//) {
397             $maxperuser = $1;
398         } elsif (s/^-d(\S+)$//) {
399             $dom = $1;
400         } elsif (s/^-q(\S+)$//) {
401             $qualdom = $1;
402         } elsif (s/^-C/-/) {
403             $showcomment = 1;
404         } elsif (s/^-h/-/) {
405             print $usage1.$usage2.$usage3 or die $!;
406             exit 0;
407         } else {
408             die "unknown option \`$_'\n";
409         }
410     }
411 }
412
413 my $dbfile = nextarg();
414
415 if (defined $ENV{'USERV_USER'}) {
416     $priv=0;
417     $user = $ENV{'USERV_USER'};
418 } else {
419     $priv=1;
420     $user = ((getpwuid $<)[0]) or die;
421 }
422
423 $usage2 .= defined $dom
424     ? "ADDR may be a local part, implicitly qualified with \@$dom\n"
425     : "ADDR must be a local part (only)\n";
426 $usage2 .= "REDIRECT is implicitly qualified with \@$qualdom if it has no \@\n"
427     if defined $qualdom;
428
429 $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","",
430                     { PrintError => 0, AutoCommit => 0, RaiseError => 1 }) 
431     or die "$dbfile $!";
432
433 my $action = nextarg();
434 $action =~ y/-/_/;
435 { no strict qw(refs); &{"action_$action"}(); }