finally working async framework
[freeside.git] / fs_selfservice / freeside-selfservice-server
1 #!/usr/bin/perl -w
2 #
3 # freeside-selfservice-server
4
5 # alas, much false laziness with freeside-queued and fs_signup_server.  at
6 # least it is slated to replace fs_{signup,passwd,mailadmin}_server
7 # should probably generalize the version in here, or better yet use
8 # Proc::Daemon or somesuch
9
10 use strict;
11 use vars qw( $kids $max_kids $shutdown $log_file );
12 use vars qw($ssh_pid);
13 use Fcntl qw(:flock);
14 use POSIX qw(setsid);
15 use Date::Format;
16 use IO::Handle;
17 use Storable qw(nstore_fd fd_retrieve);
18 use Net::SSH qw(sshopen2);
19 use FS::UID qw(adminsuidsetup forksuidsetup);
20
21 use LockFile::Simple qw(lock unlock);
22
23 #use Tie::RefHash;
24 #use FS::Conf;
25 #use FS::Record qw( qsearch qsearchs );
26 #use FS::cust_main_county;
27 #use FS::cust_main;
28 #use FS::Msgcat qw(gettext);
29
30 $shutdown = 0;
31 $max_kids = '10'; #?
32 $kids = 0;
33
34 my $user = shift or die &usage;
35 my $machine = shift or die &usage;
36 my $pid_file = "/var/run/freeside-selfservice-server.$user.pid";
37 #my $pid_file = "/var/run/freeside-selfservice-server.$user.pid"; $FS::UID::datasrc not posible, but should include machine name at least, hmm
38 my $lock_file = "/usr/local/etc/freeside/freeside-selfservice-server.$user.lock";
39 unlink $lock_file;
40
41 &init($user);
42
43 my $clientd = "/usr/local/sbin/freeside-selfservice-clientd"; #better name?
44
45 my %dispatch = (
46   'signup' => \&signup,
47   #'signup_init' => 'signup_init',
48   'passwd' => \&passwd,
49
50 );
51
52 my $warnkids=0;
53 while (1) {
54   my($reader, $writer) = (new IO::Handle, new IO::Handle);
55   warn "connecting to $machine\n";
56   $ssh_pid = sshopen2($machine,$reader,$writer,$clientd);
57
58 #  nstore_fd(\*writer, {'hi'=>'there'});
59
60   warn "entering main loop\n";
61   my $undisp = 0;
62   while (1) {
63
64     warn "waiting for packet from client\n" unless $undisp;
65     $undisp = 1;
66     my $packet = eval {
67       local $SIG{__DIE__};
68       local $SIG{ALRM} = sub { local $SIG{__DIE__};die "MyAlarm\n" }; #NB: \n required
69       alarm 5;
70       #my $string = <$reader>;
71       #die $string;
72       my $p = fd_retrieve($reader);
73       alarm 0;
74       $p;
75     };
76     if ($@) {
77       &shutdown if $shutdown && $@;
78       die "Fatal error receiving packet from client: $@" if $@ !~ /^MyAlarm/;
79       #die $@ unless $@ eq "alarm\n" || $@ eq 'Alarm'; #?????
80       #timeout
81       next unless $shutdown;
82       &shutdown;
83     }
84     warn "packet received\n".
85          join('', map { " $_=>$packet->{$_}\n" } keys %$packet );
86
87     $undisp = 0;
88
89     #prevent runaway forking
90     my $warnkids = 0;
91     while ( $kids >= $max_kids ) {
92       warn "WARNING: maximum $kids children reached\n" unless $warnkids++;
93       sleep 1;
94     }
95
96     warn "forking child\n";
97     defined( my $pid = fork ) or die "can't fork: $!";
98     if ( $pid ) {
99       warn "child $pid spawned\n";
100       $kids++;
101     } else { #kid time
102
103       #local($SIG{PIPE});
104
105       #get new db handle
106       #$FS::UID::dbh->{InactiveDestroy} = 1;
107       forksuidsetup($user);
108
109       my $sub = $dispatch{$packet->{_packet}};
110       my $rv;
111       if ( $sub ) {
112         warn "calling $sub handler\n"; 
113         $rv = &{$sub}($packet);
114       } else {
115         warn my $error = "WARNING: unknown packet type ". $packet->{_packet};
116         $rv = { _error => $error };
117       }
118       $rv->{_token} = $packet->{_token}; #identifier
119
120       warn "sending response\n";
121       flock($writer, LOCK_EX); #acquire write lock
122       #lock($lock_file);
123       nstore_fd($rv, $writer) or die "can't send response: $!";
124       $writer->flush;
125       #unlock($lock_file);
126       flock($writer, LOCK_UN); #release write lock
127 #
128       warn "child exiting\n";
129       exit; #end-of-kid
130     }
131
132   }
133
134 }
135
136 ###
137 # dispatch subroutines (should live elsewhere eventually)
138 ###
139
140 sub passwd {
141   #sleep 3;
142   use FS::Record qw(qsearchs);
143   use FS::svc_acct;
144   #use FS::svc_domain;
145
146   my $packet = shift;
147
148   #my $domain = qsearchs('svc_domain', { 'domain' => $packet->{'domain'} } )
149   #  or return { error => "Domain $domain not found" };
150
151   my $old_password = $packet->{'old_password'};
152   my $new_password = $packet->{'new_password'};
153   my $new_gecos = $packet->{'new_gecos'};
154   my $new_shell = $packet->{'new_shell'};
155
156   my $svc_acct =
157     ( length($old_password) < 13
158       && qsearchs( 'svc_acct', { 'username'  => $packet->{'username'},
159                                  #'domsvc'    => $svc_domain->domsvc,
160                                  '_password' => $old_password } )
161     )
162     || qsearchs( 'svc_acct', { 'username'  => $packet->{'username'},
163                                #'domsvc'    => $svc_domain->domsvc,
164                                '_password' => $old_password } );
165
166   unless ( $svc_acct ) { return { error => 'Incorrect password.' } }
167
168   my %hash = $svc_acct->hash;
169   my $new_svc_acct = new FS::svc_acct ( \%hash );
170   $new_svc_acct->setfield('_password', $new_password ) 
171     if $new_password && $new_password ne $old_password;
172   $new_svc_acct->setfield('finger',$new_gecos) if $new_gecos;
173   $new_svc_acct->setfield('shell',$new_shell) if $new_shell;
174   my $error = $new_svc_acct->replace($svc_acct);
175
176   return { error => $error };
177
178 }
179
180 ###
181 # utility subroutines
182 ###
183
184 sub init {
185   my $user = shift;
186
187   chdir "/" or die "Can't chdir to /: $!";
188   open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
189   defined(my $pid = fork) or die "Can't fork: $!";
190   if ( $pid ) {
191     print "freeside-selfservice-server to $machine started with pid $pid\n"; #logging to $log_file
192     exit unless $pid_file;
193     my $pidfh = new IO::File ">$pid_file" or exit;
194     print $pidfh "$pid\n";
195     exit;
196   }
197
198   sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
199   $SIG{CHLD} =  \&REAPER;
200
201   $shutdown = 0;
202   $SIG{HUP} = sub { warn "SIGHUP received; shutting down\n"; $shutdown++; };
203   $SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $shutdown++; };
204   $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $shutdown++; };
205   $SIG{QUIT} = sub { warn "SIGQUIT received; shutting down\n"; $shutdown++; };
206   $SIG{PIPE} = sub { warn "SIGPIPE received; shutting down\n"; $shutdown++; };
207
208   $> = $FS::UID::freeside_uid unless $>;
209   $< = $>;
210
211   #false laziness w/freeside-queued
212   my $freeside_gid = scalar(getgrnam('freeside'))
213     or die "can't setgid to freeside group\n";
214   $) = $freeside_gid;
215   $( = $freeside_gid;
216   #if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
217   ($(,$)) = ($),$();
218   $) = $freeside_gid;
219
220   $> = $FS::UID::freeside_uid;
221   $< = $FS::UID::freeside_uid;
222   #freebsd is sofa king broken, won't setuid()
223   ($<,$>) = ($>,$<);
224   $> = $FS::UID::freeside_uid;
225   #eslaf
226
227   $ENV{HOME} = (getpwuid($>))[7]; #for ssh
228   adminsuidsetup $user;
229
230   #$log_file = "/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc; #MACHINE NAME
231   $log_file = "/usr/local/etc/freeside/selfservice.$machine.log";
232
233   open STDOUT, '>/dev/null'
234                             or die "Can't write to /dev/null: $!";
235   setsid                  or die "Can't start a new session: $!";
236 #  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
237
238   $SIG{__DIE__} = \&_die;
239   $SIG{__WARN__} = \&_logmsg;
240
241   warn "freeside-selfservice-server starting\n";
242
243 }
244
245 sub shutdown {
246   my $wait = 12; #wait up to 1 minute
247   while ( $kids > 0 && $wait-- ) {
248     warn "waiting for $kids children to terminate";
249     sleep 5;
250   }
251   warn "abandoning $kids children" if $kids;
252   kill 'TERM', $ssh_pid if $ssh_pid;
253   die "exiting";
254 }
255
256 sub _die {
257   my $msg = shift;
258   unlink $pid_file if -e $pid_file;
259   _logmsg($msg);
260 }
261
262 sub _logmsg {
263   chomp( my $msg = shift );
264   my $log = new IO::File ">>$log_file";
265   flock($log, LOCK_EX);
266   seek($log, 0, 2);
267   print $log "[server] [". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n";
268   flock($log, LOCK_UN);
269   close $log;
270 }
271
272 sub usage {
273   die "Usage:\n\n  fs_signup_server user machine\n";
274 }
275
276 ###
277 # handlers... should go in their own files eventually...
278 ###
279