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