6cfafda5865c3d7885cfb89772d28c49c0d139cb
[freeside.git] / FS / bin / 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 subs qw( lock_write unlock_write );
13 use Fcntl qw(:flock);
14 use POSIX qw(:sys_wait_h setsid);
15 use IO::Handle;
16 use IO::Select;
17 use IO::File;
18 use Storable qw(nstore_fd fd_retrieve);
19 use Net::SSH qw(sshopen2);
20 use FS::UID qw(adminsuidsetup forksuidsetup);
21 use FS::ClientAPI;
22
23 use FS::Conf;
24 use FS::cust_bill;
25 use FS::cust_pkg;
26
27 $Debug = 2; # >= 2 will log packet contents, including potentially compromising
28             # information
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
39 my $lock_file = "/usr/local/etc/freeside/selfservice.$machine.writelock";
40 open(LOCKFILE,">$lock_file") or die "can't open $lock_file: $!";
41
42 &init($user);
43
44 my $conf = new FS::Conf;
45
46 if ($conf->exists('selfservice_server-quiet')) {
47     $FS::cust_bill::quiet = 1;
48     $FS::cust_pkg::quiet = 1;
49 }
50
51 my $clientd = "/usr/local/sbin/freeside-selfservice-clientd"; #better name?
52
53 my $warnkids=0;
54 while (1) {
55   my($writer,$reader,$error) = (new IO::Handle, new IO::Handle, new IO::Handle);
56   warn "connecting to $machine\n" if $Debug;
57
58   $ssh_pid = sshopen2($machine,$reader,$writer,$clientd);
59
60 #  nstore_fd(\*writer, {'hi'=>'there'});
61
62   warn "entering main loop\n" if $Debug;
63   my $undisp = 0;
64   my $s = IO::Select->new( $reader );
65   while (1) {
66
67     &reap_kids;
68
69     warn "waiting for packet from client\n" if $Debug && !$undisp;
70     $undisp = 1;
71     my @handles = $s->can_read(5);
72     unless ( @handles ) {
73       &shutdown if $shutdown;
74       next;
75     }
76
77     $undisp = 0;
78
79     warn "receiving packet from client\n" if $Debug;
80
81     my $packet = fd_retrieve($reader);
82     warn "packet received\n".
83          join('', map { " $_=>$packet->{$_}\n" } keys %$packet )
84       if $Debug > 1;
85
86     #prevent runaway forking
87     my $warnkids = 0;
88     while ( $kids >= $max_kids ) {
89       warn "WARNING: maximum $kids children reached\n" unless $warnkids++;
90       &reap_kids;
91       sleep 1;
92     }
93
94     warn "forking child\n" if $Debug;
95     defined( my $pid = fork ) or die "can't fork: $!";
96     if ( $pid ) {
97       $kids++;
98       $kids{$pid} = 1;
99       warn "child $pid spawned\n" if $Debug;
100     } else { #kid time
101
102       #get new db handle
103       $FS::UID::dbh->{InactiveDestroy} = 1;
104       forksuidsetup($user);
105
106       my $type = $packet->{_packet};
107       warn "calling $type handler\n" if $Debug; 
108       my $rv = eval { FS::ClientAPI->dispatch($type, $packet); };
109       if ( $@ ) {
110         warn my $error = "WARNING: error dispatching $type: $@";
111         $rv = { _error => $error };
112       }
113       $rv->{_token} = $packet->{_token}; #identifier
114
115       warn "sending response\n" if $Debug;
116       lock_write;
117       nstore_fd($rv, $writer) or die "FATAL: can't send response: $!";
118       $writer->flush or die "FATAL: can't flush: $!";
119       unlock_write;
120
121       warn "child exiting\n" if $Debug;
122       exit; #end-of-kid
123     }
124
125   }
126
127 }
128
129 ###
130 # utility subroutines
131 ###
132
133 sub reap_kids {
134   #warn "reaping kids\n";
135   foreach my $pid ( keys %kids ) {
136     my $kid = waitpid($pid, WNOHANG);
137     if ( $kid > 0 ) {
138       $kids--;
139       delete $kids{$kid};
140     }
141   }
142   #warn "done reaping\n";
143 }
144
145 sub init {
146   my $user = shift;
147
148   chdir "/" or die "Can't chdir to /: $!";
149   open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
150   defined(my $pid = fork) or die "Can't fork: $!";
151   if ( $pid ) {
152     print "freeside-selfservice-server to $machine started with pid $pid\n"; #logging to $log_file
153     exit unless $pid_file;
154     my $pidfh = new IO::File ">$pid_file" or exit;
155     print $pidfh "$pid\n";
156     exit;
157   }
158
159 #  sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
160 #  #sub REAPER { my $pid = wait; $kids--; $SIG{CHLD} = \&REAPER; }
161 #  $SIG{CHLD} =  \&REAPER;
162
163   $shutdown = 0;
164   $SIG{HUP} = sub { warn "SIGHUP received; shutting down\n"; $shutdown++; };
165   $SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $shutdown++; };
166   $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $shutdown++; };
167   $SIG{QUIT} = sub { warn "SIGQUIT received; shutting down\n"; $shutdown++; };
168   $SIG{PIPE} = sub { warn "SIGPIPE received; shutting down\n"; $shutdown++; };
169
170   #false laziness w/freeside-queued
171   my $freeside_gid = scalar(getgrnam('freeside'))
172     or die "can't setgid to freeside group\n";
173   $) = $freeside_gid;
174   $( = $freeside_gid;
175   #if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
176   ($(,$)) = ($),$();
177   $) = $freeside_gid;
178
179   $> = $FS::UID::freeside_uid;
180   $< = $FS::UID::freeside_uid;
181   #freebsd is sofa king broken, won't setuid()
182   ($<,$>) = ($>,$<);
183   $> = $FS::UID::freeside_uid;
184   #eslaf
185
186   $ENV{HOME} = (getpwuid($>))[7]; #for ssh
187   adminsuidsetup $user;
188
189   #$log_file = "/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc; #MACHINE NAME
190   $log_file = "/usr/local/etc/freeside/selfservice.$machine.log";
191
192   open STDOUT, '>/dev/null'
193                             or die "Can't write to /dev/null: $!";
194   setsid                  or die "Can't start a new session: $!";
195   open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
196
197   $SIG{__DIE__} = \&_die;
198   $SIG{__WARN__} = \&_logmsg;
199
200   warn "freeside-selfservice-server starting\n";
201
202 }
203
204 sub shutdown {
205   my $wait = 12; #wait up to 1 minute
206   while ( $kids > 0 && $wait-- ) {
207     warn "waiting for $kids children to terminate";
208     sleep 5;
209   }
210   warn "abandoning $kids children" if $kids;
211   kill 'TERM', $ssh_pid if $ssh_pid;
212   die "exiting";
213 }
214
215 sub _die {
216   my $msg = shift;
217   unlink $pid_file if -e $pid_file;
218   _logmsg($msg);
219 }
220
221 sub _logmsg {
222   chomp( my $msg = shift );
223   _do_logmsg( "[server] [". scalar(localtime). "] [$$] $msg\n" );
224 }
225
226 sub _do_logmsg {
227   chomp( my $msg = shift );
228   my $log = new IO::File ">>$log_file";
229   flock($log, LOCK_EX);
230   seek($log, 0, 2);
231   print $log "$msg\n";
232   flock($log, LOCK_UN);
233   close $log;
234 }
235
236 sub lock_write {
237   #broken on freebsd?
238   #flock($writer, LOCK_EX) or die "FATAL: can't lock write stream: $!";
239
240   flock(LOCKFILE, LOCK_EX) or die "FATAL: can't lock $lock_file: $!";
241
242 }
243
244 sub unlock_write {
245   #broken on freebsd?
246   #flock($writer, LOCK_UN) or die "WARNING: can't release write lock: $!";
247
248   flock(LOCKFILE, LOCK_UN) or die "FATAL: can't unlock $lock_file: $!";
249
250 }
251
252 sub usage {
253   die "Usage:\n\n  freeside-selfservice-server user machine\n";
254 }
255