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