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