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